diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 95ee6e87..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Always attempt union merge for project files -*.pbxproj merge=union diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8ce1fa53..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve GitHub Copilot for Xcode ---- - - - -**Describe the bug** - - -**Versions** -- Copilot for Xcode: [e.g. 0.25.0] -- Xcode: [e.g. 16.0] -- macOS: [e.g. 14.6.1] - -**Steps to reproduce** -1. -2. - -**Screenshots** - - -**Logs** - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 434de549..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Questions - url: https://github.com/orgs/community/discussions/categories/copilot - about: Please ask and answer questions about GitHub Copilot here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 3f98d708..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for GitHub Copilot for Xcode ---- - - - - \ No newline at end of file diff --git a/.github/actions/set-xcode-version/action.yml b/.github/actions/set-xcode-version/action.yml deleted file mode 100644 index 4d8dc817..00000000 --- a/.github/actions/set-xcode-version/action.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 'Composite Xcode Path' -description: 'Get Xcode version to be used across all actions' -inputs: - xcode-version: - description: - Xcode version to use, in semver(ish)-style matching the format on the Actions runner image. - See available versions at https://github.com/actions/runner-images/blame/main/images/macos/macos-14-Readme.md#xcode - required: false - default: '16.2' -outputs: - xcode-path: - description: "Path to current Xcode version" - value: ${{ steps.xcode-path.outputs.xcode-path }} -runs: - using: "composite" - steps: - - name: Set XCODE_PATH env var - env: - XCODE_PATH: "/Applications/Xcode_${{ inputs.xcode-version }}.app" - run: echo "XCODE_PATH=${{ env.XCODE_PATH }}" >> $GITHUB_ENV - shell: bash - - name: Set Xcode version - run: sudo xcode-select -s ${{ env.XCODE_PATH }} - shell: bash - - name: Enable new build system integration - run: defaults write com.apple.dt.XCBuild EnableSwiftBuildSystemIntegration 1 - shell: bash - - name: Output Xcode path - id: xcode-path - run: echo "xcode-path=$(echo ${{ env.XCODE_PATH }})" >> $GITHUB_OUTPUT - shell: bash diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 0f96f54e..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1 +0,0 @@ -At the moment we are not accepting contributions to the repository. \ No newline at end of file diff --git a/.github/workflows/auto-close-pr.yml b/.github/workflows/auto-close-pr.yml deleted file mode 100644 index de2ca780..00000000 --- a/.github/workflows/auto-close-pr.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Auto-close PR -on: - pull_request_target: - types: [opened, reopened] - -jobs: - close: - name: Run - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - run: | - gh pr close ${{ github.event.pull_request.number }} --comment \ - "At the moment we are not accepting contributions to the repository. - - Feedback for GitHub Copilot for Xcode can be given in the [Copilot community discussions](https://github.com/orgs/community/discussions/categories/copilot)." - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 78e35963..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: "CodeQL Advanced" - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '24 23 * * 1' - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: python - build-mode: none - - language: swift - build-mode: manual - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - if: matrix.build-mode == 'manual' - uses: ./.github/actions/set-xcode-version - - - if: matrix.build-mode == 'manual' - shell: bash - run: | - xcodebuild \ - -scheme 'Copilot for Xcode' \ - -quiet \ - -archivePath build/Archives/CopilotForXcode.xcarchive \ - -configuration Release \ - -skipMacroValidation \ - -disableAutomaticPackageResolution \ - -workspace 'Copilot for Xcode.xcworkspace' \ - archive \ - CODE_SIGNING_ALLOWED="NO" - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9aa8393c..00000000 --- a/.gitignore +++ /dev/null @@ -1,125 +0,0 @@ -# Created by https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager -# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,macos,swift,swiftpackagemanager - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -### Swift ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -# Swift Package Manager -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -.swiftpm - -.build/ - -# CocoaPods -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# Pods/ -# Add this line if you want to avoid checking in source code from the - -# Carthage -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -# End of https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager - -# Local build config -Config.local.xcconfig - -# Avoid checking in package resolved from swift packages -Tool/Package.resolved -Core/Package.resolved - -# Copilot language server -Server/node_modules/ -Server/dist - -# Releases -/releases/ -/release/ -/appcast.xml diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index a57b9333..00000000 --- a/.swiftformat +++ /dev/null @@ -1,56 +0,0 @@ ---allman false ---beforemarks ---binarygrouping 4,8 ---categorymark "MARK: %c" ---classthreshold 0 ---closingparen balanced ---commas always ---conflictmarkers reject ---decimalgrouping 3,6 ---elseposition same-line ---enumthreshold 0 ---exponentcase lowercase ---exponentgrouping disabled ---fractiongrouping disabled ---fragment false ---funcattributes preserve ---guardelse auto ---header ignore ---hexgrouping 4,8 ---hexliteralcase uppercase ---ifdef no-indent ---importgrouping testable-bottom ---indent 4 ---indentcase false ---lifecycle ---linebreaks lf ---maxwidth 100 ---modifierorder ---nospaceoperators ...,..< ---nowrapoperators ---octalgrouping 4,8 ---operatorfunc spaced ---patternlet hoist ---ranges spaced ---self remove ---selfrequired ---semicolons inline ---shortoptionals always ---smarttabs enabled ---stripunusedargs unnamed-only ---structthreshold 0 ---tabwidth unspecified ---trailingclosures ---trimwhitespace always ---typeattributes preserve ---varattributes preserve ---voidtype void ---wraparguments before-first ---wrapcollections disabled ---wrapparameters before-first ---xcodeindentation disabled ---yodaswap always - ---enable isEmpty - ---exclude Pods,**/Generated diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2c099b50..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,142 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## 0.41.0 - August 14, 2025 -### Added -- Code review feature. -- Chat: Support for new model GPT-5. -- Agent mode: Added support for new tool to read web URL contents. -- Support disabling MCP when it's disabled by policy. -- Support for opening MCP logs directly from the MCP settings page. -- OAuth support for remote GitHub MCP server. - -### Changed -- Performance: Improved instant-apply speed for edit_file tool. - -### Fixed -- Chat Agent repeatedly reverts its own changes when editing the same file. -- Performance: Avoid chat panel being stuck when sending a large text for chat. - -## 0.40.0 - July 24, 2025 -### Added -- Support disabling Agent mode when it's disabled by policy. - -## 0.39.0 - July 23, 2025 -### Fixed -- Performance: Fixed a freezing issue in 'Add Context' view when opening large projects. -- Login failed due to insufficient permissions on the .config folder. -- Fixed an issue that setting changes like proxy config did not take effect. -- Increased the timeout for ask mode to prevent response failures due to timeout. - -## 0.38.0 - June 30, 2025 -### Added -- Support for Claude 4 in Chat. -- Support for Copilot Vision (image attachments). -- Support for remote MCP servers. - -### Changed -- Automatically suggests a title for conversations created in agent mode. -- Improved restoration of MCP tool status after Copilot restarts. -- Reduced duplication of MCP server instances. - -### Fixed -- Switching accounts now correctly refreshes the auth token and models. -- Fixed file create/edit issues in agent mode. - -## 0.37.0 - June 18, 2025 -### Added -- **Advanced** settings: Added option to configure **Custom Instructions** for GitHub Copilot during chat sessions. -- **Advanced** settings: Added option to keep the chat window automatically attached to Xcode. - -### Changed -- Enabled support for dragging-and-dropping files into the chat panel to provide context. - -### Fixed -- "Add Context" menu didn’t show files in workspaces organized with Xcode’s group feature. -- Chat didn’t respond when the workspace was in a system folder (like Desktop, Downloads, or Documents) and access permission hadn’t been granted. - -## 0.36.0 - June 4, 2025 -### Added -- Introduced a new chat setting "**Response Language**" under **Advanced** settings to customize the natural language used in chat replies. -- Enabled support for custom instructions defined in _.github/copilot-instructions.md_ within your workspace. -- Added support for premium request handling. - -### Fixed -- Performance: Improved UI responsiveness by lazily restoring chat history. -- Performance: Fixed lagging issue when pasting large text into the chat input. -- Performance: Improved project indexing performance. -- Don't trigger / (slash) commands when pasting a file path into the chat input. -- Adjusted terminal text styling to align with Xcode’s theme. - -## 0.35.0 - May 19, 2025 -### Added -- Launched Agent Mode. Copilot will automatically use multiple requests to edit files, run terminal commands, and fix errors. -- Introduced Model Context Protocol (MCP) support in Agent Mode, allowing you to configure MCP tools to extend capabilities. - -### Changed -- Added a button to enable/disable referencing current file in conversations -- Added an animated progress icon in the response section -- Refined onboarding experience with updated instruction screens and welcome views -- Improved conversation reliability with extended timeout limits for agent requests - -### Fixed -- Addressed critical error handling issues in core functionality -- Resolved UI inconsistencies with chat interface padding adjustments -- Implemented custom certificate handling using system environment variables `NODE_EXTRA_CA_CERTS` and `NODE_TLS_REJECT_UNAUTHORIZED`, fixing network access issues - -## 0.34.0 - April 29, 2025 -### Added -- Added support for new models in Chat: OpenAI GPT-4.1, o3 and o4-mini, Gemini 2.5 Pro - -### Changed -- Switched default model to GPT-4.1 for new installations -- Enhanced model selection interface - -### Fixed -- Resolved critical error handling issues - -## 0.33.0 - April 17, 2025 -### Added -- Added support for new models in Chat: Claude 3.7 Sonnet and GPT 4.5 -- Implemented @workspace context feature allowing questions about the entire codebase in Copilot Chat - -### Changed -- Simplified access to Copilot Chat from the Copilot for Xcode app with a single click -- Enhanced instructions for granting background permissions - -### Fixed -- Resolved false alarms for sign-in and free plan limit notifications -- Improved app launch performance -- Fixed workspace and context update issues - -## 0.32.0 - March 11, 2025 (General Availability) -### Added -- Implemented model picker for selecting LLM model in chat -- Introduced new `/releaseNotes` slash command for accessing release information - -### Changed -- Improved focus handling with automatic switching between chat text field and file search bar -- Enhanced keyboard navigation support for file picker in chat context -- Refined instructions for granting accessibility and extension permissions -- Enhanced accessibility compliance for the chat window -- Redesigned notification and status bar menu styles for better usability - -### Fixed -- Resolved compatibility issues with macOS 12/13/14 -- Fixed handling of invalid workspace switch event '/' -- Corrected chat attachment file picker to respect workspace scope -- Improved icon display consistency across different themes -- Added support for previously unsupported file types (.md, .txt) in attachments -- Adjusted incorrect margins in chat window UI - -## 0.31.0 - February 11, 2025 (Public Preview) -### Added -- Added Copilot Chat support -- Added GitHub Freeplan support -- Implemented conversation and chat history management across multiple Xcode instances -- Introduced multi-file context support for comprehensive code understanding -- Added slash commands for specialized operations diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index a1f82f0d..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CommunicationBridge/ServiceDelegate.swift b/CommunicationBridge/ServiceDelegate.swift deleted file mode 100644 index 4e289e57..00000000 --- a/CommunicationBridge/ServiceDelegate.swift +++ /dev/null @@ -1,237 +0,0 @@ -import AppKit -import Foundation -import Logger -import XPCShared - -class ServiceDelegate: NSObject, NSXPCListenerDelegate { - func listener( - _: NSXPCListener, - shouldAcceptNewConnection newConnection: NSXPCConnection - ) -> Bool { - newConnection.exportedInterface = NSXPCInterface( - with: CommunicationBridgeXPCServiceProtocol.self - ) - - let exportedObject = XPCService() - newConnection.exportedObject = exportedObject - newConnection.resume() - - Logger.communicationBridge.info("Accepted new connection.") - - return true - } -} - -class XPCService: CommunicationBridgeXPCServiceProtocol { - static let eventHandler = EventHandler() - - func launchExtensionServiceIfNeeded( - withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void - ) { - Task { - await Self.eventHandler.launchExtensionServiceIfNeeded(withReply: reply) - } - } - - func quit(withReply reply: @escaping () -> Void) { - Task { - await Self.eventHandler.quit(withReply: reply) - } - } - - func updateServiceEndpoint( - endpoint: NSXPCListenerEndpoint, - withReply reply: @escaping () -> Void - ) { - Task { - await Self.eventHandler.updateServiceEndpoint(endpoint: endpoint, withReply: reply) - } - } -} - -actor EventHandler { - var endpoint: NSXPCListenerEndpoint? - let launcher = ExtensionServiceLauncher() - var exitTask: Task? - - init() { - Task { await rescheduleExitTask() } - } - - func launchExtensionServiceIfNeeded( - withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void - ) async { - rescheduleExitTask() - #if DEBUG - if let endpoint, !(await testXPCListenerEndpoint(endpoint)) { - self.endpoint = nil - } - reply(endpoint) - #else - if await launcher.isApplicationValid { - Logger.communicationBridge.info("Service app is still valid") - reply(endpoint) - } else { - endpoint = nil - await launcher.launch() - reply(nil) - } - #endif - } - - func quit(withReply reply: () -> Void) { - Logger.communicationBridge.info("Exiting service.") - listener.invalidate() - exit(0) - } - - func updateServiceEndpoint(endpoint: NSXPCListenerEndpoint, withReply reply: () -> Void) { - rescheduleExitTask() - self.endpoint = endpoint - reply() - } - - /// The bridge will kill itself when it's not used for a period. - /// It's fine that the bridge is killed because it will be launched again when needed. - private func rescheduleExitTask() { - exitTask?.cancel() - exitTask = Task { - #if DEBUG - try await Task.sleep(nanoseconds: 60_000_000_000) - Logger.communicationBridge.info("Exit will be called in release build.") - #else - try await Task.sleep(nanoseconds: 1_800_000_000_000) - Logger.communicationBridge.info("Exiting service.") - listener.invalidate() - exit(0) - #endif - } - } -} - -actor ExtensionServiceLauncher { - let appIdentifier = bundleIdentifierBase.appending(".ExtensionService") - let appURL = Bundle.main.bundleURL.appendingPathComponent( - "GitHub Copilot for Xcode Extension.app" - ) - var isLaunching: Bool = false - var application: NSRunningApplication? - var isApplicationValid: Bool { - guard let application else { return false } - if application.isTerminated { return false } - let identifier = application.processIdentifier - if let application = NSWorkspace.shared.runningApplications.first(where: { - $0.processIdentifier == identifier - }) { - Logger.communicationBridge.info( - "Service app found: \(application.processIdentifier) \(String(describing: application.bundleIdentifier))" - ) - return true - } - return false - } - - func launch() { - guard !isLaunching else { return } - isLaunching = true - - Logger.communicationBridge.info("Launching extension service app.") - - // First check if the app is already running - if let runningApp = NSWorkspace.shared.runningApplications.first(where: { - $0.bundleIdentifier == appIdentifier - }) { - Logger.communicationBridge.info("Extension service app already running with PID: \(runningApp.processIdentifier)") - self.application = runningApp - self.isLaunching = false - return - } - - // Implement a retry mechanism with exponential backoff - Task { - var retryCount = 0 - let maxRetries = 3 - var success = false - - while !success && retryCount < maxRetries { - do { - // Add a delay between retries with exponential backoff - if retryCount > 0 { - let delaySeconds = pow(2.0, Double(retryCount - 1)) - Logger.communicationBridge.info("Retrying launch after \(delaySeconds) seconds (attempt \(retryCount + 1) of \(maxRetries))") - try await Task.sleep(nanoseconds: UInt64(delaySeconds * 1_000_000_000)) - } - - // Use a task-based approach for launching with timeout - let launchTask = Task { () -> NSRunningApplication? in - return await withCheckedContinuation { continuation in - NSWorkspace.shared.openApplication( - at: appURL, - configuration: { - let configuration = NSWorkspace.OpenConfiguration() - configuration.createsNewApplicationInstance = false - configuration.addsToRecentItems = false - configuration.activates = false - return configuration - }() - ) { app, error in - if let error = error { - continuation.resume(returning: nil) - } else { - continuation.resume(returning: app) - } - } - } - } - - // Set a timeout for the launch operation - let timeoutTask = Task { - try await Task.sleep(nanoseconds: 10_000_000_000) // 10 seconds - return - } - - // Wait for either the launch or the timeout - let app = try await withTaskCancellationHandler { - try await launchTask.value ?? nil - } onCancel: { - launchTask.cancel() - } - - // Cancel the timeout task - timeoutTask.cancel() - - if let app = app { - // Success! - self.application = app - success = true - break - } else { - // App is nil, retry - retryCount += 1 - Logger.communicationBridge.info("Launch attempt \(retryCount) failed, app is nil") - } - } catch { - retryCount += 1 - Logger.communicationBridge.error("Error during launch attempt \(retryCount): \(error.localizedDescription)") - } - } - - // Double-check we have a valid application - if !success && self.application == nil { - // After all retries, check once more if the app is running (it might have launched but we missed the callback) - if let runningApp = NSWorkspace.shared.runningApplications.first(where: { - $0.bundleIdentifier == appIdentifier - }) { - Logger.communicationBridge.info("Found running extension service after retries: \(runningApp.processIdentifier)") - self.application = runningApp - success = true - } else { - Logger.communicationBridge.info("Failed to launch extension service after \(maxRetries) attempts") - } - } - - self.isLaunching = false - } - } -} - diff --git a/CommunicationBridge/main.swift b/CommunicationBridge/main.swift deleted file mode 100644 index b420b80f..00000000 --- a/CommunicationBridge/main.swift +++ /dev/null @@ -1,21 +0,0 @@ -import AppKit -import Foundation -import Logger - -class AppDelegate: NSObject, NSApplicationDelegate {} - -let bundleIdentifierBase = Bundle(url: Bundle.main.bundleURL.appendingPathComponent( - "GitHub Copilot For Xcode Extension.app" -))?.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as? String ?? "com.github.CopilotForXcode" - -let serviceIdentifier = bundleIdentifierBase + ".CommunicationBridge" -let appDelegate = AppDelegate() -let delegate = ServiceDelegate() -let listener = NSXPCListener(machServiceName: serviceIdentifier) -listener.delegate = delegate -listener.resume() -let app = NSApplication.shared -app.delegate = appDelegate -Logger.communicationBridge.info("Communication bridge started") -app.run() - diff --git a/Config.debug.xcconfig b/Config.debug.xcconfig deleted file mode 100644 index 63fae668..00000000 --- a/Config.debug.xcconfig +++ /dev/null @@ -1,17 +0,0 @@ -#include "Version.xcconfig" -SLASH = / // Otherwise a double slash is treated as a comment, even inside a quoted string - -HOST_APP_NAME = GitHub Copilot for Xcode Dev -BUNDLE_IDENTIFIER_BASE = dev.com.github.CopilotForXcode -SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)githubcopilotide.z13.web.core.windows.net/appcast.xml -SPARKLE_PUBLIC_KEY = EGlZbKpzATrZFfzr142PrZbmQr5opzdC8urMU8+dKL0= -APPLICATION_SUPPORT_FOLDER = dev.com.github.CopilotForXcode -EXTENSION_BUNDLE_NAME = GitHub Copilot Dev -EXTENSION_BUNDLE_DISPLAY_NAME = GitHub Copilot Dev -EXTENSION_SERVICE_NAME = GitHub Copilot for Xcode Extension -COPILOT_DOCS_URL = https:$(SLASH)$(SLASH)docs.github.com/en/copilot -COPILOT_FORUM_URL = https:$(SLASH)$(SLASH)github.com/orgs/community/discussions/categories/copilot - -// see also target Configs - -#include? "Config.local.xcconfig" diff --git a/Config.xcconfig b/Config.xcconfig deleted file mode 100644 index 5fba3479..00000000 --- a/Config.xcconfig +++ /dev/null @@ -1,15 +0,0 @@ -#include "Version.xcconfig" -SLASH = / // Otherwise a double slash is treated as a comment, even inside a quoted string - -HOST_APP_NAME = GitHub Copilot for Xcode -BUNDLE_IDENTIFIER_BASE = com.github.CopilotForXcode -SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)githubcopilotide.z13.web.core.windows.net/appcast.xml -SPARKLE_PUBLIC_KEY = EGlZbKpzATrZFfzr142PrZbmQr5opzdC8urMU8+dKL0= -APPLICATION_SUPPORT_FOLDER = com.github.CopilotForXcode -EXTENSION_BUNDLE_NAME = GitHub Copilot -EXTENSION_BUNDLE_DISPLAY_NAME = GitHub Copilot -EXTENSION_SERVICE_NAME = GitHub Copilot for Xcode Extension -COPILOT_DOCS_URL = https:$(SLASH)$(SLASH)docs.github.com/en/copilot -COPILOT_FORUM_URL = https:$(SLASH)$(SLASH)github.com/orgs/community/discussions/categories/copilot - -// see also target Configs diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj deleted file mode 100644 index 844f7d7c..00000000 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ /dev/null @@ -1,1372 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 70; - objects = { - -/* Begin PBXBuildFile section */ - 3ABBEA292C8B9FE100C61D61 /* copilot-language-server in Resources */ = {isa = PBXBuildFile; fileRef = 3ABBEA282C8B9FE100C61D61 /* copilot-language-server */; }; - 3ABBEA2B2C8BA00300C61D61 /* copilot-language-server-arm64 in Resources */ = {isa = PBXBuildFile; fileRef = 3ABBEA2A2C8BA00300C61D61 /* copilot-language-server-arm64 */; }; - 3ABBEA2C2C8BA00800C61D61 /* copilot-language-server-arm64 in Resources */ = {isa = PBXBuildFile; fileRef = 3ABBEA2A2C8BA00300C61D61 /* copilot-language-server-arm64 */; }; - 3ABBEA2D2C8BA00B00C61D61 /* copilot-language-server in Resources */ = {isa = PBXBuildFile; fileRef = 3ABBEA282C8B9FE100C61D61 /* copilot-language-server */; }; - 3E5DB7502D6B8FA500418952 /* ReleaseNotes.md in Resources */ = {isa = PBXBuildFile; fileRef = 3E5DB74F2D6B88EE00418952 /* ReleaseNotes.md */; }; - 424ACA212CA4697200FA20F2 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 424ACA202CA4697200FA20F2 /* Credits.rtf */; }; - 427C63282C6E868B000E557C /* OpenSettingsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427C63272C6E868B000E557C /* OpenSettingsCommand.swift */; }; - 5EC511E32C90CE7400632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; }; - 5EC511E42C90CE9800632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; }; - 5EC511E52C90CFD600632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; }; - 5EC511E62C90CFD700632BAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; }; - C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */; }; - C8009C032941C576007AA7E8 /* SyncTextSettingsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8009C022941C576007AA7E8 /* SyncTextSettingsCommand.swift */; }; - C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */; }; - C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */; }; - C81291D72994FE6900196E12 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C81291D52994FE6900196E12 /* Main.storyboard */; }; - C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C814588E2939EFDC00135263 /* Cocoa.framework */; }; - C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458932939EFDC00135263 /* SourceEditorExtension.swift */; }; - C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */; }; - C814589B2939EFDC00135263 /* Copilot.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C814588C2939EFDC00135263 /* Copilot.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - C8189B1A2938972F00C9DCDA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8189B192938972F00C9DCDA /* App.swift */; }; - C8189B1E2938973000C9DCDA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B1D2938973000C9DCDA /* Assets.xcassets */; }; - C8189B212938973000C9DCDA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8189B202938973000C9DCDA /* Preview Assets.xcassets */; }; - C8216B73298036EC00AD38C7 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8216B72298036EC00AD38C7 /* main.swift */; }; - C8216B782980370100AD38C7 /* ReloadLaunchAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */; }; - C8216B7D2980374300AD38C7 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C8216B7C2980374300AD38C7 /* ArgumentParser */; }; - C8216B802980378300AD38C7 /* Helper in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8216B70298036EC00AD38C7 /* Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */ = {isa = PBXBuildFile; fileRef = C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */; }; - C8520301293C4D9000460097 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8520300293C4D9000460097 /* Helpers.swift */; }; - C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */; }; - C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861E6102994F6070056CB02 /* AppDelegate.swift */; }; - C861E6152994F6080056CB02 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C861E6142994F6080056CB02 /* Assets.xcassets */; }; - C861E61E2994F6150056CB02 /* Service in Frameworks */ = {isa = PBXBuildFile; productRef = C861E61D2994F6150056CB02 /* Service */; }; - C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861E61F2994F6390056CB02 /* ServiceDelegate.swift */; }; - C86612F82A06AF74009197D9 /* HostApp in Frameworks */ = {isa = PBXBuildFile; productRef = C86612F72A06AF74009197D9 /* HostApp */; }; - C8738B662BE4D4B900609E7F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B652BE4D4B900609E7F /* main.swift */; }; - C8738B6B2BE4D56F00609E7F /* ServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */; }; - C8738B6F2BE4F7A600609E7F /* XPCShared in Frameworks */ = {isa = PBXBuildFile; productRef = C8738B6E2BE4F7A600609E7F /* XPCShared */; }; - C8738B712BE4F8B700609E7F /* XPCController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B702BE4F8B700609E7F /* XPCController.swift */; }; - C8738B7B2BE5363800609E7F /* SandboxedClientTesterApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */; }; - C8738B7D2BE5363800609E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8738B7C2BE5363800609E7F /* ContentView.swift */; }; - C8738B7F2BE5363900609E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B7E2BE5363900609E7F /* Assets.xcassets */; }; - C8738B822BE5363900609E7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B812BE5363900609E7F /* Preview Assets.xcassets */; }; - C8738B882BE5365000609E7F /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C8738B872BE5365000609E7F /* Client */; }; - C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */ = {isa = PBXBuildFile; fileRef = C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */; }; - C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8738B632BE4D4B900609E7F /* CommunicationBridge */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */; }; - C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */; }; - C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */; }; - C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */; }; - C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */; }; - C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */; }; - C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; }; - C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C81458902939EFDC00135263 /* XcodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; }; - C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */; }; - C8C8B60929AFA35F00034BEE /* GitHub Copilot for Xcode Extension.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* GitHub Copilot for Xcode Extension.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */; }; - C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - C81291AF2994F92700196E12 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C8189B0E2938972F00C9DCDA /* Project object */; - proxyType = 1; - remoteGlobalIDString = C861E60D2994F6070056CB02; - remoteInfo = ExtensionService; - }; - C81458992939EFDC00135263 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C8189B0E2938972F00C9DCDA /* Project object */; - proxyType = 1; - remoteGlobalIDString = C814588B2939EFDC00135263; - remoteInfo = EditorExtension; - }; - C8216B7E2980377E00AD38C7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C8189B0E2938972F00C9DCDA /* Project object */; - proxyType = 1; - remoteGlobalIDString = C8216B6F298036EC00AD38C7; - remoteInfo = Helper; - }; - C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C8189B0E2938972F00C9DCDA /* Project object */; - proxyType = 1; - remoteGlobalIDString = C8738B622BE4D4B900609E7F; - remoteInfo = CommunicationBridge; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - C814589F2939EFDC00135263 /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - C814589B2939EFDC00135263 /* Copilot.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; - C8216B6E298036EC00AD38C7 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; - files = ( - ); - runOnlyForDeploymentPostprocessing = 1; - }; - C828B27E2B1F7B3C00E7612A /* Copy Extension Point */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(EXTENSIONS_FOLDER_PATH)"; - dstSubfolderSpec = 16; - files = ( - C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */, - ); - name = "Copy Extension Point"; - runOnlyForDeploymentPostprocessing = 0; - }; - C8520306293CF0EF00460097 /* Embed XPCService */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ../Applications; - dstSubfolderSpec = 6; - files = ( - C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */, - C8216B802980378300AD38C7 /* Helper in Embed XPCService */, - C8C8B60929AFA35F00034BEE /* GitHub Copilot for Xcode Extension.app in Embed XPCService */, - ); - name = "Embed XPCService"; - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B612BE4D4B900609E7F /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 12; - dstPath = ""; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C87B03AE293B2CF300C77EAE /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - C87B03AD293B2CF300C77EAE /* XcodeKit.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - C8C8B60829AFA32800034BEE /* Embed Service */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; - dstSubfolderSpec = 16; - files = ( - ); - name = "Embed Service"; - runOnlyForDeploymentPostprocessing = 0; - }; - C8F1032A2A7A38D200D28F4F /* Copy Launch Agent */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 12; - dstPath = Contents/Library/LaunchAgents; - dstSubfolderSpec = 1; - files = ( - C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */, - ); - name = "Copy Launch Agent"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 3ABBEA282C8B9FE100C61D61 /* copilot-language-server */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = "copilot-language-server"; path = "Server/node_modules/@github/copilot-language-server/native/darwin-x64/copilot-language-server"; sourceTree = SOURCE_ROOT; }; - 3ABBEA2A2C8BA00300C61D61 /* copilot-language-server-arm64 */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = "copilot-language-server-arm64"; path = "Server/node_modules/@github/copilot-language-server/native/darwin-arm64/copilot-language-server-arm64"; sourceTree = SOURCE_ROOT; }; - 3E5DB74F2D6B88EE00418952 /* ReleaseNotes.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ReleaseNotes.md; sourceTree = ""; }; - 424ACA202CA4697200FA20F2 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; - 427C63272C6E868B000E557C /* OpenSettingsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsCommand.swift; sourceTree = ""; }; - C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRealtimeSuggestionsCommand.swift; sourceTree = ""; }; - C8009C022941C576007AA7E8 /* SyncTextSettingsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTextSettingsCommand.swift; sourceTree = ""; }; - C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSuggestionsCommand.swift; sourceTree = ""; }; - C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptPromptToCodeCommand.swift; sourceTree = ""; }; - C81291D52994FE6900196E12 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; - C81291D92994FE7900196E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - C814588C2939EFDC00135263 /* Copilot.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Copilot.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - C814588E2939EFDC00135263 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; - C81458902939EFDC00135263 /* XcodeKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeKit.framework; path = Library/Frameworks/XcodeKit.framework; sourceTree = DEVELOPER_DIR; }; - C81458932939EFDC00135263 /* SourceEditorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceEditorExtension.swift; sourceTree = ""; }; - C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSuggestionsCommand.swift; sourceTree = ""; }; - C81458972939EFDC00135263 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C81458982939EFDC00135263 /* EditorExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EditorExtension.entitlements; sourceTree = ""; }; - C81458AD293A009600135263 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - C81458AE293A009800135263 /* Config.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.debug.xcconfig; sourceTree = ""; }; - C8189B162938972F00C9DCDA /* GitHub Copilot for Xcode Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "GitHub Copilot for Xcode Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - C8189B192938972F00C9DCDA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; - C8189B1D2938973000C9DCDA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - C8189B202938973000C9DCDA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Copilot_for_Xcode.entitlements; sourceTree = ""; }; - C8189B282938979000C9DCDA /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = ""; }; - C81D181E2A1B509B006C1B70 /* Tool */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Tool; sourceTree = ""; }; - C81E867D296FE4420026E908 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; - C8216B70298036EC00AD38C7 /* Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Helper; sourceTree = BUILT_PRODUCTS_DIR; }; - C8216B72298036EC00AD38C7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadLaunchAgent.swift; sourceTree = ""; }; - C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ExtensionPoint.appextensionpoint; sourceTree = ""; }; - C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; - C8520308293D805800460097 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptToCodeCommand.swift; sourceTree = ""; }; - C861E60E2994F6070056CB02 /* GitHub Copilot for Xcode Extension.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "GitHub Copilot for Xcode Extension.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - C861E6102994F6070056CB02 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C861E6142994F6080056CB02 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - C861E6192994F6080056CB02 /* ExtensionService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ExtensionService.entitlements; sourceTree = ""; }; - C861E61F2994F6390056CB02 /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = ""; }; - C8738B632BE4D4B900609E7F /* CommunicationBridge */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = CommunicationBridge; sourceTree = BUILT_PRODUCTS_DIR; }; - C8738B652BE4D4B900609E7F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = ""; }; - C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bridgeLaunchAgent.plist; sourceTree = ""; }; - C8738B702BE4F8B700609E7F /* XPCController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCController.swift; sourceTree = ""; }; - C8738B782BE5363800609E7F /* SandboxedClientTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SandboxedClientTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxedClientTesterApp.swift; sourceTree = ""; }; - C8738B7C2BE5363800609E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - C8738B7E2BE5363900609E7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - C8738B812BE5363900609E7F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - C8738B832BE5363900609E7F /* SandboxedClientTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SandboxedClientTester.entitlements; sourceTree = ""; }; - C8738B892BE5379E00609E7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommand.swift; sourceTree = ""; }; - C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorCommand.swift; sourceTree = ""; }; - C87B03A3293B24AB00C77EAE /* Copilot-for-Xcode-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Copilot-for-Xcode-Info.plist"; sourceTree = SOURCE_ROOT; }; - C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptSuggestionCommand.swift; sourceTree = ""; }; - C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RejectSuggestionCommand.swift; sourceTree = ""; }; - C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextSuggestionCommand.swift; sourceTree = ""; }; - C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviousSuggestionCommand.swift; sourceTree = ""; }; - C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = ""; }; - C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; }; - C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; - C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChat.swift; sourceTree = ""; }; - C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseIdleTabsCommand.swift; sourceTree = ""; }; - C8F103292A7A365000D28F4F /* launchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchAgent.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 9E6A029A2DBDF64200AB6BD5 /* Server */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Server; sourceTree = SOURCE_ROOT; }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - C81458892939EFDC00135263 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C87B03AC293B2CF300C77EAE /* XcodeKit.framework in Frameworks */, - C814588F2939EFDC00135263 /* Cocoa.framework in Frameworks */, - C882175C294187EF00A22FD3 /* Client in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8189B132938972F00C9DCDA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C86612F82A06AF74009197D9 /* HostApp in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8216B6D298036EC00AD38C7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C8216B7D2980374300AD38C7 /* ArgumentParser in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C861E60B2994F6070056CB02 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C861E61E2994F6150056CB02 /* Service in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B602BE4D4B900609E7F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C8738B6F2BE4F7A600609E7F /* XPCShared in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B752BE5363800609E7F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C8738B882BE5365000609E7F /* Client in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - C814588D2939EFDC00135263 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C814588E2939EFDC00135263 /* Cocoa.framework */, - C81458902939EFDC00135263 /* XcodeKit.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - C81458922939EFDC00135263 /* EditorExtension */ = { - isa = PBXGroup; - children = ( - C81458932939EFDC00135263 /* SourceEditorExtension.swift */, - C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */, - C8520300293C4D9000460097 /* Helpers.swift */, - C81458952939EFDC00135263 /* GetSuggestionsCommand.swift */, - C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */, - C80FFB952A95F58200704A25 /* AcceptPromptToCodeCommand.swift */, - C87B03A6293B261900C77EAE /* RejectSuggestionCommand.swift */, - C87B03A8293B262600C77EAE /* NextSuggestionCommand.swift */, - C87B03AA293B262E00C77EAE /* PreviousSuggestionCommand.swift */, - C8009BFE2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift */, - C8009C022941C576007AA7E8 /* SyncTextSettingsCommand.swift */, - C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */, - C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */, - C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */, - C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */, - C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */, - C81458972939EFDC00135263 /* Info.plist */, - C81458982939EFDC00135263 /* EditorExtension.entitlements */, - 427C63272C6E868B000E557C /* OpenSettingsCommand.swift */, - ); - path = EditorExtension; - sourceTree = ""; - }; - C8189B0D2938972F00C9DCDA = { - isa = PBXGroup; - children = ( - 3E5DB74F2D6B88EE00418952 /* ReleaseNotes.md */, - C887BC832965D96000931567 /* DEVELOPMENT.md */, - C8520308293D805800460097 /* README.md */, - C8F103292A7A365000D28F4F /* launchAgent.plist */, - C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */, - C81E867D296FE4420026E908 /* Version.xcconfig */, - C81458AD293A009600135263 /* Config.xcconfig */, - C81458AE293A009800135263 /* Config.debug.xcconfig */, - C8CD828229B88006008D044D /* TestPlan.xctestplan */, - C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */, - 9E6A029A2DBDF64200AB6BD5 /* Server */, - C81D181E2A1B509B006C1B70 /* Tool */, - C8189B282938979000C9DCDA /* Core */, - C8189B182938972F00C9DCDA /* Copilot for Xcode */, - C81458922939EFDC00135263 /* EditorExtension */, - C8216B71298036EC00AD38C7 /* Helper */, - C861E60F2994F6070056CB02 /* ExtensionService */, - C8738B642BE4D4B900609E7F /* CommunicationBridge */, - C8738B792BE5363800609E7F /* SandboxedClientTester */, - C814588D2939EFDC00135263 /* Frameworks */, - C8189B172938972F00C9DCDA /* Products */, - ); - sourceTree = ""; - }; - C8189B172938972F00C9DCDA /* Products */ = { - isa = PBXGroup; - children = ( - C8189B162938972F00C9DCDA /* GitHub Copilot for Xcode Dev.app */, - C814588C2939EFDC00135263 /* Copilot.appex */, - C8216B70298036EC00AD38C7 /* Helper */, - C861E60E2994F6070056CB02 /* GitHub Copilot for Xcode Extension.app */, - C8738B632BE4D4B900609E7F /* CommunicationBridge */, - C8738B782BE5363800609E7F /* SandboxedClientTester.app */, - ); - name = Products; - sourceTree = ""; - }; - C8189B182938972F00C9DCDA /* Copilot for Xcode */ = { - isa = PBXGroup; - children = ( - 424ACA202CA4697200FA20F2 /* Credits.rtf */, - 3ABBEA2A2C8BA00300C61D61 /* copilot-language-server-arm64 */, - 3ABBEA282C8B9FE100C61D61 /* copilot-language-server */, - C87B03A3293B24AB00C77EAE /* Copilot-for-Xcode-Info.plist */, - C8189B192938972F00C9DCDA /* App.swift */, - C8189B1D2938973000C9DCDA /* Assets.xcassets */, - C8189B222938973000C9DCDA /* Copilot_for_Xcode.entitlements */, - C8189B1F2938973000C9DCDA /* Preview Content */, - ); - path = "Copilot for Xcode"; - sourceTree = ""; - }; - C8189B1F2938973000C9DCDA /* Preview Content */ = { - isa = PBXGroup; - children = ( - C8189B202938973000C9DCDA /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - C8216B71298036EC00AD38C7 /* Helper */ = { - isa = PBXGroup; - children = ( - C8216B72298036EC00AD38C7 /* main.swift */, - C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */, - ); - path = Helper; - sourceTree = ""; - }; - C861E60F2994F6070056CB02 /* ExtensionService */ = { - isa = PBXGroup; - children = ( - C81291D92994FE7900196E12 /* Info.plist */, - C861E61F2994F6390056CB02 /* ServiceDelegate.swift */, - C861E6102994F6070056CB02 /* AppDelegate.swift */, - C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */, - C8738B702BE4F8B700609E7F /* XPCController.swift */, - C81291D52994FE6900196E12 /* Main.storyboard */, - C861E6142994F6080056CB02 /* Assets.xcassets */, - C861E6192994F6080056CB02 /* ExtensionService.entitlements */, - ); - path = ExtensionService; - sourceTree = ""; - }; - C8738B642BE4D4B900609E7F /* CommunicationBridge */ = { - isa = PBXGroup; - children = ( - C8738B652BE4D4B900609E7F /* main.swift */, - C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */, - ); - path = CommunicationBridge; - sourceTree = ""; - }; - C8738B792BE5363800609E7F /* SandboxedClientTester */ = { - isa = PBXGroup; - children = ( - C8738B892BE5379E00609E7F /* Info.plist */, - C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */, - C8738B7C2BE5363800609E7F /* ContentView.swift */, - C8738B7E2BE5363900609E7F /* Assets.xcassets */, - C8738B832BE5363900609E7F /* SandboxedClientTester.entitlements */, - C8738B802BE5363900609E7F /* Preview Content */, - ); - path = SandboxedClientTester; - sourceTree = ""; - }; - C8738B802BE5363900609E7F /* Preview Content */ = { - isa = PBXGroup; - children = ( - C8738B812BE5363900609E7F /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - C814588B2939EFDC00135263 /* EditorExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = C814589C2939EFDC00135263 /* Build configuration list for PBXNativeTarget "EditorExtension" */; - buildPhases = ( - C81458882939EFDC00135263 /* Sources */, - C81458892939EFDC00135263 /* Frameworks */, - C814588A2939EFDC00135263 /* Resources */, - C87B03AE293B2CF300C77EAE /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = EditorExtension; - packageProductDependencies = ( - C882175B294187EF00A22FD3 /* Client */, - ); - productName = EditorExtension; - productReference = C814588C2939EFDC00135263 /* Copilot.appex */; - productType = "com.apple.product-type.xcode-extension"; - }; - C8189B152938972F00C9DCDA /* Copilot for Xcode */ = { - isa = PBXNativeTarget; - buildConfigurationList = C8189B252938973000C9DCDA /* Build configuration list for PBXNativeTarget "Copilot for Xcode" */; - buildPhases = ( - C8189B122938972F00C9DCDA /* Sources */, - C8189B132938972F00C9DCDA /* Frameworks */, - C8189B142938972F00C9DCDA /* Resources */, - C814589F2939EFDC00135263 /* Embed Foundation Extensions */, - C8520306293CF0EF00460097 /* Embed XPCService */, - C8C8B60829AFA32800034BEE /* Embed Service */, - C8F1032A2A7A38D200D28F4F /* Copy Launch Agent */, - ); - buildRules = ( - ); - dependencies = ( - C8738B8D2BE540F900609E7F /* PBXTargetDependency */, - C81291B02994F92700196E12 /* PBXTargetDependency */, - C8216B7F2980377E00AD38C7 /* PBXTargetDependency */, - C814589A2939EFDC00135263 /* PBXTargetDependency */, - ); - name = "Copilot for Xcode"; - packageProductDependencies = ( - C86612F72A06AF74009197D9 /* HostApp */, - ); - productName = "Copilot for Xcode"; - productReference = C8189B162938972F00C9DCDA /* GitHub Copilot for Xcode Dev.app */; - productType = "com.apple.product-type.application"; - }; - C8216B6F298036EC00AD38C7 /* Helper */ = { - isa = PBXNativeTarget; - buildConfigurationList = C8216B74298036EC00AD38C7 /* Build configuration list for PBXNativeTarget "Helper" */; - buildPhases = ( - C8216B6C298036EC00AD38C7 /* Sources */, - C8216B6D298036EC00AD38C7 /* Frameworks */, - C8216B6E298036EC00AD38C7 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Helper; - packageProductDependencies = ( - C8216B7C2980374300AD38C7 /* ArgumentParser */, - ); - productName = Helper; - productReference = C8216B70298036EC00AD38C7 /* Helper */; - productType = "com.apple.product-type.tool"; - }; - C861E60D2994F6070056CB02 /* ExtensionService */ = { - isa = PBXNativeTarget; - buildConfigurationList = C861E61A2994F6080056CB02 /* Build configuration list for PBXNativeTarget "ExtensionService" */; - buildPhases = ( - C861E60A2994F6070056CB02 /* Sources */, - C861E60B2994F6070056CB02 /* Frameworks */, - 3A60421A2C8955710006B34C /* ShellScript */, - C861E60C2994F6070056CB02 /* Resources */, - C828B27E2B1F7B3C00E7612A /* Copy Extension Point */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = ExtensionService; - packageProductDependencies = ( - C861E61D2994F6150056CB02 /* Service */, - ); - productName = ExtensionService; - productReference = C861E60E2994F6070056CB02 /* GitHub Copilot for Xcode Extension.app */; - productType = "com.apple.product-type.application"; - }; - C8738B622BE4D4B900609E7F /* CommunicationBridge */ = { - isa = PBXNativeTarget; - buildConfigurationList = C8738B672BE4D4B900609E7F /* Build configuration list for PBXNativeTarget "CommunicationBridge" */; - buildPhases = ( - C8738B5F2BE4D4B900609E7F /* Sources */, - C8738B602BE4D4B900609E7F /* Frameworks */, - C8738B612BE4D4B900609E7F /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CommunicationBridge; - packageProductDependencies = ( - C8738B6E2BE4F7A600609E7F /* XPCShared */, - ); - productName = CommunicationBridge; - productReference = C8738B632BE4D4B900609E7F /* CommunicationBridge */; - productType = "com.apple.product-type.tool"; - }; - C8738B772BE5363800609E7F /* SandboxedClientTester */ = { - isa = PBXNativeTarget; - buildConfigurationList = C8738B842BE5363900609E7F /* Build configuration list for PBXNativeTarget "SandboxedClientTester" */; - buildPhases = ( - C8738B742BE5363800609E7F /* Sources */, - C8738B752BE5363800609E7F /* Frameworks */, - C8738B762BE5363800609E7F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SandboxedClientTester; - packageProductDependencies = ( - C8738B872BE5365000609E7F /* Client */, - ); - productName = SandboxedClientTester; - productReference = C8738B782BE5363800609E7F /* SandboxedClientTester.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - C8189B0E2938972F00C9DCDA /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1520; - LastUpgradeCheck = 1410; - TargetAttributes = { - C814588B2939EFDC00135263 = { - CreatedOnToolsVersion = 14.1; - }; - C8189B152938972F00C9DCDA = { - CreatedOnToolsVersion = 14.1; - }; - C8216B6F298036EC00AD38C7 = { - CreatedOnToolsVersion = 14.1; - }; - C861E60D2994F6070056CB02 = { - CreatedOnToolsVersion = 14.2; - }; - C8738B622BE4D4B900609E7F = { - CreatedOnToolsVersion = 15.2; - }; - C8738B772BE5363800609E7F = { - CreatedOnToolsVersion = 15.2; - }; - }; - }; - buildConfigurationList = C8189B112938972F00C9DCDA /* Build configuration list for PBXProject "Copilot for Xcode" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = C8189B0D2938972F00C9DCDA; - packageReferences = ( - C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, - ); - productRefGroup = C8189B172938972F00C9DCDA /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - C8189B152938972F00C9DCDA /* Copilot for Xcode */, - C814588B2939EFDC00135263 /* EditorExtension */, - C8216B6F298036EC00AD38C7 /* Helper */, - C861E60D2994F6070056CB02 /* ExtensionService */, - C8738B622BE4D4B900609E7F /* CommunicationBridge */, - C8738B772BE5363800609E7F /* SandboxedClientTester */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - C814588A2939EFDC00135263 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5EC511E52C90CFD600632BAB /* Assets.xcassets in Resources */, - 5EC511E32C90CE7400632BAB /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8189B142938972F00C9DCDA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 424ACA212CA4697200FA20F2 /* Credits.rtf in Resources */, - C8189B212938973000C9DCDA /* Preview Assets.xcassets in Resources */, - C8189B1E2938973000C9DCDA /* Assets.xcassets in Resources */, - 3ABBEA292C8B9FE100C61D61 /* copilot-language-server in Resources */, - 3ABBEA2B2C8BA00300C61D61 /* copilot-language-server-arm64 in Resources */, - 5EC511E62C90CFD700632BAB /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C861E60C2994F6070056CB02 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3ABBEA2C2C8BA00800C61D61 /* copilot-language-server-arm64 in Resources */, - C861E6152994F6080056CB02 /* Assets.xcassets in Resources */, - 3ABBEA2D2C8BA00B00C61D61 /* copilot-language-server in Resources */, - C81291D72994FE6900196E12 /* Main.storyboard in Resources */, - 3E5DB7502D6B8FA500418952 /* ReleaseNotes.md in Resources */, - 5EC511E42C90CE9800632BAB /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B762BE5363800609E7F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8738B822BE5363900609E7F /* Preview Assets.xcassets in Resources */, - C8738B7F2BE5363900609E7F /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3A60421A2C8955710006B34C /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export PATH=/usr/local/bin:/opt/homebrew/bin:$PATH\n\nnpm -C Server install\ncp Server/node_modules/@github/copilot-language-server/native/darwin-arm64/copilot-language-server Server/node_modules/@github/copilot-language-server/native/darwin-arm64/copilot-language-server-arm64\n\necho \"Build and copy webview js/html files as the bundle resources\"\nnpm -C Server run build\nmkdir -p \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/webViewDist\"\ncp -R Server/dist/* \"${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/webViewDist/\"\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - C81458882939EFDC00135263 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */, - C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */, - C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */, - C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */, - C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */, - C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */, - C8520301293C4D9000460097 /* Helpers.swift in Sources */, - C8009BFF2941C551007AA7E8 /* ToggleRealtimeSuggestionsCommand.swift in Sources */, - C80FFB962A95F58200704A25 /* AcceptPromptToCodeCommand.swift in Sources */, - 427C63282C6E868B000E557C /* OpenSettingsCommand.swift in Sources */, - C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */, - C87B03A9293B262600C77EAE /* NextSuggestionCommand.swift in Sources */, - C87B03AB293B262E00C77EAE /* PreviousSuggestionCommand.swift in Sources */, - C87B03A7293B261900C77EAE /* RejectSuggestionCommand.swift in Sources */, - C8009C032941C576007AA7E8 /* SyncTextSettingsCommand.swift in Sources */, - C800DBB1294C624D00B04CAC /* PrefetchSuggestionsCommand.swift in Sources */, - C81458962939EFDC00135263 /* GetSuggestionsCommand.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8189B122938972F00C9DCDA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8189B1A2938972F00C9DCDA /* App.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8216B6C298036EC00AD38C7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8216B73298036EC00AD38C7 /* main.swift in Sources */, - C8216B782980370100AD38C7 /* ReloadLaunchAgent.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C861E60A2994F6070056CB02 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */, - C8738B712BE4F8B700609E7F /* XPCController.swift in Sources */, - C861E6202994F63A0056CB02 /* ServiceDelegate.swift in Sources */, - C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B5F2BE4D4B900609E7F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8738B6B2BE4D56F00609E7F /* ServiceDelegate.swift in Sources */, - C8738B662BE4D4B900609E7F /* main.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C8738B742BE5363800609E7F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C8738B7D2BE5363800609E7F /* ContentView.swift in Sources */, - C8738B7B2BE5363800609E7F /* SandboxedClientTesterApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - C81291B02994F92700196E12 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C861E60D2994F6070056CB02 /* ExtensionService */; - targetProxy = C81291AF2994F92700196E12 /* PBXContainerItemProxy */; - }; - C814589A2939EFDC00135263 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C814588B2939EFDC00135263 /* EditorExtension */; - targetProxy = C81458992939EFDC00135263 /* PBXContainerItemProxy */; - }; - C8216B7F2980377E00AD38C7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C8216B6F298036EC00AD38C7 /* Helper */; - targetProxy = C8216B7E2980377E00AD38C7 /* PBXContainerItemProxy */; - }; - C8738B8D2BE540F900609E7F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C8738B622BE4D4B900609E7F /* CommunicationBridge */; - targetProxy = C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - C814589D2939EFDC00135263 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = EditorExtension/EditorExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = EditorExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "$(EXTESNION_BUNDLE_NAME)"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).EditorExtension"; - PRODUCT_NAME = Copilot; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C814589E2939EFDC00135263 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = EditorExtension/EditorExtension.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - INFOPLIST_FILE = EditorExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "$(EXTESNION_BUNDLE_NAME)"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).EditorExtension"; - PRODUCT_NAME = Copilot; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - C8189B232938973000C9DCDA /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C81458AE293A009800135263 /* Config.debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - C8189B242938973000C9DCDA /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C81458AD293A009600135263 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - C8189B262938973000C9DCDA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "Copilot for Xcode/Copilot_for_Xcode.entitlements"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_ASSET_PATHS = "\"Copilot for Xcode/Preview Content\""; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE)"; - PRODUCT_MODULE_NAME = Copilot_for_Xcode; - PRODUCT_NAME = "$(HOST_APP_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C8189B272938973000C9DCDA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = "Copilot for Xcode/Copilot_for_Xcode.entitlements"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_ASSET_PATHS = "\"Copilot for Xcode/Preview Content\""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE)"; - PRODUCT_NAME = "$(HOST_APP_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - C8216B75298036EC00AD38C7 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C8216B76298036EC00AD38C7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - C861E61B2994F6080056CB02 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = ExtensionService/ExtensionService.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ExtensionService/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INFOPLIST_KEY_NSMainStoryboardFile = Main; - INFOPLIST_KEY_NSPrincipalClass = NSApplication; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).ExtensionService"; - PRODUCT_NAME = "$(EXTENSION_SERVICE_NAME)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C861E61C2994F6080056CB02 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = ExtensionService/ExtensionService.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ExtensionService/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INFOPLIST_KEY_NSMainStoryboardFile = Main; - INFOPLIST_KEY_NSPrincipalClass = NSApplication; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = "$(APP_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).ExtensionService"; - PRODUCT_NAME = "$(EXTENSION_SERVICE_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - C8738B682BE4D4B900609E7F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C8738B692BE4D4B900609E7F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - C8738B852BE5363900609E7F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = SandboxedClientTester/SandboxedClientTester.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"SandboxedClientTester/Preview Content\""; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = SandboxedClientTester/Info.plist; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.CopilotForXcode.SandboxedClientTester; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - C8738B862BE5363900609E7F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = SandboxedClientTester/SandboxedClientTester.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"SandboxedClientTester/Preview Content\""; - DEVELOPMENT_TEAM = VEKTX9H2N7; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = SandboxedClientTester/Info.plist; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.github.CopilotForXcode.SandboxedClientTester; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - C814589C2939EFDC00135263 /* Build configuration list for PBXNativeTarget "EditorExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C814589D2939EFDC00135263 /* Debug */, - C814589E2939EFDC00135263 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C8189B112938972F00C9DCDA /* Build configuration list for PBXProject "Copilot for Xcode" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C8189B232938973000C9DCDA /* Debug */, - C8189B242938973000C9DCDA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C8189B252938973000C9DCDA /* Build configuration list for PBXNativeTarget "Copilot for Xcode" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C8189B262938973000C9DCDA /* Debug */, - C8189B272938973000C9DCDA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C8216B74298036EC00AD38C7 /* Build configuration list for PBXNativeTarget "Helper" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C8216B75298036EC00AD38C7 /* Debug */, - C8216B76298036EC00AD38C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C861E61A2994F6080056CB02 /* Build configuration list for PBXNativeTarget "ExtensionService" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C861E61B2994F6080056CB02 /* Debug */, - C861E61C2994F6080056CB02 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C8738B672BE4D4B900609E7F /* Build configuration list for PBXNativeTarget "CommunicationBridge" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C8738B682BE4D4B900609E7F /* Debug */, - C8738B692BE4D4B900609E7F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C8738B842BE5363900609E7F /* Build configuration list for PBXNativeTarget "SandboxedClientTester" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C8738B852BE5363900609E7F /* Debug */, - C8738B862BE5363900609E7F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-argument-parser.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - C8216B7C2980374300AD38C7 /* ArgumentParser */ = { - isa = XCSwiftPackageProductDependency; - package = C8216B792980373800AD38C7 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; - productName = ArgumentParser; - }; - C861E61D2994F6150056CB02 /* Service */ = { - isa = XCSwiftPackageProductDependency; - productName = Service; - }; - C86612F72A06AF74009197D9 /* HostApp */ = { - isa = XCSwiftPackageProductDependency; - productName = HostApp; - }; - C8738B6E2BE4F7A600609E7F /* XPCShared */ = { - isa = XCSwiftPackageProductDependency; - productName = XPCShared; - }; - C8738B872BE5365000609E7F /* Client */ = { - isa = XCSwiftPackageProductDependency; - productName = Client; - }; - C882175B294187EF00A22FD3 /* Client */ = { - isa = XCSwiftPackageProductDependency; - productName = Client; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = C8189B0E2938972F00C9DCDA /* Project object */; -} diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Copilot for Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/CommunicationBridge.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/CommunicationBridge.xcscheme deleted file mode 100644 index 578b11ea..00000000 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/CommunicationBridge.xcscheme +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/Copilot for Xcode.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/Copilot for Xcode.xcscheme deleted file mode 100644 index e26142f3..00000000 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/Copilot for Xcode.xcscheme +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/EditorExtension.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/EditorExtension.xcscheme deleted file mode 100644 index 4844b100..00000000 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/EditorExtension.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme deleted file mode 100644 index f672cd16..00000000 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/ExtensionService.xcscheme +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/SandboxedClientTester.xcscheme b/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/SandboxedClientTester.xcscheme deleted file mode 100644 index 41fadd0b..00000000 --- a/Copilot for Xcode.xcodeproj/xcshareddata/xcschemes/SandboxedClientTester.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode.xcworkspace/contents.xcworkspacedata b/Copilot for Xcode.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7fb7f274..00000000 --- a/Copilot for Xcode.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/IDETemplateMacros.plist b/Copilot for Xcode.xcworkspace/xcshareddata/IDETemplateMacros.plist deleted file mode 100644 index ea58bd0f..00000000 --- a/Copilot for Xcode.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - FILEHEADER - TODO: Remove this comment - - diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Copilot for Xcode.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Copilot for Xcode.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3db257ec..00000000 --- a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,320 +0,0 @@ -{ - "pins" : [ - { - "identity" : "cgeventoverride", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devm33/CGEventOverride", - "state" : { - "branch" : "devm33/fix-stale-AXIsProcessTrusted", - "revision" : "06a9bf1f8f8d47cca221344101cc0274f04cc513" - } - }, - { - "identity" : "codablewrappers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GottaGetSwifty/CodableWrappers", - "state" : { - "revision" : "4eb46a4c656333e8514db8aad204445741de7d40", - "version" : "2.0.7" - } - }, - { - "identity" : "combine-schedulers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/combine-schedulers", - "state" : { - "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", - "version" : "1.0.0" - } - }, - { - "identity" : "copilotforxcodekit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devm33/CopilotForXcodeKit", - "state" : { - "branch" : "main", - "revision" : "1f98fe9795766d3e37b5ae3d2e5f69f9b0af308b" - } - }, - { - "identity" : "fseventswrapper", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Frizlab/FSEventsWrapper", - "state" : { - "revision" : "70bbea4b108221fcabfce8dbced8502831c0ae04", - "version" : "2.1.0" - } - }, - { - "identity" : "highlightr", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devm33/Highlightr", - "state" : { - "branch" : "master", - "revision" : "81d8c8b3733939bf5d9e52cd6318f944cc033bd2" - } - }, - { - "identity" : "jsonrpc", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/JSONRPC", - "state" : { - "revision" : "c6ec759d41a76ac88fe7327c41a77d9033943374", - "version" : "0.9.0" - } - }, - { - "identity" : "keyboardshortcuts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devm33/KeyboardShortcuts", - "state" : { - "branch" : "main", - "revision" : "65fb410b0c6d3ed96623b460bab31ffce5f48b4d" - } - }, - { - "identity" : "languageclient", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageClient", - "state" : { - "revision" : "4f28cc3cad7512470275f65ca2048359553a86f5", - "version" : "0.8.2" - } - }, - { - "identity" : "languageserverprotocol", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", - "state" : { - "revision" : "d51412945ae88ffcab65ec339ca89aed9c9f0b8a", - "version" : "0.13.3" - } - }, - { - "identity" : "networkimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/NetworkImage", - "state" : { - "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", - "version" : "6.0.0" - } - }, - { - "identity" : "processenv", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/ProcessEnv", - "state" : { - "revision" : "552f611479a4f28243a1ef2a7376a216d6899f42", - "version" : "1.0.1" - } - }, - { - "identity" : "queue", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattmassicotte/Queue", - "state" : { - "revision" : "9f941ae35f146ccadd2689b9ab8d5aebb1f5d584", - "version" : "0.2.1" - } - }, - { - "identity" : "semaphore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/Semaphore", - "state" : { - "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", - "version" : "0.1.0" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle", - "state" : { - "revision" : "0ef1ee0220239b3776f433314515fd849025673f", - "version" : "2.6.4" - } - }, - { - "identity" : "sqlite.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stephencelis/SQLite.swift.git", - "state" : { - "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", - "version" : "0.15.3" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", - "state" : { - "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", - "version" : "1.3.3" - } - }, - { - "identity" : "swift-clocks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-clocks", - "state" : { - "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-composable-architecture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-composable-architecture", - "state" : { - "revision" : "433a23118f739078644ebeb4009e23d307af694a", - "version" : "1.10.4" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "f01efb26f3a192a0e88dcdb7c3c391ec2fc25d9c", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-dependencies", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-dependencies", - "state" : { - "revision" : "350e1e119babe8525f9bd155b76640a5de270184", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-glob", - "kind" : "remoteSourceControl", - "location" : "https://github.com/davbeck/swift-glob", - "state" : { - "revision" : "07ba6f47d903a0b1b59f12ca70d6de9949b975d6", - "version" : "0.2.0" - } - }, - { - "identity" : "swift-identified-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-identified-collections", - "state" : { - "revision" : "d533cd18b0b456b106694a9899f917ee595f2666", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swift-markdown-ui", - "state" : { - "revision" : "55441810c0f678c78ed7e2ebd46dde89228e02fc", - "version" : "2.4.0" - } - }, - { - "identity" : "swift-parsing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-parsing", - "state" : { - "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", - "version" : "0.13.0" - } - }, - { - "identity" : "swift-perception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", - "state" : { - "revision" : "64f7f6c28c6a4d3c4b9da2ba02383e29ab48a8cf", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", - "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" - } - }, - { - "identity" : "swiftsoup", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scinfu/SwiftSoup.git", - "state" : { - "revision" : "dee225a3da7b68d34936abc4dc8f34f2264db647", - "version" : "2.9.6" - } - }, - { - "identity" : "swiftui-flow-layout", - "kind" : "remoteSourceControl", - "location" : "https://github.com/globulus/swiftui-flow-layout", - "state" : { - "revision" : "de7da3440c3b87ba94adfa98c698828d7746a76d", - "version" : "1.0.5" - } - }, - { - "identity" : "swiftui-navigation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", - "state" : { - "revision" : "7ab04c6e2e6a73d34d5a762970ef88bf0aedb084", - "version" : "1.4.0" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version" : "1.1.2" - } - } - ], - "version" : 2 -} diff --git a/Copilot for Xcode/App.swift b/Copilot for Xcode/App.swift deleted file mode 100644 index d8ed3cdd..00000000 --- a/Copilot for Xcode/App.swift +++ /dev/null @@ -1,223 +0,0 @@ -import SwiftUI -import Client -import HostApp -import LaunchAgentManager -import SharedUIComponents -import UpdateChecker -import XPCShared -import HostAppActivator -import ComposableArchitecture - -struct VisualEffect: NSViewRepresentable { - func makeNSView(context: Self.Context) -> NSView { return NSVisualEffectView() } - func updateNSView(_ nsView: NSView, context: Context) { } -} - -class AppDelegate: NSObject, NSApplicationDelegate { - private var permissionAlertShown = false - - // Launch modes supported by the app - enum LaunchMode { - case chat - case settings - case mcp - } - - func applicationDidFinishLaunching(_ notification: Notification) { - if #available(macOS 13.0, *) { - checkBackgroundPermissions() - } - - let launchMode = determineLaunchMode() - handleLaunchMode(launchMode) - } - - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - if #available(macOS 13.0, *) { - checkBackgroundPermissions() - } - - let launchMode = determineLaunchMode() - handleLaunchMode(launchMode) - return true - } - - // MARK: - Helper Methods - - private func determineLaunchMode() -> LaunchMode { - let launchArgs = CommandLine.arguments - if launchArgs.contains("--settings") { - return .settings - } else if launchArgs.contains("--mcp") { - return .mcp - } else { - return .chat - } - } - - private func handleLaunchMode(_ mode: LaunchMode) { - switch mode { - case .settings: - openSettings() - case .mcp: - openMCPSettings() - case .chat: - openChat() - } - } - - private func openSettings() { - DispatchQueue.main.async { - activateAndOpenSettings() - } - } - - private func openChat() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - Task { - let service = try? getService() - try? await service?.openChat() - } - } - } - - private func openMCPSettings() { - DispatchQueue.main.async { - activateAndOpenSettings() - hostAppStore.send(.setActiveTab(2)) - } - } - - @available(macOS 13.0, *) - private func checkBackgroundPermissions() { - Task { - // Direct check of permission status - let launchAgentManager = LaunchAgentManager() - let isPermissionGranted = await launchAgentManager.isBackgroundPermissionGranted() - - if !isPermissionGranted { - // Only show alert if permission isn't granted - DispatchQueue.main.async { - if !self.permissionAlertShown { - showBackgroundPermissionAlert() - self.permissionAlertShown = true - } - } - } else { - // Permission is granted, reset flag - self.permissionAlertShown = false - } - } - } - - // MARK: - Application Termination - - func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - // Immediately terminate extension service if it's running - if let extensionService = NSWorkspace.shared.runningApplications.first(where: { - $0.bundleIdentifier == "\(Bundle.main.bundleIdentifier!).ExtensionService" - }) { - extensionService.terminate() - } - - // Start cleanup in background without waiting - Task { - let quitTask = Task { - let service = try? getService() - try? await service?.quitService() - } - - // Wait just a tiny bit to allow cleanup to start - try? await Task.sleep(nanoseconds: 100_000_000) // 100ms - - DispatchQueue.main.async { - NSApp.reply(toApplicationShouldTerminate: true) - } - } - - return .terminateLater - } - - func applicationWillTerminate(_ notification: Notification) { - if let extensionService = NSWorkspace.shared.runningApplications.first(where: { - $0.bundleIdentifier == "\(Bundle.main.bundleIdentifier!).ExtensionService" - }) { - extensionService.terminate() - } - } -} - -class AppUpdateCheckerDelegate: UpdateCheckerDelegate { - func prepareForRelaunch(finish: @escaping () -> Void) { - Task { - let service = try? getService() - try? await service?.quitService() - finish() - } - } -} - -@main -struct CopilotForXcodeApp: App { - @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate - - init() { - UserDefaults.setupDefaultSettings() - - Task { - await hostAppStore - .send(.general(.setupLaunchAgentIfNeeded)) - .finish() - } - - DistributedNotificationCenter.default().addObserver( - forName: .openSettingsWindowRequest, - object: nil, - queue: .main - ) { _ in - DispatchQueue.main.async { - activateAndOpenSettings() - } - } - - DistributedNotificationCenter.default().addObserver( - forName: .openMCPSettingsWindowRequest, - object: nil, - queue: .main - ) { _ in - DispatchQueue.main.async { - activateAndOpenSettings() - hostAppStore.send(.setActiveTab(2)) - } - } - } - - var body: some Scene { - WithPerceptionTracking { - Settings { - TabContainer() - .frame(minWidth: 800, minHeight: 600) - .background(VisualEffect().ignoresSafeArea()) - .environment(\.updateChecker, UpdateChecker( - hostBundle: Bundle.main, - checkerDelegate: AppUpdateCheckerDelegate() - )) - } - } - } -} - -@MainActor -func activateAndOpenSettings() { - NSApp.activate(ignoringOtherApps: true) - if #available(macOS 14.0, *) { - let environment = SettingsEnvironment() - environment.open() - } else if #available(macOS 13.0, *) { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } else { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } -} - -var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } diff --git a/Copilot for Xcode/Assets.xcassets/AccentColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/Copilot for Xcode/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/Contents.json b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 0a30d46d..00000000 --- a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "filename" : "CopilotforXcode-Icon@16w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "CopilotforXcode-Icon@16w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "CopilotforXcode-Icon@32w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "CopilotforXcode-Icon@32w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "CopilotforXcode-Icon@128w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "CopilotforXcode-Icon@128w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "CopilotforXcode-Icon@256w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "CopilotforXcode-Icon@256w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "CopilotforXcode-Icon@512w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "CopilotforXcode-Icon@512w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png deleted file mode 100644 index 3ee52427..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png deleted file mode 100644 index 88b20d1d..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png deleted file mode 100644 index 2bb554dc..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png deleted file mode 100644 index ce02bac7..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png deleted file mode 100644 index 7674f663..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png deleted file mode 100644 index fc705969..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png deleted file mode 100644 index ce02bac7..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png deleted file mode 100644 index 4d52c81b..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png deleted file mode 100644 index fc705969..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png b/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png deleted file mode 100644 index 54da6e3f..00000000 Binary files a/Copilot for Xcode/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png and /dev/null differ diff --git a/Copilot for Xcode/Assets.xcassets/BackgroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/BackgroundColor.colorset/Contents.json deleted file mode 100644 index 37eb3c31..00000000 --- a/Copilot for Xcode/Assets.xcassets/BackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "40", - "green" : "23", - "red" : "25" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/BackgroundColorTop.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/BackgroundColorTop.colorset/Contents.json deleted file mode 100644 index 279761e1..00000000 --- a/Copilot for Xcode/Assets.xcassets/BackgroundColorTop.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "54", - "green" : "25", - "red" : "30" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorDefault.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorDefault.colorset/Contents.json deleted file mode 100644 index 3ece9084..00000000 --- a/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorDefault.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x46", - "green" : "0x24", - "red" : "0x2C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorPressed.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorPressed.colorset/Contents.json deleted file mode 100644 index 4754b656..00000000 --- a/Copilot for Xcode/Assets.xcassets/ButtonBackgroundColorPressed.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.275", - "green" : "0.141", - "red" : "0.290" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/ChatIcon.svg b/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/ChatIcon.svg deleted file mode 100644 index 74239992..00000000 --- a/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/ChatIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/Contents.json deleted file mode 100644 index 329dae48..00000000 --- a/Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "ChatIcon.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/Color.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/Color.colorset/Contents.json deleted file mode 100644 index 22c4bb0a..00000000 --- a/Copilot for Xcode/Assets.xcassets/Color.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/Contents.json b/Copilot for Xcode/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Copilot for Xcode/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/Contents.json deleted file mode 100644 index 78e08e6e..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "CopilotError.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/CopilotError.svg b/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/CopilotError.svg deleted file mode 100644 index ad107456..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotError.imageset/CopilotError.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/Contents.json deleted file mode 100644 index 9a465b02..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "CopilotIssue.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/CopilotIssue.svg b/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/CopilotIssue.svg deleted file mode 100644 index af4e8900..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotIssue.imageset/CopilotIssue.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json deleted file mode 100644 index 2e35661e..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "copilot.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg b/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg deleted file mode 100644 index 8284dce7..00000000 --- a/Copilot for Xcode/Assets.xcassets/CopilotLogo.imageset/copilot.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Copilot for Xcode/Assets.xcassets/DangerBackgroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/DangerBackgroundColor.colorset/Contents.json deleted file mode 100644 index 38242f14..00000000 --- a/Copilot for Xcode/Assets.xcassets/DangerBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0xF4", - "green" : "0xF3", - "red" : "0xFD" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/DangerForegroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/DangerForegroundColor.colorset/Contents.json deleted file mode 100644 index db248f82..00000000 --- a/Copilot for Xcode/Assets.xcassets/DangerForegroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x1C", - "green" : "0x0E", - "red" : "0xB1" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/DangerStrokeColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/DangerStrokeColor.colorset/Contents.json deleted file mode 100644 index 5fbecf46..00000000 --- a/Copilot for Xcode/Assets.xcassets/DangerStrokeColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xB2", - "green" : "0xAC", - "red" : "0xEE" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/DescriptionForegroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/DescriptionForegroundColor.colorset/Contents.json deleted file mode 100644 index bdcbb88e..00000000 --- a/Copilot for Xcode/Assets.xcassets/DescriptionForegroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x9D", - "green" : "0x9D", - "red" : "0x9D" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json deleted file mode 100644 index 1f7fbbe0..00000000 --- a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Icon.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties": { - "preserves-vector-representation": true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg b/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg deleted file mode 100644 index 15204160..00000000 --- a/Copilot for Xcode/Assets.xcassets/GitHubMark.imageset/Icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/Copilot for Xcode/Assets.xcassets/GroupBoxBackgroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/GroupBoxBackgroundColor.colorset/Contents.json deleted file mode 100644 index f7add95c..00000000 --- a/Copilot for Xcode/Assets.xcassets/GroupBoxBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.920", - "green" : "0.910", - "red" : "0.910" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.250", - "green" : "0.250", - "red" : "0.250" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/GroupBoxStrokeColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/GroupBoxStrokeColor.colorset/Contents.json deleted file mode 100644 index 35b93a68..00000000 --- a/Copilot for Xcode/Assets.xcassets/GroupBoxStrokeColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.900", - "green" : "0.900", - "red" : "0.900" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.080", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/Contents.json b/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/Contents.json deleted file mode 100644 index 1c65bf64..00000000 --- a/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "file_type_swift.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/file_type_swift.svg b/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/file_type_swift.svg deleted file mode 100644 index c232d1f7..00000000 --- a/Copilot for Xcode/Assets.xcassets/SwiftIcon.imageset/file_type_swift.svg +++ /dev/null @@ -1 +0,0 @@ -file_type_swift \ No newline at end of file diff --git a/Copilot for Xcode/Assets.xcassets/TextLinkForegroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/TextLinkForegroundColor.colorset/Contents.json deleted file mode 100644 index d892da13..00000000 --- a/Copilot for Xcode/Assets.xcassets/TextLinkForegroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x94", - "red" : "0x37" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/ToolTitleHighlightBgColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/ToolTitleHighlightBgColor.colorset/Contents.json deleted file mode 100644 index ce478f39..00000000 --- a/Copilot for Xcode/Assets.xcassets/ToolTitleHighlightBgColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.250", - "green" : "0.250", - "red" : "0.250" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/WarningBackgroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/WarningBackgroundColor.colorset/Contents.json deleted file mode 100644 index b38fae81..00000000 --- a/Copilot for Xcode/Assets.xcassets/WarningBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF5", - "green" : "0xF9", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/WarningForegroundColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/WarningForegroundColor.colorset/Contents.json deleted file mode 100644 index 2d9762a8..00000000 --- a/Copilot for Xcode/Assets.xcassets/WarningForegroundColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x07", - "green" : "0x37", - "red" : "0x8A" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot for Xcode/Assets.xcassets/WarningStrokeColor.colorset/Contents.json b/Copilot for Xcode/Assets.xcassets/WarningStrokeColor.colorset/Contents.json deleted file mode 100644 index a4651ba4..00000000 --- a/Copilot for Xcode/Assets.xcassets/WarningStrokeColor.colorset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xB4", - "green" : "0xCF", - "red" : "0xFD" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "localizable" : true - } -} diff --git a/Copilot for Xcode/Copilot_for_Xcode.entitlements b/Copilot for Xcode/Copilot_for_Xcode.entitlements deleted file mode 100644 index f557c733..00000000 --- a/Copilot for Xcode/Copilot_for_Xcode.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE) - - com.apple.security.automation.apple-events - - com.apple.security.files.user-selected.read-only - - - diff --git a/Copilot for Xcode/Credits.rtf b/Copilot for Xcode/Credits.rtf deleted file mode 100644 index 13a16781..00000000 --- a/Copilot for Xcode/Credits.rtf +++ /dev/null @@ -1,3352 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2761 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -{\*\expandedcolortbl;;} -\margl1440\margr1440\vieww27760\viewh14820\viewkind0 -\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 - -\f0\fs20 \cf0 Dependency: github.com/pointfreeco/swift-case-paths\ -Version: 1.3.3\ -License Content:\ -MIT License\ -\ -Copyright (c) 2020 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swift-custom-dump\ -Version: 1.3.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2021 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/xctest-dynamic-overlay\ -Version: 1.1.2\ -License Content:\ -MIT License\ -\ -Copyright (c) 2021 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/combine-schedulers\ -Version: 1.0.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2020 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swift-clocks\ -Version: 1.0.2\ -License Content:\ -MIT License\ -\ -Copyright (c) 2022 Point-Free\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -\ -Dependency: github.com/pointfreeco/swift-concurrency-extras\ -Version: 1.1.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2023 Point-Free\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/apple/swift-syntax\ -Version: 510.0.3\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -Dependency: github.com/pointfreeco/swift-tagged\ -Version: 0.10.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2019 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreecoswift-snapshot-testing\ -Version: 1.15.4\ -License Content:\ -MIT License\ -\ -Copyright (c) 2019 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/gonzalezreal/NetworkImage\ -Version: 6.0.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2020 Guille Gonzalez\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/apple/swift-collections-benchmark\ -Version: 0.0.3\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -Dependency: github.com/apple/swift-system\ -Version: 1.2.1\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -\ -Dependency: github.com/google/swift-benchmark\ -Version: 0.1.2\ -License Content:\ -\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -Dependency: github.com/apple/swift-argument-parser\ -Version: 1.2.2\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -Dependency: github.com/apple/swift-collections\ -Version: 1.1.0\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -Dependency: github.com/Frizlab/FSEventsWrapper\ -Version: 1.0.2\ -License Content:\ -MIT License\ -\ -Copyright \'a9 2023 Fran\'e7ois Lamboley \ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/Bouke/Glob\ -Version: 1.0.5\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright 2016 Bouke Haarsma\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -Dependency: github.com/ChimeHQ/JSONRPC\ -Version: 0.9.0\ -License Content:\ -BSD 3-Clause License\ -\ -Copyright (c) 2021, Chime\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted provided that the following conditions are met:\ -\ -1. Redistributions of source code must retain the above copyright notice, this\ - list of conditions and the following disclaimer.\ -\ -2. Redistributions in binary form must reproduce the above copyright notice,\ - this list of conditions and the following disclaimer in the documentation\ - and/or other materials provided with the distribution.\ -\ -3. Neither the name of the copyright holder nor the names of its\ - contributors may be used to endorse or promote products derived from\ - this software without specific prior written permission.\ -\ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\ -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\ -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\ -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\ -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\ -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\ -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\ -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/ChimeHQ/LanguageServerProtocol\ -Version: 0.13.3\ -License Content:\ -BSD 3-Clause License\ -\ -Copyright (c) 2021, Chime\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted provided that the following conditions are met:\ -\ -1. Redistributions of source code must retain the above copyright notice, this\ - list of conditions and the following disclaimer.\ -\ -2. Redistributions in binary form must reproduce the above copyright notice,\ - this list of conditions and the following disclaimer in the documentation\ - and/or other materials provided with the distribution.\ -\ -3. Neither the name of the copyright holder nor the names of its\ - contributors may be used to endorse or promote products derived from\ - this software without specific prior written permission.\ -\ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\ -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\ -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\ -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\ -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\ -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\ -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\ -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/ChimeHQ/OperationPlus\ -Version: 1.6.0\ -License Content:\ -BSD 3-Clause License\ -\ -Copyright (c) 2019, Chime\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted provided that the following conditions are met:\ -\ -* Redistributions of source code must retain the above copyright notice, this\ - list of conditions and the following disclaimer.\ -\ -* Redistributions in binary form must reproduce the above copyright notice,\ - this list of conditions and the following disclaimer in the documentation\ - and/or other materials provided with the distribution.\ -\ -* Neither the name of the copyright holder nor the names of its\ - contributors may be used to endorse or promote products derived from\ - this software without specific prior written permission.\ -\ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\ -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\ -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\ -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\ -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\ -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\ -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\ -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/GottaGetSwifty/CodableWrappers\ -Version: 2.0.7\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -Dependency: github.com/mattgallagher/CwlCatchException\ -Version: 2.2.0\ -License Content:\ -ISC License\ -\ -Copyright \'a9 2017 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved.\ -\ -Permission to use, copy, modify, and/or distribute this software for any\ -purpose with or without fee is hereby granted, provided that the above\ -copyright notice and this permission notice appear in all copies.\ -\ -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\ -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\ -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ -SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\ -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\ -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\ -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ -\ -\ -Dependency: github.com/mattgallagher/CwlPreconditionTesting\ -Version: 2.2.1\ -License Content:\ -ISC License\ -\ -Copyright \'a9 2017 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved.\ -\ -Permission to use, copy, modify, and/or distribute this software for any\ -purpose with or without fee is hereby granted, provided that the above\ -copyright notice and this permission notice appear in all copies.\ -\ -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\ -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\ -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ -SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\ -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\ -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\ -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\ -\ -\ -Dependency: github.com/Quick/Nimble\ -Version: 9.2.1\ -License Content:\ -Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "\{\}"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright 2016 Quick Team\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -Dependency: github.com/Quick/Quick\ -Version: 3.1.2\ -License Content:\ -Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "\{\}"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright 2014, Quick Team\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -Dependency: github.com/devm33/CGEventOverride\ -Version: 1.2.3\ -License Content:\ -MIT License\ -\ -Copyright (c) 2024 Shangxin Guo \ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/devm33/CopilotForXcodeKit\ -Version: 1f98fe9795766d3e37b5ae3d2e5f69f9b0af308b\ -License Content:\ -MIT License\ -\ -Copyright (c) 2023 Shangxin Guo\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/devm33/Highlightr\ -Version: 81d8c8b3733939bf5d9e52cd6318f944cc033bd2\ -License Content:\ -Copyright (c) 2016 Illanes, Juan Pablo \ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in\ -all copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\ -THE SOFTWARE.\ -\ -\ -Dependency: github.com/devm33/KeyboardShortcuts\ -Version: 65fb410b0c6d3ed96623b460bab31ffce5f48b4d\ -License Content:\ -MIT License\ -\ -Copyright (c) Sindre Sorhus (https://sindresorhus.com)\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ -\ -\ -Dependency: github.com/ChimeHQ/LanguageClient\ -Version: 0.8.2\ -License Content:\ -BSD 3-Clause License\ -\ -Copyright (c) 2021, Chime\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted provided that the following conditions are met:\ -\ -1. Redistributions of source code must retain the above copyright notice, this\ - list of conditions and the following disclaimer.\ -\ -2. Redistributions in binary form must reproduce the above copyright notice,\ - this list of conditions and the following disclaimer in the documentation\ - and/or other materials provided with the distribution.\ -\ -3. Neither the name of the copyright holder nor the names of its\ - contributors may be used to endorse or promote products derived from\ - this software without specific prior written permission.\ -\ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\ -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\ -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\ -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\ -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\ -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\ -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\ -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/ChimeHQ/ProcessEnv\ -Version: 1.0.1\ -License Content:\ -BSD 3-Clause License\ -\ -Copyright (c) 2020, Chime\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted provided that the following conditions are met:\ -\ -1. Redistributions of source code must retain the above copyright notice, this\ - list of conditions and the following disclaimer.\ -\ -2. Redistributions in binary form must reproduce the above copyright notice,\ - this list of conditions and the following disclaimer in the documentation\ - and/or other materials provided with the distribution.\ -\ -3. Neither the name of the copyright holder nor the names of its\ - contributors may be used to endorse or promote products derived from\ - this software without specific prior written permission.\ -\ -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\ -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\ -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\ -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\ -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\ -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\ -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\ -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\ -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/sparkle-project/Sparkle\ -Version: 2.4.2\ -License Content:\ -Copyright (c) 2006-2013 Andy Matuschak.\ -Copyright (c) 2009-2013 Elgato Systems GmbH.\ -Copyright (c) 2011-2014 Kornel Lesi\uc0\u324 ski.\ -Copyright (c) 2015-2017 Mayur Pawashe.\ -Copyright (c) 2014 C.W. Betts.\ -Copyright (c) 2014 Petroules Corporation.\ -Copyright (c) 2014 Big Nerd Ranch.\ -All rights reserved.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy of\ -this software and associated documentation files (the "Software"), to deal in\ -the Software without restriction, including without limitation the rights to\ -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\ -the Software, and to permit persons to whom the Software is furnished to do so,\ -subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\ -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\ -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\ -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\ -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ -\ -=================\ -EXTERNAL LICENSES\ -=================\ -\ -bspatch.c and bsdiff.c, from bsdiff 4.3 :\ -\ -Copyright 2003-2005 Colin Percival\ -All rights reserved\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted providing that the following conditions \ -are met:\ -1. Redistributions of source code must retain the above copyright\ - notice, this list of conditions and the following disclaimer.\ -2. Redistributions in binary form must reproduce the above copyright\ - notice, this list of conditions and the following disclaimer in the\ - documentation and/or other materials provided with the distribution.\ -\ -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\ -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\ -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\ -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\ -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\ -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\ -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\ -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\ -POSSIBILITY OF SUCH DAMAGE.\ -\ ---\ -\ -sais.c and sais.c, from sais-lite (2010/08/07) :\ -\ -The sais-lite copyright is as follows:\ -\ -Copyright (c) 2008-2010 Yuta Mori All Rights Reserved.\ -\ -Permission is hereby granted, free of charge, to any person\ -obtaining a copy of this software and associated documentation\ -files (the "Software"), to deal in the Software without\ -restriction, including without limitation the rights to use,\ -copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the\ -Software is furnished to do so, subject to the following\ -conditions:\ -\ -The above copyright notice and this permission notice shall be\ -included in all copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\ -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\ -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\ -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\ -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\ -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\ -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\ -OTHER DEALINGS IN THE SOFTWARE.\ -\ ---\ -\ -Portable C implementation of Ed25519, from https://github.com/orlp/ed25519\ -\ -Copyright (c) 2015 Orson Peters \ -\ -This software is provided 'as-is', without any express or implied warranty. In no event will the\ -authors be held liable for any damages arising from the use of this software.\ -\ -Permission is granted to anyone to use this software for any purpose, including commercial\ -applications, and to alter it and redistribute it freely, subject to the following restrictions:\ -\ -1. The origin of this software must not be misrepresented; you must not claim that you wrote the\ - original software. If you use this software in a product, an acknowledgment in the product\ - documentation would be appreciated but is not required.\ -\ -2. Altered source versions must be plainly marked as such, and must not be misrepresented as\ - being the original software.\ -\ -3. This notice may not be removed or altered from any source distribution.\ -\ ---\ -\ -SUSignatureVerifier.m:\ -\ -Copyright (c) 2011 Mark Hamlin.\ -\ -All rights reserved.\ -\ -Redistribution and use in source and binary forms, with or without\ -modification, are permitted providing that the following conditions\ -are met:\ -1. Redistributions of source code must retain the above copyright\ - notice, this list of conditions and the following disclaimer.\ -2. Redistributions in binary form must reproduce the above copyright\ - notice, this list of conditions and the following disclaimer in the\ - documentation and/or other materials provided with the distribution.\ -\ -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\ -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\ -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\ -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\ -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\ -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\ -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\ -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\ -POSSIBILITY OF SUCH DAMAGE.\ -\ -\ -Dependency: github.com/apple/swift-async-algorithms\ -Version: 1.0.0\ -License Content:\ - Apache License\ - Version 2.0, January 2004\ - http://www.apache.org/licenses/\ -\ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ -\ - 1. Definitions.\ -\ - "License" shall mean the terms and conditions for use, reproduction,\ - and distribution as defined by Sections 1 through 9 of this document.\ -\ - "Licensor" shall mean the copyright owner or entity authorized by\ - the copyright owner that is granting the License.\ -\ - "Legal Entity" shall mean the union of the acting entity and all\ - other entities that control, are controlled by, or are under common\ - control with that entity. For the purposes of this definition,\ - "control" means (i) the power, direct or indirect, to cause the\ - direction or management of such entity, whether by contract or\ - otherwise, or (ii) ownership of fifty percent (50%) or more of the\ - outstanding shares, or (iii) beneficial ownership of such entity.\ -\ - "You" (or "Your") shall mean an individual or Legal Entity\ - exercising permissions granted by this License.\ -\ - "Source" form shall mean the preferred form for making modifications,\ - including but not limited to software source code, documentation\ - source, and configuration files.\ -\ - "Object" form shall mean any form resulting from mechanical\ - transformation or translation of a Source form, including but\ - not limited to compiled object code, generated documentation,\ - and conversions to other media types.\ -\ - "Work" shall mean the work of authorship, whether in Source or\ - Object form, made available under the License, as indicated by a\ - copyright notice that is included in or attached to the work\ - (an example is provided in the Appendix below).\ -\ - "Derivative Works" shall mean any work, whether in Source or Object\ - form, that is based on (or derived from) the Work and for which the\ - editorial revisions, annotations, elaborations, or other modifications\ - represent, as a whole, an original work of authorship. For the purposes\ - of this License, Derivative Works shall not include works that remain\ - separable from, or merely link (or bind by name) to the interfaces of,\ - the Work and Derivative Works thereof.\ -\ - "Contribution" shall mean any work of authorship, including\ - the original version of the Work and any modifications or additions\ - to that Work or Derivative Works thereof, that is intentionally\ - submitted to Licensor for inclusion in the Work by the copyright owner\ - or by an individual or Legal Entity authorized to submit on behalf of\ - the copyright owner. For the purposes of this definition, "submitted"\ - means any form of electronic, verbal, or written communication sent\ - to the Licensor or its representatives, including but not limited to\ - communication on electronic mailing lists, source code control systems,\ - and issue tracking systems that are managed by, or on behalf of, the\ - Licensor for the purpose of discussing and improving the Work, but\ - excluding communication that is conspicuously marked or otherwise\ - designated in writing by the copyright owner as "Not a Contribution."\ -\ - "Contributor" shall mean Licensor and any individual or Legal Entity\ - on behalf of whom a Contribution has been received by Licensor and\ - subsequently incorporated within the Work.\ -\ - 2. Grant of Copyright License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - copyright license to reproduce, prepare Derivative Works of,\ - publicly display, publicly perform, sublicense, and distribute the\ - Work and such Derivative Works in Source or Object form.\ -\ - 3. Grant of Patent License. Subject to the terms and conditions of\ - this License, each Contributor hereby grants to You a perpetual,\ - worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ - (except as stated in this section) patent license to make, have made,\ - use, offer to sell, sell, import, and otherwise transfer the Work,\ - where such license applies only to those patent claims licensable\ - by such Contributor that are necessarily infringed by their\ - Contribution(s) alone or by combination of their Contribution(s)\ - with the Work to which such Contribution(s) was submitted. If You\ - institute patent litigation against any entity (including a\ - cross-claim or counterclaim in a lawsuit) alleging that the Work\ - or a Contribution incorporated within the Work constitutes direct\ - or contributory patent infringement, then any patent licenses\ - granted to You under this License for that Work shall terminate\ - as of the date such litigation is filed.\ -\ - 4. Redistribution. You may reproduce and distribute copies of the\ - Work or Derivative Works thereof in any medium, with or without\ - modifications, and in Source or Object form, provided that You\ - meet the following conditions:\ -\ - (a) You must give any other recipients of the Work or\ - Derivative Works a copy of this License; and\ -\ - (b) You must cause any modified files to carry prominent notices\ - stating that You changed the files; and\ -\ - (c) You must retain, in the Source form of any Derivative Works\ - that You distribute, all copyright, patent, trademark, and\ - attribution notices from the Source form of the Work,\ - excluding those notices that do not pertain to any part of\ - the Derivative Works; and\ -\ - (d) If the Work includes a "NOTICE" text file as part of its\ - distribution, then any Derivative Works that You distribute must\ - include a readable copy of the attribution notices contained\ - within such NOTICE file, excluding those notices that do not\ - pertain to any part of the Derivative Works, in at least one\ - of the following places: within a NOTICE text file distributed\ - as part of the Derivative Works; within the Source form or\ - documentation, if provided along with the Derivative Works; or,\ - within a display generated by the Derivative Works, if and\ - wherever such third-party notices normally appear. The contents\ - of the NOTICE file are for informational purposes only and\ - do not modify the License. You may add Your own attribution\ - notices within Derivative Works that You distribute, alongside\ - or as an addendum to the NOTICE text from the Work, provided\ - that such additional attribution notices cannot be construed\ - as modifying the License.\ -\ - You may add Your own copyright statement to Your modifications and\ - may provide additional or different license terms and conditions\ - for use, reproduction, or distribution of Your modifications, or\ - for any such Derivative Works as a whole, provided Your use,\ - reproduction, and distribution of the Work otherwise complies with\ - the conditions stated in this License.\ -\ - 5. Submission of Contributions. Unless You explicitly state otherwise,\ - any Contribution intentionally submitted for inclusion in the Work\ - by You to the Licensor shall be under the terms and conditions of\ - this License, without any additional terms or conditions.\ - Notwithstanding the above, nothing herein shall supersede or modify\ - the terms of any separate license agreement you may have executed\ - with Licensor regarding such Contributions.\ -\ - 6. Trademarks. This License does not grant permission to use the trade\ - names, trademarks, service marks, or product names of the Licensor,\ - except as required for reasonable and customary use in describing the\ - origin of the Work and reproducing the content of the NOTICE file.\ -\ - 7. Disclaimer of Warranty. Unless required by applicable law or\ - agreed to in writing, Licensor provides the Work (and each\ - Contributor provides its Contributions) on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ - implied, including, without limitation, any warranties or conditions\ - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ - PARTICULAR PURPOSE. You are solely responsible for determining the\ - appropriateness of using or redistributing the Work and assume any\ - risks associated with Your exercise of permissions under this License.\ -\ - 8. Limitation of Liability. In no event and under no legal theory,\ - whether in tort (including negligence), contract, or otherwise,\ - unless required by applicable law (such as deliberate and grossly\ - negligent acts) or agreed to in writing, shall any Contributor be\ - liable to You for damages, including any direct, indirect, special,\ - incidental, or consequential damages of any character arising as a\ - result of this License or out of the use or inability to use the\ - Work (including but not limited to damages for loss of goodwill,\ - work stoppage, computer failure or malfunction, or any and all\ - other commercial damages or losses), even if such Contributor\ - has been advised of the possibility of such damages.\ -\ - 9. Accepting Warranty or Additional Liability. While redistributing\ - the Work or Derivative Works thereof, You may choose to offer,\ - and charge a fee for, acceptance of support, warranty, indemnity,\ - or other liability obligations and/or rights consistent with this\ - License. However, in accepting such obligations, You may act only\ - on Your own behalf and on Your sole responsibility, not on behalf\ - of any other Contributor, and only if You agree to indemnify,\ - defend, and hold each Contributor harmless for any liability\ - incurred by, or claims asserted against, such Contributor by reason\ - of your accepting any such warranty or additional liability.\ -\ - END OF TERMS AND CONDITIONS\ -\ - APPENDIX: How to apply the Apache License to your work.\ -\ - To apply the Apache License to your work, attach the following\ - boilerplate notice, with the fields enclosed by brackets "[]"\ - replaced with your own identifying information. (Don't include\ - the brackets!) The text should be enclosed in the appropriate\ - comment syntax for the file format. We also recommend that a\ - file or class name and description of purpose be included on the\ - same "printed page" as the copyright notice for easier\ - identification within third-party archives.\ -\ - Copyright [yyyy] [name of copyright owner]\ -\ - Licensed under the Apache License, Version 2.0 (the "License");\ - you may not use this file except in compliance with the License.\ - You may obtain a copy of the License at\ -\ - http://www.apache.org/licenses/LICENSE-2.0\ -\ - Unless required by applicable law or agreed to in writing, software\ - distributed under the License is distributed on an "AS IS" BASIS,\ - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ - See the License for the specific language governing permissions and\ - limitations under the License.\ -\ -\ -\ -## Runtime Library Exception to the Apache 2.0 License: ##\ -\ -\ - As an exception, if you use this Software to compile your source code and\ - portions of this Software are embedded into the binary product as a result,\ - you may redistribute such product without providing attribution as would\ - otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ -\ -\ -Dependency: github.com/pointfreeco/swift-composable-architecture\ -Version: 1.10.4\ -License Content:\ -MIT License\ -\ -Copyright (c) 2020 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swift-dependencies\ -Version: 1.3.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2022 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swift-identified-collections\ -Version: 1.0.2\ -License Content:\ -MIT License\ -\ -Copyright (c) 2021 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/gonzalezreal/swift-markdown-ui\ -Version: 2.4.0\ -License Content:\ -The MIT License (MIT)\ -\ -Copyright (c) 2020 Guillermo Gonzalez\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -\ -Dependency: github.com/pointfreeco/swift-parsing\ -Version: 0.13.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2020 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swift-perception\ -Version: 1.2.2\ -License Content:\ -MIT License\ -\ -Copyright (c) 2023 Point-Free\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/pointfreeco/swiftui-navigation\ -Version: 1.4.0\ -License Content:\ -MIT License\ -\ -Copyright (c) 2021 Point-Free, Inc.\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: github.com/globulus/swiftui-flow-layout\ -Version: 1.0.5\ -License Content:\ -MIT License\ -\ -Copyright (c) 2021 Gordan Glavaš\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: https://github.com/stephencelis/SQLite.swift\ -Version: 0.15.3\ -License Content:\ -MIT License\ -\ -Copyright (c) 2014-2015 Stephen Celis ()\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: https://github.com/microsoft/monaco-editor\ -Version: 0.52.2\ -License Content:\ -The MIT License (MIT)\ -\ -Copyright (c) 2016 - present Microsoft Corporation\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -Dependency: https://github.com/xtermjs/xterm.js\ -Version: @xterm/addon-fit@0.10.0, @xterm/xterm@5.5.0\ -License Content:\ -The MIT License (MIT)\ -\ -Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)\ -Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)\ -Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in\ -all copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\ -THE SOFTWARE.\ -\ -\ -Dependency: https://github.com/scinfu/SwiftSoup\ -Version: 2.9.6\ -License Content:\ -The MIT License\ -\ -Copyright (c) 2009-2025 Jonathan Hedley \ -Swift port copyright (c) 2016-2025 Nabil Chatbi\ -\ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ -\ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ -\ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ -\ -\ -} \ No newline at end of file diff --git a/Copilot for Xcode/Preview Content/Preview Assets.xcassets/Contents.json b/Copilot for Xcode/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Copilot for Xcode/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Copilot-for-Xcode-Info.plist b/Copilot-for-Xcode-Info.plist deleted file mode 100644 index 12d852d9..00000000 --- a/Copilot-for-Xcode-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - APP_ID_PREFIX - $(AppIdentifierPrefix) - APPLICATION_SUPPORT_FOLDER - $(APPLICATION_SUPPORT_FOLDER) - BUNDLE_IDENTIFIER_BASE - $(BUNDLE_IDENTIFIER_BASE) - EXTENSION_BUNDLE_NAME - $(EXTENSION_BUNDLE_NAME) - HOST_APP_NAME - $(HOST_APP_NAME) - LANGUAGE_SERVER_PATH - $(LANGUAGE_SERVER_PATH) - NODE_PATH - $(NODE_PATH) - SUEnableAutomaticChecks - YES - SUScheduledCheckInterval - 3600 - SUEnableJavaScript - NO - SUFeedURL - $(SPARKLE_FEED_URL) - SUPublicEDKey - $(SPARKLE_PUBLIC_KEY) - TEAM_ID_PREFIX - $(TeamIdentifierPrefix) - STANDARD_TELEMETRY_CHANNEL_KEY - $(STANDARD_TELEMETRY_CHANNEL_KEY) - - \ No newline at end of file diff --git a/Core/Package.swift b/Core/Package.swift deleted file mode 100644 index 33ad1c48..00000000 --- a/Core/Package.swift +++ /dev/null @@ -1,304 +0,0 @@ -// swift-tools-version: 5.7 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import Foundation -import PackageDescription - -// MARK: - Package - -let package = Package( - name: "Core", - platforms: [.macOS(.v12)], - products: [ - .library( - name: "Service", - targets: [ - "Service", - "SuggestionInjector", - "FileChangeChecker", - "LaunchAgentManager", - "UpdateChecker", - ] - ), - .library( - name: "Client", - targets: [ - "Client", - ] - ), - .library( - name: "HostApp", - targets: [ - "HostApp", - "Client", - "LaunchAgentManager", - "UpdateChecker", - "GitHubCopilotViewModel", - ] - ), - ], - dependencies: [ - .package(path: "../Tool"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), - .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.4.0"), - .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), - .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", - from: "1.10.4" - ), - // quick hack to support custom UserDefaults - // https://github.com/sindresorhus/KeyboardShortcuts - .package(url: "https://github.com/devm33/KeyboardShortcuts", branch: "main"), - .package(url: "https://github.com/devm33/CGEventOverride", branch: "devm33/fix-stale-AXIsProcessTrusted"), - .package(url: "https://github.com/devm33/Highlightr", branch: "master"), - .package(url: "https://github.com/globulus/swiftui-flow-layout", from: "1.0.5") - ], - targets: [ - // MARK: - Main - - .target( - name: "Client", - dependencies: [ - .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "GitHubCopilotService", package: "Tool"), - ]), - .target( - name: "Service", - dependencies: [ - "SuggestionWidget", - "SuggestionService", - "ChatService", - "PromptToCodeService", - "ConversationTab", - "KeyBindingManager", - "XcodeThemeController", - .product(name: "TelemetryService", package: "Tool"), - .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "ConversationServiceProvider", package: "Tool"), - .product(name: "Workspace", package: "Tool"), - .product(name: "UserDefaultsObserver", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "Status", package: "Tool"), - .product(name: "StatusBarItemView", package: "Tool"), - .product(name: "ChatTab", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "AXHelper", package: "Tool"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - .product(name: "Dependencies", package: "swift-dependencies"), - .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), - ]), - .testTarget( - name: "ServiceTests", - dependencies: [ - "Service", - "Client", - "SuggestionInjector", - .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "ConversationServiceProvider", package: "Tool"), - ] - ), - - // MARK: - Host App - - .target( - name: "HostApp", - dependencies: [ - "Client", - "LaunchAgentManager", - "GitHubCopilotViewModel", - .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "Toast", package: "Tool"), - .product(name: "SharedUIComponents", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "MarkdownUI", package: "swift-markdown-ui"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), - .product(name: "GitHubCopilotService", package: "Tool"), - .product(name: "Persist", package: "Tool"), - ]), - - // MARK: - Suggestion Service - - .target( - name: "SuggestionService", - dependencies: [ - .product(name: "UserDefaultsObserver", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "BuiltinExtension", package: "Tool"), - .product(name: "GitHubCopilotService", package: "Tool"), - ]), - .target( - name: "SuggestionInjector", - dependencies: [.product(name: "SuggestionBasic", package: "Tool")] - ), - .testTarget( - name: "SuggestionInjectorTests", - dependencies: ["SuggestionInjector"] - ), - - // MARK: - Prompt To Code - - .target( - name: "PromptToCodeService", - dependencies: [ - .product(name: "SuggestionBasic", package: "Tool"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ]), - - // MARK: - Chat - - .target( - name: "ChatService", - dependencies: [ - "PersistMiddleware", - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "Parsing", package: "swift-parsing"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "AXHelper", package: "Tool"), - .product(name: "ConversationServiceProvider", package: "Tool"), - .product(name: "GitHubCopilotService", package: "Tool"), - .product(name: "Workspace", package: "Tool"), - .product(name: "Terminal", package: "Tool"), - .product(name: "SystemUtils", package: "Tool"), - .product(name: "AppKitExtension", package: "Tool"), - .product(name: "WebContentExtractor", package: "Tool"), - .product(name: "GitHelper", package: "Tool"), - .product(name: "SuggestionBasic", package: "Tool") - ]), - .testTarget( - name: "ChatServiceTests", - dependencies: ["ChatService"] - ), - - .target( - name: "ConversationTab", - dependencies: [ - "ChatService", - .product(name: "SharedUIComponents", package: "Tool"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "ChatTab", package: "Tool"), - .product(name: "Terminal", package: "Tool"), - .product(name: "Cache", package: "Tool"), - .product(name: "MarkdownUI", package: "swift-markdown-ui"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - .product(name: "SwiftUIFlowLayout", package: "swiftui-flow-layout"), - .product(name: "Persist", package: "Tool") - ] - ), - - // MARK: - UI - - .target( - name: "SuggestionWidget", - dependencies: [ - "PromptToCodeService", - "ConversationTab", - "GitHubCopilotViewModel", - "PersistMiddleware", - .product(name: "GitHubCopilotService", package: "Tool"), - .product(name: "Toast", package: "Tool"), - .product(name: "UserDefaultsObserver", package: "Tool"), - .product(name: "SharedUIComponents", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "ChatTab", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "CustomAsyncAlgorithms", package: "Tool"), - .product(name: "HostAppActivator", package: "Tool"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - .product(name: "MarkdownUI", package: "swift-markdown-ui"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), - .testTarget(name: "SuggestionWidgetTests", dependencies: ["SuggestionWidget"]), - - // MARK: - Helpers - - .target(name: "FileChangeChecker"), - .target( - name: "LaunchAgentManager", - dependencies: [ - .product(name: "Logger", package: "Tool"), - ] - ), - .target( - name: "UpdateChecker", - dependencies: [ - "Sparkle", - .product(name: "Preferences", package: "Tool"), - .product(name: "Logger", package: "Tool"), - ] - ), - .target( - name: "GitHubCopilotViewModel", - dependencies: [ - .product(name: "GitHubCopilotService", package: "Tool"), - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - .product(name: "Status", package: "Tool"), - ] - ), - - // MARK: Key Binding - - .target( - name: "KeyBindingManager", - dependencies: [ - .product(name: "Workspace", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "CGEventOverride", package: "CGEventOverride"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "UserDefaultsObserver", package: "Tool"), - .product(name: "ConversationServiceProvider", package: "Tool"), - ] - ), - .testTarget( - name: "KeyBindingManagerTests", - dependencies: ["KeyBindingManager"] - ), - - // MARK: Theming - - .target( - name: "XcodeThemeController", - dependencies: [ - .product(name: "Preferences", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "Highlightr", package: "Highlightr"), - ] - ), - - // MARK: Persist Middleware - .target( - name: "PersistMiddleware", - dependencies: [ - .product(name: "Persist", package: "Tool"), - .product(name: "ChatTab", package: "Tool"), - .product(name: "ChatAPIService", package: "Tool"), - .product(name: "ConversationServiceProvider", package: "Tool") - ] - ) - ] -) - diff --git a/Core/Sources/ChatService/ChatInjector.swift b/Core/Sources/ChatService/ChatInjector.swift deleted file mode 100644 index df3d454a..00000000 --- a/Core/Sources/ChatService/ChatInjector.swift +++ /dev/null @@ -1,147 +0,0 @@ -import SuggestionBasic -import AppKit -import XcodeInspector -import AXHelper -import ApplicationServices -import AppActivator -import LanguageServerProtocol - -public struct ChatInjector { - public init() {} - - public func insertCodeBlock(codeBlock: String) { - do { - guard let editorContent = XcodeInspector.shared.focusedEditor?.getContent(), - let focusElement = XcodeInspector.shared.focusedElement, - focusElement.description == "Source Editor" - else { return } - - var cursorPosition = editorContent.cursorPosition - guard cursorPosition.line >= 0, cursorPosition.character >= 0 else { return } - - var lines = editorContent.content.splitByNewLine( - omittingEmptySubsequences: false - ).map { String($0) } - - guard cursorPosition.line <= lines.count else { return } - - var modifications: [Modification] = [] - - // Handle selection deletion - if let selection = editorContent.selections.first, - selection.isValid, - selection.start.line < lines.endIndex { - let selectionEndLine = min(selection.end.line, lines.count - 1) - let deletedSelection = CursorRange( - start: selection.start, - end: .init(line: selectionEndLine, character: selection.end.character) - ) - modifications.append(.deletedSelection(deletedSelection)) - lines = lines.applying([.deletedSelection(deletedSelection)]) - cursorPosition = selection.start - } - - let insertionRange = CursorRange( - start: cursorPosition, - end: cursorPosition - ) - - try Self.performInsertion( - content: codeBlock, - range: insertionRange, - lines: &lines, - modifications: &modifications, - focusElement: focusElement - ) - - } catch { - print("Failed to insert code block: \(error)") - } - } - - public static func insertSuggestion(suggestion: String, range: CursorRange, lines: [String]) { - do { - guard let focusElement = XcodeInspector.shared.focusedElement, - focusElement.description == "Source Editor" - else { return } - - guard range.start.line >= 0, - range.start.line < lines.count, - range.end.line >= 0, - range.end.line < lines.count - else { return } - - var lines = lines - var modifications: [Modification] = [] - - if range.isValid { - modifications.append(.deletedSelection(range)) - lines = lines.applying([.deletedSelection(range)]) - } - - try performInsertion( - content: suggestion, - range: range, - lines: &lines, - modifications: &modifications, - focusElement: focusElement - ) - - } catch { - print("Failed to insert suggestion: \(error)") - } - } - - private static func performInsertion( - content: String, - range: CursorRange, - lines: inout [String], - modifications: inout [Modification], - focusElement: AXUIElement - ) throws { - let targetLine = lines[range.start.line] - let leadingWhitespace = range.start.character > 0 ? targetLine.prefix { $0.isWhitespace } : "" - let indentation = String(leadingWhitespace) - - let index = targetLine.index(targetLine.startIndex, offsetBy: min(range.start.character, targetLine.count)) - let before = targetLine[.. String in - return index == 0 ? String(element) : indentation + String(element) - } - - var toBeInsertedLines = [String]() - if contentLines.count > 1 { - toBeInsertedLines.append(String(before) + contentLines.first!) - toBeInsertedLines.append(contentsOf: contentLines.dropFirst().dropLast()) - toBeInsertedLines.append(contentLines.last! + String(after)) - } else { - toBeInsertedLines.append(String(before) + contentLines.first! + String(after)) - } - - lines.replaceSubrange((range.start.line)...(range.start.line), with: toBeInsertedLines) - - let newContent = String(lines.joined(separator: "\n")) - let newCursorPosition = CursorPosition( - line: range.start.line + contentLines.count - 1, - character: contentLines.last?.count ?? 0 - ) - - modifications.append(.inserted(range.start.line, toBeInsertedLines)) - - try AXHelper().injectUpdatedCodeWithAccessibilityAPI( - .init( - content: newContent, - newSelection: .cursor(newCursorPosition), - modifications: modifications - ), - focusElement: focusElement, - onSuccess: { - NSWorkspace.activatePreviousActiveXcode() - } - ) - } -} diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift deleted file mode 100644 index c48aa4c5..00000000 --- a/Core/Sources/ChatService/ChatService.swift +++ /dev/null @@ -1,1266 +0,0 @@ -import ChatAPIService -import Combine -import Foundation -import GitHubCopilotService -import Preferences -import ConversationServiceProvider -import BuiltinExtension -import JSONRPC -import Status -import Persist -import PersistMiddleware -import ChatTab -import Logger -import Workspace -import XcodeInspector -import OrderedCollections -import SystemUtils -import GitHelper -import LanguageServerProtocol -import SuggestionBasic - -public protocol ChatServiceType { - var memory: ContextAwareAutoManagedChatMemory { get set } - func send(_ id: String, content: String, contentImages: [ChatCompletionContentPartImage], contentImageReferences: [ImageReference], skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool, userLanguage: String?, turnId: String?) async throws - func stopReceivingMessage() async - func upvote(_ id: String, _ rating: ConversationRating) async - func downvote(_ id: String, _ rating: ConversationRating) async - func copyCode(_ id: String) async -} - -struct ToolCallRequest { - let requestId: JSONId - let turnId: String - let roundId: Int - let toolCallId: String - let completion: (AnyJSONRPCResponse) -> Void -} - -public struct FileEdit: Equatable { - - public enum Status: String { - case none = "none" - case kept = "kept" - case undone = "undone" - } - - public let fileURL: URL - public let originalContent: String - public var modifiedContent: String - public var status: Status - - /// Different toolName, the different undo logic. Like `insert_edit_into_file` and `create_file` - public var toolName: ToolName - - public init( - fileURL: URL, - originalContent: String, - modifiedContent: String, - status: Status = .none, - toolName: ToolName - ) { - self.fileURL = fileURL - self.originalContent = originalContent - self.modifiedContent = modifiedContent - self.status = status - self.toolName = toolName - } -} - -public final class ChatService: ChatServiceType, ObservableObject { - - public enum RequestType: String, Equatable { - case conversation, codeReview - } - - public var memory: ContextAwareAutoManagedChatMemory - @Published public internal(set) var chatHistory: [ChatMessage] = [] - @Published public internal(set) var isReceivingMessage = false - @Published public internal(set) var fileEditMap: OrderedDictionary = [:] - public internal(set) var requestType: RequestType? = nil - public let chatTabInfo: ChatTabInfo - private let conversationProvider: ConversationServiceProvider? - private let conversationProgressHandler: ConversationProgressHandler - private let conversationContextHandler: ConversationContextHandler = ConversationContextHandlerImpl.shared - // sync all the files in the workspace to watch for changes. - private let watchedFilesHandler: WatchedFilesHandler = WatchedFilesHandlerImpl.shared - private var cancellables = Set() - private var activeRequestId: String? - private(set) public var conversationId: String? - private var skillSet: [ConversationSkill] = [] - private var lastUserRequest: ConversationRequest? - private var isRestored: Bool = false - private var pendingToolCallRequests: [String: ToolCallRequest] = [:] - init(provider: any ConversationServiceProvider, - memory: ContextAwareAutoManagedChatMemory = ContextAwareAutoManagedChatMemory(), - conversationProgressHandler: ConversationProgressHandler = ConversationProgressHandlerImpl.shared, - chatTabInfo: ChatTabInfo) { - self.memory = memory - self.conversationProvider = provider - self.conversationProgressHandler = conversationProgressHandler - self.chatTabInfo = chatTabInfo - memory.chatService = self - - subscribeToNotifications() - subscribeToConversationContextRequest() - subscribeToClientToolInvokeEvent() - subscribeToClientToolConfirmationEvent() - } - - deinit { - Task { [weak self] in - await self?.stopReceivingMessage() - } - - // Clear all subscriptions - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - - // Memory will be deallocated automatically - } - - private func subscribeToNotifications() { - memory.observeHistoryChange { [weak self] in - Task { [weak self] in - guard let memory = self?.memory else { return } - self?.chatHistory = await memory.history - } - } - - conversationProgressHandler.onBegin.sink { [weak self] (token, progress) in - self?.handleProgressBegin(token: token, progress: progress) - }.store(in: &cancellables) - - conversationProgressHandler.onProgress.sink { [weak self] (token, progress) in - self?.handleProgressReport(token: token, progress: progress) - }.store(in: &cancellables) - - conversationProgressHandler.onEnd.sink { [weak self] (token, progress) in - self?.handleProgressEnd(token: token, progress: progress) - }.store(in: &cancellables) - } - - private func subscribeToConversationContextRequest() { - self.conversationContextHandler.onConversationContext.sink(receiveValue: { [weak self] (request, completion) in - guard let skills = self?.skillSet, !skills.isEmpty, request.params!.conversationId == self?.conversationId else { return } - skills.forEach { skill in - if (skill.applies(params: request.params!)) { - skill.resolveSkill(request: request, completion: completion) - } - } - }).store(in: &cancellables) - } - - private func subscribeToClientToolConfirmationEvent() { - ClientToolHandlerImpl.shared.onClientToolConfirmationEvent.sink(receiveValue: { [weak self] (request, completion) in - guard let params = request.params, params.conversationId == self?.conversationId else { return } - let editAgentRounds: [AgentRound] = [ - AgentRound(roundId: params.roundId, - reply: "", - toolCalls: [ - AgentToolCall(id: params.toolCallId, name: params.name, status: .waitForConfirmation, invokeParams: params) - ] - ) - ] - self?.appendToolCallHistory(turnId: params.turnId, editAgentRounds: editAgentRounds) - self?.pendingToolCallRequests[params.toolCallId] = ToolCallRequest( - requestId: request.id, - turnId: params.turnId, - roundId: params.roundId, - toolCallId: params.toolCallId, - completion: completion) - }).store(in: &cancellables) - } - - private func subscribeToClientToolInvokeEvent() { - ClientToolHandlerImpl.shared.onClientToolInvokeEvent.sink(receiveValue: { [weak self] (request, completion) in - guard let params = request.params, params.conversationId == self?.conversationId else { return } - guard let copilotTool = CopilotToolRegistry.shared.getTool(name: params.name) else { - completion(AnyJSONRPCResponse(id: request.id, - result: JSONValue.array([ - JSONValue.null, - JSONValue.hash( - [ - "code": .number(-32601), - "message": .string("Tool function not found") - ]) - ]) - ) - ) - return - } - - copilotTool.invokeTool(request, completion: completion, chatHistoryUpdater: self?.appendToolCallHistory, contextProvider: self) - }).store(in: &cancellables) - } - - private func appendToolCallHistory(turnId: String, editAgentRounds: [AgentRound]) { - let chatTabId = self.chatTabInfo.id - Task { - let message = ChatMessage( - assistantMessageWithId: turnId, - chatTabID: chatTabId, - editAgentRounds: editAgentRounds - ) - - await self.memory.appendMessage(message) - } - } - - public func updateFileEdits(by fileEdit: FileEdit) { - if let existingFileEdit = self.fileEditMap[fileEdit.fileURL] { - self.fileEditMap[fileEdit.fileURL] = .init( - fileURL: fileEdit.fileURL, - originalContent: existingFileEdit.originalContent, - modifiedContent: fileEdit.modifiedContent, - toolName: existingFileEdit.toolName - ) - } else { - self.fileEditMap[fileEdit.fileURL] = fileEdit - } - } - - public func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws { - try await conversationProvider?.notifyChangeTextDocument(fileURL: fileURL, content: content, version: version, workspaceURL: getWorkspaceURL()) - } - - public static func service(for chatTabInfo: ChatTabInfo) -> ChatService { - let provider = BuiltinExtensionConversationServiceProvider( - extension: GitHubCopilotExtension.self - ) - return ChatService(provider: provider, chatTabInfo: chatTabInfo) - } - - // this will be triggerred in conversation tab if needed - public func restoreIfNeeded() { - guard self.isRestored == false else { return } - - Task { - let storedChatMessages = fetchAllChatMessagesFromStorage() - await mutateHistory { history in - history.append(contentsOf: storedChatMessages) - } - } - - self.isRestored = true - } - - public func updateToolCallStatus(toolCallId: String, status: AgentToolCall.ToolCallStatus, payload: Any? = nil) { - if status == .cancelled { - resetOngoingRequest() - return - } - - // Send the tool call result back to the server - if let toolCallRequest = self.pendingToolCallRequests[toolCallId], status == .accepted { - self.pendingToolCallRequests.removeValue(forKey: toolCallId) - let toolResult = LanguageModelToolConfirmationResult(result: .Accept) - let jsonResult = try? JSONEncoder().encode(toolResult) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - toolCallRequest.completion( - AnyJSONRPCResponse( - id: toolCallRequest.requestId, - result: JSONValue.array([ - jsonValue, - JSONValue.null - ]) - ) - ) - } - - // Update the tool call status in the chat history - Task { - guard let lastMessage = await memory.history.last, lastMessage.role == .assistant else { - return - } - - var updatedAgentRounds: [AgentRound] = [] - for i in 0.. = [], - contentImageReferences: Array = [], - skillSet: Array, - references: Array, - model: String? = nil, - agentMode: Bool = false, - userLanguage: String? = nil, - turnId: String? = nil - ) async throws { - guard activeRequestId == nil else { return } - let workDoneToken = UUID().uuidString - activeRequestId = workDoneToken - - let finalImageReferences: [ImageReference] - let finalContentImages: [ChatCompletionContentPartImage] - - if !contentImageReferences.isEmpty { - // User attached images are all parsed as ImageReference - finalImageReferences = contentImageReferences - finalContentImages = contentImageReferences - .map { - ChatCompletionContentPartImage( - url: $0.dataURL(imageType: $0.source == .screenshot ? "png" : "") - ) - } - } else { - // In current implementation, only resend message will have contentImageReferences - // No need to convert ChatCompletionContentPartImage to ImageReference for persistence - finalImageReferences = [] - finalContentImages = contentImages - } - - var chatMessage = ChatMessage( - userMessageWithId: id, - chatTabId: chatTabInfo.id, - content: content, - contentImageReferences: finalImageReferences, - references: references.toConversationReferences() - ) - - let currentEditorSkill = skillSet.first(where: { $0.id == CurrentEditorSkill.ID }) as? CurrentEditorSkill - let currentFileReadability = currentEditorSkill == nil - ? nil - : FileUtils.checkFileReadability(at: currentEditorSkill!.currentFilePath) - var errorMessage: ChatMessage? - - var currentTurnId: String? = turnId - // If turnId is provided, it is used to update the existing message, no need to append the user message - if turnId == nil { - if let currentFileReadability, !currentFileReadability.isReadable { - // For associating error message with user message - currentTurnId = UUID().uuidString - chatMessage.clsTurnID = currentTurnId - errorMessage = ChatMessage( - errorMessageWithId: currentTurnId!, - chatTabID: chatTabInfo.id, - errorMessages: [ - currentFileReadability.errorMessage( - using: CurrentEditorSkill.readabilityErrorMessageProvider - ) - ].compactMap { $0 }.filter { !$0.isEmpty } - ) - } - await memory.appendMessage(chatMessage) - } - - // reset file edits - self.resetFileEdits() - - // persist - saveChatMessageToStorage(chatMessage) - - if content.hasPrefix("/releaseNotes") { - if let fileURL = Bundle.main.url(forResource: "ReleaseNotes", withExtension: "md"), - let whatsNewContent = try? String(contentsOf: fileURL) - { - // will be persist in resetOngoingRequest() - // there is no turn id from CLS, just set it as id - let clsTurnID = UUID().uuidString - let progressMessage = ChatMessage( - assistantMessageWithId: clsTurnID, - chatTabID: chatTabInfo.id, - content: whatsNewContent - ) - await memory.appendMessage(progressMessage) - } - resetOngoingRequest() - return - } - - if let errorMessage { - Task { await memory.appendMessage(errorMessage) } - } - - var activeDoc: Doc? - var validSkillSet: [ConversationSkill] = skillSet - if let currentEditorSkill, currentFileReadability?.isReadable == true { - activeDoc = Doc(uri: currentEditorSkill.currentFile.url.absoluteString) - } else { - validSkillSet.removeAll(where: { $0.id == CurrentEditorSkill.ID || $0.id == ProblemsInActiveDocumentSkill.ID }) - } - - let request = createConversationRequest( - workDoneToken: workDoneToken, - content: content, - contentImages: finalContentImages, - activeDoc: activeDoc, - references: references, - model: model, - agentMode: agentMode, - userLanguage: userLanguage, - turnId: currentTurnId, - skillSet: validSkillSet - ) - - self.lastUserRequest = request - self.skillSet = validSkillSet - try await sendConversationRequest(request) - } - - private func createConversationRequest( - workDoneToken: String, - content: String, - contentImages: [ChatCompletionContentPartImage] = [], - activeDoc: Doc?, - references: [FileReference], - model: String? = nil, - agentMode: Bool = false, - userLanguage: String? = nil, - turnId: String? = nil, - skillSet: [ConversationSkill] - ) -> ConversationRequest { - let skillCapabilities: [String] = [CurrentEditorSkill.ID, ProblemsInActiveDocumentSkill.ID] - let supportedSkills: [String] = skillSet.map { $0.id } - let ignoredSkills: [String] = skillCapabilities.filter { - !supportedSkills.contains($0) - } - - /// replace the `@workspace` to `@project` - let newContent = replaceFirstWord(in: content, from: "@workspace", to: "@project") - - return ConversationRequest( - workDoneToken: workDoneToken, - content: newContent, - contentImages: contentImages, - workspaceFolder: "", - activeDoc: activeDoc, - skills: skillCapabilities, - ignoredSkills: ignoredSkills, - references: references, - model: model, - agentMode: agentMode, - userLanguage: userLanguage, - turnId: turnId - ) - } - - public func sendAndWait(_ id: String, content: String) async throws -> String { - try await send(id, content: content, skillSet: [], references: []) - if let reply = await memory.history.last(where: { $0.role == .assistant })?.content { - return reply - } - return "" - } - - public func stopReceivingMessage() async { - if let activeRequestId = activeRequestId { - do { - try await conversationProvider?.stopReceivingMessage(activeRequestId, workspaceURL: getWorkspaceURL()) - } catch { - print("Failed to cancel ongoing request with WDT: \(activeRequestId)") - } - } - resetOngoingRequest() - } - - public func clearHistory() async { - let messageIds = await memory.history.map { $0.id } - - await memory.clearHistory() - if let activeRequestId = activeRequestId { - do { - try await conversationProvider?.stopReceivingMessage(activeRequestId, workspaceURL: getWorkspaceURL()) - } catch { - print("Failed to cancel ongoing request with WDT: \(activeRequestId)") - } - } - - deleteAllChatMessagesFromStorage(messageIds) - resetOngoingRequest() - } - - public func deleteMessage(id: String) async { - await memory.removeMessage(id) - deleteChatMessageFromStorage(id) - } - - public func resendMessage(id: String, model: String? = nil) async throws { - if let _ = (await memory.history).first(where: { $0.id == id }), - let lastUserRequest - { - // TODO: clean up contents for resend message - activeRequestId = nil - try await send( - id, - content: lastUserRequest.content, - contentImages: lastUserRequest.contentImages, - skillSet: skillSet, - references: lastUserRequest.references ?? [], - model: model != nil ? model : lastUserRequest.model, - agentMode: lastUserRequest.agentMode, - userLanguage: lastUserRequest.userLanguage, - turnId: id - ) - } - } - - public func setMessageAsExtraPrompt(id: String) async { - if let message = (await memory.history).first(where: { $0.id == id }) - { - await mutateHistory { history in - let chatMessage: ChatMessage = .init( - chatTabID: self.chatTabInfo.id, - role: .assistant, - content: message.content - ) - - history.append(chatMessage) - self.saveChatMessageToStorage(chatMessage) - } - } - } - - public func mutateHistory(_ mutator: @escaping (inout [ChatMessage]) -> Void) async { - await memory.mutateHistory(mutator) - } - - public func handleCustomCommand(_ command: CustomCommand) async throws { - struct CustomCommandInfo { - var specifiedSystemPrompt: String? - var extraSystemPrompt: String? - var sendingMessageImmediately: String? - var name: String? - } - - let info: CustomCommandInfo? = { - switch command.feature { - case let .chatWithSelection(extraSystemPrompt, prompt, useExtraSystemPrompt): - let updatePrompt = useExtraSystemPrompt ?? true - return .init( - extraSystemPrompt: updatePrompt ? extraSystemPrompt : nil, - sendingMessageImmediately: prompt, - name: command.name - ) - case let .customChat(systemPrompt, prompt): - return .init( - specifiedSystemPrompt: systemPrompt, - extraSystemPrompt: "", - sendingMessageImmediately: prompt, - name: command.name - ) - case .promptToCode: return nil - case .singleRoundDialog: return nil - } - }() - - guard let info else { return } - - let templateProcessor = CustomCommandTemplateProcessor() - - if info.specifiedSystemPrompt != nil || info.extraSystemPrompt != nil { - await mutateHistory { history in - let chatMessage: ChatMessage = .init( - chatTabID: self.chatTabInfo.id, - role: .assistant, - content: "" - ) - history.append(chatMessage) - self.saveChatMessageToStorage(chatMessage) - } - } - - if let sendingMessageImmediately = info.sendingMessageImmediately, - !sendingMessageImmediately.isEmpty - { - try await send(UUID().uuidString, content: templateProcessor.process(sendingMessageImmediately), skillSet: [], references: []) - } - } - - public func getWorkspaceURL() -> URL? { - guard !chatTabInfo.workspacePath.isEmpty else { - return nil - } - return URL(fileURLWithPath: chatTabInfo.workspacePath) - } - - private func getProjectRootURL() async throws -> URL? { - guard let workspaceURL = getWorkspaceURL() else { return nil } - return WorkspaceXcodeWindowInspector.extractProjectURL( - workspaceURL: workspaceURL, - documentURL: nil - ) - } - - public func upvote(_ id: String, _ rating: ConversationRating) async { - try? await conversationProvider?.rateConversation(turnId: id, rating: rating, workspaceURL: getWorkspaceURL()) - } - - public func downvote(_ id: String, _ rating: ConversationRating) async { - try? await conversationProvider?.rateConversation(turnId: id, rating: rating, workspaceURL: getWorkspaceURL()) - } - - public func copyCode(_ id: String) async { - // TODO: pass copy code info to Copilot server - } - - // not used - public func handleSingleRoundDialogCommand( - systemPrompt: String?, - overwriteSystemPrompt: Bool, - prompt: String - ) async throws -> String { - let templateProcessor = CustomCommandTemplateProcessor() - return try await sendAndWait(UUID().uuidString, content: templateProcessor.process(prompt)) - } - - private func handleProgressBegin(token: String, progress: ConversationProgressBegin) { - guard let workDoneToken = activeRequestId, workDoneToken == token else { return } - conversationId = progress.conversationId - let turnId = progress.turnId - - Task { - if var lastUserMessage = await memory.history.last(where: { $0.role == .user }) { - - // Case: New conversation where error message was generated before CLS request - // Using clsTurnId to associate this error message with the corresponding user message - // When merging error messages with bot responses from CLS, these properties need to be updated - await memory.mutateHistory { history in - if let existingBotIndex = history.lastIndex(where: { - $0.role == .assistant && $0.clsTurnID == lastUserMessage.clsTurnID - }) { - history[existingBotIndex].id = turnId - history[existingBotIndex].clsTurnID = turnId - } - } - - lastUserMessage.clsTurnID = progress.turnId - saveChatMessageToStorage(lastUserMessage) - } - - /// Display an initial assistant message immediately after the user sends a message. - /// This improves perceived responsiveness, especially in Agent Mode where the first - /// ProgressReport may take long time. - let message = ChatMessage(assistantMessageWithId: turnId, chatTabID: chatTabInfo.id) - - // will persist in resetOngoingRequest() - await memory.appendMessage(message) - } - } - - private func handleProgressReport(token: String, progress: ConversationProgressReport) { - guard let workDownToken = activeRequestId, workDownToken == token else { - return - } - - let id = progress.turnId - var content = "" - var references: [ConversationReference] = [] - var steps: [ConversationProgressStep] = [] - var editAgentRounds: [AgentRound] = [] - - if let reply = progress.reply { - content = reply - } - - if let progressReferences = progress.references, !progressReferences.isEmpty { - references = progressReferences.toConversationReferences() - } - - if let progressSteps = progress.steps, !progressSteps.isEmpty { - steps = progressSteps - } - - if let progressAgentRounds = progress.editAgentRounds, !progressAgentRounds.isEmpty { - editAgentRounds = progressAgentRounds - } - - if content.isEmpty && references.isEmpty && steps.isEmpty && editAgentRounds.isEmpty { - return - } - - // create immutable copies - let messageContent = content - let messageReferences = references - let messageSteps = steps - let messageAgentRounds = editAgentRounds - - Task { - let message = ChatMessage( - assistantMessageWithId: id, - chatTabID: chatTabInfo.id, - content: messageContent, - references: messageReferences, - steps: messageSteps, - editAgentRounds: messageAgentRounds - ) - - // will persist in resetOngoingRequest() - await memory.appendMessage(message) - } - } - - private func handleProgressEnd(token: String, progress: ConversationProgressEnd) { - guard let workDoneToken = activeRequestId, workDoneToken == token else { return } - let followUp = progress.followUp - - if let CLSError = progress.error { - // CLS Error Code 402: reached monthly chat messages limit - if CLSError.code == 402 { - Task { - await Status.shared - .updateCLSStatus(.warning, busy: false, message: CLSError.message) - let errorMessage = ChatMessage( - errorMessageWithId: progress.turnId, - chatTabID: chatTabInfo.id, - panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)] - ) - // will persist in resetongoingRequest() - await memory.appendMessage(errorMessage) - - if let lastUserRequest, - let currentUserPlan = await Status.shared.currentUserPlan(), - currentUserPlan != "free" { - guard let fallbackModel = CopilotModelManager.getFallbackLLM( - scope: lastUserRequest.agentMode ? .agentPanel : .chatPanel - ) else { - resetOngoingRequest() - return - } - do { - CopilotModelManager.switchToFallbackModel() - try await resendMessage(id: progress.turnId, model: fallbackModel.id) - } catch { - Logger.gitHubCopilot.error(error) - resetOngoingRequest() - } - return - } - } - } else if CLSError.code == 400 && CLSError.message.contains("model is not supported") { - Task { - let errorMessage = ChatMessage( - errorMessageWithId: progress.turnId, - chatTabID: chatTabInfo.id, - errorMessages: ["Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."] - ) - await memory.appendMessage(errorMessage) - resetOngoingRequest() - return - } - } else { - Task { - let errorMessage = ChatMessage( - errorMessageWithId: progress.turnId, - chatTabID: chatTabInfo.id, - errorMessages: [CLSError.message] - ) - // will persist in resetOngoingRequest() - await memory.appendMessage(errorMessage) - resetOngoingRequest() - return - } - } - } - - Task { - let message = ChatMessage( - assistantMessageWithId: progress.turnId, - chatTabID: chatTabInfo.id, - followUp: followUp, - suggestedTitle: progress.suggestedTitle - ) - // will persist in resetOngoingRequest() - await memory.appendMessage(message) - resetOngoingRequest() - } - } - - private func resetOngoingRequest() { - activeRequestId = nil - isReceivingMessage = false - requestType = nil - - // cancel all pending tool call requests - for (_, request) in pendingToolCallRequests { - pendingToolCallRequests.removeValue(forKey: request.toolCallId) - let toolResult = LanguageModelToolConfirmationResult(result: .Dismiss) - let jsonResult = try? JSONEncoder().encode(toolResult) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - request.completion( - AnyJSONRPCResponse( - id: request.requestId, - result: JSONValue.array([ - jsonValue, - JSONValue.null - ]) - ) - ) - } - - Task { - // mark running steps to cancelled - await mutateHistory({ history in - guard !history.isEmpty, - let lastIndex = history.indices.last, - history[lastIndex].role == .assistant else { return } - - for i in 0.. 0 { - // invoke history turns - let turns = chatHistory.toTurns() - requestWithTurns.turns = turns - } - - try await conversationProvider?.createConversation(requestWithTurns, workspaceURL: getWorkspaceURL()) - } - } catch { - resetOngoingRequest() - throw error - } - } - - // MARK: - File Edit - public func undoFileEdit(for fileURL: URL) throws { - guard let fileEdit = self.fileEditMap[fileURL], - fileEdit.status == .none - else { return } - - switch fileEdit.toolName { - case .insertEditIntoFile: - InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self) - case .createFile: - try CreateFileTool.undo(for: fileURL) - default: - return - } - - self.fileEditMap[fileURL]!.status = .undone - } - - public func keepFileEdit(for fileURL: URL) { - guard let fileEdit = self.fileEditMap[fileURL], fileEdit.status == .none - else { return } - self.fileEditMap[fileURL]!.status = .kept - } - - public func resetFileEdits() { - self.fileEditMap = [:] - } - - public func discardFileEdit(for fileURL: URL) throws { - try self.undoFileEdit(for: fileURL) - self.fileEditMap.removeValue(forKey: fileURL) - } -} - - -public final class SharedChatService { - public var chatTemplates: [ChatTemplate]? = nil - public var chatAgents: [ChatAgent]? = nil - private let conversationProvider: ConversationServiceProvider? - - public static let shared = SharedChatService.service() - - init(provider: any ConversationServiceProvider) { - self.conversationProvider = provider - } - - public static func service() -> SharedChatService { - let provider = BuiltinExtensionConversationServiceProvider( - extension: GitHubCopilotExtension.self - ) - return SharedChatService(provider: provider) - } - - public func loadChatTemplates() async -> [ChatTemplate]? { - guard self.chatTemplates == nil else { return self.chatTemplates } - - do { - if let templates = (try await conversationProvider?.templates()) { - self.chatTemplates = templates - return templates - } - } catch { - // handle error if desired - } - - return nil - } - - public func copilotModels() async -> [CopilotModel] { - guard let models = try? await conversationProvider?.models() else { return [] } - return models - } - - public func loadChatAgents() async -> [ChatAgent]? { - guard self.chatAgents == nil else { return self.chatAgents } - - do { - if let chatAgents = (try await conversationProvider?.agents()) { - self.chatAgents = chatAgents - return chatAgents - } - } catch { - // handle error if desired - } - - return nil - } -} - - -extension ChatService { - - // do storage operatoin in the background - private func runInBackground(_ operation: @escaping () -> Void) { - Task.detached(priority: .utility) { - operation() - } - } - - func saveChatMessageToStorage(_ message: ChatMessage) { - runInBackground { - ChatMessageStore.save(message, with: .init(workspacePath: self.chatTabInfo.workspacePath, username: self.chatTabInfo.username)) - } - } - - func deleteChatMessageFromStorage(_ id: String) { - runInBackground { - ChatMessageStore.delete(by: id, with: .init(workspacePath: self.chatTabInfo.workspacePath, username: self.chatTabInfo.username)) - } - } - func deleteAllChatMessagesFromStorage(_ ids: [String]) { - runInBackground { - ChatMessageStore.deleteAll(by: ids, with: .init(workspacePath: self.chatTabInfo.workspacePath, username: self.chatTabInfo.username)) - } - } - - func fetchAllChatMessagesFromStorage() -> [ChatMessage] { - return ChatMessageStore.getAll(by: self.chatTabInfo.id, metadata: .init(workspacePath: self.chatTabInfo.workspacePath, username: self.chatTabInfo.username)) - } -} - -func replaceFirstWord(in content: String, from oldWord: String, to newWord: String) -> String { - let pattern = "^\(oldWord)\\b" - - if let regex = try? NSRegularExpression(pattern: pattern, options: []) { - let range = NSRange(location: 0, length: content.utf16.count) - return regex.stringByReplacingMatches(in: content, options: [], range: range, withTemplate: newWord) - } - - return content -} - -extension Array where Element == Reference { - func toConversationReferences() -> [ConversationReference] { - return self.map { - .init(uri: $0.uri, status: .included, kind: .reference($0)) - } - } -} - -extension Array where Element == FileReference { - func toConversationReferences() -> [ConversationReference] { - return self.map { - .init(uri: $0.url.path, status: .included, kind: .fileReference($0)) - } - } -} -extension [ChatMessage] { - // transfer chat messages to turns - // used to restore chat history for CLS - func toTurns() -> [TurnSchema] { - var turns: [TurnSchema] = [] - let count = self.count - var index = 0 - - while index < count { - let message = self[index] - if case .user = message.role { - var turn = TurnSchema(request: message.content, turnId: message.clsTurnID) - // has next message - if index + 1 < count { - let nextMessage = self[index + 1] - if nextMessage.role == .assistant { - turn.response = nextMessage.content + extractContentFromEditAgentRounds(nextMessage.editAgentRounds) - index += 1 - } - } - turns.append(turn) - } - index += 1 - } - - return turns - } - - private func extractContentFromEditAgentRounds(_ editAgentRounds: [AgentRound]) -> String { - var content = "" - for round in editAgentRounds { - if !round.reply.isEmpty { - content += round.reply - } - } - return content - } -} - -// MARK: Copilot Code Review - -extension ChatService { - - public func requestCodeReview(_ group: GitDiffGroup) async throws { - guard activeRequestId == nil else { return } - activeRequestId = UUID().uuidString - - guard !isReceivingMessage else { - activeRequestId = nil - throw CancellationError() - } - isReceivingMessage = true - requestType = .codeReview - - do { - await CodeReviewService.shared.resetComments() - - let turnId = UUID().uuidString - - await addCodeReviewUserMessage(id: UUID().uuidString, turnId: turnId, group: group) - - let initialBotMessage = ChatMessage( - assistantMessageWithId: turnId, - chatTabID: chatTabInfo.id - ) - await memory.appendMessage(initialBotMessage) - - guard let projectRootURL = try await getProjectRootURL() - else { - let round = CodeReviewRound.fromError(turnId: turnId, error: "Invalid git repository.") - await appendCodeReviewRound(round) - resetOngoingRequest() - return - } - - let prChanges = await CurrentChangeService.getPRChanges( - projectRootURL, - group: group, - shouldIncludeFile: shouldIncludeFileForReview - ) - guard !prChanges.isEmpty else { - let round = CodeReviewRound.fromError( - turnId: turnId, - error: group == .index ? "No staged changes found to review." : "No unstaged changes found to review." - ) - await appendCodeReviewRound(round) - resetOngoingRequest() - return - } - - let round: CodeReviewRound = .init( - turnId: turnId, - status: .waitForConfirmation, - request: .from(prChanges) - ) - await appendCodeReviewRound(round) - } catch { - resetOngoingRequest() - throw error - } - } - - private func shouldIncludeFileForReview(url: URL) -> Bool { - let codeLanguage = CodeLanguage(fileURL: url) - - if case .builtIn = codeLanguage { - return true - } else { - return false - } - } - - private func appendCodeReviewRound(_ round: CodeReviewRound) async { - let message = ChatMessage( - assistantMessageWithId: round.turnId, chatTabID: chatTabInfo.id, codeReviewRound: round - ) - - await memory.appendMessage(message) - } - - private func getCurrentCodeReviewRound(_ id: String) async -> CodeReviewRound? { - guard let lastBotMessage = await memory.history.last, - lastBotMessage.role == .assistant, - let codeReviewRound = lastBotMessage.codeReviewRound, - codeReviewRound.id == id - else { - return nil - } - - return codeReviewRound - } - - public func acceptCodeReview(_ id: String, selectedFileUris: [DocumentUri]) async { - guard activeRequestId != nil, isReceivingMessage else { return } - - guard var round = await getCurrentCodeReviewRound(id), - var request = round.request, - round.status.canTransitionTo(.accepted) - else { return } - - guard selectedFileUris.count > 0 else { - round = round.withError("No files are selected to review.") - await appendCodeReviewRound(round) - resetOngoingRequest() - return - } - - round.status = .accepted - request.updateSelectedChanges(by: selectedFileUris) - round.request = request - await appendCodeReviewRound(round) - - round.status = .running - await appendCodeReviewRound(round) - - let (fileComments, errorMessage) = await CodeReviewProvider.invoke( - request, - context: CodeReviewServiceProvider(conversationServiceProvider: conversationProvider) - ) - - if let errorMessage = errorMessage { - round = round.withError(errorMessage) - await appendCodeReviewRound(round) - resetOngoingRequest() - return - } - - round = round.withResponse(.init(fileComments: fileComments)) - await CodeReviewService.shared.updateComments(fileComments) - await appendCodeReviewRound(round) - - round.status = .completed - await appendCodeReviewRound(round) - - resetOngoingRequest() - } - - public func cancelCodeReview(_ id: String) async { - guard activeRequestId != nil, isReceivingMessage else { return } - - guard var round = await getCurrentCodeReviewRound(id), - round.status.canTransitionTo(.cancelled) - else { return } - - round.status = .cancelled - await appendCodeReviewRound(round) - - resetOngoingRequest() - } - - private func addCodeReviewUserMessage(id: String, turnId: String, group: GitDiffGroup) async { - let content = group == .index - ? "Code review for staged changes." - : "Code review for unstaged changes." - let chatMessage = ChatMessage(userMessageWithId: id, chatTabId: chatTabInfo.id, content: content) - await memory.appendMessage(chatMessage) - saveChatMessageToStorage(chatMessage) - } -} diff --git a/Core/Sources/ChatService/CodeReview/CodeReviewProvider.swift b/Core/Sources/ChatService/CodeReview/CodeReviewProvider.swift deleted file mode 100644 index 2fddf1b3..00000000 --- a/Core/Sources/ChatService/CodeReview/CodeReviewProvider.swift +++ /dev/null @@ -1,59 +0,0 @@ -import ChatAPIService -import ConversationServiceProvider -import Foundation -import Logger -import GitHelper - -public struct CodeReviewServiceProvider { - public var conversationServiceProvider: (any ConversationServiceProvider)? -} - -public struct CodeReviewProvider { - public static func invoke( - _ request: CodeReviewRequest, - context: CodeReviewServiceProvider - ) async -> (fileComments: [CodeReviewResponse.FileComment], errorMessage: String?) { - var fileComments: [CodeReviewResponse.FileComment] = [] - var errorMessage: String? - - do { - if let result = try await requestReviewChanges(request.fileChange.selectedChanges, context: context) { - for comment in result.comments { - guard let change = request.fileChange.selectedChanges.first(where: { $0.uri == comment.uri }) else { - continue - } - - if let index = fileComments.firstIndex(where: { $0.uri == comment.uri }) { - var currentFileComments = fileComments[index] - currentFileComments.comments.append(comment) - fileComments[index] = currentFileComments - - } else { - fileComments.append( - .init(uri: change.uri, originalContent: change.originalContent, comments: [comment]) - ) - } - } - } - } catch { - Logger.gitHubCopilot.error("Failed to review change: \(error)") - errorMessage = "Oops, failed to review changes." - } - - return (fileComments, errorMessage) - } - - private static func requestReviewChanges( - _ changes: [PRChange], - context: CodeReviewServiceProvider - ) async throws -> CodeReviewResult? { - return try await context.conversationServiceProvider? - .reviewChanges( - .init( - changes: changes.map { - .init(uri: $0.uri, path: $0.path, baseContent: $0.baseContent, headContent: $0.headContent) - } - ) - ) - } -} diff --git a/Core/Sources/ChatService/CodeReview/CodeReviewService.swift b/Core/Sources/ChatService/CodeReview/CodeReviewService.swift deleted file mode 100644 index 4ae308d1..00000000 --- a/Core/Sources/ChatService/CodeReview/CodeReviewService.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Collections -import ConversationServiceProvider -import Foundation -import LanguageServerProtocol - -public struct DocumentReview: Equatable { - public var comments: [ReviewComment] - public let originalContent: String -} - -public typealias DocumentReviewsByUri = OrderedDictionary - -@MainActor -public class CodeReviewService: ObservableObject { - @Published public private(set) var documentReviews: DocumentReviewsByUri = [:] - - public static let shared = CodeReviewService() - - private init() {} - - public func updateComments(for uri: DocumentUri, comments: [ReviewComment], originalContent: String) { - if var existing = documentReviews[uri] { - existing.comments.append(contentsOf: comments) - existing.comments = sortedComments(existing.comments) - documentReviews[uri] = existing - } else { - documentReviews[uri] = .init(comments: comments, originalContent: originalContent) - } - } - - public func updateComments(_ fileComments: [CodeReviewResponse.FileComment]) { - for fileComment in fileComments { - updateComments( - for: fileComment.uri, - comments: fileComment.comments, - originalContent: fileComment.originalContent - ) - } - } - - private func sortedComments(_ comments: [ReviewComment]) -> [ReviewComment] { - return comments.sorted { $0.range.end.line < $1.range.end.line } - } - - public func resetComments() { - documentReviews = [:] - } -} diff --git a/Core/Sources/ChatService/ContextAwareAutoManagedChatMemory.swift b/Core/Sources/ChatService/ContextAwareAutoManagedChatMemory.swift deleted file mode 100644 index f185f9b1..00000000 --- a/Core/Sources/ChatService/ContextAwareAutoManagedChatMemory.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import ChatAPIService - -public final class ContextAwareAutoManagedChatMemory: ChatMemory { - private let memory: AutoManagedChatMemory - weak var chatService: ChatService? - - public var history: [ChatMessage] { - get async { await memory.history } - } - - func observeHistoryChange(_ observer: @escaping () -> Void) { - memory.observeHistoryChange(observer) - } - - init() { - memory = AutoManagedChatMemory( - systemPrompt: "" - ) - } - - deinit { } - - public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async { - await memory.mutateHistory(update) - } -} - diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift deleted file mode 100644 index 2a54d320..00000000 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ /dev/null @@ -1,53 +0,0 @@ -import AppKit -import Foundation -import SuggestionBasic -import XcodeInspector - -public struct CustomCommandTemplateProcessor { - public init() {} - - public func process(_ text: String) async -> String { - let info = await getEditorInformation() - let editorContent = info.editorContent - let updatedText = text - .replacingOccurrences(of: "{{selected_code}}", with: """ - \(editorContent?.selectedContent.trimmingCharacters(in: .whitespacesAndNewlines) ?? "") - """) - .replacingOccurrences( - of: "{{active_editor_language}}", - with: info.language.rawValue - ) - .replacingOccurrences( - of: "{{active_editor_file_url}}", - with: info.documentURL?.path ?? "" - ) - .replacingOccurrences( - of: "{{active_editor_file_name}}", - with: info.documentURL?.lastPathComponent ?? "" - ) - .replacingOccurrences( - of: "{{clipboard}}", - with: NSPasteboard.general.string(forType: .string) ?? "" - ) - return updatedText - } - - struct EditorInformation { - let editorContent: SourceEditor.Content? - let language: CodeLanguage - let documentURL: URL? - } - - func getEditorInformation() async -> EditorInformation { - let editorContent = await XcodeInspector.shared.safe.focusedEditor?.getContent() - let documentURL = await XcodeInspector.shared.safe.activeDocumentURL - let language = documentURL.map(languageIdentifierFromFileURL) ?? .plaintext - - return .init( - editorContent: editorContent, - language: language, - documentURL: documentURL - ) - } -} - diff --git a/Core/Sources/ChatService/Skills/ConversationSkill.swift b/Core/Sources/ChatService/Skills/ConversationSkill.swift deleted file mode 100644 index d7883b8e..00000000 --- a/Core/Sources/ChatService/Skills/ConversationSkill.swift +++ /dev/null @@ -1,10 +0,0 @@ -import JSONRPC -import GitHubCopilotService - -public typealias JSONRPCResponseHandler = (AnyJSONRPCResponse) -> Void - -public protocol ConversationSkill { - var id: String { get } - func applies(params: ConversationContextParams) -> Bool - func resolveSkill(request: ConversationContextRequest, completion: @escaping JSONRPCResponseHandler) -} diff --git a/Core/Sources/ChatService/Skills/CurrentEditorSkill.swift b/Core/Sources/ChatService/Skills/CurrentEditorSkill.swift deleted file mode 100644 index 5800820a..00000000 --- a/Core/Sources/ChatService/Skills/CurrentEditorSkill.swift +++ /dev/null @@ -1,46 +0,0 @@ -import ConversationServiceProvider -import Foundation -import GitHubCopilotService -import JSONRPC -import SystemUtils - -public class CurrentEditorSkill: ConversationSkill { - public static let ID = "current-editor" - public let currentFile: FileReference - public var id: String { - return CurrentEditorSkill.ID - } - public var currentFilePath: String { currentFile.url.path } - - public init( - currentFile: FileReference - ) { - self.currentFile = currentFile - } - - public func applies(params: ConversationContextParams) -> Bool { - return params.skillId == self.id - } - - public static let readabilityErrorMessageProvider: FileUtils.ReadabilityErrorMessageProvider = { status in - switch status { - case .readable: - return nil - case .notFound: - return "Copilot can’t find the current file, so it's not included." - case .permissionDenied: - return "Copilot can't access the current file. Enable \"Files & Folders\" access in [System Settings](x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders)." - } - } - - public func resolveSkill(request: ConversationContextRequest, completion: JSONRPCResponseHandler){ - let uri: String? = self.currentFile.url.absoluteString - completion( - AnyJSONRPCResponse(id: request.id, - result: JSONValue.array([ - JSONValue.hash(["uri" : .string(uri ?? "")]), - JSONValue.null - ])) - ) - } -} diff --git a/Core/Sources/ChatService/Skills/ProblemsInActiveDocumentSkill.swift b/Core/Sources/ChatService/Skills/ProblemsInActiveDocumentSkill.swift deleted file mode 100644 index 203872db..00000000 --- a/Core/Sources/ChatService/Skills/ProblemsInActiveDocumentSkill.swift +++ /dev/null @@ -1,52 +0,0 @@ -import ConversationServiceProvider -import Foundation -import GitHubCopilotService -import JSONRPC -import XcodeInspector - -public class ProblemsInActiveDocumentSkill: ConversationSkill { - public static let ID = "problems-in-active-document" - public var id: String { - return ProblemsInActiveDocumentSkill.ID - } - - public init() { - } - - public func applies(params: ConversationContextParams) -> Bool { - return params.skillId == self.id - } - - public func resolveSkill(request: ConversationContextRequest, completion: @escaping JSONRPCResponseHandler) { - Task { - let editor = await XcodeInspector.shared.getFocusedEditorContent() - let result: JSONValue = JSONValue.hash([ - "uri": JSONValue.string(editor?.documentURL.absoluteString ?? ""), - "problems": JSONValue.array(editor?.editorContent?.lineAnnotations.map { annotation in - JSONValue.hash([ - "message": JSONValue.string(annotation.message), - "range": JSONValue.hash([ - "start": JSONValue.hash([ - "line": JSONValue.number(Double(annotation.line)), - "character": JSONValue.number(0) - ]), - "end": JSONValue.hash([ - "line": JSONValue.number(Double(annotation.line)), - "character": JSONValue.number(0) - ]) - ]) - ]) - } ?? []) - ]) - - completion( - AnyJSONRPCResponse(id: request.id, - result: JSONValue.array([ - result, - JSONValue.null - ])) - ) - } - } -} - diff --git a/Core/Sources/ChatService/Skills/ProjectContextSkill.swift b/Core/Sources/ChatService/Skills/ProjectContextSkill.swift deleted file mode 100644 index 1575db9b..00000000 --- a/Core/Sources/ChatService/Skills/ProjectContextSkill.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import Workspace -import GitHubCopilotService -import JSONRPC -import XcodeInspector - -/* - * project-context is different from others - * 1. The CLS only request this skill once `after initialized` instead of during conversation / turn. - * 2. After resolved skill, a file watcher needs to be start for syncing file modification to CLS - */ -public class ProjectContextSkill { - public static let ID = "project-context" - public static let ProgressID = "collect-project-context" - - public static var resolvedWorkspace: Set = Set() - - public static func isWorkspaceResolved(_ path: String) -> Bool { - return ProjectContextSkill.resolvedWorkspace.contains(path) - } - - public init() { } - - /* - * The request from CLS only contain the projectPath (a initialization paramter for CLS) - * whereas to get files for xcode workspace, the workspacePath is needed. - */ - public static func resolveSkill( - request: WatchedFilesRequest, - workspacePath: String, - completion: JSONRPCResponseHandler - ) { - guard !ProjectContextSkill.isWorkspaceResolved(workspacePath) else {return } - - let params = request.params! - - guard params.workspaceFolder.uri != "/" else { return } - - /// build workspace URL - let workspaceURL = URL(fileURLWithPath: workspacePath) - /// refer to `init` in `Workspace` - let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL( - workspaceURL: workspaceURL, - documentURL: nil - ) ?? workspaceURL - - /// ignore invalid resolve request - guard projectURL.absoluteString == params.workspaceFolder.uri else { return } - - let files = WorkspaceFile.getWatchedFiles( - workspaceURL: workspaceURL, - projectURL: projectURL, - excludeGitIgnoredFiles: params.excludeGitignoredFiles, - excludeIDEIgnoredFiles: params.excludeIDEIgnoredFiles - ) - - let jsonResult = try? JSONEncoder().encode(["files": files]) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - - completion(AnyJSONRPCResponse(id: request.id, result: jsonValue)) - - ProjectContextSkill.resolvedWorkspace.insert(workspacePath) - } -} diff --git a/Core/Sources/ChatService/ToolCalls/CopilotToolRegistry.swift b/Core/Sources/ChatService/ToolCalls/CopilotToolRegistry.swift deleted file mode 100644 index f03d2fe5..00000000 --- a/Core/Sources/ChatService/ToolCalls/CopilotToolRegistry.swift +++ /dev/null @@ -1,19 +0,0 @@ -import ConversationServiceProvider - -public class CopilotToolRegistry { - public static let shared = CopilotToolRegistry() - private var tools: [String: ICopilotTool] = [:] - - private init() { - tools[ToolName.runInTerminal.rawValue] = RunInTerminalTool() - tools[ToolName.getTerminalOutput.rawValue] = GetTerminalOutputTool() - tools[ToolName.getErrors.rawValue] = GetErrorsTool() - tools[ToolName.insertEditIntoFile.rawValue] = InsertEditIntoFileTool() - tools[ToolName.createFile.rawValue] = CreateFileTool() - tools[ToolName.fetchWebPage.rawValue] = FetchWebPageTool() - } - - public func getTool(name: String) -> ICopilotTool? { - return tools[name] - } -} diff --git a/Core/Sources/ChatService/ToolCalls/CreateFileTool.swift b/Core/Sources/ChatService/ToolCalls/CreateFileTool.swift deleted file mode 100644 index f811901a..00000000 --- a/Core/Sources/ChatService/ToolCalls/CreateFileTool.swift +++ /dev/null @@ -1,101 +0,0 @@ -import JSONRPC -import AppKit -import ConversationServiceProvider -import Foundation -import Logger - -public class CreateFileTool: ICopilotTool { - public static let name = ToolName.createFile - - public func invokeTool( - _ request: InvokeClientToolRequest, - completion: @escaping (AnyJSONRPCResponse) -> Void, - chatHistoryUpdater: ChatHistoryUpdater?, - contextProvider: (any ToolContextProvider)? - ) -> Bool { - guard let params = request.params, - let input = params.input, - let filePath = input["filePath"]?.value as? String, - let content = input["content"]?.value as? String - else { - completeResponse(request, status: .error, response: "Invalid parameters", completion: completion) - return true - } - - let fileURL = URL(fileURLWithPath: filePath) - - guard !FileManager.default.fileExists(atPath: filePath) - else { - Logger.client.info("CreateFileTool: File already exists at \(filePath)") - completeResponse(request, status: .error, response: "File already exists at \(filePath)", completion: completion) - return true - } - - do { - // Create intermediate directories if they don't exist - let parentDirectory = fileURL.deletingLastPathComponent() - try FileManager.default.createDirectory(at: parentDirectory, withIntermediateDirectories: true, attributes: nil) - try content.write(to: fileURL, atomically: true, encoding: .utf8) - } catch { - Logger.client.error("CreateFileTool: Failed to write content to file at \(filePath): \(error)") - completeResponse(request, status: .error, response: "Failed to write content to file: \(error)", completion: completion) - return true - } - - guard FileManager.default.fileExists(atPath: filePath), - let writtenContent = try? String(contentsOf: fileURL, encoding: .utf8) - else { - Logger.client.info("CreateFileTool: Failed to verify file creation at \(filePath)") - completeResponse(request, status: .error, response: "Failed to verify file creation.", completion: completion) - return true - } - - contextProvider?.updateFileEdits(by: .init( - fileURL: URL(fileURLWithPath: filePath), - originalContent: "", - modifiedContent: writtenContent, - toolName: CreateFileTool.name - )) - - NSWorkspace.openFileInXcode(fileURL: URL(fileURLWithPath: filePath)) { _, error in - if let error = error { - Logger.client.info("Failed to open file at \(filePath), \(error)") - } - } - - let editAgentRounds: [AgentRound] = [ - .init( - roundId: params.roundId, - reply: "", - toolCalls: [ - .init( - id: params.toolCallId, - name: params.name, - status: .completed, - invokeParams: params - ) - ] - ) - ] - - if let chatHistoryUpdater { - chatHistoryUpdater(params.turnId, editAgentRounds) - } - - completeResponse( - request, - response: "File created at \(filePath).", - completion: completion - ) - return true - } - - public static func undo(for fileURL: URL) throws { - var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory), - !isDirectory.boolValue - else { return } - - try FileManager.default.removeItem(at: fileURL) - } -} diff --git a/Core/Sources/ChatService/ToolCalls/FetchWebPageTool.swift b/Core/Sources/ChatService/ToolCalls/FetchWebPageTool.swift deleted file mode 100644 index 5ff5f6b9..00000000 --- a/Core/Sources/ChatService/ToolCalls/FetchWebPageTool.swift +++ /dev/null @@ -1,46 +0,0 @@ -import AppKit -import AXExtension -import AXHelper -import ConversationServiceProvider -import Foundation -import JSONRPC -import Logger -import WebKit -import WebContentExtractor - -public class FetchWebPageTool: ICopilotTool { - public static let name = ToolName.fetchWebPage - - public func invokeTool( - _ request: InvokeClientToolRequest, - completion: @escaping (AnyJSONRPCResponse) -> Void, - chatHistoryUpdater: ChatHistoryUpdater?, - contextProvider: (any ToolContextProvider)? - ) -> Bool { - guard let params = request.params, - let input = params.input, - let urls = input["urls"]?.value as? [String] - else { - completeResponse(request, status: .error, response: "Invalid parameters", completion: completion) - return true - } - - guard !urls.isEmpty else { - completeResponse(request, status: .error, response: "No valid URLs provided", completion: completion) - return true - } - - // Use the improved WebContentFetcher to fetch content from all URLs - Task { - let results = await WebContentFetcher.fetchMultipleContentAsync(from: urls) - - completeResponses( - request, - responses: results, - completion: completion - ) - } - - return true - } -} diff --git a/Core/Sources/ChatService/ToolCalls/GetErrorsTool.swift b/Core/Sources/ChatService/ToolCalls/GetErrorsTool.swift deleted file mode 100644 index f95625dc..00000000 --- a/Core/Sources/ChatService/ToolCalls/GetErrorsTool.swift +++ /dev/null @@ -1,74 +0,0 @@ -import JSONRPC -import Foundation -import ConversationServiceProvider -import XcodeInspector -import AppKit - -public class GetErrorsTool: ICopilotTool { - public func invokeTool( - _ request: InvokeClientToolRequest, - completion: @escaping (AnyJSONRPCResponse) -> Void, - chatHistoryUpdater: ChatHistoryUpdater?, - contextProvider: ToolContextProvider? - ) -> Bool { - guard let params = request.params, - let input = params.input, - let filePaths = input["filePaths"]?.value as? [String] - else { - completeResponse(request, completion: completion) - return true - } - - guard let xcodeInstance = XcodeInspector.shared.xcodes.first( - where: { - $0.workspaceURL?.path == contextProvider?.chatTabInfo.workspacePath - }), - let documentURL = xcodeInstance.realtimeDocumentURL, - filePaths.contains(where: { URL(fileURLWithPath: $0) == documentURL }) - else { - completeResponse(request, completion: completion) - return true - } - - /// Not leveraging the `getFocusedEditorContent` in `XcodeInspector`. - /// As the resolving should be sync. Especially when completion the JSONRPCResponse - let focusedElement: AXUIElement? = try? xcodeInstance.appElement.copyValue(key: kAXFocusedUIElementAttribute) - let focusedEditor: SourceEditor? - if let editorElement = focusedElement, editorElement.isSourceEditor { - focusedEditor = .init(runningApplication: xcodeInstance.runningApplication, element: editorElement) - } else if let element = focusedElement, let editorElement = element.firstParent(where: \.isSourceEditor) { - focusedEditor = .init(runningApplication: xcodeInstance.runningApplication, element: editorElement) - } else { - focusedEditor = nil - } - - var errors: String = "" - - if let focusedEditor - { - let editorContent = focusedEditor.getContent() - let errorArray: [String] = editorContent.lineAnnotations.map { - """ - \(documentURL.absoluteString) - - \($0.message) - - - \($0.line) - 0 - - - \($0.line) - 0 - - - - """ - } - errors = errorArray.joined(separator: "\n") - } - - completeResponse(request, response: errors, completion: completion) - return true - } -} diff --git a/Core/Sources/ChatService/ToolCalls/GetTerminalOutputTool.swift b/Core/Sources/ChatService/ToolCalls/GetTerminalOutputTool.swift deleted file mode 100644 index 1d298711..00000000 --- a/Core/Sources/ChatService/ToolCalls/GetTerminalOutputTool.swift +++ /dev/null @@ -1,33 +0,0 @@ -import ConversationServiceProvider -import Foundation -import JSONRPC -import Terminal - -public class GetTerminalOutputTool: ICopilotTool { - public func invokeTool(_ request: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void, chatHistoryUpdater: ChatHistoryUpdater?, contextProvider: (any ToolContextProvider)?) -> Bool { - var result: String = "" - if let input = request.params?.input as? [String: AnyCodable], let terminalId = input["id"]?.value as? String{ - let session = TerminalSessionManager.shared.getSession(for: terminalId) - result = session?.getCommandOutput() ?? "Terminal id \(terminalId) not found" - } else { - result = "Invalid arguments for \(ToolName.getTerminalOutput.rawValue) tool call" - } - - let toolResult = LanguageModelToolResult(content: [ - .init(value: result) - ]) - let jsonResult = try? JSONEncoder().encode(toolResult) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - completion( - AnyJSONRPCResponse( - id: request.id, - result: JSONValue.array([ - jsonValue, - JSONValue.null - ]) - ) - ) - - return true - } -} diff --git a/Core/Sources/ChatService/ToolCalls/ICopilotTool.swift b/Core/Sources/ChatService/ToolCalls/ICopilotTool.swift deleted file mode 100644 index cbe9e2ec..00000000 --- a/Core/Sources/ChatService/ToolCalls/ICopilotTool.swift +++ /dev/null @@ -1,88 +0,0 @@ -import ChatTab -import ConversationServiceProvider -import Foundation -import JSONRPC - -public protocol ToolContextProvider { - // MARK: insert_edit_into_file - var chatTabInfo: ChatTabInfo { get } - func updateFileEdits(by fileEdit: FileEdit) -> Void - func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws -} - -public typealias ChatHistoryUpdater = (String, [AgentRound]) -> Void - -public protocol ICopilotTool { - /** - * Invokes the Copilot tool with the given request. - * - Parameters: - * - request: The tool invocation request. - * - completion: Closure called with JSON-RPC response when tool execution completes. - * - chatHistoryUpdater: Optional closure to update chat history during tool execution. - * - contextProvider: Optional provider that supplies additional context information - * needed for tool execution, such as chat tab data and file editing capabilities. - * - Returns: Boolean indicating if the tool call has completed. True if the tool call is completed, false otherwise. - */ - func invokeTool( - _ request: InvokeClientToolRequest, - completion: @escaping (AnyJSONRPCResponse) -> Void, - chatHistoryUpdater: ChatHistoryUpdater?, - contextProvider: ToolContextProvider? - ) -> Bool -} - -extension ICopilotTool { - /** - * Completes a tool response. - * - Parameters: - * - request: The original tool invocation request. - * - status: The completion status of the tool execution (success, error, or cancelled). - * - response: The string value to include in the response content. - * - completion: The completion handler to call with the response. - */ - func completeResponse( - _ request: InvokeClientToolRequest, - status: ToolInvocationStatus = .success, - response: String = "", - completion: @escaping (AnyJSONRPCResponse) -> Void - ) { - completeResponses( - request, - status: status, - responses: [response], - completion: completion - ) - } - - /// - /// Completes a tool response with multiple data entries. - /// - Parameters: - /// - request: The original tool invocation request. - /// - status: The completion status of the tool execution (success, error, or cancelled). - /// - responses: Array of string values to include in the response content. - /// - completion: The completion handler to call with the response. - /// - func completeResponses( - _ request: InvokeClientToolRequest, - status: ToolInvocationStatus = .success, - responses: [String], - completion: @escaping (AnyJSONRPCResponse) -> Void - ) { - let toolResult = LanguageModelToolResult(status: status, content: responses.map { response in - LanguageModelToolResult.Content(value: response) - }) - let jsonResult = try? JSONEncoder().encode(toolResult) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - completion( - AnyJSONRPCResponse( - id: request.id, - result: JSONValue.array([ - jsonValue, - JSONValue.null, - ]) - ) - ) - } -} - -extension ChatService: ToolContextProvider { } diff --git a/Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift b/Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift deleted file mode 100644 index db89c57c..00000000 --- a/Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift +++ /dev/null @@ -1,209 +0,0 @@ -import AppKit -import AXExtension -import AXHelper -import ConversationServiceProvider -import Foundation -import JSONRPC -import Logger -import XcodeInspector - -public class InsertEditIntoFileTool: ICopilotTool { - public static let name = ToolName.insertEditIntoFile - - public func invokeTool( - _ request: InvokeClientToolRequest, - completion: @escaping (AnyJSONRPCResponse) -> Void, - chatHistoryUpdater: ChatHistoryUpdater?, - contextProvider: (any ToolContextProvider)? - ) -> Bool { - guard let params = request.params, - let input = request.params?.input, - let code = input["code"]?.value as? String, - let filePath = input["filePath"]?.value as? String, - let contextProvider - else { - completeResponse(request, status: .error, response: "Invalid parameters", completion: completion) - return true - } - - do { - let fileURL = URL(fileURLWithPath: filePath) - let originalContent = try String(contentsOf: fileURL, encoding: .utf8) - - InsertEditIntoFileTool.applyEdit(for: fileURL, content: code, contextProvider: contextProvider) { newContent, error in - if let error = error { - self.completeResponse( - request, - status: .error, - response: error.localizedDescription, - completion: completion - ) - return - } - - guard let newContent = newContent - else { - self.completeResponse(request, status: .error, response: "Failed to apply edit", completion: completion) - return - } - - contextProvider.updateFileEdits( - by: .init(fileURL: fileURL, originalContent: originalContent, modifiedContent: code, toolName: InsertEditIntoFileTool.name) - ) - - let editAgentRounds: [AgentRound] = [ - .init( - roundId: params.roundId, - reply: "", - toolCalls: [ - .init( - id: params.toolCallId, - name: params.name, - status: .completed, - invokeParams: params - ) - ] - ) - ] - - if let chatHistoryUpdater { - chatHistoryUpdater(params.turnId, editAgentRounds) - } - - self.completeResponse(request, response: newContent, completion: completion) - } - - } catch { - completeResponse( - request, - status: .error, - response: error.localizedDescription, - completion: completion - ) - } - - return true - } - - public static func applyEdit( - for fileURL: URL, - content: String, - contextProvider: any ToolContextProvider, - xcodeInstance: AppInstanceInspector - ) throws -> String { - // Get the focused element directly from the app (like XcodeInspector does) - guard let focusedElement: AXUIElement = try? xcodeInstance.appElement.copyValue(key: kAXFocusedUIElementAttribute) - else { - throw NSError(domain: "Failed to access xcode element", code: 0) - } - - // Find the source editor element using XcodeInspector's logic - guard let editorElement = focusedElement.findSourceEditorElement() else { - throw NSError(domain: "Could not find source editor element", code: 0) - } - - // Check if element supports kAXValueAttribute before reading - var value: String = "" - do { - value = try editorElement.copyValue(key: kAXValueAttribute) - } catch { - if let axError = error as? AXError { - Logger.client.error("AX Error code: \(axError.rawValue)") - } - throw error - } - - let lines = value.components(separatedBy: .newlines) - - var isInjectedSuccess = false - var injectionError: Error? - - do { - try AXHelper().injectUpdatedCodeWithAccessibilityAPI( - .init( - content: content, - newSelection: nil, - modifications: [ - .deletedSelection( - .init(start: .init(line: 0, character: 0), end: .init(line: lines.count - 1, character: (lines.last?.count ?? 100) - 1)) - ), - .inserted(0, [content]) - ] - ), - focusElement: editorElement, - onSuccess: { - Logger.client.info("Content injection succeeded") - isInjectedSuccess = true - }, - onError: { - Logger.client.error("Content injection failed in onError callback") - } - ) - } catch { - Logger.client.error("Content injection threw error: \(error)") - if let axError = error as? AXError { - Logger.client.error("AX Error code during injection: \(axError.rawValue)") - } - injectionError = error - } - - if !isInjectedSuccess { - let errorMessage = injectionError?.localizedDescription ?? "Failed to apply edit" - Logger.client.error("Edit application failed: \(errorMessage)") - throw NSError(domain: "Failed to apply edit: \(errorMessage)", code: 0) - } - - // Verify the content was applied by reading it back - do { - let newContent: String = try editorElement.copyValue(key: kAXValueAttribute) - Logger.client.info("Successfully read back new content, length: \(newContent.count)") - return newContent - } catch { - Logger.client.error("Failed to read back new content: \(error)") - if let axError = error as? AXError { - Logger.client.error("AX Error code when reading back: \(axError.rawValue)") - } - throw error - } - } - - public static func applyEdit( - for fileURL: URL, - content: String, - contextProvider: any ToolContextProvider, - completion: ((String?, Error?) -> Void)? = nil - ) { - NSWorkspace.openFileInXcode(fileURL: fileURL) { app, error in - do { - if let error = error { throw error } - - guard let app = app - else { - throw NSError(domain: "Failed to get the app that opens file.", code: 0) - } - - let appInstanceInspector = AppInstanceInspector(runningApplication: app) - guard appInstanceInspector.isXcode - else { - throw NSError(domain: "The file is not opened in Xcode.", code: 0) - } - - let newContent = try applyEdit( - for: fileURL, - content: content, - contextProvider: contextProvider, - xcodeInstance: appInstanceInspector - ) - - Task { - // Force to notify the CLS about the new change within the document before edit_file completion. - try? await contextProvider.notifyChangeTextDocument(fileURL: fileURL, content: newContent, version: 0) - if let completion = completion { completion(newContent, nil) } - } - } catch { - if let completion = completion { completion(nil, error) } - Logger.client.info("Failed to apply edit for file at \(fileURL), \(error)") - } - } - } -} diff --git a/Core/Sources/ChatService/ToolCalls/RunInTerminalTool.swift b/Core/Sources/ChatService/ToolCalls/RunInTerminalTool.swift deleted file mode 100644 index fba3e4a0..00000000 --- a/Core/Sources/ChatService/ToolCalls/RunInTerminalTool.swift +++ /dev/null @@ -1,42 +0,0 @@ -import ConversationServiceProvider -import Terminal -import XcodeInspector -import JSONRPC - -public class RunInTerminalTool: ICopilotTool { - public func invokeTool(_ request: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void, chatHistoryUpdater: ChatHistoryUpdater?, contextProvider: (any ToolContextProvider)?) -> Bool { - let params = request.params! - - Task { - var currentDirectory: String = "" - if let workspacePath = contextProvider?.chatTabInfo.workspacePath, - let xcodeIntance = Utils.getXcode(by: workspacePath) { - currentDirectory = xcodeIntance.realtimeProjectURL?.path ?? xcodeIntance.projectRootURL?.path ?? "" - } else { - currentDirectory = await XcodeInspector.shared.safe.realtimeActiveProjectURL?.path ?? "" - } - if let input = params.input { - let command = input["command"]?.value as? String - let isBackground = input["isBackground"]?.value as? Bool - let toolId = params.toolCallId - let session = TerminalSessionManager.shared.createSession(for: toolId) - if isBackground == true { - session.executeCommand( - currentDirectory: currentDirectory, - command: command!) { result in - // do nothing - } - completeResponse(request, response: "Command is running in terminal with ID=\(toolId)", completion: completion) - } else { - session.executeCommand( - currentDirectory: currentDirectory, - command: command!) { result in - self.completeResponse(request, response: result.output, completion: completion) - } - } - } - } - - return true - } -} diff --git a/Core/Sources/ChatService/ToolCalls/Utils.swift b/Core/Sources/ChatService/ToolCalls/Utils.swift deleted file mode 100644 index 507714cf..00000000 --- a/Core/Sources/ChatService/ToolCalls/Utils.swift +++ /dev/null @@ -1,14 +0,0 @@ -import AppKit -import AppKitExtension -import Foundation -import Logger -import XcodeInspector - -class Utils { - public static func getXcode(by workspacePath: String) -> XcodeAppInstanceInspector? { - return XcodeInspector.shared.xcodes.first( - where: { - $0.workspaceURL?.path == workspacePath - }) - } -} diff --git a/Core/Sources/Client/XPCService.swift b/Core/Sources/Client/XPCService.swift deleted file mode 100644 index 24a50bab..00000000 --- a/Core/Sources/Client/XPCService.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import Logger -import os.log -import XPCShared - -let shared = XPCExtensionService(logger: .client) - -public func getService() throws -> XPCExtensionService { - if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { - struct RunningInPreview: Error {} - throw RunningInPreview() - } - return shared -} diff --git a/Core/Sources/ConversationTab/Chat.swift b/Core/Sources/ConversationTab/Chat.swift deleted file mode 100644 index 2d1a68c1..00000000 --- a/Core/Sources/ConversationTab/Chat.swift +++ /dev/null @@ -1,634 +0,0 @@ -import ChatService -import ComposableArchitecture -import Foundation -import ChatAPIService -import Preferences -import Terminal -import ConversationServiceProvider -import Persist -import GitHubCopilotService -import Logger -import OrderedCollections -import SwiftUI -import GitHelper - -public struct DisplayedChatMessage: Equatable { - public enum Role: Equatable { - case user - case assistant - case ignored - } - - public var id: String - public var role: Role - public var text: String - public var imageReferences: [ImageReference] = [] - public var references: [ConversationReference] = [] - public var followUp: ConversationFollowUp? = nil - public var suggestedTitle: String? = nil - public var errorMessages: [String] = [] - public var steps: [ConversationProgressStep] = [] - public var editAgentRounds: [AgentRound] = [] - public var panelMessages: [CopilotShowMessageParams] = [] - public var codeReviewRound: CodeReviewRound? = nil - - public init( - id: String, - role: Role, - text: String, - imageReferences: [ImageReference] = [], - references: [ConversationReference] = [], - followUp: ConversationFollowUp? = nil, - suggestedTitle: String? = nil, - errorMessages: [String] = [], - steps: [ConversationProgressStep] = [], - editAgentRounds: [AgentRound] = [], - panelMessages: [CopilotShowMessageParams] = [], - codeReviewRound: CodeReviewRound? = nil - ) { - self.id = id - self.role = role - self.text = text - self.imageReferences = imageReferences - self.references = references - self.followUp = followUp - self.suggestedTitle = suggestedTitle - self.errorMessages = errorMessages - self.steps = steps - self.editAgentRounds = editAgentRounds - self.panelMessages = panelMessages - self.codeReviewRound = codeReviewRound - } -} - -private var isPreview: Bool { - ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" -} - -@Reducer -struct Chat { - public typealias MessageID = String - - @ObservableState - struct State: Equatable { - // Not use anymore. the title of history tab will get from chat tab info - // Keep this var as `ChatTabItemView` reference this - var title: String = "New Chat" - var typedMessage = "" - var history: [DisplayedChatMessage] = [] - var isReceivingMessage = false - var requestType: ChatService.RequestType? = nil - var chatMenu = ChatMenu.State() - var focusedField: Field? - var currentEditor: FileReference? = nil - var selectedFiles: [FileReference] = [] - var attachedImages: [ImageReference] = [] - /// Cache the original content - var fileEditMap: OrderedDictionary = [:] - var diffViewerController: DiffViewWindowController? = nil - var isAgentMode: Bool = AppState.shared.isAgentModeEnabled() - var workspaceURL: URL? = nil - enum Field: String, Hashable { - case textField - case fileSearchBar - } - - var codeReviewState = ConversationCodeReviewFeature.State() - } - - enum Action: Equatable, BindableAction { - case binding(BindingAction) - - case appear - case refresh - case sendButtonTapped(String) - case returnButtonTapped - case stopRespondingButtonTapped - case clearButtonTap - case deleteMessageButtonTapped(MessageID) - case resendMessageButtonTapped(MessageID) - case setAsExtraPromptButtonTapped(MessageID) - case focusOnTextField - case referenceClicked(ConversationReference) - case upvote(MessageID, ConversationRating) - case downvote(MessageID, ConversationRating) - case copyCode(MessageID) - case insertCode(String) - case toolCallAccepted(String) - case toolCallCompleted(String, String) - case toolCallCancelled(String) - - case observeChatService - case observeHistoryChange - case observeIsReceivingMessageChange - case observeFileEditChange - - case historyChanged - case isReceivingMessageChanged - case fileEditChanged - - case chatMenu(ChatMenu.Action) - - // File context - case addSelectedFile(FileReference) - case removeSelectedFile(FileReference) - case resetCurrentEditor - case setCurrentEditor(FileReference) - - // Image context - case addSelectedImage(ImageReference) - case removeSelectedImage(ImageReference) - - case followUpButtonClicked(String, String) - - // Agent File Edit - case undoEdits(fileURLs: [URL]) - case keepEdits(fileURLs: [URL]) - case resetEdits - case discardFileEdits(fileURLs: [URL]) - case openDiffViewWindow(fileURL: URL) - case setDiffViewerController(chat: StoreOf) - - case agentModeChanged(Bool) - - // Code Review - case codeReview(ConversationCodeReviewFeature.Action) - } - - let service: ChatService - let id = UUID() - - enum CancelID: Hashable { - case observeHistoryChange(UUID) - case observeIsReceivingMessageChange(UUID) - case sendMessage(UUID) - case observeFileEditChange(UUID) - } - - @Dependency(\.openURL) var openURL - @AppStorage(\.enableCurrentEditorContext) var enableCurrentEditorContext: Bool - @AppStorage(\.chatResponseLocale) var chatResponseLocale - - var body: some ReducerOf { - BindingReducer() - - Scope(state: \.chatMenu, action: /Action.chatMenu) { - ChatMenu(service: service) - } - - Scope(state: \.codeReviewState, action: /Action.codeReview) { - ConversationCodeReviewFeature(service: service) - } - - Reduce { state, action in - switch action { - case .appear: - return .run { send in - if isPreview { return } - await send(.observeChatService) - await send(.historyChanged) - await send(.isReceivingMessageChanged) - await send(.focusOnTextField) - await send(.refresh) - - let publisher = NotificationCenter.default.publisher(for: .gitHubCopilotChatModeDidChange) - for await _ in publisher.values { - let isAgentMode = AppState.shared.isAgentModeEnabled() - await send(.agentModeChanged(isAgentMode)) - } - } - - case .refresh: - return .run { send in - await send(.chatMenu(.refresh)) - } - - case let .sendButtonTapped(id): - guard !state.typedMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return .none } - let message = state.typedMessage - let skillSet = state.buildSkillSet( - isCurrentEditorContextEnabled: enableCurrentEditorContext - ) - state.typedMessage = "" - - let selectedFiles = state.selectedFiles - let selectedModelFamily = AppState.shared.getSelectedModelFamily() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.modelFamily - let agentMode = AppState.shared.isAgentModeEnabled() - - let shouldAttachImages = AppState.shared.isSelectedModelSupportVision() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.supportVision ?? false - let attachedImages: [ImageReference] = shouldAttachImages ? state.attachedImages : [] - state.attachedImages = [] - return .run { _ in - try await service - .send( - id, - content: message, - contentImageReferences: attachedImages, - skillSet: skillSet, - references: selectedFiles, - model: selectedModelFamily, - agentMode: agentMode, - userLanguage: chatResponseLocale - ) - }.cancellable(id: CancelID.sendMessage(self.id)) - - case let .toolCallAccepted(toolCallId): - guard !toolCallId.isEmpty else { return .none } - return .run { _ in - service.updateToolCallStatus(toolCallId: toolCallId, status: .accepted) - }.cancellable(id: CancelID.sendMessage(self.id)) - case let .toolCallCancelled(toolCallId): - guard !toolCallId.isEmpty else { return .none } - return .run { _ in - service.updateToolCallStatus(toolCallId: toolCallId, status: .cancelled) - }.cancellable(id: CancelID.sendMessage(self.id)) - case let .toolCallCompleted(toolCallId, result): - guard !toolCallId.isEmpty else { return .none } - return .run { _ in - service.updateToolCallStatus(toolCallId: toolCallId, status: .completed, payload: result) - }.cancellable(id: CancelID.sendMessage(self.id)) - - case let .followUpButtonClicked(id, message): - guard !message.isEmpty else { return .none } - let skillSet = state.buildSkillSet( - isCurrentEditorContextEnabled: enableCurrentEditorContext - ) - - let selectedFiles = state.selectedFiles - let selectedModelFamily = AppState.shared.getSelectedModelFamily() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.modelFamily - - return .run { _ in - try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily, userLanguage: chatResponseLocale) - }.cancellable(id: CancelID.sendMessage(self.id)) - - case .returnButtonTapped: - state.typedMessage += "\n" - return .none - - case .stopRespondingButtonTapped: - return .merge( - .run { _ in - await service.stopReceivingMessage() - }, - .cancel(id: CancelID.sendMessage(id)) - ) - - case .clearButtonTap: - return .run { _ in - await service.clearHistory() - } - - case let .deleteMessageButtonTapped(id): - return .run { _ in - await service.deleteMessage(id: id) - } - - case let .resendMessageButtonTapped(id): - return .run { _ in - try await service.resendMessage(id: id) - } - - case let .setAsExtraPromptButtonTapped(id): - return .run { _ in - await service.setMessageAsExtraPrompt(id: id) - } - - case let .referenceClicked(reference): - guard let fileURL = reference.url else { - return .none - } - return .run { _ in - if FileManager.default.fileExists(atPath: fileURL.path) { - let terminal = Terminal() - do { - _ = try await terminal.runCommand( - "/bin/bash", - arguments: [ - "-c", - "xed -l 0 \"\(reference.filePath)\"", - ], - environment: [:] - ) - } catch { - print(error) - } - } else if let url = URL(string: reference.uri), url.scheme != nil { - await openURL(url) - } - } - - case .focusOnTextField: - state.focusedField = .textField - return .none - - case .observeChatService: - return .run { send in - await send(.observeHistoryChange) - await send(.observeIsReceivingMessageChange) - await send(.observeFileEditChange) - } - - case .observeHistoryChange: - return .run { send in - let stream = AsyncStream { continuation in - let cancellable = service.$chatHistory.sink { _ in - continuation.yield() - } - continuation.onTermination = { _ in - cancellable.cancel() - } - } - let debouncedHistoryChange = TimedDebounceFunction(duration: 0.2) { - await send(.historyChanged) - } - - for await _ in stream { - await debouncedHistoryChange() - } - }.cancellable(id: CancelID.observeHistoryChange(id), cancelInFlight: true) - - case .observeIsReceivingMessageChange: - return .run { send in - let stream = AsyncStream { continuation in - let cancellable = service.$isReceivingMessage - .sink { _ in - continuation.yield() - } - continuation.onTermination = { _ in - cancellable.cancel() - } - } - for await _ in stream { - await send(.isReceivingMessageChanged) - } - }.cancellable( - id: CancelID.observeIsReceivingMessageChange(id), - cancelInFlight: true - ) - - case .observeFileEditChange: - return .run { send in - let stream = AsyncStream { continuation in - let cancellable = service.$fileEditMap - .sink { _ in - continuation.yield() - } - continuation.onTermination = { _ in - cancellable.cancel() - } - } - for await _ in stream { - await send(.fileEditChanged) - } - }.cancellable( - id: CancelID.observeFileEditChange(id), - cancelInFlight: true - ) - - case .historyChanged: - state.history = service.chatHistory.flatMap { message in - var all = [DisplayedChatMessage]() - all.append(.init( - id: message.id, - role: { - switch message.role { - case .user: return .user - case .assistant: return .assistant - case .system: return .ignored - } - }(), - text: message.content, - imageReferences: message.contentImageReferences, - references: message.references.map { - .init( - uri: $0.uri, - status: $0.status, - kind: $0.kind - ) - }, - followUp: message.followUp, - suggestedTitle: message.suggestedTitle, - errorMessages: message.errorMessages, - steps: message.steps, - editAgentRounds: message.editAgentRounds, - panelMessages: message.panelMessages, - codeReviewRound: message.codeReviewRound - )) - - return all - } - - return .none - - case .isReceivingMessageChanged: - state.isReceivingMessage = service.isReceivingMessage - state.requestType = service.requestType - return .none - - case .fileEditChanged: - state.fileEditMap = service.fileEditMap - let fileEditMap = state.fileEditMap - - let diffViewerController = state.diffViewerController - - return .run { _ in - /// refresh diff view - - guard let diffViewerController, - diffViewerController.diffViewerState == .shown - else { return } - - if fileEditMap.isEmpty { - await diffViewerController.hideWindow() - return - } - - guard let currentFileEdit = diffViewerController.currentFileEdit - else { return } - - if let updatedFileEdit = fileEditMap[currentFileEdit.fileURL] { - if updatedFileEdit != currentFileEdit { - if updatedFileEdit.status == .undone, - updatedFileEdit.toolName == .createFile - { - await diffViewerController.hideWindow() - } else { - await diffViewerController.showDiffWindow(fileEdit: updatedFileEdit) - } - } - } else { - await diffViewerController.hideWindow() - } - } - - case .binding: - return .none - - case .chatMenu: - return .none - case let .upvote(id, rating): - return .run { _ in - await service.upvote(id, rating) - } - case let .downvote(id, rating): - return .run { _ in - await service.downvote(id, rating) - } - case let .copyCode(id): - return .run { _ in - await service.copyCode(id) - } - - case let .insertCode(code): - ChatInjector().insertCodeBlock(codeBlock: code) - return .none - - // MARK: - File Context - case let .addSelectedFile(fileReference): - guard !state.selectedFiles.contains(fileReference) else { return .none } - state.selectedFiles.append(fileReference) - return .none - case let .removeSelectedFile(fileReference): - guard let index = state.selectedFiles.firstIndex(of: fileReference) else { return .none } - state.selectedFiles.remove(at: index) - return .none - case .resetCurrentEditor: - state.currentEditor = nil - return .none - case let .setCurrentEditor(fileReference): - state.currentEditor = fileReference - return .none - - // MARK: - Image Context - case let .addSelectedImage(imageReference): - guard !state.attachedImages.contains(imageReference) else { return .none } - state.attachedImages.append(imageReference) - return .none - case let .removeSelectedImage(imageReference): - guard let index = state.attachedImages.firstIndex(of: imageReference) else { return .none } - state.attachedImages.remove(at: index) - return .none - - // MARK: - Agent Edits - - case let .undoEdits(fileURLs): - for fileURL in fileURLs { - do { - try service.undoFileEdit(for: fileURL) - } catch { - Logger.service.error("Failed to undo edit, \(error)") - } - } - - return .none - - case let .keepEdits(fileURLs): - for fileURL in fileURLs { - service.keepFileEdit(for: fileURL) - } - - return .none - - case .resetEdits: - service.resetFileEdits() - - return .none - - case let .discardFileEdits(fileURLs): - for fileURL in fileURLs { - try? service.discardFileEdit(for: fileURL) - } - return .none - - case let .openDiffViewWindow(fileURL): - guard let fileEdit = state.fileEditMap[fileURL], - let diffViewerController = state.diffViewerController - else { return .none } - - return .run { _ in - await diffViewerController.showDiffWindow(fileEdit: fileEdit) - } - - case let .setDiffViewerController(chat): - state.diffViewerController = .init(chat: chat) - return .none - - case let .agentModeChanged(isAgentMode): - state.isAgentMode = isAgentMode - return .none - - case .codeReview: - return .none - } - } - } -} - -@Reducer -struct ChatMenu { - @ObservableState - struct State: Equatable { - var systemPrompt: String = "" - var extraSystemPrompt: String = "" - var temperatureOverride: Double? = nil - var chatModelIdOverride: String? = nil - } - - enum Action: Equatable { - case appear - case refresh - case customCommandButtonTapped(CustomCommand) - } - - let service: ChatService - - var body: some ReducerOf { - Reduce { state, action in - switch action { - case .appear: - return .run { - await $0(.refresh) - } - - case .refresh: - return .none - - case let .customCommandButtonTapped(command): - return .run { _ in - try await service.handleCustomCommand(command) - } - } - } - } -} - -private actor TimedDebounceFunction { - let duration: TimeInterval - let block: () async -> Void - - var task: Task? - var lastFireTime: Date = .init(timeIntervalSince1970: 0) - - init(duration: TimeInterval, block: @escaping () async -> Void) { - self.duration = duration - self.block = block - } - - func callAsFunction() async { - task?.cancel() - if lastFireTime.timeIntervalSinceNow < -duration { - await fire() - task = nil - } else { - task = Task.detached { [weak self, duration] in - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) - await self?.fire() - } - } - } - - func fire() async { - lastFireTime = Date() - await block() - } -} diff --git a/Core/Sources/ConversationTab/ChatContextMenu.swift b/Core/Sources/ConversationTab/ChatContextMenu.swift deleted file mode 100644 index 3e1ac095..00000000 --- a/Core/Sources/ConversationTab/ChatContextMenu.swift +++ /dev/null @@ -1,87 +0,0 @@ -import AppKit -import ChatService -import ComposableArchitecture -import SharedUIComponents -import SwiftUI - -struct ChatTabItemView: View { - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - Text(chat.title) - } - } -} - -struct ChatConversationItemView: View { - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - Text(chat.title) - .frame(alignment: .leading) - } - } -} - -struct ChatContextMenu: View { - let store: StoreOf - @AppStorage(\.customCommands) var customCommands - - var body: some View { - WithPerceptionTracking { - currentSystemPrompt - .onAppear { store.send(.appear) } - currentExtraSystemPrompt - - Divider() - - customCommandMenu - } - } - - @ViewBuilder - var currentSystemPrompt: some View { - Text("System Prompt:") - Text({ - var text = store.systemPrompt - if text.isEmpty { text = "N/A" } - if text.count > 30 { text = String(text.prefix(30)) + "..." } - return text - }() as String) - } - - @ViewBuilder - var currentExtraSystemPrompt: some View { - Text("Extra Prompt:") - Text({ - var text = store.extraSystemPrompt - if text.isEmpty { text = "N/A" } - if text.count > 30 { text = String(text.prefix(30)) + "..." } - return text - }() as String) - } - - var customCommandMenu: some View { - Menu("Custom Commands") { - ForEach( - customCommands.filter { - switch $0.feature { - case .chatWithSelection, .customChat: return true - case .promptToCode: return false - case .singleRoundDialog: return false - } - }, - id: \.name - ) { command in - Button(action: { - store.send(.customCommandButtonTapped(command)) - }) { - Text(command.name) - } - } - } - } -} - diff --git a/Core/Sources/ConversationTab/ChatDropdownView.swift b/Core/Sources/ConversationTab/ChatDropdownView.swift deleted file mode 100644 index 0e109584..00000000 --- a/Core/Sources/ConversationTab/ChatDropdownView.swift +++ /dev/null @@ -1,129 +0,0 @@ -import ConversationServiceProvider -import AppKit -import SwiftUI -import ComposableArchitecture - -protocol DropDownItem: Equatable { - var id: String { get } - var displayName: String { get } - var displayDescription: String { get } -} - -extension ChatTemplate: DropDownItem { - var displayName: String { id } - var displayDescription: String { shortDescription } -} - -extension ChatAgent: DropDownItem { - var id: String { slug } - var displayName: String { slug } - var displayDescription: String { description } -} - -struct ChatDropdownView: View { - @Binding var items: [T] - let prefixSymbol: String - let onSelect: (T) -> Void - @State private var selectedIndex = 0 - @State private var frameHeight: CGFloat = 0 - @State private var localMonitor: Any? = nil - - public var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 0) { - ForEach(Array(items.enumerated()), id: \.element.id) { index, item in - HStack { - Text(prefixSymbol + item.displayName) - .hoverPrimaryForeground(isHovered: selectedIndex == index) - Spacer() - Text(item.displayDescription) - .hoverSecondaryForeground(isHovered: selectedIndex == index) - } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .contentShape(Rectangle()) - .onTapGesture { - onSelect(item) - } - .hoverBackground(isHovered: selectedIndex == index) - .onHover { isHovered in - if isHovered { - selectedIndex = index - } - } - } - } - .background( - GeometryReader { geometry in - Color.clear - .onAppear { frameHeight = geometry.size.height } - .onChange(of: geometry.size.height) { newHeight in - frameHeight = newHeight - } - } - ) - .background(.ultraThickMaterial) - .cornerRadius(6) - .shadow(radius: 2) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - .frame(maxWidth: .infinity) - .offset(y: -1 * frameHeight) - .onChange(of: items) { _ in - selectedIndex = 0 - } - .onAppear { - selectedIndex = 0 - localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - switch event.keyCode { - case 126: // Up arrow - moveSelection(up: true) - return nil - case 125: // Down arrow - moveSelection(up: false) - return nil - case 36: // Return key - handleEnter() - return nil - case 48: // Tab key - handleTab() - return nil // not forwarding the Tab Event which will replace the typed message to "\t" - default: - break - } - return event - } - } - .onDisappear { - if let monitor = localMonitor { - NSEvent.removeMonitor(monitor) - localMonitor = nil - } - } - } - } - - private func moveSelection(up: Bool) { - guard !items.isEmpty else { return } - let lowerBound = 0 - let upperBound = items.count - 1 - let newIndex = selectedIndex + (up ? -1 : 1) - selectedIndex = newIndex < lowerBound ? upperBound : (newIndex > upperBound ? lowerBound : newIndex) - } - - private func handleEnter() { - handleTemplateSelection() - } - - private func handleTab() { - handleTemplateSelection() - } - - private func handleTemplateSelection() { - if items.count > 0 && selectedIndex < items.count { - onSelect(items[selectedIndex]) - } - } -} diff --git a/Core/Sources/ConversationTab/ChatExtension.swift b/Core/Sources/ConversationTab/ChatExtension.swift deleted file mode 100644 index 27220a96..00000000 --- a/Core/Sources/ConversationTab/ChatExtension.swift +++ /dev/null @@ -1,17 +0,0 @@ -import ChatService -import ConversationServiceProvider - -extension Chat.State { - func buildSkillSet(isCurrentEditorContextEnabled: Bool) -> [ConversationSkill] { - guard let currentFile = self.currentEditor, isCurrentEditorContextEnabled else { - return [] - } - let fileReference = FileReference( - url: currentFile.url, - relativePath: currentFile.relativePath, - fileName: currentFile.fileName, - isCurrentEditor: currentFile.isCurrentEditor - ) - return [CurrentEditorSkill(currentFile: fileReference), ProblemsInActiveDocumentSkill()] - } -} diff --git a/Core/Sources/ConversationTab/ChatPanel.swift b/Core/Sources/ConversationTab/ChatPanel.swift deleted file mode 100644 index ce783401..00000000 --- a/Core/Sources/ConversationTab/ChatPanel.swift +++ /dev/null @@ -1,1096 +0,0 @@ -import AppKit -import Combine -import ComposableArchitecture -import ConversationServiceProvider -import MarkdownUI -import ChatAPIService -import SharedUIComponents -import SwiftUI -import ChatService -import SwiftUIFlowLayout -import XcodeInspector -import ChatTab -import Workspace -import Persist -import UniformTypeIdentifiers -import Status -import GitHubCopilotService -import GitHubCopilotViewModel - -private let r: Double = 4 - -public struct ChatPanel: View { - @Perception.Bindable var chat: StoreOf - @Namespace var inputAreaNamespace - - public var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - - if chat.history.isEmpty { - VStack { - Spacer() - Instruction(isAgentMode: $chat.isAgentMode) - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .padding(.trailing, 16) - } else { - ChatPanelMessages(chat: chat) - .accessibilityElement(children: .combine) - .accessibilityLabel("Chat Messages Group") - - if let _ = chat.history.last?.followUp { - ChatFollowUp(chat: chat) - .padding(.trailing, 16) - .padding(.vertical, 8) - } - } - - if chat.fileEditMap.count > 0 { - WorkingSetView(chat: chat) - .padding(.trailing, 16) - } - - ChatPanelInputArea(chat: chat) - .padding(.trailing, 16) - } - .padding(.leading, 16) - .padding(.bottom, 16) - .background(Color(nsColor: .windowBackgroundColor)) - .onAppear { - chat.send(.appear) - } - .onDrop(of: [.fileURL], isTargeted: nil) { providers in - onFileDrop(providers) - } - } - } - - private func onFileDrop(_ providers: [NSItemProvider]) -> Bool { - for provider in providers { - if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { - provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier) { item, error in - let url: URL? = { - if let data = item as? Data { - return URL(dataRepresentation: data, relativeTo: nil) - } else if let url = item as? URL { - return url - } - return nil - }() - - guard let url else { return } - if let isValidFile = try? WorkspaceFile.isValidFile(url), isValidFile { - DispatchQueue.main.async { - let fileReference = FileReference(url: url, isCurrentEditor: false) - chat.send(.addSelectedFile(fileReference)) - } - } else if let data = try? Data(contentsOf: url), - ["png", "jpeg", "jpg", "bmp", "gif", "tiff", "tif", "webp"].contains(url.pathExtension.lowercased()) { - DispatchQueue.main.async { - chat.send(.addSelectedImage(ImageReference(data: data, fileUrl: url))) - } - } - } - } - } - - return true - } -} - - - -private struct ScrollViewOffsetPreferenceKey: PreferenceKey { - static var defaultValue = CGFloat.zero - - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value += nextValue() - } -} - -private struct ListHeightPreferenceKey: PreferenceKey { - static var defaultValue = CGFloat.zero - - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value += nextValue() - } -} - -struct ChatPanelMessages: View { - let chat: StoreOf - @State var cancellable = Set() - @State var isScrollToBottomButtonDisplayed = true - @State var isPinnedToBottom = true - @Namespace var bottomID - @Namespace var topID - @Namespace var scrollSpace - @State var scrollOffset: Double = 0 - @State var listHeight: Double = 0 - @State var didScrollToBottomOnAppearOnce = false - @State var isBottomHidden = true - @Environment(\.isEnabled) var isEnabled - - var body: some View { - WithPerceptionTracking { - ScrollViewReader { proxy in - GeometryReader { listGeo in - List { - Group { - - ChatHistory(chat: chat) - .listItemTint(.clear) - - ExtraSpacingInResponding(chat: chat) - - Spacer(minLength: 12) - .id(bottomID) - .onAppear { - isBottomHidden = false - if !didScrollToBottomOnAppearOnce { - proxy.scrollTo(bottomID, anchor: .bottom) - didScrollToBottomOnAppearOnce = true - } - } - .onDisappear { - isBottomHidden = true - } - .background(GeometryReader { geo in - let offset = geo.frame(in: .named(scrollSpace)).minY - Color.clear.preference( - key: ScrollViewOffsetPreferenceKey.self, - value: offset - ) - }) - } - .modify { view in - if #available(macOS 13.0, *) { - view - .listRowSeparator(.hidden) - } else { - view - } - } - } - .padding(.leading, -8) - .listStyle(.plain) - .listRowBackground(EmptyView()) - .modify { view in - if #available(macOS 13.0, *) { - view.scrollContentBackground(.hidden) - } else { - view - } - } - .coordinateSpace(name: scrollSpace) - .preference( - key: ListHeightPreferenceKey.self, - value: listGeo.size.height - ) - .onPreferenceChange(ListHeightPreferenceKey.self) { value in - listHeight = value - updatePinningState() - } - .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - scrollOffset = value - updatePinningState() - } - .overlay(alignment: .bottomTrailing) { - scrollToBottomButton(proxy: proxy) - } - .background { - PinToBottomHandler( - chat: chat, - isBottomHidden: isBottomHidden, - pinnedToBottom: $isPinnedToBottom - ) { - proxy.scrollTo(bottomID, anchor: .bottom) - } - } - .onAppear { - proxy.scrollTo(bottomID, anchor: .bottom) - } - .task { - proxy.scrollTo(bottomID, anchor: .bottom) - } - } - } - .onAppear { - trackScrollWheel() - } - .onDisappear { - cancellable.forEach { $0.cancel() } - cancellable = [] - } - } - } - - func trackScrollWheel() { - NSApplication.shared.publisher(for: \.currentEvent) - .filter { - if !isEnabled { return false } - return $0?.type == .scrollWheel - } - .compactMap { $0 } - .sink { event in - guard isPinnedToBottom else { return } - let delta = event.deltaY - let scrollUp = delta > 0 - if scrollUp { - isPinnedToBottom = false - } - } - .store(in: &cancellable) - } - - @MainActor - func updatePinningState() { - // where does the 32 come from? - withAnimation(.linear(duration: 0.1)) { - isScrollToBottomButtonDisplayed = scrollOffset > listHeight + 32 + 20 - || scrollOffset <= 0 - } - } - - @ViewBuilder - func scrollToBottomButton(proxy: ScrollViewProxy) -> some View { - Button(action: { - isPinnedToBottom = true - withAnimation(.easeInOut(duration: 0.1)) { - proxy.scrollTo(bottomID, anchor: .bottom) - } - }) { - Image(systemName: "chevron.down") - .padding(8) - .background { - Circle() - .fill(.thickMaterial) - .shadow(color: .black.opacity(0.2), radius: 2) - } - .overlay { - Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .foregroundStyle(.secondary) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .padding(4) - .keyboardShortcut(.downArrow, modifiers: [.command]) - .opacity(isScrollToBottomButtonDisplayed ? 1 : 0) - .help("Scroll Down") - } - - struct ExtraSpacingInResponding: View { - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - if chat.isReceivingMessage { - Spacer(minLength: 12) - } - } - } - } - - struct PinToBottomHandler: View { - let chat: StoreOf - let isBottomHidden: Bool - @Binding var pinnedToBottom: Bool - let scrollToBottom: () -> Void - - @State var isInitialLoad = true - - var body: some View { - WithPerceptionTracking { - EmptyView() - .onChange(of: chat.isReceivingMessage) { isReceiving in - if isReceiving { - Task { - pinnedToBottom = true - await Task.yield() - withAnimation(.easeInOut(duration: 0.1)) { - scrollToBottom() - } - } - } else { - Task { pinnedToBottom = false } - } - } - .onChange(of: chat.history.last) { _ in - if pinnedToBottom || isInitialLoad { - if isInitialLoad { - isInitialLoad = false - } - Task { - await Task.yield() - withAnimation(.easeInOut(duration: 0.1)) { - scrollToBottom() - } - } - } - } - .onChange(of: isBottomHidden) { value in - // This is important to prevent it from jumping to the top! - if value, pinnedToBottom { - scrollToBottom() - } - } - } - } - } -} - -struct ChatHistory: View { - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - ForEach(Array(chat.history.enumerated()), id: \.element.id) { index, message in - VStack(spacing: 0) { - WithPerceptionTracking { - ChatHistoryItem(chat: chat, message: message) - .id(message.id) - .padding(.top, 4) - .padding(.bottom, 12) - } - - // add divider between messages - if message.role != .ignored && index < chat.history.count - 1 { - Divider() } - } - } - } - } -} - -struct ChatHistoryItem: View { - let chat: StoreOf - let message: DisplayedChatMessage - - var body: some View { - WithPerceptionTracking { - let text = message.text - switch message.role { - case .user: - UserMessage( - id: message.id, - text: text, - imageReferences: message.imageReferences, - chat: chat - ) - case .assistant: - BotMessage( - id: message.id, - text: text, - references: message.references, - followUp: message.followUp, - errorMessages: message.errorMessages, - chat: chat, - steps: message.steps, - editAgentRounds: message.editAgentRounds, - panelMessages: message.panelMessages, - codeReviewRound: message.codeReviewRound - ) - case .ignored: - EmptyView() - } - } - } -} - -struct ChatFollowUp: View { - let chat: StoreOf - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - WithPerceptionTracking { - HStack { - if let followUp = chat.history.last?.followUp { - Button(action: { - chat.send(.followUpButtonClicked(UUID().uuidString, followUp.message)) - }) { - HStack(spacing: 4) { - Image(systemName: "sparkles") - .foregroundColor(.blue) - - Text(followUp.message) - .font(.system(size: chatFontSize)) - .foregroundColor(.blue) - } - } - .buttonStyle(.plain) - .onHover { isHovered in - if isHovered { - NSCursor.pointingHand.push() - } else { - NSCursor.pop() - } - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - } -} - -struct ChatCLSError: View { - let chat: StoreOf - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - WithPerceptionTracking { - HStack(alignment: .top) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.blue) - .padding(.leading, 8) - - Text("Monthly chat limit reached. [Upgrade now](https://github.com/github-copilot/signup/copilot_individual) or wait until your usage resets.") - .font(.system(size: chatFontSize)) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 8) - .background( - RoundedCorners(tl: r, tr: r, bl: 0, br: 0) - .fill(.ultraThickMaterial) - ) - .overlay( - RoundedCorners(tl: r, tr: r, bl: 0, br: 0) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - .padding(.top, 4) - } - } -} - -struct ChatPanelInputArea: View { - let chat: StoreOf - @FocusState var focusedField: Chat.State.Field? - - var body: some View { - HStack { - InputAreaTextEditor(chat: chat, focusedField: $focusedField) - } - .background(Color.clear) - } - - @MainActor - var clearButton: some View { - Button(action: { - chat.send(.clearButtonTap) - }) { - Group { - if #available(macOS 13.0, *) { - Image(systemName: "eraser.line.dashed.fill") - } else { - Image(systemName: "trash.fill") - } - } - .padding(6) - .background { - Circle().fill(Color(nsColor: .controlBackgroundColor)) - } - .overlay { - Circle().stroke(Color(nsColor: .controlColor), lineWidth: 1) - } - } - .buttonStyle(.plain) - } - - enum ShowingType { case template, agent } - - struct InputAreaTextEditor: View { - @Perception.Bindable var chat: StoreOf - var focusedField: FocusState.Binding - @State var cancellable = Set() - @State private var isFilePickerPresented = false - @State private var allFiles: [FileReference]? = nil - @State private var filteredTemplates: [ChatTemplate] = [] - @State private var filteredAgent: [ChatAgent] = [] - @State private var showingTemplates = false - @State private var dropDownShowingType: ShowingType? = nil - - @AppStorage(\.enableCurrentEditorContext) var enableCurrentEditorContext: Bool - @State private var isCurrentEditorContextEnabled: Bool = UserDefaults.shared.value( - for: \.enableCurrentEditorContext - ) - @ObservedObject private var status: StatusObserver = .shared - @State private var isCCRFFEnabled: Bool - @State private var cancellables = Set() - - init( - chat: StoreOf, - focusedField: FocusState.Binding - ) { - self.chat = chat - self.focusedField = focusedField - self.isCCRFFEnabled = FeatureFlagNotifierImpl.shared.featureFlags.ccr - } - - var isRequestingConversation: Bool { - if chat.isReceivingMessage, - let requestType = chat.requestType, - requestType == .conversation { - return true - } - return false - } - - var isRequestingCodeReview: Bool { - if chat.isReceivingMessage, - let requestType = chat.requestType, - requestType == .codeReview { - return true - } - - return false - } - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - chatContextView - - if isFilePickerPresented { - FilePicker( - allFiles: $allFiles, - workspaceURL: chat.workspaceURL, - onSubmit: { file in - chat.send(.addSelectedFile(file)) - }, - onExit: { - isFilePickerPresented = false - focusedField.wrappedValue = .textField - } - ) - .onAppear() { - allFiles = ContextUtils.getFilesFromWorkspaceIndex(workspaceURL: chat.workspaceURL) - } - } - - if !chat.state.attachedImages.isEmpty { - ImagesScrollView(chat: chat) - } - - ZStack(alignment: .topLeading) { - if chat.typedMessage.isEmpty { - Group { - chat.isAgentMode ? - Text("Edit files in your workspace in agent mode") : - Text("Ask Copilot or type / for commands") - } - .font(.system(size: 14)) - .foregroundColor(Color(nsColor: .placeholderTextColor)) - .padding(8) - .padding(.horizontal, 4) - } - - HStack(spacing: 0) { - AutoresizingCustomTextEditor( - text: $chat.typedMessage, - font: .systemFont(ofSize: 14), - isEditable: true, - maxHeight: 400, - onSubmit: { - if (dropDownShowingType == nil) { - submitChatMessage() - } - dropDownShowingType = nil - } - ) - .focused(focusedField, equals: .textField) - .bind($chat.focusedField, to: focusedField) - .padding(8) - .fixedSize(horizontal: false, vertical: true) - .onChange(of: chat.typedMessage) { newValue in - Task { - await onTypedMessageChanged(newValue: newValue) - } - } - /// When chat mode changed, the chat tamplate and agent need to be reloaded - .onChange(of: chat.isAgentMode) { _ in - Task { - await onTypedMessageChanged(newValue: chat.typedMessage) - } - } - } - .frame(maxWidth: .infinity) - } - .padding(.top, 4) - - HStack(spacing: 0) { - ModelPicker() - - Spacer() - - codeReviewButton - .buttonStyle(HoverButtonStyle(padding: 0)) - .disabled(isRequestingConversation) - - ZStack { - sendButton - .opacity(isRequestingConversation ? 0 : 1) - - stopButton - .opacity(isRequestingConversation ? 1 : 0) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .disabled(isRequestingCodeReview) - } - .padding(8) - .padding(.top, -4) - } - .overlay(alignment: .top) { - dropdownOverlay - } - .onAppear() { - subscribeToActiveDocumentChangeEvent() - // Check quota for CCR - Task { - if status.quotaInfo == nil, - let service = try? GitHubCopilotViewModel.shared.getGitHubCopilotAuthService() { - _ = try? await service.checkQuota() - } - } - } - .task { - subscribeToFeatureFlagsDidChangeEvent() - } - .background { - RoundedRectangle(cornerRadius: 6) - .fill(Color(nsColor: .controlBackgroundColor)) - } - .overlay { - RoundedRectangle(cornerRadius: 6) - .stroke(Color(nsColor: .controlColor), lineWidth: 1) - } - .background { - Button(action: { - chat.send(.returnButtonTapped) - }) { - EmptyView() - } - .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) - .accessibilityHidden(true) - - Button(action: { - focusedField.wrappedValue = .textField - }) { - EmptyView() - } - .keyboardShortcut("l", modifiers: [.command]) - .accessibilityHidden(true) - } - - } - } - - private var sendButton: some View { - Button(action: { - submitChatMessage() - }) { - Image(systemName: "paperplane.fill") - .padding(4) - } - .keyboardShortcut(KeyEquivalent.return, modifiers: []) - .help("Send") - } - - private var stopButton: some View { - Button(action: { - chat.send(.stopRespondingButtonTapped) - }) { - Image(systemName: "stop.circle") - .padding(4) - } - } - - private var isFreeUser: Bool { - guard let quotaInfo = status.quotaInfo else { return true } - - return quotaInfo.isFreeUser - } - - private var ccrDisabledTooltip: String { - if !isCCRFFEnabled { - return "GitHub Copilot Code Review is disabled by org policy. Contact your admin." - } - - return "GitHub Copilot Code Review is temporarily unavailable." - } - - var codeReviewIcon: some View { - Image("codeReview") - .padding(6) - } - - private var codeReviewButton: some View { - Group { - if isFreeUser { - // Show nothing - } else if isCCRFFEnabled { - ZStack { - stopButton - .opacity(isRequestingCodeReview ? 1 : 0) - .help("Stop Code Review") - - Menu { - Button(action: { - chat.send(.codeReview(.request(.index))) - }) { - Text("Review Staged Changes") - } - - Button(action: { - chat.send(.codeReview(.request(.workingTree))) - }) { - Text("Review Unstaged Changes") - } - } label: { - codeReviewIcon - } - .opacity(isRequestingCodeReview ? 0 : 1) - .help("Code Review") - } - .buttonStyle(HoverButtonStyle(padding: 0)) - } else { - codeReviewIcon - .foregroundColor(Color(nsColor: .tertiaryLabelColor)) - .help(ccrDisabledTooltip) - } - } - } - - private func subscribeToFeatureFlagsDidChangeEvent() { - FeatureFlagNotifierImpl.shared.featureFlagsDidChange - .sink(receiveValue: { isCCRFFEnabled = $0.ccr }) - .store(in: &cancellables) - } - - private var dropdownOverlay: some View { - Group { - if dropDownShowingType != nil { - if dropDownShowingType == .template { - ChatDropdownView(items: $filteredTemplates, prefixSymbol: "/") { template in - chat.typedMessage = "/" + template.id + " " - if template.id == "releaseNotes" { - submitChatMessage() - } - } - } else if dropDownShowingType == .agent { - ChatDropdownView(items: $filteredAgent, prefixSymbol: "@") { agent in - chat.typedMessage = "@" + agent.id + " " - } - } - } - } - } - - func onTypedMessageChanged(newValue: String) async { - if newValue.hasPrefix("/") { - filteredTemplates = await chatTemplateCompletion(text: newValue) - dropDownShowingType = filteredTemplates.isEmpty ? nil : .template - } else if newValue.hasPrefix("@") && !chat.isAgentMode { - filteredAgent = await chatAgentCompletion(text: newValue) - dropDownShowingType = filteredAgent.isEmpty ? nil : .agent - } else { - dropDownShowingType = nil - } - } - - enum ChatContextButtonType { case imageAttach, contextAttach} - - private var chatContextView: some View { - let buttonItems: [ChatContextButtonType] = [.contextAttach, .imageAttach] - let currentEditorItem: [FileReference] = [chat.state.currentEditor].compactMap { - $0 - } - let selectedFileItems = chat.state.selectedFiles - let chatContextItems: [Any] = buttonItems.map { - $0 as ChatContextButtonType - } + currentEditorItem + selectedFileItems - return FlowLayout(mode: .scrollable, items: chatContextItems, itemSpacing: 4) { item in - if let buttonType = item as? ChatContextButtonType { - if buttonType == .imageAttach { - VisionMenuView(chat: chat) - } else if buttonType == .contextAttach { - // File picker button - Button(action: { - withAnimation { - isFilePickerPresented.toggle() - if !isFilePickerPresented { - focusedField.wrappedValue = .textField - } - } - }) { - Image(systemName: "paperclip") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 16, height: 16) - .padding(4) - .foregroundColor(.primary.opacity(0.85)) - .font(Font.system(size: 11, weight: .semibold)) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .help("Add Context") - .cornerRadius(6) - } - } else if let select = item as? FileReference { - HStack(spacing: 0) { - drawFileIcon(select.url) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.primary.opacity(0.85)) - .padding(4) - .opacity(select.isCurrentEditor && !isCurrentEditorContextEnabled ? 0.4 : 1.0) - - Text(select.url.lastPathComponent) - .lineLimit(1) - .truncationMode(.middle) - .foregroundColor( - select.isCurrentEditor && !isCurrentEditorContextEnabled - ? .secondary - : .primary.opacity(0.85) - ) - .font(.body) - .opacity(select.isCurrentEditor && !isCurrentEditorContextEnabled ? 0.4 : 1.0) - .help(select.getPathRelativeToHome()) - - if select.isCurrentEditor { - Toggle("", isOn: $isCurrentEditorContextEnabled) - .toggleStyle(SwitchToggleStyle(tint: .blue)) - .controlSize(.mini) - .padding(.trailing, 4) - .onChange(of: isCurrentEditorContextEnabled) { newValue in - enableCurrentEditorContext = newValue - } - } else { - Button(action: { chat.send(.removeSelectedFile(select)) }) { - Image(systemName: "xmark") - .resizable() - .frame(width: 8, height: 8) - .foregroundColor(.primary.opacity(0.85)) - .padding(4) - } - .buttonStyle(HoverButtonStyle()) - } - } - .background( - Color(nsColor: .windowBackgroundColor).opacity(0.5) - ) - .cornerRadius(select.isCurrentEditor ? 99 : r) - .overlay( - RoundedRectangle(cornerRadius: select.isCurrentEditor ? 99 : r) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - } - } - .padding(.horizontal, 8) - .padding(.top, 8) - } - - func chatTemplateCompletion(text: String) async -> [ChatTemplate] { - guard text.count >= 1 && text.first == "/" else { return [] } - - let prefix = text.dropFirst() - var promptTemplates: [ChatTemplate] = [] - let releaseNotesTemplate: ChatTemplate = .init( - id: "releaseNotes", - description: "What's New", - shortDescription: "What's New", - scopes: [.chatPanel, .agentPanel] - ) - - if !chat.isAgentMode { - promptTemplates = await SharedChatService.shared.loadChatTemplates() ?? [] - } - - let templates = promptTemplates + [releaseNotesTemplate] - let skippedTemplates = [ "feedback", "help" ] - - return templates.filter { - $0.scopes.contains(chat.isAgentMode ? .agentPanel : .chatPanel) && - $0.id.hasPrefix(prefix) && - !skippedTemplates.contains($0.id) - } - } - - func chatAgentCompletion(text: String) async -> [ChatAgent] { - guard text.count >= 1 && text.first == "@" else { return [] } - let prefix = text.dropFirst() - var chatAgents = await SharedChatService.shared.loadChatAgents() ?? [] - - if let index = chatAgents.firstIndex(where: { $0.slug == "project" }) { - let projectAgent = chatAgents[index] - chatAgents[index] = .init(slug: "workspace", name: "workspace", description: "Ask about your workspace", avatarUrl: projectAgent.avatarUrl) - } - - /// only enable the @workspace - let includedAgents = ["workspace"] - - return chatAgents.filter { $0.slug.hasPrefix(prefix) && includedAgents.contains($0.slug) } - } - - func subscribeToActiveDocumentChangeEvent() { - Publishers.CombineLatest( - XcodeInspector.shared.$latestActiveXcode, - XcodeInspector.shared.$activeDocumentURL - .removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { newXcode, newDocURL in - // First check for realtimeWorkspaceURL if activeWorkspaceURL is nil - if let realtimeURL = newXcode?.realtimeDocumentURL, newDocURL == nil { - if supportedFileExtensions.contains(realtimeURL.pathExtension) { - let currentEditor = FileReference(url: realtimeURL, isCurrentEditor: true) - chat.send(.setCurrentEditor(currentEditor)) - } - } else { - if supportedFileExtensions.contains(newDocURL?.pathExtension ?? "") { - let currentEditor = FileReference(url: newDocURL!, isCurrentEditor: true) - chat.send(.setCurrentEditor(currentEditor)) - } - } - } - .store(in: &cancellable) - } - - func submitChatMessage() { - chat.send(.sendButtonTapped(UUID().uuidString)) - } - } -} -// MARK: - Previews - -struct ChatPanel_Preview: PreviewProvider { - static let history: [DisplayedChatMessage] = [ - .init( - id: "1", - role: .user, - text: "**Hello**", - references: [] - ), - .init( - id: "2", - role: .assistant, - text: """ - ```swift - func foo() {} - ``` - **Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you? - """, - references: [ - .init( - uri: "Hi Hi Hi Hi", - status: .included, - kind: .class - ), - ] - ), - .init( - id: "7", - role: .ignored, - text: "Ignored", - references: [] - ), - .init( - id: "5", - role: .assistant, - text: "Yooo", - references: [] - ), - .init( - id: "4", - role: .user, - text: "Yeeeehh", - references: [] - ), - .init( - id: "3", - role: .user, - text: #""" - Please buy me a coffee! - | Coffee | Milk | - |--------|------| - | Espresso | No | - | Latte | Yes | - - ```swift - func foo() {} - ``` - ```objectivec - - (void)bar {} - ``` - """#, - references: [], - followUp: .init(message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce turpis dolor, malesuada quis fringilla sit amet, placerat at nunc. Suspendisse orci tortor, tempor nec blandit a, malesuada vel tellus. Nunc sed leo ligula. Ut at ligula eget turpis pharetra tristique. Integer luctus leo non elit rhoncus fermentum.", id: "3", type: "type") - ), - ] - - static let chatTabInfo = ChatTabInfo(id: "", workspacePath: "path", username: "name") - - static var previews: some View { - ChatPanel(chat: .init( - initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true), - reducer: { Chat(service: ChatService.service(for: chatTabInfo)) } - )) - .frame(width: 450, height: 1200) - .colorScheme(.dark) - } -} - -struct ChatPanel_EmptyChat_Preview: PreviewProvider { - static var previews: some View { - ChatPanel(chat: .init( - initialState: .init(history: [DisplayedChatMessage](), isReceivingMessage: false), - reducer: { Chat(service: ChatService.service(for: ChatPanel_Preview.chatTabInfo)) } - )) - .padding() - .frame(width: 450, height: 600) - .colorScheme(.dark) - } -} - -struct ChatPanel_InputText_Preview: PreviewProvider { - static var previews: some View { - ChatPanel(chat: .init( - initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: false), - reducer: { Chat(service: ChatService.service(for: ChatPanel_Preview.chatTabInfo)) } - )) - .padding() - .frame(width: 450, height: 600) - .colorScheme(.dark) - } -} - -struct ChatPanel_InputMultilineText_Preview: PreviewProvider { - static var previews: some View { - ChatPanel( - chat: .init( - initialState: .init( - typedMessage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce turpis dolor, malesuada quis fringilla sit amet, placerat at nunc. Suspendisse orci tortor, tempor nec blandit a, malesuada vel tellus. Nunc sed leo ligula. Ut at ligula eget turpis pharetra tristique. Integer luctus leo non elit rhoncus fermentum.", - - history: ChatPanel_Preview.history, - isReceivingMessage: false - ), - reducer: { Chat(service: ChatService.service(for: ChatPanel_Preview.chatTabInfo)) } - ) - ) - .padding() - .frame(width: 450, height: 600) - .colorScheme(.dark) - } -} - -struct ChatPanel_Light_Preview: PreviewProvider { - static var previews: some View { - ChatPanel(chat: .init( - initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true), - reducer: { Chat(service: ChatService.service(for: ChatPanel_Preview.chatTabInfo)) } - )) - .padding() - .frame(width: 450, height: 600) - .colorScheme(.light) - } -} - diff --git a/Core/Sources/ConversationTab/CodeBlockHighlighter.swift b/Core/Sources/ConversationTab/CodeBlockHighlighter.swift deleted file mode 100644 index 553f5976..00000000 --- a/Core/Sources/ConversationTab/CodeBlockHighlighter.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Combine -import ComposableArchitecture -import DebounceFunction -import Foundation -import MarkdownUI -import Perception -import SharedUIComponents -import SwiftUI - -/// Use this instead of the built in ``CodeBlockView`` to highlight code blocks asynchronously, -/// so that the UI doesn't freeze when rendering large code blocks. -struct AsyncCodeBlockView: View { - @Perceptible - class Storage { - static let queue = DispatchQueue( - label: "chat-code-block-highlight", - qos: .userInteractive, - attributes: .concurrent - ) - - var highlighted: AttributedString? - @PerceptionIgnored var debounceFunction: DebounceFunction? - @PerceptionIgnored private var highlightTask: Task? - - init() { - debounceFunction = .init(duration: 0.5, block: { [weak self] view in - self?.highlight(for: view) - }) - } - - func highlight(debounce: Bool, for view: AsyncCodeBlockView) { - if debounce { - Task { await debounceFunction?(view) } - } else { - highlight(for: view) - } - } - - func highlight(for view: AsyncCodeBlockView) { - highlightTask?.cancel() - let content = view.content - let language = view.fenceInfo ?? "" - let brightMode = view.colorScheme != .dark - let font = view.font - highlightTask = Task { - let string = await withUnsafeContinuation { continuation in - Self.queue.async { - let content = CodeHighlighting.highlightedCodeBlock( - code: content, - language: language, - scenario: "chat", - brightMode: brightMode, - font: font - ) - continuation.resume(returning: AttributedString(content)) - } - } - try Task.checkCancellation() - await MainActor.run { - self.highlighted = string - } - } - } - } - - let fenceInfo: String? - let content: String - let font: NSFont - - @Environment(\.colorScheme) var colorScheme - @State var storage = Storage() - @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - - init(fenceInfo: String?, content: String, font: NSFont) { - self.fenceInfo = fenceInfo - self.content = content.hasSuffix("\n") ? String(content.dropLast()) : content - self.font = font - } - - var body: some View { - WithPerceptionTracking { - Group { - if let highlighted = storage.highlighted { - Text(highlighted) - .frame(maxWidth: .infinity, alignment: .leading) - } else { - Text(content).font(.init(font)) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - .frame(maxWidth: .infinity) - .onAppear { - storage.highlight(debounce: false, for: self) - } - .onChange(of: colorScheme) { _ in - storage.highlight(debounce: false, for: self) - } - .onChange(of: syncCodeHighlightTheme) { _ in - storage.highlight(debounce: true, for: self) - } - .onChange(of: codeForegroundColorLight) { _ in - storage.highlight(debounce: true, for: self) - } - .onChange(of: codeBackgroundColorLight) { _ in - storage.highlight(debounce: true, for: self) - } - .onChange(of: codeForegroundColorDark) { _ in - storage.highlight(debounce: true, for: self) - } - .onChange(of: codeBackgroundColorDark) { _ in - storage.highlight(debounce: true, for: self) - } - } - } -} - diff --git a/Core/Sources/ConversationTab/ContextUtils.swift b/Core/Sources/ConversationTab/ContextUtils.swift deleted file mode 100644 index 5e05927a..00000000 --- a/Core/Sources/ConversationTab/ContextUtils.swift +++ /dev/null @@ -1,39 +0,0 @@ -import ConversationServiceProvider -import XcodeInspector -import Foundation -import Logger -import Workspace -import SystemUtils - -public struct ContextUtils { - - public static func getFilesFromWorkspaceIndex(workspaceURL: URL?) -> [FileReference]? { - guard let workspaceURL = workspaceURL else { return [] } - return WorkspaceFileIndex.shared.getFiles(for: workspaceURL) - } - - public static func getFilesInActiveWorkspace(workspaceURL: URL?) -> [FileReference] { - if let workspaceURL = workspaceURL, let info = WorkspaceFile.getWorkspaceInfo(workspaceURL: workspaceURL) { - return WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: info.workspaceURL, workspaceRootURL: info.projectURL) - } - - guard let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, - let workspaceRootURL = XcodeInspector.shared.realtimeActiveProjectURL else { - return [] - } - - let files = WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: workspaceURL, workspaceRootURL: workspaceRootURL) - - return files - } - - public static let workspaceReadabilityErrorMessageProvider: FileUtils.ReadabilityErrorMessageProvider = { status in - switch status { - case .readable: return nil - case .notFound: - return "Copilot can't access this workspace. It may have been removed or is temporarily unavailable." - case .permissionDenied: - return "Copilot can't access this workspace. Enable \"Files & Folders\" access in [System Settings](x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders)" - } - } -} diff --git a/Core/Sources/ConversationTab/Controller/DiffViewWindowController.swift b/Core/Sources/ConversationTab/Controller/DiffViewWindowController.swift deleted file mode 100644 index e4da4784..00000000 --- a/Core/Sources/ConversationTab/Controller/DiffViewWindowController.swift +++ /dev/null @@ -1,159 +0,0 @@ -import SwiftUI -import ChatService -import ComposableArchitecture -import WebKit - -enum Style { - /// default diff view frame. Same as the `ChatPanel` - static let diffViewHeight: Double = 560 - static let diffViewWidth: Double = 504 -} - -class DiffViewWindowController: NSObject, NSWindowDelegate { - enum DiffViewerState { - case shown, closed - } - - private var diffWindow: NSWindow? - private var hostingView: NSHostingView? - private weak var chat: StoreOf? - public private(set) var currentFileEdit: FileEdit? = nil - public private(set) var diffViewerState: DiffViewerState = .closed - - public init(chat: StoreOf) { - self.chat = chat - } - - deinit { - // Break the delegate cycle - diffWindow?.delegate = nil - - // Close and release the wi - diffWindow?.close() - diffWindow = nil - - // Clear hosting view - hostingView = nil - - // Reset state - currentFileEdit = nil - diffViewerState = .closed - } - - @MainActor - func showDiffWindow(fileEdit: FileEdit) { - guard let chat else { return } - - currentFileEdit = fileEdit - // Create diff view - let newDiffView = DiffView(chat: chat, fileEdit: fileEdit) - - if let window = diffWindow, let _ = hostingView { - window.title = "Diff View" - - let newHostingView = NSHostingView(rootView: newDiffView) - // Ensure the hosting view fills the window - newHostingView.translatesAutoresizingMaskIntoConstraints = false - - self.hostingView = newHostingView - window.contentView = newHostingView - - // Set constraints to fill the window - if let contentView = window.contentView { - newHostingView.frame = contentView.bounds - newHostingView.autoresizingMask = [.width, .height] - } - - window.makeKeyAndOrderFront(nil) - } else { - let newHostingView = NSHostingView(rootView: newDiffView) - newHostingView.translatesAutoresizingMaskIntoConstraints = false - self.hostingView = newHostingView - - let window = NSWindow( - contentRect: getDiffViewFrame(), - styleMask: [.titled, .closable, .miniaturizable, .resizable], - backing: .buffered, - defer: false - ) - - window.title = "Diff View" - window.contentView = newHostingView - - // Set constraints to fill the window - if let contentView = window.contentView { - newHostingView.frame = contentView.bounds - newHostingView.autoresizingMask = [.width, .height] - } - - window.center() - window.delegate = self - window.isReleasedWhenClosed = false - - self.diffWindow = window - } - - NSApp.activate(ignoringOtherApps: true) - diffWindow?.makeKeyAndOrderFront(nil) - - diffViewerState = .shown - } - - func windowWillClose(_ notification: Notification) { - if let window = notification.object as? NSWindow, window == diffWindow { - DispatchQueue.main.async { - self.diffWindow?.orderOut(nil) - } - } - } - - @MainActor - func hideWindow() { - guard diffViewerState != .closed else { return } - diffWindow?.orderOut(nil) - diffViewerState = .closed - } - - func getDiffViewFrame() -> NSRect { - guard let mainScreen = NSScreen.screens.first(where: { $0.frame.origin == .zero }) - else { - /// default value - return .init(x: 0, y:0, width: Style.diffViewWidth, height: Style.diffViewHeight) - } - - let visibleScreenFrame = mainScreen.visibleFrame - // avoid too wide - let width = min(Style.diffViewWidth, visibleScreenFrame.width * 0.3) - let height = visibleScreenFrame.height - - return CGRect(x: 0, y: 0, width: width, height: height) - } - - func windowDidResize(_ notification: Notification) { - if let window = notification.object as? NSWindow, window == diffWindow { - if let hostingView = self.hostingView, - let webView = findWebView(in: hostingView) { - let script = """ - if (window.DiffViewer && window.DiffViewer.handleResize) { - window.DiffViewer.handleResize(); - } - """ - webView.evaluateJavaScript(script) - } - } - } - - private func findWebView(in view: NSView) -> WKWebView? { - if let webView = view as? WKWebView { - return webView - } - - for subview in view.subviews { - if let webView = findWebView(in: subview) { - return webView - } - } - - return nil - } -} diff --git a/Core/Sources/ConversationTab/ConversationTab.swift b/Core/Sources/ConversationTab/ConversationTab.swift deleted file mode 100644 index 50ebe68f..00000000 --- a/Core/Sources/ConversationTab/ConversationTab.swift +++ /dev/null @@ -1,277 +0,0 @@ -import ChatService -import ChatTab -import CodableWrappers -import Combine -import ComposableArchitecture -import DebounceFunction -import Foundation -import ChatAPIService -import Preferences -import SwiftUI -import AppKit -import Workspace -import ConversationServiceProvider - -/// A chat tab that provides a context aware chat bot, powered by Chat. -public class ConversationTab: ChatTab { - - public static var name: String { "Chat" } - - public let service: ChatService - let chat: StoreOf - private var cancellable = Set() - private var observer = NSObject() - private let updateContentDebounce = DebounceRunner(duration: 0.5) - private var isRestored: Bool = false - - // Get chat tab title. As the tab title is always "Chat" and won't be modified. - // Use the chat title as the tab title. - // TODO: modify tab title dynamicly - public func getChatTabTitle() -> String { - return chat.title - } - - struct RestorableState: Codable { - var history: [ChatAPIService.ChatMessage] - } - - struct Builder: ChatTabBuilder { - var title: String - var customCommand: CustomCommand? - var afterBuild: (ConversationTab) async -> Void = { _ in } - - func build(store: StoreOf) async -> (any ChatTab)? { - let tab = await ConversationTab(store: store) - if let customCommand { - try? await tab.service.handleCustomCommand(customCommand) - } - await afterBuild(tab) - return tab - } - } - - public func buildView() -> any View { - ChatPanel(chat: chat) - } - - public func buildTabItem() -> any View { - ChatTabItemView(chat: chat) - } - - public func buildChatConversationItem() -> any View { - ChatConversationItemView(chat: chat) - } - - public func buildIcon() -> any View { - WithPerceptionTracking { - if self.chat.isReceivingMessage { - Image(systemName: "ellipsis.message") - } else { - Image(systemName: "message") - } - } - } - - public func buildMenu() -> any View { - ChatContextMenu(store: chat.scope(state: \.chatMenu, action: \.chatMenu)) - } - - public func restorableState() async -> Data { - let state = RestorableState( - history: await service.memory.history - ) - return (try? JSONEncoder().encode(state)) ?? Data() - } - - public static func restore( - from data: Data, - externalDependency: Void - ) async throws -> any ChatTabBuilder { - let state = try JSONDecoder().decode(RestorableState.self, from: data) - let builder = Builder(title: "Chat") { @MainActor tab in - await tab.service.memory.mutateHistory { history in - history = state.history - } - tab.chat.send(.refresh) - } - return builder - } - - public static func chatBuilders(externalDependency: Void) -> [ChatTabBuilder] { - let customCommands = UserDefaults.shared.value(for: \.customCommands).compactMap { - command in - if case .customChat = command.feature { - return Builder(title: command.name, customCommand: command) - } - return nil - } - - return [Builder(title: "New Chat", customCommand: nil)] + customCommands - } - - // store.state is type of ChatTabInfo - // add the with parameters to avoiding must override the init - @MainActor - public init(store: StoreOf, with chatTabInfo: ChatTabInfo? = nil) { - let info = chatTabInfo ?? store.state - - let service = ChatService.service(for: info) - self.service = service - chat = .init(initialState: .init(workspaceURL: service.getWorkspaceURL()), reducer: { Chat(service: service) }) - super.init(store: store) - - // Start to observe changes of Chat Message - self.start() - - // new created tab do not need restore - self.isRestored = true - } - - // for restore - @MainActor - public init(service: ChatService, store: StoreOf, with chatTabInfo: ChatTabInfo) { - self.service = service - chat = .init(initialState: .init(workspaceURL: service.getWorkspaceURL()), reducer: { Chat(service: service) }) - super.init(store: store) - } - - deinit { - // Cancel all Combine subscriptions - cancellable.forEach { $0.cancel() } - cancellable.removeAll() - - // Stop the debounce runner - Task { @MainActor [weak self] in - await self?.updateContentDebounce.cancel() - } - - // Clear observer - observer = NSObject() - - // The deallocation of ChatService will be called automatically - // The TCA Store (chat) handles its own cleanup automatically - } - - @MainActor - public static func restoreConversation(by chatTabInfo: ChatTabInfo, store: StoreOf) -> ConversationTab { - let service = ChatService.service(for: chatTabInfo) - let tab = ConversationTab(service: service, store: store, with: chatTabInfo) - - // lazy restore converstaion tab for not selected - if chatTabInfo.isSelected { - tab.restoreIfNeeded() - } - - return tab - } - - @MainActor - public func restoreIfNeeded() { - guard self.isRestored == false else { return } - // restore chat history - self.service.restoreIfNeeded() - // start observer - self.start() - - self.isRestored = true - } - - public func start() { - observer = .init() - cancellable = [] - - chat.send(.setDiffViewerController(chat: chat)) - -// chatTabStore.send(.updateTitle("Chat")) - -// do { -// var lastTrigger = -1 -// observer.observe { [weak self] in -// guard let self else { return } -// let trigger = chatTabStore.focusTrigger -// guard lastTrigger != trigger else { return } -// lastTrigger = trigger -// Task { @MainActor [weak self] in -// self?.chat.send(.focusOnTextField) -// } -// } -// } - -// do { -// var lastTitle = "" -// observer.observe { [weak self] in -// guard let self else { return } -// let title = self.chatTabStore.state.title -// guard lastTitle != title else { return } -// lastTitle = title -// Task { @MainActor [weak self] in -// self?.chatTabStore.send(.updateTitle(title)) -// } -// } -// } - - var lastIsReceivingMessage = false - - observer.observe { [weak self] in - guard let self else { return } -// let history = chat.history -// _ = chat.title -// _ = chat.isReceivingMessage - - // As the observer won't check the state if changed, we need to check it manually. - // Currently, only receciving message is used. If more states are needed, we can add them here. - let currentIsReceivingMessage = chat.isReceivingMessage - - // Only trigger when isReceivingMessage changes - if lastIsReceivingMessage != currentIsReceivingMessage { - lastIsReceivingMessage = currentIsReceivingMessage - Task { - await self.updateContentDebounce.debounce { @MainActor [weak self] in - guard let self else { return } - self.chatTabStore.send(.tabContentUpdated) - - if let suggestedTitle = chat.history.last?.suggestedTitle { - self.chatTabStore.send(.updateTitle(suggestedTitle)) - } - - if let CLSConversationID = self.service.conversationId, - self.chatTabStore.CLSConversationID != CLSConversationID - { - self.chatTabStore.send(.setCLSConversationID(CLSConversationID)) - } - } - } - } - } - } - - public func handlePasteEvent() -> Bool { - let pasteboard = NSPasteboard.general - if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL], !urls.isEmpty { - for url in urls { - if let isValidFile = try? WorkspaceFile.isValidFile(url), isValidFile { - DispatchQueue.main.async { - let fileReference = FileReference(url: url, isCurrentEditor: false) - self.chat.send(.addSelectedFile(fileReference)) - } - } else if let data = try? Data(contentsOf: url), - ["png", "jpeg", "jpg", "bmp", "gif", "tiff", "tif", "webp"].contains(url.pathExtension.lowercased()) { - DispatchQueue.main.async { - self.chat.send(.addSelectedImage(ImageReference(data: data, fileUrl: url))) - } - } - } - } else if let data = pasteboard.data(forType: .png) { - chat.send(.addSelectedImage(ImageReference(data: data, source: .pasted))) - } else if let tiffData = pasteboard.data(forType: .tiff), - let imageRep = NSBitmapImageRep(data: tiffData), - let pngData = imageRep.representation(using: .png, properties: [:]) { - chat.send(.addSelectedImage(ImageReference(data: pngData, source: .pasted))) - } else { - return false - } - - return true - } -} - diff --git a/Core/Sources/ConversationTab/DiffViews/DiffView.swift b/Core/Sources/ConversationTab/DiffViews/DiffView.swift deleted file mode 100644 index c857528e..00000000 --- a/Core/Sources/ConversationTab/DiffViews/DiffView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import SwiftUI -import WebKit -import ComposableArchitecture -import Logger -import ConversationServiceProvider -import ChatService -import ChatTab - -extension FileEdit { - var originalContentByStatus: String { - return status == .kept ? modifiedContent : originalContent - } - - var modifiedContentByStatus: String { - return status == .undone ? originalContent : modifiedContent - } -} - -struct DiffView: View { - @Perception.Bindable var chat: StoreOf - @State public var fileEdit: FileEdit - - var body: some View { - WithPerceptionTracking { - DiffWebView( - chat: chat, - fileEdit: fileEdit - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .edgesIgnoringSafeArea(.all) - } - } -} - -// preview -struct DiffView_Previews: PreviewProvider { - static var oldText = """ - import Foundation - - func calculateTotal(items: [Double]) -> Double { - var sum = 0.0 - for item in items { - sum += item - } - return sum - } - - func main() { - let prices = [10.5, 20.0, 15.75] - let total = calculateTotal(items: prices) - print("Total: \\(total)") - } - - main() - """ - - static var newText = """ - import Foundation - - func calculateTotal(items: [Double], applyDiscount: Bool = false) -> Double { - var sum = 0.0 - for item in items { - sum += item - } - - // Apply 10% discount if requested - if applyDiscount { - sum *= 0.9 - } - - return sum - } - - func main() { - let prices = [10.5, 20.0, 15.75, 5.0] - let total = calculateTotal(items: prices) - let discountedTotal = calculateTotal(items: prices, applyDiscount: true) - - print("Total: \\(total)") - print("With discount: \\(discountedTotal)") - } - - main() - """ - static let chatTabInfo = ChatTabInfo(id: "", workspacePath: "path", username: "name") - static var previews: some View { - DiffView( - chat: .init( - initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true), - reducer: { Chat(service: ChatService.service(for: chatTabInfo)) } - ), - fileEdit: .init(fileURL: URL(fileURLWithPath: "file:///f1.swift"), originalContent: "test", modifiedContent: "abc", toolName: ToolName.insertEditIntoFile) - ) - .frame(width: 800, height: 600) - } -} diff --git a/Core/Sources/ConversationTab/DiffViews/DiffWebView.swift b/Core/Sources/ConversationTab/DiffViews/DiffWebView.swift deleted file mode 100644 index cc42af5d..00000000 --- a/Core/Sources/ConversationTab/DiffViews/DiffWebView.swift +++ /dev/null @@ -1,184 +0,0 @@ -import ComposableArchitecture -import ChatService -import SwiftUI -import WebKit -import Logger - -struct DiffWebView: NSViewRepresentable { - @Perception.Bindable var chat: StoreOf - var fileEdit: FileEdit - - init(chat: StoreOf, fileEdit: FileEdit) { - self.chat = chat - self.fileEdit = fileEdit - } - - func makeNSView(context: Context) -> WKWebView { - let configuration = WKWebViewConfiguration() - let userContentController = WKUserContentController() - - #if DEBUG - let scriptSource = """ - function captureLog(msg) { window.webkit.messageHandlers.logging.postMessage(Array.prototype.slice.call(arguments)); } - console.log = captureLog; - console.error = captureLog; - console.warn = captureLog; - console.info = captureLog; - """ - let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true) - userContentController.addUserScript(script) - userContentController.add(context.coordinator, name: "logging") - #endif - - userContentController.add(context.coordinator, name: "swiftHandler") - configuration.userContentController = userContentController - - let webView = WKWebView(frame: .zero, configuration: configuration) - webView.navigationDelegate = context.coordinator - #if DEBUG - webView.configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") - #endif - - // Configure WebView - webView.wantsLayer = true - webView.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor - webView.layer?.borderWidth = 1 - - // Make the webview auto-resize with its container - webView.autoresizingMask = [.width, .height] - webView.translatesAutoresizingMaskIntoConstraints = true - - // Notify the webview of resize events explicitly - let resizeNotificationScript = WKUserScript( - source: """ - window.addEventListener('resize', function() { - if (window.DiffViewer && window.DiffViewer.handleResize) { - window.DiffViewer.handleResize(); - } - }); - """, - injectionTime: .atDocumentEnd, - forMainFrameOnly: true - ) - webView.configuration.userContentController.addUserScript(resizeNotificationScript) - - /// Load web asset resources - let bundleBaseURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/webViewDist/diffView") - let htmlFileURL = bundleBaseURL.appendingPathComponent("diffView.html") - webView.loadFileURL(htmlFileURL, allowingReadAccessTo: bundleBaseURL) - - return webView - } - - func updateNSView(_ webView: WKWebView, context: Context) { - if context.coordinator.shouldUpdate(fileEdit) { - // Update content via JavaScript API - let script = """ - if (typeof window.DiffViewer !== 'undefined') { - window.DiffViewer.update( - `\(escapeJSString(fileEdit.originalContentByStatus))`, - `\(escapeJSString(fileEdit.modifiedContentByStatus))`, - `\(escapeJSString(fileEdit.fileURL.absoluteString))`, - `\(fileEdit.status.rawValue)` - ); - } else { - console.error("DiffViewer is not defined in update"); - } - """ - webView.evaluateJavaScript(script) - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { - var parent: DiffWebView - private var fileEdit: FileEdit - - init(_ parent: DiffWebView) { - self.parent = parent - self.fileEdit = parent.fileEdit - } - - func shouldUpdate(_ fileEdit: FileEdit) -> Bool { - let shouldUpdate = self.fileEdit != fileEdit - - if shouldUpdate { - self.fileEdit = fileEdit - } - - return shouldUpdate - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - #if DEBUG - if message.name == "logging" { - if let logs = message.body as? [Any] { - let logString = logs.map { "\($0)" }.joined(separator: " ") - Logger.client.info("WebView console: \(logString)") - } - return - } - #endif - - guard message.name == "swiftHandler", - let body = message.body as? [String: Any], - let event = body["event"] as? String, - let data = body["data"] as? [String: String], - let filePath = data["filePath"], - let fileURL = URL(string: filePath) - else { return } - - switch event { - case "undoButtonClicked": - self.parent.chat.send(.undoEdits(fileURLs: [fileURL])) - case "keepButtonClicked": - self.parent.chat.send(.keepEdits(fileURLs: [fileURL])) - default: - break - } - } - - // Initialize content when the page has finished loading - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - let script = """ - if (typeof window.DiffViewer !== 'undefined') { - window.DiffViewer.init( - `\(escapeJSString(fileEdit.originalContentByStatus))`, - `\(escapeJSString(fileEdit.modifiedContentByStatus))`, - `\(escapeJSString(fileEdit.fileURL.absoluteString))`, - `\(fileEdit.status.rawValue)` - ); - } else { - console.error("DiffViewer is not defined on page load"); - } - """ - webView.evaluateJavaScript(script) { result, error in - if let error = error { - Logger.client.error("Error evaluating JavaScript: \(error)") - } - } - } - - // Handle navigation errors - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - Logger.client.error("WebView navigation failed: \(error)") - } - - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - Logger.client.error("WebView provisional navigation failed: \(error)") - } - } -} - -func escapeJSString(_ string: String) -> String { - return string - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "`", with: "\\`") - .replacingOccurrences(of: "\n", with: "\\n") - .replacingOccurrences(of: "\r", with: "\\r") - .replacingOccurrences(of: "\"", with: "\\\"") - .replacingOccurrences(of: "$", with: "\\$") -} diff --git a/Core/Sources/ConversationTab/Features/ConversationCodeReviewFeature.swift b/Core/Sources/ConversationTab/Features/ConversationCodeReviewFeature.swift deleted file mode 100644 index c85ca212..00000000 --- a/Core/Sources/ConversationTab/Features/ConversationCodeReviewFeature.swift +++ /dev/null @@ -1,90 +0,0 @@ -import ComposableArchitecture -import ChatService -import Foundation -import ConversationServiceProvider -import GitHelper -import LanguageServerProtocol -import Terminal -import Combine - -@MainActor -public class CodeReviewStateService: ObservableObject { - public static let shared = CodeReviewStateService() - - public let fileClickedEvent = PassthroughSubject() - - private init() { } - - func notifyFileClicked() { - fileClickedEvent.send() - } -} - -@Reducer -public struct ConversationCodeReviewFeature { - @ObservableState - public struct State: Equatable { - - public init() { } - } - - public enum Action: Equatable { - case request(GitDiffGroup) - case accept(id: String, selectedFiles: [DocumentUri]) - case cancel(id: String) - - case onFileClicked(URL, Int) - } - - public let service: ChatService - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .request(let group): - - return .run { _ in - try await service.requestCodeReview(group) - } - - case let .accept(id, selectedFileUris): - - return .run { _ in - await service.acceptCodeReview(id, selectedFileUris: selectedFileUris) - } - - case .cancel(let id): - - return .run { _ in - await service.cancelCodeReview(id) - } - - // lineNumber: 0-based - case .onFileClicked(let fileURL, let lineNumber): - - return .run { _ in - if FileManager.default.fileExists(atPath: fileURL.path) { - let terminal = Terminal() - do { - _ = try await terminal.runCommand( - "/bin/bash", - arguments: [ - "-c", - "xed -l \(lineNumber+1) \"\(fileURL.path)\"" - ], - environment: [:] - ) - } catch { - print(error) - } - } - - Task { @MainActor in - CodeReviewStateService.shared.notifyFileClicked() - } - } - - } - } - } -} diff --git a/Core/Sources/ConversationTab/FilePicker.swift b/Core/Sources/ConversationTab/FilePicker.swift deleted file mode 100644 index 8ae83e10..00000000 --- a/Core/Sources/ConversationTab/FilePicker.swift +++ /dev/null @@ -1,215 +0,0 @@ -import ComposableArchitecture -import ConversationServiceProvider -import SharedUIComponents -import SwiftUI -import SystemUtils - -public struct FilePicker: View { - @Binding var allFiles: [FileReference]? - let workspaceURL: URL? - var onSubmit: (_ file: FileReference) -> Void - var onExit: () -> Void - @FocusState private var isSearchBarFocused: Bool - @State private var searchText = "" - @State private var selectedId: Int = 0 - @State private var localMonitor: Any? = nil - - private var filteredFiles: [FileReference]? { - if searchText.isEmpty { - return allFiles - } - - return allFiles?.filter { doc in - (doc.fileName ?? doc.url.lastPathComponent) .localizedCaseInsensitiveContains(searchText) - } - } - - private static let defaultEmptyStateText = "No results found." - private static let isIndexingStateText = "Indexing files, try later..." - - private var emptyStateAttributedString: AttributedString? { - var message = allFiles == nil ? FilePicker.isIndexingStateText : FilePicker.defaultEmptyStateText - if let workspaceURL = workspaceURL { - let status = FileUtils.checkFileReadability(at: workspaceURL.path) - if let errorMessage = status.errorMessage(using: ContextUtils.workspaceReadabilityErrorMessageProvider) { - message = errorMessage - } - } - - return try? AttributedString(markdown: message) - } - - private var emptyStateView: some View { - Group { - if let attributedString = emptyStateAttributedString { - Text(attributedString) - } else { - Text(FilePicker.defaultEmptyStateText) - } - } - } - - public var body: some View { - WithPerceptionTracking { - VStack(spacing: 8) { - HStack { - Image(systemName: "magnifyingglass") - .foregroundColor(.secondary) - - TextField("Search files...", text: $searchText) - .textFieldStyle(PlainTextFieldStyle()) - .foregroundColor(searchText.isEmpty ? Color(nsColor: .placeholderTextColor) : Color(nsColor: .textColor)) - .focused($isSearchBarFocused) - .onChange(of: searchText) { newValue in - selectedId = 0 - } - .onAppear() { - isSearchBarFocused = true - } - - Button(action: { - withAnimation { - onExit() - } - }) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.secondary) - } - .buttonStyle(HoverButtonStyle()) - .help("Close") - } - .padding(8) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.1)) - ) - .cornerRadius(6) - .padding(.horizontal, 4) - .padding(.top, 4) - - ScrollViewReader { proxy in - ScrollView { - LazyVStack(alignment: .leading, spacing: 4) { - if allFiles == nil || filteredFiles?.isEmpty == true { - emptyStateView - .foregroundColor(.secondary) - .padding(.leading, 4) - .padding(.vertical, 4) - } else { - ForEach(Array((filteredFiles ?? []).enumerated()), id: \.element) { index, doc in - FileRowView(doc: doc, id: index, selectedId: $selectedId) - .contentShape(Rectangle()) - .onTapGesture { - onSubmit(doc) - selectedId = index - isSearchBarFocused = true - } - .id(index) - } - } - } - .id(filteredFiles?.hashValue) - } - .frame(maxHeight: 200) - .padding(.horizontal, 4) - .padding(.bottom, 4) - .onAppear { - localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - if !isSearchBarFocused { // if file search bar is not focused, ignore the event - return event - } - - switch event.keyCode { - case 126: // Up arrow - moveSelection(up: true, proxy: proxy) - return nil - case 125: // Down arrow - moveSelection(up: false, proxy: proxy) - return nil - case 36: // Return key - handleEnter() - return nil - case 53: // Esc key - withAnimation { - onExit() - } - return nil - default: - break - } - return event - } - } - .onDisappear { - if let monitor = localMonitor { - NSEvent.removeMonitor(monitor) - localMonitor = nil - } - } - } - } - .fixedSize(horizontal: false, vertical: true) - .cornerRadius(6) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - .padding(.horizontal, 12) - } - } - - private func moveSelection(up: Bool, proxy: ScrollViewProxy) { - guard let files = filteredFiles, !files.isEmpty else { return } - let nextId = selectedId + (up ? -1 : 1) - selectedId = max(0, min(nextId, files.count - 1)) - proxy.scrollTo(selectedId, anchor: .bottom) - } - - private func handleEnter() { - guard let files = filteredFiles, !files.isEmpty && selectedId < files.count else { return } - onSubmit(files[selectedId]) - } -} - -struct FileRowView: View { - @State private var isHovered = false - let doc: FileReference - let id: Int - @Binding var selectedId: Int - - var body: some View { - WithPerceptionTracking { - HStack { - drawFileIcon(doc.url) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.secondary) - .padding(.leading, 4) - - VStack(alignment: .leading) { - Text(doc.fileName ?? doc.url.lastPathComponent) - .font(.body) - .hoverPrimaryForeground(isHovered: selectedId == id) - .lineLimit(1) - .truncationMode(.middle) - Text(doc.relativePath ?? doc.url.path) - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - - Spacer() - } - .padding(.vertical, 4) - .hoverRadiusBackground(isHovered: isHovered || selectedId == id, - hoverColor: (selectedId == id ? nil : Color.gray.opacity(0.1)), - cornerRadius: 6) - .onHover(perform: { hovering in - isHovered = hovering - }) - .help(doc.relativePath ?? doc.url.path) - } - } -} diff --git a/Core/Sources/ConversationTab/ModelPicker/ChatModePicker.swift b/Core/Sources/ConversationTab/ModelPicker/ChatModePicker.swift deleted file mode 100644 index 94cd8051..00000000 --- a/Core/Sources/ConversationTab/ModelPicker/ChatModePicker.swift +++ /dev/null @@ -1,95 +0,0 @@ -import SwiftUI -import Persist -import ConversationServiceProvider -import GitHubCopilotService -import Combine - -public extension Notification.Name { - static let gitHubCopilotChatModeDidChange = Notification - .Name("com.github.CopilotForXcode.ChatModeDidChange") -} - -public enum ChatMode: String { - case Ask = "Ask" - case Agent = "Agent" -} - -public struct ChatModePicker: View { - @Binding var chatMode: String - @Environment(\.colorScheme) var colorScheme - @State var isAgentModeFFEnabled: Bool - @State private var cancellables = Set() - var onScopeChange: (PromptTemplateScope) -> Void - - public init(chatMode: Binding, onScopeChange: @escaping (PromptTemplateScope) -> Void = { _ in }) { - self._chatMode = chatMode - self.onScopeChange = onScopeChange - self.isAgentModeFFEnabled = FeatureFlagNotifierImpl.shared.featureFlags.agentMode - } - - private func setChatMode(mode: ChatMode) { - chatMode = mode.rawValue - AppState.shared.setSelectedChatMode(mode.rawValue) - onScopeChange(mode == .Ask ? .chatPanel : .agentPanel) - NotificationCenter.default.post( - name: .gitHubCopilotChatModeDidChange, - object: nil - ) - } - - private func subscribeToFeatureFlagsDidChangeEvent() { - FeatureFlagNotifierImpl.shared.featureFlagsDidChange.sink(receiveValue: { featureFlags in - isAgentModeFFEnabled = featureFlags.agentMode - }) - .store(in: &cancellables) - } - - public var body: some View { - VStack { - if isAgentModeFFEnabled { - HStack(spacing: -1) { - ModeButton( - title: "Ask", - isSelected: chatMode == "Ask", - activeBackground: colorScheme == .dark ? Color.white.opacity(0.25) : Color.white, - activeTextColor: Color.primary, - inactiveTextColor: Color.primary.opacity(0.5), - action: { - setChatMode(mode: .Ask) - } - ) - - ModeButton( - title: "Agent", - isSelected: chatMode == "Agent", - activeBackground: Color.blue, - activeTextColor: Color.white, - inactiveTextColor: Color.primary.opacity(0.5), - action: { - setChatMode(mode: .Agent) - } - ) - } - .padding(1) - .frame(height: 20, alignment: .topLeading) - .background(.primary.opacity(0.1)) - .cornerRadius(5) - .padding(4) - .help("Set Mode") - } else { - EmptyView() - } - } - .task { - subscribeToFeatureFlagsDidChangeEvent() - if !isAgentModeFFEnabled { - setChatMode(mode: .Ask) - } - } - .onChange(of: isAgentModeFFEnabled) { newAgentModeFFEnabled in - if !newAgentModeFFEnabled { - setChatMode(mode: .Ask) - } - } - } -} diff --git a/Core/Sources/ConversationTab/ModelPicker/ModeButton.swift b/Core/Sources/ConversationTab/ModelPicker/ModeButton.swift deleted file mode 100644 index b204e04c..00000000 --- a/Core/Sources/ConversationTab/ModelPicker/ModeButton.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -public struct ModeButton: View { - let title: String - let isSelected: Bool - let activeBackground: Color - let activeTextColor: Color - let inactiveTextColor: Color - let action: () -> Void - - public var body: some View { - Button(action: action) { - Text(title) - .padding(.horizontal, 6) - .padding(.vertical, 0) - .frame(maxHeight: .infinity, alignment: .center) - .background(isSelected ? activeBackground : Color.clear) - .foregroundColor(isSelected ? activeTextColor : inactiveTextColor) - .cornerRadius(5) - .shadow(color: .black.opacity(0.05), radius: 0.375, x: 0, y: 1) - .shadow(color: .black.opacity(0.15), radius: 0.125, x: 0, y: 0.25) - .overlay( - RoundedRectangle(cornerRadius: 5) - .inset(by: -0.25) - .stroke(.black.opacity(0.02), lineWidth: 0.5) - ) - } - .buttonStyle(PlainButtonStyle()) - } -} diff --git a/Core/Sources/ConversationTab/ModelPicker/ModelPicker.swift b/Core/Sources/ConversationTab/ModelPicker/ModelPicker.swift deleted file mode 100644 index 0f76adea..00000000 --- a/Core/Sources/ConversationTab/ModelPicker/ModelPicker.swift +++ /dev/null @@ -1,522 +0,0 @@ -import SwiftUI -import ChatService -import Persist -import ComposableArchitecture -import GitHubCopilotService -import Combine -import HostAppActivator -import SharedUIComponents -import ConversationServiceProvider - -public let SELECTED_LLM_KEY = "selectedLLM" -public let SELECTED_CHATMODE_KEY = "selectedChatMode" - -extension Notification.Name { - static let gitHubCopilotSelectedModelDidChange = Notification.Name("com.github.CopilotForXcode.SelectedModelDidChange") -} - -extension AppState { - func getSelectedModelFamily() -> String? { - if let savedModel = get(key: SELECTED_LLM_KEY), - let modelFamily = savedModel["modelFamily"]?.stringValue { - return modelFamily - } - return nil - } - - func getSelectedModelName() -> String? { - if let savedModel = get(key: SELECTED_LLM_KEY), - let modelName = savedModel["modelName"]?.stringValue { - return modelName - } - return nil - } - - func isSelectedModelSupportVision() -> Bool? { - if let savedModel = get(key: SELECTED_LLM_KEY) { - return savedModel["supportVision"]?.boolValue - } - return nil - } - - func setSelectedModel(_ model: LLMModel) { - update(key: SELECTED_LLM_KEY, value: model) - NotificationCenter.default.post(name: .gitHubCopilotSelectedModelDidChange, object: nil) - } - - func modelScope() -> PromptTemplateScope { - return isAgentModeEnabled() ? .agentPanel : .chatPanel - } - - func getSelectedChatMode() -> String { - if let savedMode = get(key: SELECTED_CHATMODE_KEY), - let modeName = savedMode.stringValue { - return convertChatMode(modeName) - } - return "Ask" - } - - func setSelectedChatMode(_ mode: String) { - update(key: SELECTED_CHATMODE_KEY, value: mode) - } - - func isAgentModeEnabled() -> Bool { - return getSelectedChatMode() == "Agent" - } - - private func convertChatMode(_ mode: String) -> String { - switch mode { - case "Agent": - return "Agent" - default: - return "Ask" - } - } -} - -class CopilotModelManagerObservable: ObservableObject { - static let shared = CopilotModelManagerObservable() - - @Published var availableChatModels: [LLMModel] = [] - @Published var availableAgentModels: [LLMModel] = [] - @Published var defaultChatModel: LLMModel? - @Published var defaultAgentModel: LLMModel? - private var cancellables = Set() - - private init() { - // Initial load - availableChatModels = CopilotModelManager.getAvailableChatLLMs(scope: .chatPanel) - availableAgentModels = CopilotModelManager.getAvailableChatLLMs(scope: .agentPanel) - defaultChatModel = CopilotModelManager.getDefaultChatModel(scope: .chatPanel) - defaultAgentModel = CopilotModelManager.getDefaultChatModel(scope: .agentPanel) - - - // Setup notification to update when models change - NotificationCenter.default.publisher(for: .gitHubCopilotModelsDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.availableChatModels = CopilotModelManager.getAvailableChatLLMs(scope: .chatPanel) - self?.availableAgentModels = CopilotModelManager.getAvailableChatLLMs(scope: .agentPanel) - self?.defaultChatModel = CopilotModelManager.getDefaultChatModel(scope: .chatPanel) - self?.defaultAgentModel = CopilotModelManager.getDefaultChatModel(scope: .agentPanel) - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: .gitHubCopilotShouldSwitchFallbackModel) - .receive(on: DispatchQueue.main) - .sink { _ in - if let fallbackModel = CopilotModelManager.getFallbackLLM( - scope: AppState.shared - .isAgentModeEnabled() ? .agentPanel : .chatPanel - ) { - AppState.shared.setSelectedModel( - .init( - modelName: fallbackModel.modelName, - modelFamily: fallbackModel.id, - billing: fallbackModel.billing, - supportVision: fallbackModel.capabilities.supports.vision - ) - ) - } - } - .store(in: &cancellables) - } -} - -extension CopilotModelManager { - static func getAvailableChatLLMs(scope: PromptTemplateScope = .chatPanel) -> [LLMModel] { - let LLMs = CopilotModelManager.getAvailableLLMs() - return LLMs.filter( - { $0.scopes.contains(scope) } - ).map { - return LLMModel( - modelName: $0.modelName, - modelFamily: $0.isChatFallback ? $0.id : $0.modelFamily, - billing: $0.billing, - supportVision: $0.capabilities.supports.vision - ) - } - } - - static func getDefaultChatModel(scope: PromptTemplateScope = .chatPanel) -> LLMModel? { - let LLMs = CopilotModelManager.getAvailableLLMs() - let LLMsInScope = LLMs.filter({ $0.scopes.contains(scope) }) - let defaultModel = LLMsInScope.first(where: { $0.isChatDefault }) - // If a default model is found, return it - if let defaultModel = defaultModel { - return LLMModel( - modelName: defaultModel.modelName, - modelFamily: defaultModel.modelFamily, - billing: defaultModel.billing, - supportVision: defaultModel.capabilities.supports.vision - ) - } - - // Fallback to gpt-4.1 if available - let gpt4_1 = LLMsInScope.first(where: { $0.modelFamily == "gpt-4.1" }) - if let gpt4_1 = gpt4_1 { - return LLMModel( - modelName: gpt4_1.modelName, - modelFamily: gpt4_1.modelFamily, - billing: gpt4_1.billing, - supportVision: gpt4_1.capabilities.supports.vision - ) - } - - // If no default model is found, fallback to the first available model - if let firstModel = LLMsInScope.first { - return LLMModel( - modelName: firstModel.modelName, - modelFamily: firstModel.modelFamily, - billing: firstModel.billing, - supportVision: firstModel.capabilities.supports.vision - ) - } - - return nil - } -} - -struct LLMModel: Codable, Hashable { - let modelName: String - let modelFamily: String - let billing: CopilotModelBilling? - let supportVision: Bool -} - -struct ScopeCache { - var modelMultiplierCache: [String: String] = [:] - var cachedMaxWidth: CGFloat = 0 - var lastModelsHash: Int = 0 -} - -struct ModelPicker: View { - @State private var selectedModel = "" - @State private var isHovered = false - @State private var isPressed = false - @ObservedObject private var modelManager = CopilotModelManagerObservable.shared - static var lastRefreshModelsTime: Date = .init(timeIntervalSince1970: 0) - - @State private var chatMode = "Ask" - @State private var isAgentPickerHovered = false - - // Separate caches for both scopes - @State private var askScopeCache: ScopeCache = ScopeCache() - @State private var agentScopeCache: ScopeCache = ScopeCache() - - @State var isMCPFFEnabled: Bool - @State private var cancellables = Set() - - let minimumPadding: Int = 48 - let attributes: [NSAttributedString.Key: NSFont] = [.font: NSFont.systemFont(ofSize: NSFont.systemFontSize)] - - var spaceWidth: CGFloat { - "\u{200A}".size(withAttributes: attributes).width - } - - var minimumPaddingWidth: CGFloat { - spaceWidth * CGFloat(minimumPadding) - } - - init() { - let initialModel = AppState.shared.getSelectedModelName() ?? CopilotModelManager.getDefaultChatModel()?.modelName ?? "" - self._selectedModel = State(initialValue: initialModel) - self.isMCPFFEnabled = FeatureFlagNotifierImpl.shared.featureFlags.mcp - updateAgentPicker() - } - - private func subscribeToFeatureFlagsDidChangeEvent() { - FeatureFlagNotifierImpl.shared.featureFlagsDidChange.sink(receiveValue: { featureFlags in - isMCPFFEnabled = featureFlags.mcp - }) - .store(in: &cancellables) - } - - var models: [LLMModel] { - AppState.shared.isAgentModeEnabled() ? modelManager.availableAgentModels : modelManager.availableChatModels - } - - var defaultModel: LLMModel? { - AppState.shared.isAgentModeEnabled() ? modelManager.defaultAgentModel : modelManager.defaultChatModel - } - - // Get the current cache based on scope - var currentCache: ScopeCache { - AppState.shared.isAgentModeEnabled() ? agentScopeCache : askScopeCache - } - - // Helper method to format multiplier text - func formatMultiplierText(for billing: CopilotModelBilling?) -> String { - guard let billingInfo = billing else { return "" } - - let multiplier = billingInfo.multiplier - if multiplier == 0 { - return "Included" - } else { - let numberPart = multiplier.truncatingRemainder(dividingBy: 1) == 0 - ? String(format: "%.0f", multiplier) - : String(format: "%.2f", multiplier) - return "\(numberPart)x" - } - } - - // Update cache for specific scope only if models changed - func updateModelCacheIfNeeded(for scope: PromptTemplateScope) { - let currentModels = scope == .agentPanel ? modelManager.availableAgentModels : modelManager.availableChatModels - let modelsHash = currentModels.hashValue - - if scope == .agentPanel { - guard agentScopeCache.lastModelsHash != modelsHash else { return } - agentScopeCache = buildCache(for: currentModels, currentHash: modelsHash) - } else { - guard askScopeCache.lastModelsHash != modelsHash else { return } - askScopeCache = buildCache(for: currentModels, currentHash: modelsHash) - } - } - - // Build cache for given models - private func buildCache(for models: [LLMModel], currentHash: Int) -> ScopeCache { - var newCache: [String: String] = [:] - var maxWidth: CGFloat = 0 - - for model in models { - let multiplierText = formatMultiplierText(for: model.billing) - newCache[model.modelName] = multiplierText - - let displayName = "✓ \(model.modelName)" - let displayNameWidth = displayName.size(withAttributes: attributes).width - let multiplierWidth = multiplierText.isEmpty ? 0 : multiplierText.size(withAttributes: attributes).width - let totalWidth = displayNameWidth + minimumPaddingWidth + multiplierWidth - maxWidth = max(maxWidth, totalWidth) - } - - if maxWidth == 0 { - maxWidth = selectedModel.size(withAttributes: attributes).width - } - - return ScopeCache( - modelMultiplierCache: newCache, - cachedMaxWidth: maxWidth, - lastModelsHash: currentHash - ) - } - - func updateCurrentModel() { - selectedModel = AppState.shared.getSelectedModelName() ?? defaultModel?.modelName ?? "" - } - - func updateAgentPicker() { - self.chatMode = AppState.shared.getSelectedChatMode() - } - - func switchModelsForScope(_ scope: PromptTemplateScope) { - let newModeModels = CopilotModelManager.getAvailableChatLLMs(scope: scope) - - if let currentModel = AppState.shared.getSelectedModelName() { - if !newModeModels.isEmpty && !newModeModels.contains(where: { $0.modelName == currentModel }) { - let defaultModel = CopilotModelManager.getDefaultChatModel(scope: scope) - if let defaultModel = defaultModel { - AppState.shared.setSelectedModel(defaultModel) - } else { - AppState.shared.setSelectedModel(newModeModels[0]) - } - } - } - - self.updateCurrentModel() - updateModelCacheIfNeeded(for: scope) - } - - // Model picker menu component - private var modelPickerMenu: some View { - Menu(selectedModel) { - // Group models by premium status - let premiumModels = models.filter { $0.billing?.isPremium == true } - let standardModels = models.filter { $0.billing?.isPremium == false || $0.billing == nil } - - // Display standard models section if available - modelSection(title: "Standard Models", models: standardModels) - - // Display premium models section if available - modelSection(title: "Premium Models", models: premiumModels) - - if standardModels.isEmpty { - Link("Add Premium Models", destination: URL(string: "https://aka.ms/github-copilot-upgrade-plan")!) - } - } - .menuStyle(BorderlessButtonMenuStyle()) - .frame(maxWidth: labelWidth()) - .padding(4) - .background( - RoundedRectangle(cornerRadius: 5) - .fill(isHovered ? Color.gray.opacity(0.1) : Color.clear) - ) - .onHover { hovering in - isHovered = hovering - } - } - - // Helper function to create a section of model options - @ViewBuilder - private func modelSection(title: String, models: [LLMModel]) -> some View { - if !models.isEmpty { - Section(title) { - ForEach(models, id: \.self) { model in - modelButton(for: model) - } - } - } - } - - // Helper function to create a model selection button - private func modelButton(for model: LLMModel) -> some View { - Button { - AppState.shared.setSelectedModel(model) - } label: { - Text(createModelMenuItemAttributedString( - modelName: model.modelName, - isSelected: selectedModel == model.modelName, - cachedMultiplierText: currentCache.modelMultiplierCache[model.modelName] ?? "" - )) - } - } - - private var mcpButton: some View { - Group { - if isMCPFFEnabled { - Button(action: { - try? launchHostAppMCPSettings() - }) { - mcpIcon.foregroundColor(.primary.opacity(0.85)) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .help("Configure your MCP server") - } else { - // Non-interactive view that looks like a button but only shows tooltip - mcpIcon.foregroundColor(Color(nsColor: .tertiaryLabelColor)) - .padding(0) - .help("MCP servers are disabled by org policy. Contact your admin.") - } - } - .cornerRadius(6) - } - - private var mcpIcon: some View { - Image(systemName: "wrench.and.screwdriver") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .padding(4) - .font(Font.system(size: 11, weight: .semibold)) - } - - // Main view body - var body: some View { - WithPerceptionTracking { - HStack(spacing: 0) { - // Custom segmented control with color change - ChatModePicker(chatMode: $chatMode, onScopeChange: switchModelsForScope) - .onAppear() { - updateAgentPicker() - } - - if chatMode == "Agent" { - mcpButton - } - - // Model Picker - Group { - if !models.isEmpty && !selectedModel.isEmpty { - modelPickerMenu - } else { - EmptyView() - } - } - } - .onAppear() { - updateCurrentModel() - // Initialize both caches - updateModelCacheIfNeeded(for: .chatPanel) - updateModelCacheIfNeeded(for: .agentPanel) - Task { - await refreshModels() - } - } - .onChange(of: defaultModel) { _ in - updateCurrentModel() - } - .onChange(of: modelManager.availableChatModels) { _ in - updateCurrentModel() - updateModelCacheIfNeeded(for: .chatPanel) - } - .onChange(of: modelManager.availableAgentModels) { _ in - updateCurrentModel() - updateModelCacheIfNeeded(for: .agentPanel) - } - .onChange(of: chatMode) { _ in - updateCurrentModel() - } - .onReceive(NotificationCenter.default.publisher(for: .gitHubCopilotSelectedModelDidChange)) { _ in - updateCurrentModel() - } - .task { - subscribeToFeatureFlagsDidChangeEvent() - } - } - } - - func labelWidth() -> CGFloat { - let width = selectedModel.size(withAttributes: attributes).width - return CGFloat(width + 20) - } - - @MainActor - func refreshModels() async { - let now = Date() - if now.timeIntervalSince(Self.lastRefreshModelsTime) < 60 { - return - } - - Self.lastRefreshModelsTime = now - let copilotModels = await SharedChatService.shared.copilotModels() - if !copilotModels.isEmpty { - CopilotModelManager.updateLLMs(copilotModels) - } - } - - private func createModelMenuItemAttributedString( - modelName: String, - isSelected: Bool, - cachedMultiplierText: String - ) -> AttributedString { - let displayName = isSelected ? "✓ \(modelName)" : " \(modelName)" - - var fullString = displayName - var attributedString = AttributedString(fullString) - - if !cachedMultiplierText.isEmpty { - let displayNameWidth = displayName.size(withAttributes: attributes).width - let multiplierTextWidth = cachedMultiplierText.size(withAttributes: attributes).width - let neededPaddingWidth = currentCache.cachedMaxWidth - displayNameWidth - multiplierTextWidth - let finalPaddingWidth = max(neededPaddingWidth, minimumPaddingWidth) - - let numberOfSpaces = Int(round(finalPaddingWidth / spaceWidth)) - let padding = String(repeating: "\u{200A}", count: max(minimumPadding, numberOfSpaces)) - fullString = "\(displayName)\(padding)\(cachedMultiplierText)" - - attributedString = AttributedString(fullString) - - if let range = attributedString.range(of: cachedMultiplierText) { - attributedString[range].foregroundColor = .secondary - } - } - - return attributedString - } -} - -struct ModelPicker_Previews: PreviewProvider { - static var previews: some View { - ModelPicker() - } -} diff --git a/Core/Sources/ConversationTab/Styles.swift b/Core/Sources/ConversationTab/Styles.swift deleted file mode 100644 index 996593f3..00000000 --- a/Core/Sources/ConversationTab/Styles.swift +++ /dev/null @@ -1,208 +0,0 @@ -import AppKit -import MarkdownUI -import SharedUIComponents -import SwiftUI - -extension Color { - static var contentBackground: Color { - Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in - if appearance.isDarkMode { - return #colorLiteral(red: 0.1580096483, green: 0.1730263829, blue: 0.2026666105, alpha: 1) - } - return #colorLiteral(red: 0.9896564803, green: 0.9896564803, blue: 0.9896564803, alpha: 1) - })) - } - - static var userChatContentBackground: Color { - Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in - if appearance.isDarkMode { - return #colorLiteral(red: 0.2284317913, green: 0.2145925438, blue: 0.3214019983, alpha: 1) - } - return #colorLiteral(red: 0.9458052187, green: 0.9311983998, blue: 0.9906365955, alpha: 1) - })) - } -} - -extension NSAppearance { - var isDarkMode: Bool { - if bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { - return true - } else { - return false - } - } -} - -extension View { - var messageBubbleCornerRadius: Double { 8 } - var hoverableImageCornerRadius: Double { 4 } - - func codeBlockLabelStyle() -> some View { - relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(.leading, 8) - .padding(.top, 24) - .padding(.bottom, 8) - } - - func codeBlockStyle( - _ configuration: CodeBlockConfiguration, - backgroundColor: Color, - labelColor: Color, - context: MarkdownActionProvider? = nil - ) -> some View { - background(backgroundColor) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay(alignment: .top) { - HStack(alignment: .center) { - Text(configuration.language ?? "code") - .foregroundStyle(labelColor) - .font(.callout.bold()) - .padding(.leading, 8) - .lineLimit(1) - Spacer() - - HStack(spacing: 4) { - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(configuration.content, forType: .string) - } - - if let context = context, context.supportInsert { - InsertButton { - if let onInsert = context.onInsert { - onInsert(configuration.content) - } - } - } - } - } - .padding(.trailing, 8) - } - .overlay { - RoundedRectangle(cornerRadius: 6).stroke(Color.primary.opacity(0.05), lineWidth: 1) - } - .markdownMargin(top: 4, bottom: 16) - .frame(maxWidth: .infinity) - } -} - -final class VerticalScrollingFixHostingView: NSHostingView where Content: View { - override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { - return axis == .vertical - } -} - -struct VerticalScrollingFixViewRepresentable: NSViewRepresentable where Content: View { - let content: Content - - func makeNSView(context: Context) -> NSHostingView { - return VerticalScrollingFixHostingView(rootView: content) - } - - func updateNSView(_ nsView: NSHostingView, context: Context) {} -} - -struct VerticalScrollingFixWrapper: View where Content: View { - let content: () -> Content - - init(@ViewBuilder content: @escaping () -> Content) { - self.content = content - } - - var body: some View { - VerticalScrollingFixViewRepresentable(content: self.content()) - } -} - -extension View { - /// https://stackoverflow.com/questions/64920744/swiftui-nested-scrollviews-problem-on-macos - @ViewBuilder func workaroundForVerticalScrollingBugInMacOS() -> some View { - VerticalScrollingFixWrapper { self } - } -} - -struct RoundedCorners: Shape { - var tl: CGFloat = 0.0 - var tr: CGFloat = 0.0 - var bl: CGFloat = 0.0 - var br: CGFloat = 0.0 - - func path(in rect: CGRect) -> Path { - Path { path in - - let w = rect.size.width - let h = rect.size.height - - // Make sure we do not exceed the size of the rectangle - let tr = min(min(self.tr, h / 2), w / 2) - let tl = min(min(self.tl, h / 2), w / 2) - let bl = min(min(self.bl, h / 2), w / 2) - let br = min(min(self.br, h / 2), w / 2) - - path.move(to: CGPoint(x: w / 2.0, y: 0)) - path.addLine(to: CGPoint(x: w - tr, y: 0)) - path.addArc( - center: CGPoint(x: w - tr, y: tr), - radius: tr, - startAngle: Angle(degrees: -90), - endAngle: Angle(degrees: 0), - clockwise: false - ) - path.addLine(to: CGPoint(x: w, y: h - br)) - path.addArc( - center: CGPoint(x: w - br, y: h - br), - radius: br, - startAngle: Angle(degrees: 0), - endAngle: Angle(degrees: 90), - clockwise: false - ) - path.addLine(to: CGPoint(x: bl, y: h)) - path.addArc( - center: CGPoint(x: bl, y: h - bl), - radius: bl, - startAngle: Angle(degrees: 90), - endAngle: Angle(degrees: 180), - clockwise: false - ) - path.addLine(to: CGPoint(x: 0, y: tl)) - path.addArc( - center: CGPoint(x: tl, y: tl), - radius: tl, - startAngle: Angle(degrees: 180), - endAngle: Angle(degrees: 270), - clockwise: false - ) - path.closeSubpath() - } - } -} - -// Chat Message Styles -extension View { - func chatMessageHeaderTextStyle() -> some View { - // semibold -> 600 - font(.system(size: 13, weight: .semibold)) - } -} - -// MARK: - Code Review Background Styles - -struct CodeReviewCardBackground: View { - var body: some View { - RoundedRectangle(cornerRadius: 4) - .stroke(.black.opacity(0.17), lineWidth: 1) - .background(Color.gray.opacity(0.05)) - } -} - -struct CodeReviewHeaderBackground: View { - var body: some View { - RoundedRectangle(cornerRadius: 4) - .stroke(.black.opacity(0.17), lineWidth: 1) - .background(Color.gray.opacity(0.1)) - } -} diff --git a/Core/Sources/ConversationTab/TerminalViews/RunInTerminalToolView.swift b/Core/Sources/ConversationTab/TerminalViews/RunInTerminalToolView.swift deleted file mode 100644 index c75e864e..00000000 --- a/Core/Sources/ConversationTab/TerminalViews/RunInTerminalToolView.swift +++ /dev/null @@ -1,166 +0,0 @@ -import SwiftUI -import XcodeInspector -import ConversationServiceProvider -import ComposableArchitecture -import Terminal - -struct RunInTerminalToolView: View { - let tool: AgentToolCall - let command: String? - let explanation: String? - let isBackground: Bool? - let chat: StoreOf - private var title: String = "Run command in terminal" - - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFont) var chatCodeFont - @Environment(\.colorScheme) var colorScheme - - init(tool: AgentToolCall, chat: StoreOf) { - self.tool = tool - self.chat = chat - if let input = tool.invokeParams?.input as? [String: AnyCodable] { - self.command = input["command"]?.value as? String - self.explanation = input["explanation"]?.value as? String - self.isBackground = input["isBackground"]?.value as? Bool - self.title = (isBackground != nil && isBackground!) ? "Run command in background terminal" : "Run command in terminal" - } else { - self.command = nil - self.explanation = nil - self.isBackground = nil - } - } - - var terminalSession: TerminalSession? { - return TerminalSessionManager.shared.getSession(for: tool.id) - } - - var statusIcon: some View { - Group { - switch tool.status { - case .running: - ProgressView() - .controlSize(.small) - .scaleEffect(0.7) - case .completed: - Image(systemName: "checkmark") - .foregroundColor(.green.opacity(0.5)) - case .error: - Image(systemName: "xmark.circle") - .foregroundColor(.red.opacity(0.5)) - case .cancelled: - Image(systemName: "slash.circle") - .foregroundColor(.gray.opacity(0.5)) - case .waitForConfirmation: - EmptyView() - case .accepted: - EmptyView() - } - } - } - - var body: some View { - WithPerceptionTracking { - if tool.status == .waitForConfirmation || terminalSession != nil { - VStack { - HStack { - Image("Terminal") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - - Text(self.title) - .font(.system(size: chatFontSize)) - .fontWeight(.semibold) - .foregroundStyle(.primary) - .background(Color.clear) - .frame(maxWidth: .infinity, alignment: .leading) - } - - toolView - } - .padding(8) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.gray.opacity(0.2), lineWidth: 1) - ) - } else { - toolView - } - } - } - - var codeBackgroundColor: Color { - if colorScheme == .light, let color = codeBackgroundColorLight.value { - return color.swiftUIColor - } else if let color = codeBackgroundColorDark.value { - return color.swiftUIColor - } - return Color(nsColor: .textBackgroundColor).opacity(0.7) - } - - var codeForegroundColor: Color { - if colorScheme == .light, let color = codeForegroundColorLight.value { - return color.swiftUIColor - } else if let color = codeForegroundColorDark.value { - return color.swiftUIColor - } - return Color(nsColor: .textColor) - } - - var toolView: some View { - WithPerceptionTracking { - VStack { - if command != nil { - HStack(spacing: 4) { - statusIcon - .frame(width: 16, height: 16) - - Text(command!) - .textSelection(.enabled) - .font(.system(size: chatFontSize, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(codeForegroundColor) - .background(codeBackgroundColor) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay { - RoundedRectangle(cornerRadius: 6).stroke(Color.primary.opacity(0.05), lineWidth: 1) - } - } - } else { - Text("Invalid parameter in the toolcall for runInTerminal") - } - - if let terminalSession = terminalSession { - XTermView( - terminalSession: terminalSession, - onTerminalInput: terminalSession.handleTerminalInput - ) - .frame(minHeight: 200, maxHeight: 400) - } else if tool.status == .waitForConfirmation { - ThemedMarkdownText(text: explanation ?? "", chat: chat) - .frame(maxWidth: .infinity, alignment: .leading) - - HStack { - Button("Cancel") { - chat.send(.toolCallCancelled(tool.id)) - } - - Button("Continue") { - chat.send(.toolCallAccepted(tool.id)) - } - .buttonStyle(BorderedProminentButtonStyle()) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 4) - } - } - } - } -} diff --git a/Core/Sources/ConversationTab/TerminalViews/XTermView.swift b/Core/Sources/ConversationTab/TerminalViews/XTermView.swift deleted file mode 100644 index 23e1fbd0..00000000 --- a/Core/Sources/ConversationTab/TerminalViews/XTermView.swift +++ /dev/null @@ -1,100 +0,0 @@ -import SwiftUI -import Logger -import WebKit -import Terminal - -struct XTermView: NSViewRepresentable { - @ObservedObject var terminalSession: TerminalSession - var onTerminalInput: (String) -> Void - - var terminalOutput: String { - terminalSession.terminalOutput - } - - func makeNSView(context: Context) -> WKWebView { - let webpagePrefs = WKWebpagePreferences() - webpagePrefs.allowsContentJavaScript = true - let preferences = WKWebViewConfiguration() - preferences.defaultWebpagePreferences = webpagePrefs - preferences.userContentController.add(context.coordinator, name: "terminalInput") - - let webView = WKWebView(frame: .zero, configuration: preferences) - webView.navigationDelegate = context.coordinator - #if DEBUG - webView.configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") - #endif - - // Load the terminal bundle resources - let terminalBundleBaseURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/webViewDist/terminal") - let htmlFileURL = terminalBundleBaseURL.appendingPathComponent("terminal.html") - webView.loadFileURL(htmlFileURL, allowingReadAccessTo: terminalBundleBaseURL) - return webView - } - - func updateNSView(_ webView: WKWebView, context: Context) { - // When terminalOutput changes, send the new data to the terminal - if context.coordinator.lastOutput != terminalOutput { - let newOutput = terminalOutput.suffix(from: - terminalOutput.index(terminalOutput.startIndex, - offsetBy: min(context.coordinator.lastOutput.count, terminalOutput.count))) - - if !newOutput.isEmpty { - context.coordinator.lastOutput = terminalOutput - if context.coordinator.isWebViewLoaded { - context.coordinator.writeToTerminal(text: String(newOutput), webView: webView) - } else { - context.coordinator.pendingOutput = (context.coordinator.pendingOutput ?? "") + String(newOutput) - } - } - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { - var parent: XTermView - var lastOutput: String = "" - var isWebViewLoaded = false - var pendingOutput: String? - - init(_ parent: XTermView) { - self.parent = parent - super.init() - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - isWebViewLoaded = true - if let pending = pendingOutput { - writeToTerminal(text: pending, webView: webView) - pendingOutput = nil - } - } - - func writeToTerminal(text: String, webView: WKWebView) { - let escapedOutput = text - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "'", with: "\\'") - .replacingOccurrences(of: "\n", with: "\\r\\n") - .replacingOccurrences(of: "\r", with: "\\r") - - let jsCode = "writeToTerminal('\(escapedOutput)');" - DispatchQueue.main.async { - webView.evaluateJavaScript(jsCode) { _, error in - if let error = error { - Logger.client.info("XTerm: Error writing to terminal: \(error)") - } - } - } - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name == "terminalInput", let input = message.body as? String { - DispatchQueue.main.async { - self.parent.onTerminalInput(input) - } - } - } - } -} diff --git a/Core/Sources/ConversationTab/ViewExtension.swift b/Core/Sources/ConversationTab/ViewExtension.swift deleted file mode 100644 index 912c9687..00000000 --- a/Core/Sources/ConversationTab/ViewExtension.swift +++ /dev/null @@ -1,85 +0,0 @@ -import SwiftUI - -let ITEM_SELECTED_COLOR = Color("ItemSelectedColor") - -struct HoverBackgroundModifier: ViewModifier { - var isHovered: Bool - - func body(content: Content) -> some View { - content - .background(isHovered ? ITEM_SELECTED_COLOR : Color.clear) - } -} - -struct HoverRadiusBackgroundModifier: ViewModifier { - var isHovered: Bool - var hoverColor: Color? - var cornerRadius: CGFloat = 0 - var showBorder: Bool = false - var borderColor: Color = .white.opacity(0.07) - var borderWidth: CGFloat = 1 - - func body(content: Content) -> some View { - content - .background( - RoundedRectangle(cornerRadius: cornerRadius) - .fill(isHovered ? hoverColor ?? ITEM_SELECTED_COLOR : Color.clear) - ) - .clipShape( - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - ) - .overlay( - (isHovered && showBorder) ? - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .strokeBorder(borderColor, lineWidth: borderWidth) : - nil - ) - } -} - -struct HoverForegroundModifier: ViewModifier { - var isHovered: Bool - var defaultColor: Color - - func body(content: Content) -> some View { - content.foregroundColor(isHovered ? Color.white : defaultColor) - } -} - -extension View { - public func hoverBackground(isHovered: Bool) -> some View { - self.modifier(HoverBackgroundModifier(isHovered: isHovered)) - } - - public func hoverRadiusBackground(isHovered: Bool, cornerRadius: CGFloat) -> some View { - self.modifier(HoverRadiusBackgroundModifier(isHovered: isHovered, cornerRadius: cornerRadius)) - } - - public func hoverRadiusBackground(isHovered: Bool, hoverColor: Color?, cornerRadius: CGFloat) -> some View { - self.modifier(HoverRadiusBackgroundModifier(isHovered: isHovered, hoverColor: hoverColor, cornerRadius: cornerRadius)) - } - - public func hoverRadiusBackground(isHovered: Bool, hoverColor: Color?, cornerRadius: CGFloat, showBorder: Bool, borderColor: Color = .white.opacity(0.07)) -> some View { - self.modifier( - HoverRadiusBackgroundModifier( - isHovered: isHovered, - hoverColor: hoverColor, - cornerRadius: cornerRadius, - showBorder: true, - borderColor: borderColor - ) - ) - } - - public func hoverForeground(isHovered: Bool, defaultColor: Color) -> some View { - self.modifier(HoverForegroundModifier(isHovered: isHovered, defaultColor: defaultColor)) - } - - public func hoverPrimaryForeground(isHovered: Bool) -> some View { - self.hoverForeground(isHovered: isHovered, defaultColor: .primary) - } - - public func hoverSecondaryForeground(isHovered: Bool) -> some View { - self.hoverForeground(isHovered: isHovered, defaultColor: .secondary) - } -} diff --git a/Core/Sources/ConversationTab/Views/BotMessage.swift b/Core/Sources/ConversationTab/Views/BotMessage.swift deleted file mode 100644 index a67dbcc5..00000000 --- a/Core/Sources/ConversationTab/Views/BotMessage.swift +++ /dev/null @@ -1,392 +0,0 @@ -import ComposableArchitecture -import ChatService -import Foundation -import MarkdownUI -import SharedUIComponents -import SwiftUI -import ConversationServiceProvider -import ChatTab -import ChatAPIService - -struct BotMessage: View { - var r: Double { messageBubbleCornerRadius } - let id: String - let text: String - let references: [ConversationReference] - let followUp: ConversationFollowUp? - let errorMessages: [String] - let chat: StoreOf - let steps: [ConversationProgressStep] - let editAgentRounds: [AgentRound] - let panelMessages: [CopilotShowMessageParams] - let codeReviewRound: CodeReviewRound? - - @Environment(\.colorScheme) var colorScheme - @AppStorage(\.chatFontSize) var chatFontSize - - @State var isReferencesPresented = false - - struct ResponseToolBar: View { - let id: String - let chat: StoreOf - let text: String - - var body: some View { - HStack(spacing: 4) { - - UpvoteButton { rating in - chat.send(.upvote(id, rating)) - } - - DownvoteButton { rating in - chat.send(.downvote(id, rating)) - } - - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - chat.send(.copyCode(id)) - } - - Spacer() // Pushes the buttons to the left - } - } - } - - struct ReferenceButton: View { - var r: Double { messageBubbleCornerRadius } - let references: [ConversationReference] - let chat: StoreOf - - @Binding var isReferencesPresented: Bool - - @State var isReferencesHovered = false - - @AppStorage(\.chatFontSize) var chatFontSize - - func MakeReferenceTitle(references: [ConversationReference]) -> String { - guard !references.isEmpty else { - return "" - } - - let count = references.count - let title = count > 1 ? "Used \(count) references" : "Used \(count) reference" - return title - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Button(action: { - isReferencesPresented.toggle() - }, label: { - HStack(spacing: 4) { - Image(systemName: isReferencesPresented ? "chevron.down" : "chevron.right") - - Text(MakeReferenceTitle(references: references)) - .font(.system(size: chatFontSize)) - } - .background { - RoundedRectangle(cornerRadius: r - 4) - .fill(isReferencesHovered ? Color.gray.opacity(0.1) : Color.clear) - } - .foregroundStyle(.secondary) - }) - .buttonStyle(HoverButtonStyle()) - .accessibilityValue(isReferencesPresented ? "Collapse" : "Expand") - - if isReferencesPresented { - ReferenceList(references: references, chat: chat) - .background( - RoundedRectangle(cornerRadius: 5) - .stroke(Color.gray, lineWidth: 0.2) - ) - } - } - } - } - - private var agentWorkingStatus: some View { - HStack(spacing: 4) { - ProgressView() - .controlSize(.small) - .frame(width: 20, height: 16) - .scaleEffect(0.7) - - Text("Working...") - .font(.system(size: chatFontSize)) - .foregroundColor(.secondary) - } - } - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 8) { - CopilotMessageHeader() - - if !references.isEmpty { - WithPerceptionTracking { - ReferenceButton( - references: references, - chat: chat, - isReferencesPresented: $isReferencesPresented - ) - } - } - - // progress step - if steps.count > 0 { - ProgressStep(steps: steps) - } - - if !panelMessages.isEmpty { - WithPerceptionTracking { - ForEach(panelMessages.indices, id: \.self) { index in - FunctionMessage(text: panelMessages[index].message, chat: chat) - } - } - } - - if editAgentRounds.count > 0 { - ProgressAgentRound(rounds: editAgentRounds, chat: chat) - } - - if !text.isEmpty { - ThemedMarkdownText(text: text, chat: chat) - } - - if let codeReviewRound = codeReviewRound { - CodeReviewMainView( - store: chat, round: codeReviewRound - ) - } - - if !errorMessages.isEmpty { - VStack(spacing: 4) { - ForEach(errorMessages.indices, id: \.self) { index in - if let attributedString = try? AttributedString(markdown: errorMessages[index]) { - NotificationBanner(style: .warning) { - Text(attributedString) - } - } - } - } - } - - if shouldShowWorkingStatus() { - agentWorkingStatus - } - - if shouldShowToolBar() { - ResponseToolBar(id: id, chat: chat, text: text) - } - } - .shadow(color: .black.opacity(0.05), radius: 6) - .contextMenu { - Button("Copy") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - - Button("Set as Extra System Prompt") { - chat.send(.setAsExtraPromptButtonTapped(id)) - } - - Divider() - - Button("Delete") { - chat.send(.deleteMessageButtonTapped(id)) - } - } - } - } - - private func shouldShowWorkingStatus() -> Bool { - let hasRunningStep: Bool = steps.contains(where: { $0.status == .running }) - let hasRunningRound: Bool = editAgentRounds.contains(where: { round in - return round.toolCalls?.contains(where: { $0.status == .running }) ?? false - }) - - if hasRunningStep || hasRunningRound { - return false - } - - // Only show working status for the current bot message being received - return chat.isReceivingMessage && isLatestAssistantMessage() - } - - private func shouldShowToolBar() -> Bool { - // Always show toolbar for historical messages - if !isLatestAssistantMessage() { return true } - - // For current message, only show toolbar when message is complete - return !chat.isReceivingMessage - } - - private func isLatestAssistantMessage() -> Bool { - let lastMessage = chat.history.last - return lastMessage?.role == .assistant && lastMessage?.id == id - } -} - -struct ReferenceList: View { - let references: [ConversationReference] - let chat: StoreOf - - private let maxVisibleItems: Int = 6 - @State private var itemHeight: CGFloat = 16 - - @AppStorage(\.chatFontSize) var chatFontSize - - struct ReferenceView: View { - let references: [ConversationReference] - let chat: StoreOf - @AppStorage(\.chatFontSize) var chatFontSize - @Binding var itemHeight: CGFloat - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - ForEach(0.. - let round: CodeReviewRound - @State private var selectedFileUris: [DocumentUri] - @AppStorage(\.chatFontSize) var chatFontSize - - private var changedFileUris: [DocumentUri] { - round.request?.changedFileUris ?? [] - } - - private var hasChangedFiles: Bool { - !changedFileUris.isEmpty - } - - private var hasFileComments: Bool { - guard let fileComments = round.response?.fileComments else { return false } - return !fileComments.isEmpty - } - - static let HelloMessage: String = "Sure, I can help you with that." - - public init(store: StoreOf, round: CodeReviewRound) { - self.store = store - self.round = round - self.selectedFileUris = round.request?.selectedFileUris ?? [] - } - - var helloMessageView: some View { - Text(Self.HelloMessage) - .font(.system(size: chatFontSize)) - } - - var statusIcon: some View { - Group { - switch round.status { - case .running: - ProgressView() - .controlSize(.small) - .frame(width: 16, height: 16) - .scaleEffect(0.7) - case .completed: - Image(systemName: "checkmark") - .foregroundColor(.green) - case .error: - Image(systemName: "xmark.circle") - .foregroundColor(.red) - case .cancelled: - Image(systemName: "slash.circle") - .foregroundColor(.gray) - case .waitForConfirmation: - EmptyView() - case .accepted: - EmptyView() - } - } - } - - var statusView: some View { - Group { - switch round.status { - case .waitForConfirmation, .accepted: - EmptyView() - default: - HStack(spacing: 4) { - statusIcon - .frame(width: 16, height: 16) - - Text("Running Code Review...") - .font(.system(size: chatFontSize)) - .foregroundColor(.secondary) - - Spacer() - } - } - } - } - - var shouldShowHelloMessage: Bool { round.statusHistory.contains(.waitForConfirmation) } - var shouldShowRunningStatus: Bool { round.statusHistory.contains(.running) } - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 8) { - if shouldShowHelloMessage { - helloMessageView - } - - if hasChangedFiles { - FileSelectionSection( - store: store, - round: round, - changedFileUris: changedFileUris, - selectedFileUris: $selectedFileUris - ) - } - - if shouldShowRunningStatus { - statusView - } - - if hasFileComments { - ReviewResultsSection(store: store, round: round) - } - - if round.status == .completed || round.status == .error { - ReviewSummarySection(round: round) - } - } - } - } -} diff --git a/Core/Sources/ConversationTab/Views/CodeReviewRound/FileSelectionSection.swift b/Core/Sources/ConversationTab/Views/CodeReviewRound/FileSelectionSection.swift deleted file mode 100644 index 76f613a7..00000000 --- a/Core/Sources/ConversationTab/Views/CodeReviewRound/FileSelectionSection.swift +++ /dev/null @@ -1,213 +0,0 @@ -import ComposableArchitecture -import ConversationServiceProvider -import LanguageServerProtocol -import SharedUIComponents -import SwiftUI - -// MARK: - File Selection Section - -struct FileSelectionSection: View { - let store: StoreOf - let round: CodeReviewRound - let changedFileUris: [DocumentUri] - @Binding var selectedFileUris: [DocumentUri] - @AppStorage(\.chatFontSize) private var chatFontSize - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - FileSelectionHeader(fileCount: selectedFileUris.count) - .frame(maxWidth: .infinity, alignment: .leading) - - FileSelectionList( - store: store, - fileUris: changedFileUris, - reviewStatus: round.status, - selectedFileUris: $selectedFileUris - ) - - if round.status == .waitForConfirmation { - FileSelectionActions( - store: store, - roundId: round.id, - selectedFileUris: selectedFileUris - ) - } - } - .padding(12) - .background(CodeReviewCardBackground()) - } -} - -// MARK: - File Selection Components - -private struct FileSelectionHeader: View { - let fileCount: Int - @AppStorage(\.chatFontSize) private var chatFontSize - - var body: some View { - HStack(alignment: .top, spacing: 6) { - Image("Sparkle") - .resizable() - .frame(width: 16, height: 16) - - Text("You’ve selected following \(fileCount) file(s) with code changes. Review them or unselect any files you don't need, then click Continue.") - .font(.system(size: chatFontSize)) - .multilineTextAlignment(.leading) - } - } -} - -private struct FileSelectionActions: View { - let store: StoreOf - let roundId: String - let selectedFileUris: [DocumentUri] - - var body: some View { - HStack(spacing: 4) { - Button("Cancel") { - store.send(.codeReview(.cancel(id: roundId))) - } - .buttonStyle(.bordered) - .controlSize(.large) - - Button("Continue") { - store.send(.codeReview(.accept(id: roundId, selectedFiles: selectedFileUris))) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - } - } -} - -// MARK: - File Selection List - -private struct FileSelectionList: View { - let store: StoreOf - let fileUris: [DocumentUri] - let reviewStatus: CodeReviewRound.Status - @State private var isExpanded = false - @Binding var selectedFileUris: [DocumentUri] - @AppStorage(\.chatFontSize) private var chatFontSize - - private static let defaultVisibleFileCount = 5 - - private var hasMoreFiles: Bool { - fileUris.count > Self.defaultVisibleFileCount - } - - var body: some View { - let visibleFileUris = Array(fileUris.prefix(Self.defaultVisibleFileCount)) - let additionalFileUris = Array(fileUris.dropFirst(Self.defaultVisibleFileCount)) - - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 4) { - FileToggleList( - fileUris: visibleFileUris, - reviewStatus: reviewStatus, - selectedFileUris: $selectedFileUris - ) - - if hasMoreFiles { - if !isExpanded { - ExpandFilesButton(isExpanded: $isExpanded) - } - - if isExpanded { - FileToggleList( - fileUris: additionalFileUris, - reviewStatus: reviewStatus, - selectedFileUris: $selectedFileUris - ) - } - } - } - } - .frame(alignment: .leading) - } -} - -private struct ExpandFilesButton: View { - @Binding var isExpanded: Bool - @AppStorage(\.chatFontSize) private var chatFontSize - - var body: some View { - HStack(spacing: 2) { - Image("chevron.down") - .resizable() - .frame(width: 16, height: 16) - - Button(action: { isExpanded = true }) { - Text("Show more") - .font(.system(size: chatFontSize)) - .underline() - .lineSpacing(20) - } - .buttonStyle(PlainButtonStyle()) - } - .foregroundColor(.blue) - } -} - -private struct FileToggleList: View { - let fileUris: [DocumentUri] - let reviewStatus: CodeReviewRound.Status - @Binding var selectedFileUris: [DocumentUri] - - var body: some View { - ForEach(fileUris, id: \.self) { fileUri in - FileSelectionRow( - fileUri: fileUri, - reviewStatus: reviewStatus, - isSelected: createSelectionBinding(for: fileUri) - ) - } - } - - private func createSelectionBinding(for fileUri: DocumentUri) -> Binding { - Binding( - get: { selectedFileUris.contains(fileUri) }, - set: { isSelected in - if isSelected { - if !selectedFileUris.contains(fileUri) { - selectedFileUris.append(fileUri) - } - } else { - selectedFileUris.removeAll { $0 == fileUri } - } - } - ) - } -} - -private struct FileSelectionRow: View { - let fileUri: DocumentUri - let reviewStatus: CodeReviewRound.Status - @Binding var isSelected: Bool - - private var fileURL: URL? { - URL(string: fileUri) - } - - private var isInteractionEnabled: Bool { - reviewStatus == .waitForConfirmation - } - - var body: some View { - HStack { - Toggle(isOn: $isSelected) { - HStack(spacing: 8) { - drawFileIcon(fileURL) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - - Text(fileURL?.lastPathComponent ?? fileUri) - .lineLimit(1) - .truncationMode(.middle) - } - } - .toggleStyle(CheckboxToggleStyle()) - .disabled(!isInteractionEnabled) - } - } -} diff --git a/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewResultsSection.swift b/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewResultsSection.swift deleted file mode 100644 index d2e74d9d..00000000 --- a/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewResultsSection.swift +++ /dev/null @@ -1,182 +0,0 @@ -import SwiftUI -import ComposableArchitecture -import ConversationServiceProvider -import SharedUIComponents - -// MARK: - Review Results Section - -struct ReviewResultsSection: View { - let store: StoreOf - let round: CodeReviewRound - @State private var isExpanded = false - @AppStorage(\.chatFontSize) private var chatFontSize - - private static let defaultVisibleReviewCount = 5 - - private var fileComments: [CodeReviewResponse.FileComment] { - round.response?.fileComments ?? [] - } - - private var visibleReviewCount: Int { - isExpanded ? fileComments.count : min(fileComments.count, Self.defaultVisibleReviewCount) - } - - private var hasMoreReviews: Bool { - fileComments.count > Self.defaultVisibleReviewCount - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - ReviewResultsHeader( - reviewStatus: round.status, - chatFontSize: chatFontSize - ) - .padding(8) - .background(CodeReviewHeaderBackground()) - - if !fileComments.isEmpty { - VStack(alignment: .leading, spacing: 4) { - ReviewResultsList( - store: store, - fileComments: Array(fileComments.prefix(visibleReviewCount)) - ) - } - .padding(.horizontal, 8) - .padding(.bottom, !hasMoreReviews || isExpanded ? 8 : 0) - } - - if hasMoreReviews && !isExpanded { - ExpandReviewsButton(isExpanded: $isExpanded) - } - } - .background(CodeReviewCardBackground()) - } -} - -private struct ReviewResultsHeader: View { - let reviewStatus: CodeReviewRound.Status - let chatFontSize: CGFloat - - var body: some View { - HStack(spacing: 4) { - Text("Reviewed Changes") - .font(.system(size: chatFontSize)) - - Spacer() - } - } -} - - -private struct ExpandReviewsButton: View { - @Binding var isExpanded: Bool - - var body: some View { - HStack { - Spacer() - - Button { - isExpanded = true - } label: { - Image("chevron.down") - .resizable() - .frame(width: 16, height: 16) - } - .buttonStyle(PlainButtonStyle()) - - Spacer() - } - .padding(.vertical, 2) - .background(CodeReviewHeaderBackground()) - } -} - -private struct ReviewResultsList: View { - let store: StoreOf - let fileComments: [CodeReviewResponse.FileComment] - - var body: some View { - ForEach(fileComments, id: \.self) { fileComment in - if let fileURL = fileComment.url { - ReviewResultRow( - store: store, - fileURL: fileURL, - comments: fileComment.comments - ) - } - } - } -} - -private struct ReviewResultRow: View { - let store: StoreOf - let fileURL: URL - let comments: [ReviewComment] - @State private var isExpanded = false - - private var commentCountText: String { - comments.count == 1 ? "1 comment" : "\(comments.count) comments" - } - - private var hasComments: Bool { - !comments.isEmpty - } - - var body: some View { - VStack(alignment: .leading) { - ReviewResultRowContent( - store: store, - fileURL: fileURL, - comments: comments, - commentCountText: commentCountText, - hasComments: hasComments - ) - } - } -} - -private struct ReviewResultRowContent: View { - let store: StoreOf - let fileURL: URL - let comments: [ReviewComment] - let commentCountText: String - let hasComments: Bool - @State private var isHovered: Bool = false - - @AppStorage(\.chatFontSize) private var chatFontSize - - var body: some View { - HStack(spacing: 4) { - drawFileIcon(fileURL) - .resizable() - .frame(width: 16, height: 16) - - Button(action: { - if hasComments { - store.send(.codeReview(.onFileClicked(fileURL, comments[0].range.end.line))) - } - }) { - Text(fileURL.lastPathComponent) - .font(.system(size: chatFontSize)) - .foregroundColor(isHovered ? Color("ItemSelectedColor") : .primary) - } - .buttonStyle(PlainButtonStyle()) - .disabled(!hasComments) - .onHover { hovering in - isHovered = hovering - if hovering { - NSCursor.pointingHand.push() - } else { - NSCursor.pop() - } - } - - Text(commentCountText) - .font(.system(size: chatFontSize - 1)) - .lineSpacing(20) - .foregroundColor(.secondary) - - Spacer() - } - } -} diff --git a/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewSummarySection.swift b/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewSummarySection.swift deleted file mode 100644 index e924f1bb..00000000 --- a/Core/Sources/ConversationTab/Views/CodeReviewRound/ReviewSummarySection.swift +++ /dev/null @@ -1,44 +0,0 @@ -import SwiftUI -import ConversationServiceProvider - -struct ReviewSummarySection: View { - var round: CodeReviewRound - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - if round.status == .error, let errorMessage = round.error { - Text(errorMessage) - .font(.system(size: chatFontSize)) - } else if round.status == .completed, let request = round.request, let response = round.response { - CompletedSummary(request: request, response: response) - } else { - Text("Oops, failed to review changes.") - .font(.system(size: chatFontSize)) - } - } -} - -struct CompletedSummary: View { - var request: CodeReviewRequest - var response: CodeReviewResponse - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - let changedFileUris = request.changedFileUris - let selectedFileUris = request.selectedFileUris - let allComments = response.allComments - - VStack(alignment: .leading, spacing: 8) { - - Text("Total comments: \(allComments.count)") - - if allComments.count > 0 { - Text("Review complete! We found \(allComments.count) comment(s) in your selected file(s). Click a file name to see details in the editor.") - } else { - Text("Copilot reviewed \(selectedFileUris.count) out of \(changedFileUris.count) changed files, and no comments were found.") - } - - } - .font(.system(size: chatFontSize)) - } -} diff --git a/Core/Sources/ConversationTab/Views/ConversationAgentProgressView.swift b/Core/Sources/ConversationTab/Views/ConversationAgentProgressView.swift deleted file mode 100644 index cf4b8a61..00000000 --- a/Core/Sources/ConversationTab/Views/ConversationAgentProgressView.swift +++ /dev/null @@ -1,221 +0,0 @@ -import SwiftUI -import ConversationServiceProvider -import ComposableArchitecture -import Combine -import ChatTab -import ChatService - -struct ProgressAgentRound: View { - let rounds: [AgentRound] - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 4) { - ForEach(rounds, id: \.roundId) { round in - VStack(alignment: .leading, spacing: 4) { - ThemedMarkdownText(text: round.reply, chat: chat) - if let toolCalls = round.toolCalls, !toolCalls.isEmpty { - ProgressToolCalls(tools: toolCalls, chat: chat) - .padding(.vertical, 8) - } - } - } - } - .foregroundStyle(.secondary) - } - } -} - -struct ProgressToolCalls: View { - let tools: [AgentToolCall] - let chat: StoreOf - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 4) { - ForEach(tools) { tool in - if tool.name == ToolName.runInTerminal.rawValue && tool.invokeParams != nil { - RunInTerminalToolView(tool: tool, chat: chat) - } else if tool.invokeParams != nil && tool.status == .waitForConfirmation { - ToolConfirmationView(tool: tool, chat: chat) - } else { - ToolStatusItemView(tool: tool) - } - } - } - } - } -} - -struct ToolConfirmationView: View { - let tool: AgentToolCall - let chat: StoreOf - - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 8) { - GenericToolTitleView(toolStatus: "Run", toolName: tool.name, fontWeight: .semibold) - - ThemedMarkdownText(text: tool.invokeParams?.message ?? "", chat: chat) - .frame(maxWidth: .infinity, alignment: .leading) - - HStack { - Button("Cancel") { - chat.send(.toolCallCancelled(tool.id)) - } - - Button("Continue") { - chat.send(.toolCallAccepted(tool.id)) - } - .buttonStyle(BorderedProminentButtonStyle()) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 4) - } - .padding(8) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.gray.opacity(0.2), lineWidth: 1) - ) - } - } -} - -struct GenericToolTitleView: View { - var toolStatus: String - var toolName: String - var fontWeight: Font.Weight = .regular - - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - HStack(spacing: 4) { - Text(toolStatus) - .textSelection(.enabled) - .font(.system(size: chatFontSize, weight: fontWeight)) - .foregroundStyle(.primary) - .background(Color.clear) - Text(toolName) - .textSelection(.enabled) - .font(.system(size: chatFontSize, weight: fontWeight)) - .foregroundStyle(.primary) - .padding(.vertical, 2) - .padding(.horizontal, 4) - .background(Color("ToolTitleHighlightBgColor")) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .inset(by: 0.5) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} - -struct ToolStatusItemView: View { - - let tool: AgentToolCall - - @AppStorage(\.chatFontSize) var chatFontSize - - var statusIcon: some View { - Group { - switch tool.status { - case .running: - ProgressView() - .controlSize(.small) - .scaleEffect(0.7) - case .completed: - Image(systemName: "checkmark") - .foregroundColor(.green.opacity(0.5)) - case .error: - Image(systemName: "xmark.circle") - .foregroundColor(.red.opacity(0.5)) - case .cancelled: - Image(systemName: "slash.circle") - .foregroundColor(.gray.opacity(0.5)) - case .waitForConfirmation: - EmptyView() - case .accepted: - EmptyView() - } - } - } - - var progressTitleText: some View { - let message: String = { - var msg = tool.progressMessage ?? "" - if tool.name == ToolName.createFile.rawValue { - if let input = tool.invokeParams?.input, let filePath = input["filePath"]?.value as? String { - let fileURL = URL(fileURLWithPath: filePath) - msg += ": [\(fileURL.lastPathComponent)](\(fileURL.absoluteString))" - } - } - return msg - }() - - return Group { - if message.isEmpty { - GenericToolTitleView(toolStatus: "Running", toolName: tool.name) - } else { - if let attributedString = try? AttributedString(markdown: message) { - Text(attributedString) - .environment(\.openURL, OpenURLAction { url in - if url.scheme == "file" || url.isFileURL { - NSWorkspace.shared.open(url) - return .handled - } else { - return .systemAction - } - }) - } else { - Text(message) - } - } - } - } - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 4) { - statusIcon - .frame(width: 16, height: 16) - - progressTitleText - .font(.system(size: chatFontSize)) - .lineLimit(1) - - Spacer() - } - } - } -} - -struct ProgressAgentRound_Preview: PreviewProvider { - static let agentRounds: [AgentRound] = [ - .init(roundId: 1, reply: "this is agent step", toolCalls: [ - .init( - id: "toolcall_001", - name: "Tool Call 1", - progressMessage: "Read Tool Call 1", - status: .completed, - error: nil), - .init( - id: "toolcall_002", - name: "Tool Call 2", - progressMessage: "Running Tool Call 2", - status: .running) - ]) - ] - - static var previews: some View { - let chatTabInfo = ChatTabInfo(id: "id", workspacePath: "path", username: "name") - ProgressAgentRound(rounds: agentRounds, chat: .init(initialState: .init(), reducer: { Chat(service: ChatService.service(for: chatTabInfo)) })) - .frame(width: 300, height: 300) - } -} diff --git a/Core/Sources/ConversationTab/Views/ConversationProgressStepView.swift b/Core/Sources/ConversationTab/Views/ConversationProgressStepView.swift deleted file mode 100644 index 7b6c845e..00000000 --- a/Core/Sources/ConversationTab/Views/ConversationProgressStepView.swift +++ /dev/null @@ -1,83 +0,0 @@ -import SwiftUI -import ConversationServiceProvider -import ComposableArchitecture -import Combine -import ChatService - -struct ProgressStep: View { - let steps: [ConversationProgressStep] - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 4) { - ForEach(steps) { StatusItemView(step: $0) } - } - .foregroundStyle(.secondary) - } - } -} - - -struct StatusItemView: View { - - let step: ConversationProgressStep - - @AppStorage(\.chatFontSize) var chatFontSize - - var statusIcon: some View { - Group { - switch step.status { - case .running: - ProgressView() - .controlSize(.small) - .frame(width: 16, height: 16) - .scaleEffect(0.7) - case .completed: - Image(systemName: "checkmark") - .foregroundColor(.green) - case .failed: - Image(systemName: "xmark.circle") - .foregroundColor(.red) - case .cancelled: - Image(systemName: "slash.circle") - .foregroundColor(.gray) - } - } - } - - var statusTitle: some View { - var title = step.title - if step.id == ProjectContextSkill.ProgressID && step.status == .failed { - title = step.error?.message ?? step.title - } - return Text(title) - } - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 4) { - statusIcon - .frame(width: 16, height: 16) - - statusTitle - .font(.system(size: chatFontSize)) - .lineLimit(1) - - Spacer() - } - } - } -} - -struct ProgressStep_Preview: PreviewProvider { - static let steps: [ConversationProgressStep] = [ - .init(id: "001", title: "running step", description: "this is running step", status: .running, error: nil), - .init(id: "002", title: "completed step", description: "this is completed step", status: .completed, error: nil), - .init(id: "003", title: "failed step", description: "this is failed step", status: .failed, error: nil), - .init(id: "004", title: "cancelled step", description: "this is cancelled step", status: .cancelled, error: nil) - ] - static var previews: some View { - ProgressStep(steps: steps) - .frame(width: 300, height: 300) - } -} diff --git a/Core/Sources/ConversationTab/Views/FunctionCallMarkdownTheme.swift b/Core/Sources/ConversationTab/Views/FunctionCallMarkdownTheme.swift deleted file mode 100644 index f5d83a76..00000000 --- a/Core/Sources/ConversationTab/Views/FunctionCallMarkdownTheme.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import MarkdownUI -import SwiftUI - -extension MarkdownUI.Theme { - static func functionCall(fontSize: Double) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.secondary) - BackgroundColor(Color.clear) - FontSize(fontSize - 1) - } - .list { configuration in - configuration.label - .markdownMargin(top: 4, bottom: 4) - } - .paragraph { configuration in - configuration.label - .markdownMargin(top: 0, bottom: 4) - } - .codeBlock { configuration in - configuration.label - .relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(16) - .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .markdownMargin(top: 4, bottom: 4) - } - } -} diff --git a/Core/Sources/ConversationTab/Views/FunctionMessage.swift b/Core/Sources/ConversationTab/Views/FunctionMessage.swift deleted file mode 100644 index 8fbd6ac9..00000000 --- a/Core/Sources/ConversationTab/Views/FunctionMessage.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation -import SwiftUI -import ChatService -import SharedUIComponents -import ComposableArchitecture -import ChatTab -import GitHubCopilotService - -struct FunctionMessage: View { - let text: String - let chat: StoreOf - @AppStorage(\.chatFontSize) var chatFontSize - @Environment(\.openURL) private var openURL - - private var isFreePlanUser: Bool { - text.contains("30-day free trial") - } - - private var isOrgUser: Bool { - text.contains("reach out to your organization's Copilot admin") - } - - private var switchToFallbackModelText: String { - if let fallbackModelName = CopilotModelManager.getFallbackLLM( - scope: chat.isAgentMode ? .agentPanel : .chatPanel - )?.modelName { - return "We have automatically switched you to \(fallbackModelName) which is included with your plan." - } else { - return "" - } - } - - private var errorContent: Text { - switch (isFreePlanUser, isOrgUser) { - case (true, _): - return Text("Monthly message limit reached. Upgrade to Copilot Pro (30-day free trial) or wait for your limit to reset.") - - case (_, true): - let parts = [ - "You have exceeded your free request allowance.", - switchToFallbackModelText, - "To enable additional paid premium requests, contact your organization admin." - ].filter { !$0.isEmpty } - return Text(attributedString(from: parts)) - - default: - let parts = [ - "You have exceeded your premium request allowance.", - switchToFallbackModelText, - "[Enable additional paid premium requests](https://aka.ms/github-copilot-manage-overage) to continue using premium models." - ].filter { !$0.isEmpty } - return Text(attributedString(from: parts)) - } - } - - private func attributedString(from parts: [String]) -> AttributedString { - do { - return try AttributedString(markdown: parts.joined(separator: " ")) - } catch { - return AttributedString(parts.joined(separator: " ")) - } - } - - var body: some View { - NotificationBanner(style: .warning) { - errorContent - - if isFreePlanUser { - Button("Update to Copilot Pro") { - if let url = URL(string: "https://aka.ms/github-copilot-upgrade-plan") { - openURL(url) - } - } - .buttonStyle(.borderedProminent) - .controlSize(.regular) - .onHover { isHovering in - if isHovering { - NSCursor.pointingHand.push() - } else { - NSCursor.pop() - } - } - } - } - } -} - -struct FunctionMessage_Previews: PreviewProvider { - static var previews: some View { - let chatTabInfo = ChatTabInfo(id: "id", workspacePath: "path", username: "name") - FunctionMessage( - text: "You've reached your monthly chat limit. Upgrade to Copilot Pro (30-day free trial) or wait until 1/17/2025, 8:00:00 AM for your limit to reset.", - chat: .init(initialState: .init(), reducer: { Chat(service: ChatService.service(for: chatTabInfo)) }) - ) - .padding() - .fixedSize() - } -} diff --git a/Core/Sources/ConversationTab/Views/ImageReferenceItemView.swift b/Core/Sources/ConversationTab/Views/ImageReferenceItemView.swift deleted file mode 100644 index ef2ac6c7..00000000 --- a/Core/Sources/ConversationTab/Views/ImageReferenceItemView.swift +++ /dev/null @@ -1,69 +0,0 @@ -import ConversationServiceProvider -import SwiftUI -import Foundation - -struct ImageReferenceItemView: View { - let item: ImageReference - @State private var showPopover = false - - private func getImageTitle() -> String { - switch item.source { - case .file: - if let fileUrl = item.fileUrl { - return fileUrl.lastPathComponent - } else { - return "Attached Image" - } - case .pasted: - return "Pasted Image" - case .screenshot: - return "Screenshot" - } - } - - var body: some View { - HStack(alignment: .center, spacing: 4) { - let image = loadImageFromData(data: item.data).image - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 28, height: 28) - .clipShape(RoundedRectangle(cornerRadius: 1.72)) - .overlay( - RoundedRectangle(cornerRadius: 1.72) - .inset(by: 0.21) - .stroke(Color(nsColor: .separatorColor), lineWidth: 0.43) - ) - - let text = getImageTitle() - let font = NSFont.systemFont(ofSize: 12) - let attributes = [NSAttributedString.Key.font: font] - let size = (text as NSString).size(withAttributes: attributes) - let textWidth = min(size.width, 105) - - Text(text) - .lineLimit(1) - .font(.system(size: 12)) - .foregroundColor(.primary.opacity(0.85)) - .truncationMode(.middle) - .frame(width: textWidth, alignment: .leading) - } - .padding(4) - .background( - Color(nsColor: .windowBackgroundColor).opacity(0.5) - ) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .inset(by: 0.5) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - .popover(isPresented: $showPopover, arrowEdge: .bottom) { - PopoverImageView(data: item.data) - } - .onTapGesture { - self.showPopover = true - } - } -} - diff --git a/Core/Sources/ConversationTab/Views/InstructionMarkdownTheme.swift b/Core/Sources/ConversationTab/Views/InstructionMarkdownTheme.swift deleted file mode 100644 index 30e786ea..00000000 --- a/Core/Sources/ConversationTab/Views/InstructionMarkdownTheme.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import MarkdownUI -import SwiftUI - -extension MarkdownUI.Theme { - static func instruction(fontSize: Double) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.primary) - BackgroundColor(Color.clear) - FontSize(fontSize) - } - .code { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - BackgroundColor(Color.secondary.opacity(0.2)) - } - .codeBlock { configuration in - let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock) - - if wrapCode { - configuration.label - .codeBlockLabelStyle() - .codeBlockStyle( - configuration, - backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7), - labelColor: Color.secondary.opacity(0.7) - ) - } else { - ScrollView(.horizontal) { - configuration.label - .codeBlockLabelStyle() - } - .workaroundForVerticalScrollingBugInMacOS() - .codeBlockStyle( - configuration, - backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7), - labelColor: Color.secondary.opacity(0.7) - ) - } - } - .table { configuration in - configuration.label - .fixedSize(horizontal: false, vertical: true) - .markdownTableBorderStyle(.init( - color: .init(nsColor: .separatorColor), - strokeStyle: .init(lineWidth: 1) - )) - .markdownTableBackgroundStyle( - .alternatingRows(Color.secondary.opacity(0.1), Color.secondary.opacity(0.2)) - ) - .markdownMargin(top: 0, bottom: 16) - } - .tableCell { configuration in - configuration.label - .markdownTextStyle { - if configuration.row == 0 { - FontWeight(.semibold) - } - BackgroundColor(nil) - } - .fixedSize(horizontal: false, vertical: true) - .padding(.vertical, 6) - .padding(.horizontal, 13) - .relativeLineSpacing(.em(0.25)) - } - } -} - diff --git a/Core/Sources/ConversationTab/Views/NotificationBanner.swift b/Core/Sources/ConversationTab/Views/NotificationBanner.swift deleted file mode 100644 index 68c40d57..00000000 --- a/Core/Sources/ConversationTab/Views/NotificationBanner.swift +++ /dev/null @@ -1,44 +0,0 @@ -import SwiftUI - -public enum BannerStyle { - case warning - - var iconName: String { - switch self { - case .warning: return "exclamationmark.triangle" - } - } - - var color: Color { - switch self { - case .warning: return .orange - } - } -} - -struct NotificationBanner: View { - var style: BannerStyle - @ViewBuilder var content: () -> Content - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .top, spacing: 6) { - Image(systemName: style.iconName) - .font(Font.system(size: 12)) - .foregroundColor(style.color) - - VStack(alignment: .leading, spacing: 8) { - content() - } - } - } - .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.vertical, 10) - .padding(.horizontal, 12) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - .padding(.vertical, 4) - } -} diff --git a/Core/Sources/ConversationTab/Views/ThemedMarkdownText.swift b/Core/Sources/ConversationTab/Views/ThemedMarkdownText.swift deleted file mode 100644 index 3af730eb..00000000 --- a/Core/Sources/ConversationTab/Views/ThemedMarkdownText.swift +++ /dev/null @@ -1,163 +0,0 @@ -import Foundation -import MarkdownUI -import SwiftUI -import ChatService -import ComposableArchitecture -import SuggestionBasic -import ChatTab - -public struct MarkdownActionProvider { - let supportInsert: Bool - let onInsert: ((String) -> Void)? - - public init(supportInsert: Bool = true, onInsert: ((String) -> Void)? = nil) { - self.supportInsert = supportInsert - self.onInsert = onInsert - } -} - -public struct ThemedMarkdownText: View { - @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFont) var chatCodeFont - @Environment(\.colorScheme) var colorScheme - - let text: String - let context: MarkdownActionProvider - - public init(text: String, context: MarkdownActionProvider) { - self.text = text - self.context = context - } - - init(text: String, chat: StoreOf) { - self.text = text - - self.context = .init(onInsert: { content in - chat.send(.insertCode(content)) - }) - } - - public var body: some View { - Markdown(text) - .textSelection(.enabled) - .markdownTheme(.custom( - fontSize: chatFontSize, - codeFont: chatCodeFont.value.nsFont, - codeBlockBackgroundColor: { - if syncCodeHighlightTheme { - if colorScheme == .light, let color = codeBackgroundColorLight.value { - return color.swiftUIColor - } else if let color = codeBackgroundColorDark.value { - return color.swiftUIColor - } - } - - return Color(nsColor: .textBackgroundColor).opacity(0.7) - }(), - codeBlockLabelColor: { - if syncCodeHighlightTheme { - if colorScheme == .light, - let color = codeForegroundColorLight.value - { - return color.swiftUIColor.opacity(0.5) - } else if let color = codeForegroundColorDark.value { - return color.swiftUIColor.opacity(0.5) - } - } - return Color.secondary.opacity(0.7) - }(), - context: context - )) - } -} - -// MARK: - Theme - -extension MarkdownUI.Theme { - static func custom( - fontSize: Double, - codeFont: NSFont, - codeBlockBackgroundColor: Color, - codeBlockLabelColor: Color, - context: MarkdownActionProvider - ) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.primary) - BackgroundColor(Color.clear) - FontSize(fontSize) - } - .codeBlock { configuration in - MarkdownCodeBlockView( - codeBlockConfiguration: configuration, - codeFont: codeFont, - codeBlockBackgroundColor: codeBlockBackgroundColor, - codeBlockLabelColor: codeBlockLabelColor, - context: context - ) - } - } -} - -struct MarkdownCodeBlockView: View { - let codeBlockConfiguration: CodeBlockConfiguration - let codeFont: NSFont - let codeBlockBackgroundColor: Color - let codeBlockLabelColor: Color - let context: MarkdownActionProvider - - var body: some View { - let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock) - - if wrapCode { - AsyncCodeBlockView( - fenceInfo: codeBlockConfiguration.language, - content: codeBlockConfiguration.content, - font: codeFont - ) - .codeBlockLabelStyle() - .codeBlockStyle( - codeBlockConfiguration, - backgroundColor: codeBlockBackgroundColor, - labelColor: codeBlockLabelColor, - context: context - ) - } else { - ScrollView(.horizontal) { - AsyncCodeBlockView( - fenceInfo: codeBlockConfiguration.language, - content: codeBlockConfiguration.content, - font: codeFont - ) - .codeBlockLabelStyle() - } - .workaroundForVerticalScrollingBugInMacOS() - .codeBlockStyle( - codeBlockConfiguration, - backgroundColor: codeBlockBackgroundColor, - labelColor: codeBlockLabelColor, - context: context - ) - } - } -} - -struct ThemedMarkdownText_Previews: PreviewProvider { - static var previews: some View { - let chatTabInfo = ChatTabInfo(id: "id", workspacePath: "path", username: "name") - ThemedMarkdownText( - text:""" - ```swift - let sumClosure: (Int, Int) -> Int = { (a: Int, b: Int) in - return a + b - } - ``` - """, - context: .init(onInsert: {_ in print("Inserted") })) - } -} - diff --git a/Core/Sources/ConversationTab/Views/UserMessage.swift b/Core/Sources/ConversationTab/Views/UserMessage.swift deleted file mode 100644 index e7dc2d32..00000000 --- a/Core/Sources/ConversationTab/Views/UserMessage.swift +++ /dev/null @@ -1,106 +0,0 @@ -import ComposableArchitecture -import ChatService -import Foundation -import MarkdownUI -import SharedUIComponents -import SwiftUI -import Status -import Cache -import ChatTab -import ConversationServiceProvider -import SwiftUIFlowLayout - -private let MAX_TEXT_LENGTH = 10000 // Maximum characters to prevent crashes - -struct UserMessage: View { - var r: Double { messageBubbleCornerRadius } - let id: String - let text: String - let imageReferences: [ImageReference] - let chat: StoreOf - @Environment(\.colorScheme) var colorScheme - @ObservedObject private var statusObserver = StatusObserver.shared - - struct AvatarView: View { - @ObservedObject private var avatarViewModel = AvatarViewModel.shared - - var body: some View { - if let avatarImage = avatarViewModel.avatarImage { - avatarImage - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 24, height: 24) - .clipShape(Circle()) - } else { - Image(systemName: "person.circle") - .resizable() - .frame(width: 24, height: 24) - } - } - } - - // Truncate the displayed user message if it's too long. - private var displayText: String { - if text.count > MAX_TEXT_LENGTH { - return String(text.prefix(MAX_TEXT_LENGTH)) + "\n… (message too long, rest hidden)" - } - return text - } - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 4) { - AvatarView() - - Text(statusObserver.authStatus.username ?? "") - .chatMessageHeaderTextStyle() - .padding(2) - - Spacer() - } - - ThemedMarkdownText(text: displayText, chat: chat) - .frame(maxWidth: .infinity, alignment: .leading) - - if !imageReferences.isEmpty { - FlowLayout(mode: .scrollable, items: imageReferences, itemSpacing: 4) { item in - ImageReferenceItemView(item: item) - } - } - } - } - .shadow(color: .black.opacity(0.05), radius: 6) - } -} - -struct UserMessage_Previews: PreviewProvider { - static var previews: some View { - let chatTabInfo = ChatTabInfo(id: "id", workspacePath: "path", username: "name") - UserMessage( - id: "A", - text: #""" - Please buy me a coffee! - | Coffee | Milk | - |--------|------| - | Espresso | No | - | Latte | Yes | - ```swift - func foo() {} - ``` - ```objectivec - - (void)bar {} - ``` - """#, - imageReferences: [], - chat: .init( - initialState: .init(history: [] as [DisplayedChatMessage], isReceivingMessage: false), - reducer: { Chat(service: ChatService.service(for: chatTabInfo)) } - ) - ) - .padding() - .fixedSize(horizontal: true, vertical: true) - .background(Color.yellow) - - } -} diff --git a/Core/Sources/ConversationTab/Views/WorkingSetView.swift b/Core/Sources/ConversationTab/Views/WorkingSetView.swift deleted file mode 100644 index 677c44dc..00000000 --- a/Core/Sources/ConversationTab/Views/WorkingSetView.swift +++ /dev/null @@ -1,246 +0,0 @@ -import SwiftUI -import ChatService -import Perception -import ComposableArchitecture -import GitHubCopilotService -import JSONRPC -import SharedUIComponents -import OrderedCollections -import ConversationServiceProvider - -struct WorkingSetView: View { - let chat: StoreOf - - private let r: Double = 8 - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 4) { - - WorkingSetHeader(chat: chat) - .frame(height: 24) - .padding(.leading, 12) - .padding(.trailing, 5) - - VStack(spacing: 0) { - ForEach(chat.fileEditMap.elements, id: \.key.path) { element in - FileEditView(chat: chat, fileEdit: element.value) - } - } - .padding(.horizontal, 5) - } - .padding(.top, 8) - .padding(.bottom, 10) - .frame(maxWidth: .infinity) - .background( - RoundedCorners(tl: r, tr: r, bl: 0, br: 0) - .fill(.ultraThickMaterial) - ) - .overlay( - RoundedCorners(tl: r, tr: r, bl: 0, br: 0) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - ) - } - } -} - -struct WorkingSetHeader: View { - let chat: StoreOf - - @Environment(\.colorScheme) var colorScheme - - func getTitle() -> String { - return chat.fileEditMap.count > 1 ? "\(chat.fileEditMap.count) files changed" : "1 file changed" - } - - @ViewBuilder - private func buildActionButton( - text: String, - textForegroundColor: Color = .white, - textBackgroundColor: Color = .gray, - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - Text(text) - .foregroundColor(textForegroundColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(textBackgroundColor) - .cornerRadius(2) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.white.opacity(0.07), lineWidth: 1) - ) - .frame(width: 60, height: 15, alignment: .center) - } - .buttonStyle(PlainButtonStyle()) - } - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 0) { - Text(getTitle()) - .foregroundColor(.secondary) - .font(.system(size: 13)) - - Spacer() - - if chat.fileEditMap.contains(where: {_, fileEdit in - return fileEdit.status == .none - }) { - HStack(spacing: -10) { - /// Undo all edits - buildActionButton( - text: "Undo", - textForegroundColor: colorScheme == .dark ? .white : .black, - textBackgroundColor: Color("WorkingSetHeaderUndoButtonColor") - ) { - chat.send(.undoEdits(fileURLs: chat.fileEditMap.values.map { $0.fileURL })) - } - .help("Undo All Edits") - - /// Keep all edits - buildActionButton(text: "Keep", textBackgroundColor: Color("WorkingSetHeaderKeepButtonColor")) { - chat.send(.keepEdits(fileURLs: chat.fileEditMap.values.map { $0.fileURL })) - } - .help("Keep All Edits") - } - - } else { - buildActionButton(text: "Done") { - chat.send(.resetEdits) - } - .help("Done") - } - } - - } - } -} - -struct FileEditView: View { - let chat: StoreOf - let fileEdit: FileEdit - @State private var isHovering = false - - enum ActionButtonImageType { - case system(String), asset(String) - } - - @ViewBuilder - private func buildActionButton( - imageType: ActionButtonImageType, - help: String, - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - Group { - switch imageType { - case .system(let name): - Image(systemName: name) - .font(.system(size: 16, weight: .regular)) - case .asset(let name): - Image(name) - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 16) - } - } - .foregroundColor(.white) - .frame(width: 22) - .frame(maxHeight: .infinity) - } - .buttonStyle(HoverButtonStyle(padding: 0, hoverColor: .white.opacity(0.2))) - .help(help) - } - - var actionButtons: some View { - HStack(spacing: 0) { - if fileEdit.status == .none { - buildActionButton( - imageType: .system("xmark"), - help: "Remove file" - ) { - chat.send(.discardFileEdits(fileURLs: [fileEdit.fileURL])) - } - buildActionButton( - imageType: .asset("DiffEditor"), - help: "Open changes in Diff Editor" - ) { - chat.send(.openDiffViewWindow(fileURL: fileEdit.fileURL)) - } - buildActionButton( - imageType: .asset("Discard"), - help: "Undo" - ) { - chat.send(.undoEdits(fileURLs: [fileEdit.fileURL])) - } - buildActionButton( - imageType: .system("checkmark"), - help: "Keep" - ) { - chat.send(.keepEdits(fileURLs: [fileEdit.fileURL])) - } - } - } - } - - var body: some View { - HStack(spacing: 0) { - HStack(spacing: 4) { - drawFileIcon(fileEdit.fileURL) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.secondary) - - Text(fileEdit.fileURL.lastPathComponent) - .font(.system(size: 13)) - .foregroundColor(isHovering ? .white : Color("WorkingSetItemColor")) - } - - Spacer() - - if isHovering { - actionButtons - .padding(.trailing, 8) - } - } - .onHover { hovering in - isHovering = hovering - } - .padding(.leading, 7) - .frame(height: 24) - .hoverRadiusBackground( - isHovered: isHovering, - hoverColor: Color.blue, - cornerRadius: 5, - showBorder: true - ) - .onTapGesture { - chat.send(.openDiffViewWindow(fileURL: fileEdit.fileURL)) - } - } -} - - -struct WorkingSetView_Previews: PreviewProvider { - static let fileEditMap: OrderedDictionary = [ - URL(fileURLWithPath: "file:///f1.swift"): FileEdit(fileURL: URL(fileURLWithPath: "file:///f1.swift"), originalContent: "single line", modifiedContent: "single line 1", toolName: ToolName.insertEditIntoFile), - URL(fileURLWithPath: "file:///f2.swift"): FileEdit(fileURL: URL(fileURLWithPath: "file:///f2.swift"), originalContent: "multi \n line \n end", modifiedContent: "another \n mut \n li \n", status: .kept, toolName: ToolName.insertEditIntoFile) - ] - - static var previews: some View { - WorkingSetView( - chat: .init( - initialState: .init( - history: ChatPanel_Preview.history, - isReceivingMessage: true, - fileEditMap: fileEditMap - ), - reducer: { Chat(service: ChatService.service(for: ChatPanel_Preview.chatTabInfo)) } - ) - ) - } -} diff --git a/Core/Sources/ConversationTab/VisionViews/HoverableImageView.swift b/Core/Sources/ConversationTab/VisionViews/HoverableImageView.swift deleted file mode 100644 index 56383d34..00000000 --- a/Core/Sources/ConversationTab/VisionViews/HoverableImageView.swift +++ /dev/null @@ -1,159 +0,0 @@ -import SwiftUI -import ComposableArchitecture -import Persist -import ConversationServiceProvider -import GitHubCopilotService - -public struct HoverableImageView: View { - @Environment(\.colorScheme) var colorScheme - - let image: ImageReference - let chat: StoreOf - @State private var isHovered = false - @State private var hoverTask: Task? - @State private var isSelectedModelSupportVision = AppState.shared.isSelectedModelSupportVision() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.supportVision ?? false - @State private var showPopover = false - - let maxWidth: CGFloat = 330 - let maxHeight: CGFloat = 160 - - private var visionNotSupportedOverlay: some View { - Group { - if !isSelectedModelSupportVision { - ZStack { - Color.clear - .background(.regularMaterial) - .opacity(0.4) - .clipShape(RoundedRectangle(cornerRadius: hoverableImageCornerRadius)) - - VStack(alignment: .center, spacing: 8) { - Image(systemName: "eye.slash") - .font(.system(size: 14, weight: .semibold)) - Text("Vision not supported by current model") - .font(.system(size: 12, weight: .semibold)) - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - } - .foregroundColor(colorScheme == .dark ? .primary : .white) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .colorScheme(colorScheme == .dark ? .light : .dark) - } - } - } - - private var borderOverlay: some View { - RoundedRectangle(cornerRadius: hoverableImageCornerRadius) - .strokeBorder(Color(nsColor: .separatorColor), lineWidth: 1) - } - - private var removeButton: some View { - Button(action: { - chat.send(.removeSelectedImage(image)) - }) { - Image(systemName: "xmark") - .foregroundColor(.primary) - .font(.system(size: 13)) - .frame(width: 24, height: 24) - .background( - RoundedRectangle(cornerRadius: hoverableImageCornerRadius) - .fill(Color.contentBackground.opacity(0.72)) - .shadow(color: .black.opacity(0.3), radius: 1.5, x: 0, y: 0) - .shadow(color: .black.opacity(0.25), radius: 50, x: 0, y: 36) - ) - } - .buttonStyle(.plain) - .padding(1) - .onHover { buttonHovering in - hoverTask?.cancel() - if buttonHovering { - isHovered = true - } - } - } - - private var hoverOverlay: some View { - Group { - if isHovered { - VStack { - Spacer() - HStack { - removeButton - Spacer() - } - } - } - } - } - - private var baseImageView: some View { - let (image, nsImage) = loadImageFromData(data: image.data) - let imageSize = nsImage?.size ?? CGSize(width: maxWidth, height: maxHeight) - let isWideImage = imageSize.height < 160 && imageSize.width >= maxWidth - - return image - .resizable() - .aspectRatio(contentMode: isWideImage ? .fill : .fit) - .blur(radius: !isSelectedModelSupportVision ? 2.5 : 0) - .frame( - width: isWideImage ? min(imageSize.width, maxWidth) : nil, - height: isWideImage ? min(imageSize.height, maxHeight) : maxHeight, - alignment: .leading - ) - .clipShape( - RoundedRectangle(cornerRadius: hoverableImageCornerRadius), - style: .init(eoFill: true, antialiased: true) - ) - } - - private func handleHover(_ hovering: Bool) { - hoverTask?.cancel() - - if hovering { - isHovered = true - } else { - // Add a small delay before hiding to prevent flashing - hoverTask = Task { - try? await Task.sleep(nanoseconds: 10_000_000) // 0.01 seconds - if !Task.isCancelled { - isHovered = false - } - } - } - } - - private func updateVisionSupport() { - isSelectedModelSupportVision = AppState.shared.isSelectedModelSupportVision() ?? CopilotModelManager.getDefaultChatModel(scope: AppState.shared.modelScope())?.supportVision ?? false - } - - public var body: some View { - if NSImage(data: image.data) != nil { - baseImageView - .frame(height: maxHeight, alignment: .leading) - .background( - Color(nsColor: .windowBackgroundColor).opacity(0.5) - ) - .overlay(visionNotSupportedOverlay) - .overlay(borderOverlay) - .onHover(perform: handleHover) - .overlay(hoverOverlay) - .onReceive(NotificationCenter.default.publisher(for: .gitHubCopilotSelectedModelDidChange)) { _ in - updateVisionSupport() - } - .onTapGesture { - showPopover.toggle() - } - .popover(isPresented: $showPopover, attachmentAnchor: .rect(.bounds), arrowEdge: .bottom) { - PopoverImageView(data: image.data) - } - } - } -} - -public func loadImageFromData(data: Data) -> (image: Image, nsImage: NSImage?) { - if let nsImage = NSImage(data: data) { - return (Image(nsImage: nsImage), nsImage) - } else { - return (Image(systemName: "photo.trianglebadge.exclamationmark"), nil) - } -} diff --git a/Core/Sources/ConversationTab/VisionViews/ImagesScrollView.swift b/Core/Sources/ConversationTab/VisionViews/ImagesScrollView.swift deleted file mode 100644 index 87e7179a..00000000 --- a/Core/Sources/ConversationTab/VisionViews/ImagesScrollView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI -import ComposableArchitecture - -public struct ImagesScrollView: View { - let chat: StoreOf - - public var body: some View { - let attachedImages = chat.state.attachedImages.reversed() - return ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 2) { - ForEach(attachedImages, id: \.self) { image in - HoverableImageView(image: image, chat: chat) - } - } - } - .padding(.horizontal, 8) - .padding(.top, 8) - } -} diff --git a/Core/Sources/ConversationTab/VisionViews/PopoverImageView.swift b/Core/Sources/ConversationTab/VisionViews/PopoverImageView.swift deleted file mode 100644 index 0beddb8c..00000000 --- a/Core/Sources/ConversationTab/VisionViews/PopoverImageView.swift +++ /dev/null @@ -1,18 +0,0 @@ -import SwiftUI - -public struct PopoverImageView: View { - let data: Data - - public var body: some View { - let maxHeight: CGFloat = 400 - let (image, nsImage) = loadImageFromData(data: data) - let height = nsImage.map { min($0.size.height, maxHeight) } ?? maxHeight - - return image - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: height) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .padding(10) - } -} diff --git a/Core/Sources/ConversationTab/VisionViews/VisionMenuView.swift b/Core/Sources/ConversationTab/VisionViews/VisionMenuView.swift deleted file mode 100644 index 8e18d40d..00000000 --- a/Core/Sources/ConversationTab/VisionViews/VisionMenuView.swift +++ /dev/null @@ -1,130 +0,0 @@ -import SwiftUI -import SharedUIComponents -import Logger -import ComposableArchitecture -import ConversationServiceProvider -import AppKit -import UniformTypeIdentifiers - -public struct VisionMenuView: View { - let chat: StoreOf - @AppStorage(\.capturePermissionShown) var capturePermissionShown: Bool - @State private var shouldPresentScreenRecordingPermissionAlert: Bool = false - - func showImagePicker() { - let panel = NSOpenPanel() - panel.allowedContentTypes = [.png, .jpeg, .bmp, .gif, .tiff, .webP] - panel.allowsMultipleSelection = true - panel.canChooseFiles = true - panel.canChooseDirectories = false - panel.level = .modalPanel - - // Position the panel relative to the current window - if let window = NSApplication.shared.keyWindow { - let windowFrame = window.frame - let panelSize = CGSize(width: 600, height: 400) - let x = windowFrame.midX - panelSize.width / 2 - let y = windowFrame.midY - panelSize.height / 2 - panel.setFrame(NSRect(origin: CGPoint(x: x, y: y), size: panelSize), display: true) - } - - panel.begin { response in - if response == .OK { - let selectedImageURLs = panel.urls - handleSelectedImages(selectedImageURLs) - } - } - } - - func handleSelectedImages(_ urls: [URL]) { - for url in urls { - let gotAccess = url.startAccessingSecurityScopedResource() - if gotAccess { - // Process the image file - if let imageData = try? Data(contentsOf: url) { - // imageData now contains the binary data of the image - Logger.client.info("Add selected image from URL: \(url)") - let imageReference = ImageReference(data: imageData, fileUrl: url) - chat.send(.addSelectedImage(imageReference)) - } - - url.stopAccessingSecurityScopedResource() - } - } - } - - func runScreenCapture(args: [String] = []) { - let hasScreenRecordingPermission = CGPreflightScreenCaptureAccess() - if !hasScreenRecordingPermission { - if capturePermissionShown { - shouldPresentScreenRecordingPermissionAlert = true - } else { - CGRequestScreenCaptureAccess() - capturePermissionShown = true - } - return - } - - let task = Process() - task.launchPath = "/usr/sbin/screencapture" - task.arguments = args - task.terminationHandler = { _ in - DispatchQueue.main.async { - if task.terminationStatus == 0 { - if let data = NSPasteboard.general.data(forType: .png) { - chat.send(.addSelectedImage(ImageReference(data: data, source: .screenshot))) - } else if let tiffData = NSPasteboard.general.data(forType: .tiff), - let imageRep = NSBitmapImageRep(data: tiffData), - let pngData = imageRep.representation(using: .png, properties: [:]) { - chat.send(.addSelectedImage(ImageReference(data: pngData, source: .screenshot))) - } - } - } - } - task.launch() - task.waitUntilExit() - } - - public var body: some View { - Menu { - Button(action: { runScreenCapture(args: ["-w", "-c"]) }) { - Image(systemName: "macwindow") - Text("Capture Window") - } - - Button(action: { runScreenCapture(args: ["-s", "-c"]) }) { - Image(systemName: "macwindow.and.cursorarrow") - Text("Capture Selection") - } - - Button(action: { showImagePicker() }) { - Image(systemName: "photo") - Text("Attach File") - } - } label: { - Image(systemName: "photo.badge.plus") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 16, height: 16) - .padding(4) - .foregroundColor(.primary.opacity(0.85)) - .font(Font.system(size: 11, weight: .semibold)) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .help("Attach images") - .cornerRadius(6) - .alert( - "Enable Screen & System Recording Permission", - isPresented: $shouldPresentScreenRecordingPermissionAlert - ) { - Button( - "Open System Settings", - action: { - NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture")!) - }).keyboardShortcut(.defaultAction) - Button("Deny", role: .cancel, action: {}) - } message: { - Text("Grant access to this application in Privacy & Security settings, located in System Settings") - } - } -} diff --git a/Core/Sources/FileChangeChecker/FileChangeChecker.swift b/Core/Sources/FileChangeChecker/FileChangeChecker.swift deleted file mode 100644 index a8444044..00000000 --- a/Core/Sources/FileChangeChecker/FileChangeChecker.swift +++ /dev/null @@ -1,39 +0,0 @@ -import CryptoKit -import Dispatch -import Foundation - -/// Check that a file is changed. -public actor FileChangeChecker { - let url: URL - var checksum: Data? - - public init(fileURL: URL) async { - url = fileURL - checksum = getChecksum() - } - - public func checkIfChanged() -> Bool { - guard let newChecksum = getChecksum() else { return false } - return newChecksum != checksum - } - - func getChecksum() -> Data? { - let bufferSize = 16 * 1024 - guard let file = try? FileHandle(forReadingFrom: url) else { return nil } - defer { try? file.close() } - var md5 = CryptoKit.Insecure.MD5() - while autoreleasepool(invoking: { - let data = file.readData(ofLength: bufferSize) - if !data.isEmpty { - md5.update(data: data) - return true // Continue - } else { - return false // End of file - } - }) {} - - let data = Data(md5.finalize()) - - return data - } -} diff --git a/Core/Sources/GitHubCopilotViewModel/GitHubCopilotViewModel.swift b/Core/Sources/GitHubCopilotViewModel/GitHubCopilotViewModel.swift deleted file mode 100644 index bca4079f..00000000 --- a/Core/Sources/GitHubCopilotViewModel/GitHubCopilotViewModel.swift +++ /dev/null @@ -1,360 +0,0 @@ -import Foundation -import GitHubCopilotService -import ComposableArchitecture -import Status -import SwiftUI -import Cache - -public struct SignInResponse { - public let status: SignInInitiateStatus - public let userCode: String - public let verificationURL: URL -} - -@MainActor -public class GitHubCopilotViewModel: ObservableObject { - // Add static shared instance - public static let shared = GitHubCopilotViewModel() - - @Dependency(\.toast) var toast - - @AppStorage("username") var username: String = "" - - @Published public var isRunningAction: Bool = false - @Published public var status: GitHubCopilotAccountStatus? - @Published public var version: String? - @Published public var userCode: String? - @Published public var isSignInAlertPresented = false - @Published public var signInResponse: SignInResponse? - @Published public var waitingForSignIn = false - - static var copilotAuthService: GitHubCopilotService? - - // Make init private to enforce singleton pattern - private init() {} - - public func getGitHubCopilotAuthService() throws -> GitHubCopilotService { - if let service = Self.copilotAuthService { return service } - let service = try GitHubCopilotService() - Self.copilotAuthService = service - return service - } - - public func preSignIn() async throws -> SignInResponse? { - let service = try getGitHubCopilotAuthService() - let result = try await service.signInInitiate() - - if result.status == .alreadySignedIn { - guard let user = result.user else { - toast("Missing user info.", .error) - throw NSError(domain: "Missing user info.", code: 0, userInfo: nil) - } - await Status.shared.updateAuthStatus(.loggedIn, username: user) - self.username = user - broadcastStatusChange() - return nil - } - - guard let uri = result.verificationUri, - let userCode = result.userCode, - let url = URL(string: uri) else { - toast("Verification URI is incorrect.", .error) - throw NSError(domain: "Verification URI is incorrect.", code: 0, userInfo: nil) - } - return SignInResponse( - status: SignInInitiateStatus.promptUserDeviceFlow, - userCode: userCode, - verificationURL: url - ) - } - - public func signIn() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - guard let result = try await preSignIn() else { return } - self.signInResponse = result - self.isSignInAlertPresented = true - } catch { - toast(error.localizedDescription, .error) - } - } - } - - public func checkStatus() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - let service = try getGitHubCopilotAuthService() - status = try await service.checkStatus() - version = try await service.version() - isRunningAction = false - } catch { - toast(error.localizedDescription, .error) - } - } - } - - public func signOut() { - Task { - isRunningAction = true - defer { isRunningAction = false } - do { - let service = try getGitHubCopilotAuthService() - status = try await service.signOut() - await Status.shared.updateAuthStatus(.notLoggedIn) - await Status.shared.updateCLSStatus(.unknown, busy: false, message: "") - await Status.shared.updateQuotaInfo(nil) - username = "" - broadcastStatusChange() - } catch { - toast(error.localizedDescription, .error) - } - - // Sign out all other CLS instances - do { - try await GitHubCopilotService.signOutAll() - } catch { - // ignore - } - } - } - - public func cancelWaiting() { - waitingForSignIn = false - } - - public func copyAndOpen() { - waitingForSignIn = true - guard let signInResponse else { - toast("Missing sign in details.", .error) - return - } - let pasteboard = NSPasteboard.general - pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil) - pasteboard.setString(signInResponse.userCode, forType: NSPasteboard.PasteboardType.string) - toast("Sign-in code \(signInResponse.userCode) copied", .info) - NSWorkspace.shared.open(signInResponse.verificationURL) - waitForSignIn() - } - - public func waitForSignIn() { - Task { - do { - guard waitingForSignIn else { return } - guard let signInResponse else { - waitingForSignIn = false - return - } - let service = try getGitHubCopilotAuthService() - let (username, status) = try await service.signInConfirm(userCode: signInResponse.userCode) - waitingForSignIn = false - self.username = username - self.status = status - await Status.shared.updateAuthStatus(.loggedIn, username: username) - broadcastStatusChange() - let models = try? await service.models() - if let models = models, !models.isEmpty { - CopilotModelManager.updateLLMs(models) - } - } catch let error as GitHubCopilotError { - switch error { - case .languageServerError(.timeout): - waitForSignIn() - return - case .languageServerError( - .serverError( - code: CLSErrorCode.deviceFlowFailed.rawValue, - message: _, - data: _ - ) - ): - await showSignInFailedAlert(error: error) - waitingForSignIn = false - return - default: - throw error - } - } catch { - toast(error.localizedDescription, .error) - } - } - } - - private func extractSigninErrorMessage(error: GitHubCopilotError) -> String { - let errorDescription = error.localizedDescription - - // Handle specific EACCES permission denied errors - if errorDescription.contains("EACCES") { - // Look for paths wrapped in single quotes - let pattern = "'([^']+)'" - if let regex = try? NSRegularExpression(pattern: pattern, options: []) { - let range = NSRange(location: 0, length: errorDescription.utf16.count) - if let match = regex.firstMatch(in: errorDescription, options: [], range: range) { - let pathRange = Range(match.range(at: 1), in: errorDescription)! - let path = String(errorDescription[pathRange]) - return path - } - } - } - - return errorDescription - } - - private func getSigninErrorTitle(error: GitHubCopilotError) -> String { - let errorDescription = error.localizedDescription - - if errorDescription.contains("EACCES") { - return "Can't sign you in. The app couldn't create or access files in" - } - - return "Error details:" - } - - private var accessPermissionCommands: String { - """ - sudo mkdir -p ~/.config/github-copilot - sudo chown -R $(whoami):staff ~/.config - chmod -N ~/.config ~/.config/github-copilot - """ - } - - private var containerBackgroundColor: CGColor { - let isDarkMode = NSApp.effectiveAppearance.name == .darkAqua - return isDarkMode - ? NSColor.black.withAlphaComponent(0.85).cgColor - : NSColor.white.withAlphaComponent(0.85).cgColor - } - - // MARK: - Alert Building Functions - - private func showSignInFailedAlert(error: GitHubCopilotError) async { - let alert = NSAlert() - alert.messageText = "GitHub Copilot Sign-in Failed" - alert.alertStyle = .critical - - let accessoryView = createAlertAccessoryView(error: error) - alert.accessoryView = accessoryView - alert.addButton(withTitle: "Copy Commands") - alert.addButton(withTitle: "Cancel") - - let response = alert.runModal() - - if response == .alertFirstButtonReturn { - copyCommandsToClipboard() - } - } - - private func createAlertAccessoryView(error: GitHubCopilotError) -> NSView { - let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 400, height: 142)) - - let detailsHeader = createDetailsHeader(error: error) - accessoryView.addSubview(detailsHeader) - - let errorContainer = createErrorContainer(error: error) - accessoryView.addSubview(errorContainer) - - let terminalHeader = createTerminalHeader() - accessoryView.addSubview(terminalHeader) - - let commandsContainer = createCommandsContainer() - accessoryView.addSubview(commandsContainer) - - return accessoryView - } - - private func createDetailsHeader(error: GitHubCopilotError) -> NSView { - let detailsHeader = NSView(frame: NSRect(x: 16, y: 122, width: 368, height: 20)) - - let warningIcon = NSImageView(frame: NSRect(x: 0, y: 4, width: 16, height: 16)) - warningIcon.image = NSImage(systemSymbolName: "exclamationmark.triangle.fill", accessibilityDescription: "Warning") - warningIcon.contentTintColor = NSColor.systemOrange - detailsHeader.addSubview(warningIcon) - - let detailsLabel = NSTextField(wrappingLabelWithString: getSigninErrorTitle(error: error)) - detailsLabel.frame = NSRect(x: 20, y: 0, width: 346, height: 20) - detailsLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) - detailsLabel.textColor = NSColor.labelColor - detailsHeader.addSubview(detailsLabel) - - return detailsHeader - } - - private func createErrorContainer(error: GitHubCopilotError) -> NSView { - let errorContainer = NSView(frame: NSRect(x: 16, y: 96, width: 368, height: 22)) - errorContainer.wantsLayer = true - errorContainer.layer?.backgroundColor = containerBackgroundColor - errorContainer.layer?.borderColor = NSColor.separatorColor.cgColor - errorContainer.layer?.borderWidth = 1 - errorContainer.layer?.cornerRadius = 6 - - let errorMessage = NSTextField(wrappingLabelWithString: extractSigninErrorMessage(error: error)) - errorMessage.frame = NSRect(x: 8, y: 4, width: 368, height: 14) - errorMessage.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular) - errorMessage.textColor = NSColor.labelColor - errorMessage.backgroundColor = .clear - errorMessage.isBordered = false - errorMessage.isEditable = false - errorMessage.drawsBackground = false - errorMessage.usesSingleLineMode = true - errorContainer.addSubview(errorMessage) - - return errorContainer - } - - private func createTerminalHeader() -> NSView { - let terminalHeader = NSView(frame: NSRect(x: 16, y: 66, width: 368, height: 20)) - - let toolIcon = NSImageView(frame: NSRect(x: 0, y: 4, width: 16, height: 16)) - toolIcon.image = NSImage(systemSymbolName: "terminal.fill", accessibilityDescription: "Terminal") - toolIcon.contentTintColor = NSColor.secondaryLabelColor - terminalHeader.addSubview(toolIcon) - - let terminalLabel = NSTextField(wrappingLabelWithString: "Copy and run the commands below in Terminal, then retry.") - terminalLabel.frame = NSRect(x: 20, y: 0, width: 346, height: 20) - terminalLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) - terminalLabel.textColor = NSColor.labelColor - terminalHeader.addSubview(terminalLabel) - - return terminalHeader - } - - private func createCommandsContainer() -> NSView { - let commandsContainer = NSView(frame: NSRect(x: 16, y: 4, width: 368, height: 58)) - commandsContainer.wantsLayer = true - commandsContainer.layer?.backgroundColor = containerBackgroundColor - commandsContainer.layer?.borderColor = NSColor.separatorColor.cgColor - commandsContainer.layer?.borderWidth = 1 - commandsContainer.layer?.cornerRadius = 6 - - let commandsText = NSTextField(wrappingLabelWithString: accessPermissionCommands) - commandsText.frame = NSRect(x: 8, y: 8, width: 344, height: 42) - commandsText.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular) - commandsText.textColor = NSColor.labelColor - commandsText.backgroundColor = .clear - commandsText.isBordered = false - commandsText.isEditable = false - commandsText.isSelectable = true - commandsText.drawsBackground = false - commandsContainer.addSubview(commandsText) - - return commandsContainer - } - - private func copyCommandsToClipboard() { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString( - self.accessPermissionCommands.replacingOccurrences(of: "\n", with: " && "), - forType: .string - ) - } - - public func broadcastStatusChange() { - DistributedNotificationCenter.default().post( - name: .authStatusDidChange, - object: nil - ) - } -} diff --git a/Core/Sources/HostApp/AdvancedSettings/AdvancedSettings.swift b/Core/Sources/HostApp/AdvancedSettings/AdvancedSettings.swift deleted file mode 100644 index f0cfbaca..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/AdvancedSettings.swift +++ /dev/null @@ -1,21 +0,0 @@ -import SwiftUI - -struct AdvancedSettings: View { - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 30) { - SuggestionSection() - ChatSection() - EnterpriseSection() - ProxySection() - LoggingSection() - } - .padding(20) - } - } -} - -#Preview { - AdvancedSettings() - .frame(width: 800, height: 600) -} diff --git a/Core/Sources/HostApp/AdvancedSettings/ChatSection.swift b/Core/Sources/HostApp/AdvancedSettings/ChatSection.swift deleted file mode 100644 index a71e2aa3..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/ChatSection.swift +++ /dev/null @@ -1,165 +0,0 @@ -import Client -import ComposableArchitecture -import SwiftUI -import Toast -import XcodeInspector - -struct ChatSection: View { - @AppStorage(\.autoAttachChatToXcode) var autoAttachChatToXcode - - var body: some View { - SettingsSection(title: "Chat Settings") { - // Auto Attach toggle - SettingsToggle( - title: "Auto-attach Chat Window to Xcode", - isOn: $autoAttachChatToXcode - ) - - Divider() - - // Response language picker - ResponseLanguageSetting() - .padding(SettingsToggle.defaultPadding) - - Divider() - - // Custom instructions - CustomInstructionSetting() - .padding(SettingsToggle.defaultPadding) - } - } -} - -struct ResponseLanguageSetting: View { - @AppStorage(\.chatResponseLocale) var chatResponseLocale - - // Locale codes mapped to language display names - // reference: https://code.visualstudio.com/docs/configure/locales#_available-locales - private let localeLanguageMap: [String: String] = [ - "en": "English", - "zh-cn": "Chinese, Simplified", - "zh-tw": "Chinese, Traditional", - "fr": "French", - "de": "German", - "it": "Italian", - "es": "Spanish", - "ja": "Japanese", - "ko": "Korean", - "ru": "Russian", - "pt-br": "Portuguese (Brazil)", - "tr": "Turkish", - "pl": "Polish", - "cs": "Czech", - "hu": "Hungarian" - ] - - var selectedLanguage: String { - if chatResponseLocale == "" { - return "English" - } - - return localeLanguageMap[chatResponseLocale] ?? "English" - } - - // Display name to locale code mapping (for the picker UI) - var sortedLanguageOptions: [(displayName: String, localeCode: String)] { - localeLanguageMap.map { (displayName: $0.value, localeCode: $0.key) } - .sorted { $0.displayName < $1.displayName } - } - - var body: some View { - WithPerceptionTracking { - HStack { - VStack(alignment: .leading) { - Text("Response Language") - .font(.body) - Text("This change applies only to new chat sessions. Existing ones won’t be impacted.") - .font(.footnote) - } - - Spacer() - - Picker("", selection: $chatResponseLocale) { - ForEach(sortedLanguageOptions, id: \.localeCode) { option in - Text(option.displayName).tag(option.localeCode) - } - } - .frame(maxWidth: 200, alignment: .leading) - } - } - } -} - -struct CustomInstructionSetting: View { - @State var isGlobalInstructionsViewOpen = false - @Environment(\.toast) var toast - - var body: some View { - WithPerceptionTracking { - HStack { - VStack(alignment: .leading) { - Text("Custom Instructions") - .font(.body) - Text("Configure custom instructions for GitHub Copilot to follow during chat sessions.") - .font(.footnote) - } - - Spacer() - - Button("Current Workspace") { - openCustomInstructions() - } - - Button("Global") { - isGlobalInstructionsViewOpen = true - } - } - .sheet(isPresented: $isGlobalInstructionsViewOpen) { - GlobalInstructionsView(isOpen: $isGlobalInstructionsViewOpen) - } - } - } - - func openCustomInstructions() { - Task { - let service = try? getService() - let inspectorData = try? await service?.getXcodeInspectorData() - var currentWorkspace: URL? = nil - if let url = inspectorData?.realtimeActiveWorkspaceURL, let workspaceURL = URL(string: url), workspaceURL.path != "/" { - currentWorkspace = workspaceURL - } else if let url = inspectorData?.latestNonRootWorkspaceURL { - currentWorkspace = URL(string: url) - } - - // Open custom instructions for the current workspace - if let workspaceURL = currentWorkspace, let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL(workspaceURL: workspaceURL, documentURL: nil) { - - let configFile = projectURL.appendingPathComponent(".github/copilot-instructions.md") - - // If the file doesn't exist, create one with a proper structure - if !FileManager.default.fileExists(atPath: configFile.path) { - do { - // Create directory if it doesn't exist - try FileManager.default.createDirectory( - at: projectURL.appendingPathComponent(".github"), - withIntermediateDirectories: true - ) - // Create empty file - try "".write(to: configFile, atomically: true, encoding: .utf8) - } catch { - toast("Failed to create config file .github/copilot-instructions.md: \(error)", .error) - } - } - - if FileManager.default.fileExists(atPath: configFile.path) { - NSWorkspace.shared.open(configFile) - } - } - } - } -} - -#Preview { - ChatSection() - .frame(width: 600) -} diff --git a/Core/Sources/HostApp/AdvancedSettings/DisabledLanguageList.swift b/Core/Sources/HostApp/AdvancedSettings/DisabledLanguageList.swift deleted file mode 100644 index d869b9ca..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/DisabledLanguageList.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SuggestionBasic -import SwiftUI -import SharedUIComponents - -extension List { - @ViewBuilder - func removeBackground() -> some View { - if #available(macOS 13.0, *) { - scrollContentBackground(.hidden) - .listRowBackground(EmptyView()) - } else { - background(Color.clear) - .listRowBackground(EmptyView()) - } - } -} - -struct DisabledLanguageList: View { - final class Settings: ObservableObject { - @AppStorage(\.suggestionFeatureDisabledLanguageList) - var suggestionFeatureDisabledLanguageList: [String] - - init(suggestionFeatureDisabledLanguageList: AppStorage<[String]>? = nil) { - if let list = suggestionFeatureDisabledLanguageList { - _suggestionFeatureDisabledLanguageList = list - } - } - } - - var isOpen: Binding - @State var isAddingNewProject = false - @StateObject var settings = Settings() - - var body: some View { - VStack(spacing: 0) { - ZStack(alignment: .topLeading) { - Rectangle().fill(Color(nsColor: .separatorColor)).frame(height: 28) - - HStack { - Button(action: { - self.isOpen.wrappedValue = false - }) { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) - .padding() - } - .buttonStyle(.plain) - Text("Disabled Languages") - .font(.system(size: 13, weight: .bold)) - Spacer() - } - .frame(height: 28) - } - - List { - ForEach( - settings.suggestionFeatureDisabledLanguageList, - id: \.self - ) { language in - HStack { - Text(language.capitalized) - .contextMenu { - Button("Remove") { - settings.suggestionFeatureDisabledLanguageList.removeAll( - where: { $0 == language } - ) - } - } - Spacer() - - Button(action: { - settings.suggestionFeatureDisabledLanguageList.removeAll( - where: { $0 == language } - ) - }) { - Image(systemName: "trash.fill") - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - } - } - .modify { view in - if #available(macOS 13.0, *) { - view.listRowSeparator(.hidden).listSectionSeparator(.hidden) - } else { - view - } - } - } - .removeBackground() - .overlay { - if settings.suggestionFeatureDisabledLanguageList.isEmpty { - Text(""" - Empty - Disable the language of a file from the Copilot menu in the status bar. - """) - .multilineTextAlignment(.center) - .padding() - } - } - } - .focusable(false) - .frame(width: 300, height: 400) - .background(Color(nsColor: .windowBackgroundColor)) - } -} - -#Preview { - DisabledLanguageList( - isOpen: .constant(true), - settings: .init(suggestionFeatureDisabledLanguageList: .init(wrappedValue: [ - "hello/2", - "hello/3", - "hello/4", - ], "SuggestionFeatureDisabledLanguageListView_Preview")) - ) -} - - diff --git a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift b/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift deleted file mode 100644 index f0a21a57..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/EnterpriseSection.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Combine -import Client -import SwiftUI -import Toast - -struct EnterpriseSection: View { - @AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI - @Environment(\.toast) var toast - - var body: some View { - SettingsSection(title: "Enterprise") { - SettingsTextField( - title: "Auth provider URL", - prompt: "https://your-enterprise.ghe.com", - text: $gitHubCopilotEnterpriseURI, - onDebouncedChange: { url in urlChanged(url)} - ) - } - } - - func urlChanged(_ url: String) { - if !url.isEmpty { - validateAuthURL(url) - } - NotificationCenter.default.post( - name: .gitHubCopilotShouldRefreshEditorInformation, - object: nil - ) - Task { - do { - let service = try getService() - try await service.postNotification( - name: Notification.Name - .gitHubCopilotShouldRefreshEditorInformation.rawValue - ) - } catch { - toast(error.localizedDescription, .error) - } - } - } - - func validateAuthURL(_ url: String) { - let maybeURL = URL(string: url) - guard let parsedURL = maybeURL else { - toast("Invalid URL", .error) - return - } - if parsedURL.scheme != "https" { - toast("URL scheme must be https://", .error) - return - } - } -} - -#Preview { - EnterpriseSection() -} diff --git a/Core/Sources/HostApp/AdvancedSettings/GlobalInstructionsView.swift b/Core/Sources/HostApp/AdvancedSettings/GlobalInstructionsView.swift deleted file mode 100644 index b429f581..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/GlobalInstructionsView.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Client -import SwiftUI -import Toast - -struct GlobalInstructionsView: View { - var isOpen: Binding - @State var initValue: String = "" - @AppStorage(\.globalCopilotInstructions) var globalInstructions: String - @Environment(\.toast) var toast - - init(isOpen: Binding) { - self.isOpen = isOpen - self.initValue = globalInstructions - } - - var body: some View { - VStack(spacing: 0) { - ZStack(alignment: .topLeading) { - Rectangle().fill(Color(nsColor: .separatorColor)).frame(height: 28) - - HStack { - Button(action: { - self.isOpen.wrappedValue = false - }) { - Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) - .padding() - } - .buttonStyle(.plain) - Text("Global Copilot Instructions") - .font(.system(size: 13, weight: .bold)) - Spacer() - } - .frame(height: 28) - } - - ZStack(alignment: .topLeading) { - TextEditor(text: $globalInstructions) - .font(.body) - - if globalInstructions.isEmpty { - Text("Type your global instructions here...") - .foregroundColor(Color(nsColor: .placeholderTextColor)) - .font(.body) - .allowsHitTesting(false) - } - } - .padding(8) - .background(Color(nsColor: .textBackgroundColor)) - } - .focusable(false) - .frame(width: 300, height: 400) - .onAppear() { - self.initValue = globalInstructions - } - .onDisappear(){ - self.isOpen.wrappedValue = false - if globalInstructions != initValue { - refreshConfiguration() - } - } - } - - func refreshConfiguration() { - NotificationCenter.default.post( - name: .gitHubCopilotShouldRefreshEditorInformation, - object: nil - ) - Task { - do { - let service = try getService() - // Notify extension service process to refresh all its CLS subprocesses to apply new configuration - try await service.postNotification( - name: Notification.Name - .gitHubCopilotShouldRefreshEditorInformation.rawValue - ) - } catch { - toast(error.localizedDescription, .error) - } - } - } -} diff --git a/Core/Sources/HostApp/AdvancedSettings/LoggingSection.swift b/Core/Sources/HostApp/AdvancedSettings/LoggingSection.swift deleted file mode 100644 index 113c2e41..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/LoggingSection.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Logger -import SwiftUI - -struct LoggingSection: View { - @AppStorage(\.verboseLoggingEnabled) var verboseLoggingEnabled: Bool - @State private var shouldPresentRestartAlert = false - - var verboseLoggingBinding: Binding { - Binding( - get: { verboseLoggingEnabled }, - set: { - verboseLoggingEnabled = $0 - shouldPresentRestartAlert = $0 - } - ) - } - - var body: some View { - SettingsSection(title: "Logging") { - SettingsToggle( - title: "Verbose Logging", - isOn: verboseLoggingBinding - ) - Divider() - SettingsLink( - URL(fileURLWithPath: FileLoggingLocation.path.string), - title: "Open Copilot Log Folder" - ) - .environment(\.openURL, OpenURLAction { url in - NSWorkspace.shared.open(url) - return .handled - }) - } - .alert(isPresented: $shouldPresentRestartAlert) { - Alert( - title: Text("Quit And Restart Xcode"), - message: Text( - """ - Logging level changes will take effect the next time Copilot \ - for Xcode is started. To update logging now, please quit \ - Copilot for Xcode and restart Xcode. - """ - ), - dismissButton: .default(Text("OK")) - ) - } - } -} - -#Preview { - LoggingSection() -} diff --git a/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift b/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift deleted file mode 100644 index ab2062c7..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/ProxySection.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Client -import SwiftUI -import Toast - -struct ProxySection: View { - @AppStorage(\.gitHubCopilotProxyUrl) var gitHubCopilotProxyUrl - @AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername - @AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword - @AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL - - @Environment(\.toast) var toast - - var body: some View { - SettingsSection(title: "Proxy") { - SettingsTextField( - title: "Proxy URL", - prompt: "http://host:port", - text: $gitHubCopilotProxyUrl, - onDebouncedChange: { _ in refreshConfiguration() } - ) - SettingsTextField( - title: "Proxy username", - prompt: "username", - text: $gitHubCopilotProxyUsername, - onDebouncedChange: { _ in refreshConfiguration() } - ) - SettingsTextField( - title: "Proxy password", - prompt: "password", - text: $gitHubCopilotProxyPassword, - isSecure: true, - onDebouncedChange: { _ in refreshConfiguration() } - ) - SettingsToggle( - title: "Proxy strict SSL", - isOn: $gitHubCopilotUseStrictSSL - ) - .onChange(of: gitHubCopilotUseStrictSSL) { _ in refreshConfiguration() } - } - } - - func refreshConfiguration() { - NotificationCenter.default.post( - name: .gitHubCopilotShouldRefreshEditorInformation, - object: nil - ) - Task { - do { - let service = try getService() - try await service.postNotification( - name: Notification.Name - .gitHubCopilotShouldRefreshEditorInformation.rawValue - ) - } catch { - toast(error.localizedDescription, .error) - } - } - } -} - -#Preview { - ProxySection() -} diff --git a/Core/Sources/HostApp/AdvancedSettings/SuggestionSection.swift b/Core/Sources/HostApp/AdvancedSettings/SuggestionSection.swift deleted file mode 100644 index cb86bde3..00000000 --- a/Core/Sources/HostApp/AdvancedSettings/SuggestionSection.swift +++ /dev/null @@ -1,62 +0,0 @@ -import SwiftUI - -struct SuggestionSection: View { - @AppStorage(\.realtimeSuggestionToggle) var realtimeSuggestionToggle - @AppStorage(\.suggestionFeatureEnabledProjectList) var suggestionFeatureEnabledProjectList - @AppStorage(\.acceptSuggestionWithTab) var acceptSuggestionWithTab - @State var isSuggestionFeatureDisabledLanguageListViewOpen = false - @State private var shouldPresentTurnoffSheet = false - - var realtimeSuggestionBinding : Binding { - Binding( - get: { realtimeSuggestionToggle }, - set: { - if !$0 { - shouldPresentTurnoffSheet = true - } else { - realtimeSuggestionToggle = $0 - } - } - ) - } - - var body: some View { - SettingsSection(title: "Suggestion Settings") { - SettingsToggle( - title: "Request suggestions while typing", - isOn: realtimeSuggestionBinding - ) - Divider() - SettingsToggle( - title: "Accept suggestions with Tab", - isOn: $acceptSuggestionWithTab - ) - } footer: { - HStack { - Spacer() - Button("Disabled language list") { - isSuggestionFeatureDisabledLanguageListViewOpen = true - } - } - } - .sheet(isPresented: $isSuggestionFeatureDisabledLanguageListViewOpen) { - DisabledLanguageList(isOpen: $isSuggestionFeatureDisabledLanguageListViewOpen) - } - .alert( - "Disable suggestions while typing", - isPresented: $shouldPresentTurnoffSheet - ) { - Button("Disable") { realtimeSuggestionToggle = false } - Button("Cancel", role: .cancel, action: {}) - } message: { - Text(""" - If you disable requesting suggestions while typing, you will \ - not see any suggestions until requested manually. - """) - } - } -} - -#Preview { - SuggestionSection() -} diff --git a/Core/Sources/HostApp/General.swift b/Core/Sources/HostApp/General.swift deleted file mode 100644 index 92d78a25..00000000 --- a/Core/Sources/HostApp/General.swift +++ /dev/null @@ -1,147 +0,0 @@ -import Client -import ComposableArchitecture -import Foundation -import LaunchAgentManager -import Status -import SwiftUI -import XPCShared -import Logger - -@Reducer -public struct General { - @ObservableState - public struct State: Equatable { - var xpcServiceVersion: String? - var xpcCLSVersion: String? - var isAccessibilityPermissionGranted: ObservedAXStatus = .unknown - var isExtensionPermissionGranted: ExtensionPermissionStatus = .unknown - var xpcServiceAuthStatus: AuthStatus = .init(status: .unknown) - var isReloading = false - } - - public enum Action: Equatable { - case appear - case setupLaunchAgentIfNeeded - case openExtensionManager - case reloadStatus - case finishReloading( - xpcServiceVersion: String, - xpcCLSVersion: String?, - axStatus: ObservedAXStatus, - extensionStatus: ExtensionPermissionStatus, - authStatus: AuthStatus - ) - case failedReloading - case retryReloading - } - - @Dependency(\.toast) var toast - - struct ReloadStatusCancellableId: Hashable {} - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .appear: - return .run { send in - await send(.setupLaunchAgentIfNeeded) - for await _ in DistributedNotificationCenter.default().notifications(named: .serviceStatusDidChange) { - await send(.reloadStatus) - } - } - - case .setupLaunchAgentIfNeeded: - return .run { send in - #if DEBUG - // do not auto install on debug build - await send(.reloadStatus) - #else - Task { - do { - try await LaunchAgentManager() - .setupLaunchAgentForTheFirstTimeIfNeeded() - } catch { - Logger.ui.error("Failed to setup launch agent. \(error.localizedDescription)") - toast("Operation failed: permission denied. This may be due to missing background permissions.", .error) - } - await send(.reloadStatus) - } - #endif - } - - case .openExtensionManager: - return .run { send in - let service = try getService() - do { - _ = try await service - .send(requestBody: ExtensionServiceRequests.OpenExtensionManager()) - } catch { - Logger.ui.error("Failed to open extension manager. \(error.localizedDescription)") - toast(error.localizedDescription, .error) - await send(.failedReloading) - } - } - - case .reloadStatus: - guard !state.isReloading else { return .none } - state.isReloading = true - return .run { send in - let service = try getService() - do { - let isCommunicationReady = try await service.launchIfNeeded() - if isCommunicationReady { - let xpcServiceVersion = try await service.getXPCServiceVersion().version - let isAccessibilityPermissionGranted = try await service - .getXPCServiceAccessibilityPermission() - let isExtensionPermissionGranted = try await service.getXPCServiceExtensionPermission() - let xpcServiceAuthStatus = try await service.getXPCServiceAuthStatus() ?? .init(status: .unknown) - let xpcCLSVersion = try await service.getXPCCLSVersion() - await send(.finishReloading( - xpcServiceVersion: xpcServiceVersion, - xpcCLSVersion: xpcCLSVersion, - axStatus: isAccessibilityPermissionGranted, - extensionStatus: isExtensionPermissionGranted, - authStatus: xpcServiceAuthStatus - )) - } else { - toast("Launching service app.", .info) - try await Task.sleep(nanoseconds: 5_000_000_000) - await send(.retryReloading) - } - } catch let error as XPCCommunicationBridgeError { - Logger.ui.error("Failed to reach communication bridge. \(error.localizedDescription)") - toast( - "Unable to connect to the communication bridge. The helper application didn't respond. This may be due to missing background permissions.", - .error - ) - await send(.failedReloading) - } catch { - Logger.ui.error("Failed to reload status. \(error.localizedDescription)") - toast(error.localizedDescription, .error) - await send(.failedReloading) - } - }.cancellable(id: ReloadStatusCancellableId(), cancelInFlight: true) - - case let .finishReloading(version, clsVersion, axStatus, extensionStatus, authStatus): - state.xpcServiceVersion = version - state.isAccessibilityPermissionGranted = axStatus - state.isExtensionPermissionGranted = extensionStatus - state.xpcServiceAuthStatus = authStatus - state.xpcCLSVersion = clsVersion - state.isReloading = false - return .none - - case .failedReloading: - state.isReloading = false - return .none - - case .retryReloading: - state.isReloading = false - return .run { send in - await send(.reloadStatus) - } - } - } - } -} - diff --git a/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift b/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift deleted file mode 100644 index 0cf5e8af..00000000 --- a/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift +++ /dev/null @@ -1,74 +0,0 @@ -import ComposableArchitecture -import GitHubCopilotService -import SwiftUI - -struct AppInfoView: View { - class Settings: ObservableObject { - @AppStorage(\.installPrereleases) - var installPrereleases - } - - static var copilotAuthService: GitHubCopilotAuthServiceType? - - @Environment(\.updateChecker) var updateChecker - @Environment(\.toast) var toast - - @StateObject var settings = Settings() - - @State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - @State var automaticallyCheckForUpdates: Bool? - - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - HStack(alignment: .center, spacing: 16) { - let appImage = if let nsImage = NSImage(named: "AppIcon") { - Image(nsImage: nsImage) - } else { - Image(systemName: "app") - } - appImage - .resizable() - .frame(width: 110, height: 110) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(Bundle.main.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String ?? "GitHub Copilot for Xcode") - .font(.title) - Text("(\(appVersion ?? ""))") - .font(.title) - } - Text("Language Server Version: \(store.xpcCLSVersion ?? "Loading...")") - Button(action: { - updateChecker.checkForUpdates() - }) { - HStack(spacing: 2) { - Text("Check for Updates") - } - } - HStack { - Toggle(isOn: .init( - get: { automaticallyCheckForUpdates ?? updateChecker.getAutomaticallyChecksForUpdates() }, - set: { updateChecker.setAutomaticallyChecksForUpdates($0); automaticallyCheckForUpdates = $0 } - )) { - Text("Automatically Check for Updates") - } - - Toggle(isOn: $settings.installPrereleases) { - Text("Install pre-releases") - } - } - } - Spacer() - } - .padding(.horizontal, 2) - .padding(.vertical, 15) - } - } -} - -#Preview { - AppInfoView( - store: .init(initialState: .init(), reducer: { General() }) - ) -} diff --git a/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift b/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift deleted file mode 100644 index 5a454b7a..00000000 --- a/Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift +++ /dev/null @@ -1,142 +0,0 @@ -import ComposableArchitecture -import GitHubCopilotViewModel -import SwiftUI -import Client - -struct CopilotConnectionView: View { - @AppStorage("username") var username: String = "" - @Environment(\.toast) var toast - @StateObject var viewModel: GitHubCopilotViewModel - - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - VStack { - connection - .padding(.bottom, 20) - copilotResources - } - } - } - - var accountStatusString: String { - switch store.xpcServiceAuthStatus.status { - case .loggedIn: - return "Active" - case .notLoggedIn: - return "Not Signed In" - case .notAuthorized: - return "No Subscription" - case .unknown: - return "Loading..." - } - } - - var accountStatus: some View { - SettingsButtonRow( - title: "GitHub Account Status Permissions", - subtitle: "GitHub Account: \(accountStatusString)" - ) { - if viewModel.isRunningAction || viewModel.waitingForSignIn { - ProgressView().controlSize(.small) - } - Button("Refresh Connection") { - store.send(.reloadStatus) - } - if viewModel.waitingForSignIn { - Button("Cancel") { - viewModel.cancelWaiting() - } - } else if store.xpcServiceAuthStatus.status == .notLoggedIn { - Button("Log in to GitHub") { - viewModel.signIn() - } - .alert( - viewModel.signInResponse?.userCode ?? "", - isPresented: $viewModel.isSignInAlertPresented, - presenting: viewModel.signInResponse) { _ in - Button("Cancel", role: .cancel, action: {}) - Button("Copy Code and Open", action: viewModel.copyAndOpen) - } message: { response in - Text(""" - Please enter the above code in the \ - GitHub website to authorize your \ - GitHub account with Copilot for Xcode. - - \(response?.verificationURL.absoluteString ?? "") - """) - } - } - if store.xpcServiceAuthStatus.status == .loggedIn || store.xpcServiceAuthStatus.status == .notAuthorized { - Button("Log Out from GitHub") { - Task { - viewModel.signOut() - viewModel.isSignInAlertPresented = false - let service = try getService() - do { - try await service.signOutAllGitHubCopilotService() - } catch { - toast(error.localizedDescription, .error) - } - } - } - } - } - } - - var connection: some View { - SettingsSection( - title: "Account Settings", - showWarning: store.xpcServiceAuthStatus.status == .notAuthorized - ) { - accountStatus - Divider() - if store.xpcServiceAuthStatus.status == .notAuthorized { - SettingsLink( - url: "https://github.com/features/copilot/plans", - title: "Enable powerful AI features for free with the GitHub Copilot Free plan" - ) - Divider() - } - SettingsLink( - url: "https://github.com/settings/copilot", - title: "GitHub Copilot Account Settings" - ) - } - .onReceive(DistributedNotificationCenter.default().publisher(for: .authStatusDidChange)) { _ in - store.send(.reloadStatus) - } - } - - var copilotResources: some View { - SettingsSection(title: "Copilot Resources") { - SettingsLink( - url: "https://docs.github.com/en/copilot", - title: "View Copilot Documentation" - ) - Divider() - SettingsLink( - url: "https://github.com/orgs/community/discussions/categories/copilot", - title: "View Copilot Feedback Forum" - ) - } - } -} - - -#Preview { - CopilotConnectionView( - viewModel: GitHubCopilotViewModel.shared, - store: .init(initialState: .init(), reducer: { General() }) - ) -} - -#Preview("Running") { - let runningModel = GitHubCopilotViewModel.shared - runningModel.isRunningAction = true - return CopilotConnectionView( - viewModel: GitHubCopilotViewModel.shared, - store: .init(initialState: .init(), reducer: { General() }) - ) -} diff --git a/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift b/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift deleted file mode 100644 index 6c264821..00000000 --- a/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift +++ /dev/null @@ -1,141 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -struct GeneralSettingsView: View { - @AppStorage(\.extensionPermissionShown) var extensionPermissionShown: Bool - @AppStorage(\.quitXPCServiceOnXcodeAndAppQuit) var quitXPCServiceOnXcodeAndAppQuit: Bool - @State private var shouldPresentExtensionPermissionAlert = false - @State private var shouldShowRestartXcodeAlert = false - - let store: StoreOf - - var accessibilityPermissionSubtitle: String { - switch store.isAccessibilityPermissionGranted { - case .granted: - return "Granted" - case .notGranted: - return "Enable accessibility in system preferences" - case .unknown: - return "" - } - } - - var extensionPermissionSubtitle: any View { - switch store.isExtensionPermissionGranted { - case .notGranted: - return HStack(spacing: 0) { - Text("Enable ") - Text( - "Extensions \(Image(systemName: "puzzlepiece.extension.fill")) → Xcode Source Editor \(Image(systemName: "info.circle")) → GitHub Copilot for Xcode" - ) - .bold() - .foregroundStyle(.primary) - Text(" for faster and full-featured code completion.") - } - case .disabled: - return Text("Quit and restart Xcode to enable extension") - case .granted: - return Text("Granted") - case .unknown: - return Text("") - } - } - - - var extensionPermissionBadge: BadgeItem? { - switch store.isExtensionPermissionGranted { - case .notGranted: - return .init(text: "Not Granted", level: .danger) - case .disabled: - return .init(text: "Disabled", level: .danger) - default: - return nil - } - } - - var extensionPermissionAction: ()->Void { - switch store.isExtensionPermissionGranted { - case .disabled: - return { shouldShowRestartXcodeAlert = true } - default: - return NSWorkspace.openXcodeExtensionsPreferences - } - } - - var body: some View { - SettingsSection(title: "General") { - SettingsToggle( - title: "Quit GitHub Copilot when Xcode App is closed", - isOn: $quitXPCServiceOnXcodeAndAppQuit - ) - Divider() - SettingsLink( - url: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility", - title: "Accessibility Permission", - subtitle: accessibilityPermissionSubtitle, - badge: store.isAccessibilityPermissionGranted == .notGranted ? - .init( - text: "Not Granted", - level: .danger - ) : nil - ) - Divider() - SettingsLink( - action: extensionPermissionAction, - title: "Extension Permission", - subtitle: extensionPermissionSubtitle, - badge: extensionPermissionBadge - ) - } footer: { - HStack { - Spacer() - Button("?") { - NSWorkspace.shared.open( - URL(string: "https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md")! - ) - } - .clipShape(Circle()) - } - } - .alert( - "Enable Extension Permission", - isPresented: $shouldPresentExtensionPermissionAlert - ) { - Button( - "Open System Preferences", - action: { - NSWorkspace.openXcodeExtensionsPreferences() - }).keyboardShortcut(.defaultAction) - Button("View How-to Guide", action: { - let url = "https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md#extension-permission" - NSWorkspace.shared.open(URL(string: url)!) - }) - Button("Close", role: .cancel, action: {}) - } message: { - Text("To enable faster and full-featured code completion, navigate to:\nExtensions → Xcode Source Editor → GitHub Copilot for Xcode.") - } - .task { - if extensionPermissionShown { return } - extensionPermissionShown = true - shouldPresentExtensionPermissionAlert = true - } - .alert( - "Restart Xcode?", - isPresented: $shouldShowRestartXcodeAlert - ) { - Button("Restart Now") { - NSWorkspace.restartXcode() - }.keyboardShortcut(.defaultAction) - - Button("Cancel", role: .cancel) {} - } message: { - Text("Quit and restart Xcode to enable Github Copilot for Xcode extension.") - } - } -} - -#Preview { - GeneralSettingsView( - store: .init(initialState: .init(), reducer: { General() }) - ) -} diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift deleted file mode 100644 index e80c9491..00000000 --- a/Core/Sources/HostApp/GeneralView.swift +++ /dev/null @@ -1,44 +0,0 @@ -import ComposableArchitecture -import GitHubCopilotViewModel -import SwiftUI - -struct GeneralView: View { - let store: StoreOf - @StateObject private var viewModel = GitHubCopilotViewModel.shared - - var body: some View { - WithPerceptionTracking { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - generalView.padding(20) - Divider() - rightsView.padding(20) - } - .frame(maxWidth: .infinity) - } - .task { - if isPreview { return } - await store.send(.appear).finish() - } - } - } - - private var generalView: some View { - VStack(alignment: .leading, spacing: 30) { - AppInfoView(store: store) - GeneralSettingsView(store: store) - CopilotConnectionView(viewModel: viewModel, store: store) - } - } - - private var rightsView: some View { - Text("GitHub. All rights reserved.") - .font(.caption2) - .foregroundColor(.secondary.opacity(0.5)) - } -} - -#Preview { - GeneralView(store: .init(initialState: .init(), reducer: { General() })) - .frame(width: 800, height: 600) -} diff --git a/Core/Sources/HostApp/HandleToast.swift b/Core/Sources/HostApp/HandleToast.swift deleted file mode 100644 index 8f5d7779..00000000 --- a/Core/Sources/HostApp/HandleToast.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Dependencies -import SwiftUI -import Toast - -struct ToastHandler: View { - @ObservedObject var toastController: ToastController - let namespace: String? - - init(toastController: ToastController, namespace: String?) { - _toastController = .init(wrappedValue: toastController) - self.namespace = namespace - } - - var body: some View { - VStack(spacing: 4) { - ForEach(toastController.messages) { message in - if let n = message.namespace, n != namespace { - EmptyView() - } else { - NotificationView(message: message) - .shadow(color: Color.black.opacity(0.2), radius: 4) - } - } - } - .padding() - .allowsHitTesting(false) - } -} - -extension View { - func handleToast(namespace: String? = nil) -> some View { - @Dependency(\.toastController) var toastController - return overlay(alignment: .bottom) { - ToastHandler(toastController: toastController, namespace: namespace) - }.environment(\.toast) { [toastController] content, level in - toastController.toast(content: content, level: level, namespace: namespace) - } - } -} - diff --git a/Core/Sources/HostApp/HostApp.swift b/Core/Sources/HostApp/HostApp.swift deleted file mode 100644 index 93c8725a..00000000 --- a/Core/Sources/HostApp/HostApp.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Client -import ComposableArchitecture -import Foundation -import KeyboardShortcuts - -extension KeyboardShortcuts.Name { - static let showHideWidget = Self("ShowHideWidget") -} - -@Reducer -public struct HostApp { - @ObservableState - public struct State: Equatable { - var general = General.State() - public var activeTabIndex: Int = 0 - } - - public enum Action: Equatable { - case appear - case general(General.Action) - case setActiveTab(Int) - } - - @Dependency(\.toast) var toast - - init() { - KeyboardShortcuts.userDefaults = .shared - } - - public var body: some ReducerOf { - Scope(state: \.general, action: /Action.general) { - General() - } - - Reduce { state, action in - switch action { - case .appear: - return .none - - case .general: - return .none - - case .setActiveTab(let index): - state.activeTabIndex = index - return .none - } - } - } -} - -import Dependencies -import Preferences - -struct UserDefaultsDependencyKey: DependencyKey { - static var liveValue: UserDefaultsType = UserDefaults.shared - static var previewValue: UserDefaultsType = { - let it = UserDefaults(suiteName: "HostAppPreview")! - it.removePersistentDomain(forName: "HostAppPreview") - return it - }() - - static var testValue: UserDefaultsType = { - let it = UserDefaults(suiteName: "HostAppTest")! - it.removePersistentDomain(forName: "HostAppTest") - return it - }() -} - -extension DependencyValues { - var userDefaults: UserDefaultsType { - get { self[UserDefaultsDependencyKey.self] } - set { self[UserDefaultsDependencyKey.self] = newValue } - } -} diff --git a/Core/Sources/HostApp/IsPreview.swift b/Core/Sources/HostApp/IsPreview.swift deleted file mode 100644 index 4409ad0f..00000000 --- a/Core/Sources/HostApp/IsPreview.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } diff --git a/Core/Sources/HostApp/LaunchAgentManager.swift b/Core/Sources/HostApp/LaunchAgentManager.swift deleted file mode 100644 index ba8a4126..00000000 --- a/Core/Sources/HostApp/LaunchAgentManager.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import LaunchAgentManager - -public extension LaunchAgentManager { - init() { - self.init( - serviceIdentifier: Bundle.main - .object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String + - ".CommunicationBridge", - executablePath: Bundle.main.bundleURL - .appendingPathComponent("Contents") - .appendingPathComponent("Applications") - .appendingPathComponent("CommunicationBridge") - .path, - bundleIdentifier: Bundle.main - .object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String - ) - } -} - diff --git a/Core/Sources/HostApp/MCPConfigView.swift b/Core/Sources/HostApp/MCPConfigView.swift deleted file mode 100644 index df80423a..00000000 --- a/Core/Sources/HostApp/MCPConfigView.swift +++ /dev/null @@ -1,207 +0,0 @@ -import Client -import Foundation -import Logger -import SharedUIComponents -import SwiftUI -import Toast -import ConversationServiceProvider -import GitHubCopilotService -import ComposableArchitecture - -struct MCPConfigView: View { - @State private var mcpConfig: String = "" - @Environment(\.toast) var toast - @State private var configFilePath: String = mcpConfigFilePath - @State private var isMonitoring: Bool = false - @State private var lastModificationDate: Date? = nil - @State private var fileMonitorTask: Task? = nil - @State private var isMCPFFEnabled = false - @Environment(\.colorScheme) var colorScheme - - private static var lastSyncTimestamp: Date? = nil - - var body: some View { - WithPerceptionTracking { - ScrollView { - VStack(alignment: .leading, spacing: 8) { - MCPIntroView(isMCPFFEnabled: $isMCPFFEnabled) - if isMCPFFEnabled { - MCPToolsListView() - } - } - .padding(20) - .onAppear { - setupConfigFilePath() - Task { - await updateMCPFeatureFlag() - } - } - .onDisappear { - stopMonitoringConfigFile() - } - .onChange(of: isMCPFFEnabled) { newMCPFFEnabled in - if newMCPFFEnabled { - startMonitoringConfigFile() - refreshConfiguration(()) - } else { - stopMonitoringConfigFile() - } - } - .onReceive(DistributedNotificationCenter.default() - .publisher(for: .gitHubCopilotFeatureFlagsDidChange)) { _ in - Task { - await updateMCPFeatureFlag() - } - } - } - } - } - - private func updateMCPFeatureFlag() async { - do { - let service = try getService() - if let featureFlags = try await service.getCopilotFeatureFlags() { - isMCPFFEnabled = featureFlags.mcp - } - } catch { - Logger.client.error("Failed to get copilot feature flags: \(error)") - } - } - - private func setupConfigFilePath() { - let fileManager = FileManager.default - - if !fileManager.fileExists(atPath: configDirectory.path) { - try? fileManager.createDirectory(at: configDirectory, withIntermediateDirectories: true) - } - - // If the file doesn't exist, create one with a proper structure - let configFileURL = URL(fileURLWithPath: configFilePath) - if !fileManager.fileExists(atPath: configFilePath) { - try? """ - { - "servers": { - - } - } - """.write(to: configFileURL, atomically: true, encoding: .utf8) - } - - // Read the current content from file and ensure it's valid JSON - mcpConfig = readAndValidateJSON(from: configFileURL) ?? "{}" - - // Get initial modification date - lastModificationDate = getFileModificationDate(url: configFileURL) - } - - /// Reads file content and validates it as JSON, returning only the "servers" object - private func readAndValidateJSON(from url: URL) -> String? { - guard let data = try? Data(contentsOf: url) else { - return nil - } - - // Try to parse as JSON to validate - do { - // First verify it's valid JSON - let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] - - // Extract the "servers" object - guard let servers = jsonObject?["servers"] as? [String: Any] else { - Logger.client.info("No 'servers' key found in MCP configuration") - toast("No 'servers' key found in MCP configuration", .error) - // Return empty object if no servers section - return "{}" - } - - // Convert the servers object back to JSON data - let serversData = try JSONSerialization.data( - withJSONObject: servers, options: [.prettyPrinted]) - - // Return as a string - return String(data: serversData, encoding: .utf8) - } catch { - // If parsing fails, return nil - Logger.client.info("Parsing MCP JSON error: \(error)") - toast("Invalid JSON in MCP configuration file", .error) - return nil - } - } - - private func getFileModificationDate(url: URL) -> Date? { - let attributes = try? FileManager.default.attributesOfItem(atPath: url.path) - return attributes?[.modificationDate] as? Date - } - - private func startMonitoringConfigFile() { - stopMonitoringConfigFile() // Stop existing monitoring if any - - isMonitoring = true - - fileMonitorTask = Task { - let configFileURL = URL(fileURLWithPath: configFilePath) - - // Check for file changes periodically - while isMonitoring { - try? await Task.sleep(nanoseconds: 3_000_000_000) // Check every 3 seconds - - let currentDate = getFileModificationDate(url: configFileURL) - - if let currentDate = currentDate, currentDate != lastModificationDate { - // File modification date has changed, update our record - lastModificationDate = currentDate - - // Read and validate the updated content - if let validJson = readAndValidateJSON(from: configFileURL) { - await MainActor.run { - mcpConfig = validJson - refreshConfiguration(validJson) - toast("MCP configuration file updated", .info) - } - } else { - // If JSON is invalid, show error - await MainActor.run { - toast("Invalid JSON in MCP configuration file", .error) - } - } - } - } - } - } - - private func stopMonitoringConfigFile() { - isMonitoring = false - fileMonitorTask?.cancel() - fileMonitorTask = nil - } - - func refreshConfiguration(_: Any) { - if MCPConfigView.lastSyncTimestamp == lastModificationDate { - return - } - - MCPConfigView.lastSyncTimestamp = lastModificationDate - - let fileURL = URL(fileURLWithPath: configFilePath) - if let jsonString = readAndValidateJSON(from: fileURL) { - UserDefaults.shared.set(jsonString, for: \.gitHubCopilotMCPConfig) - } - - Task { - do { - let service = try getService() - try await service.postNotification( - name: Notification.Name - .gitHubCopilotShouldRefreshEditorInformation.rawValue - ) - toast("MCP configuration updated", .info) - } catch { - toast(error.localizedDescription, .error) - } - } - } -} - -#Preview { - MCPConfigView() - .frame(width: 800, height: 600) -} diff --git a/Core/Sources/HostApp/MCPSettings/CopilotMCPToolManagerObservable.swift b/Core/Sources/HostApp/MCPSettings/CopilotMCPToolManagerObservable.swift deleted file mode 100644 index d493b8be..00000000 --- a/Core/Sources/HostApp/MCPSettings/CopilotMCPToolManagerObservable.swift +++ /dev/null @@ -1,53 +0,0 @@ -import SwiftUI -import Combine -import Persist -import GitHubCopilotService -import Client -import Logger - -class CopilotMCPToolManagerObservable: ObservableObject { - static let shared = CopilotMCPToolManagerObservable() - - @Published var availableMCPServerTools: [MCPServerToolsCollection] = [] - private var cancellables = Set() - - private init() { - DistributedNotificationCenter.default() - .publisher(for: .gitHubCopilotMCPToolsDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - Task { - await self.refreshMCPServerTools() - } - } - .store(in: &cancellables) - - Task { - // Initial load of MCP server tools collections from ExtensionService process - await refreshMCPServerTools() - } - } - - @MainActor - private func refreshMCPServerTools() async { - do { - let service = try getService() - let mcpTools = try await service.getAvailableMCPServerToolsCollections() - refreshTools(tools: mcpTools) - } catch { - Logger.client.error("Failed to fetch MCP server tools: \(error)") - } - } - - private func refreshTools(tools: [MCPServerToolsCollection]?) { - guard let tools = tools else { - // nil means the tools data is ready, and skip it first. - return - } - - AppState.shared.cleanupMCPToolsStatus(availableTools: tools) - AppState.shared.createMCPToolsStatus(tools) - self.availableMCPServerTools = tools - } -} diff --git a/Core/Sources/HostApp/MCPSettings/MCPAppState.swift b/Core/Sources/HostApp/MCPSettings/MCPAppState.swift deleted file mode 100644 index f6d16d98..00000000 --- a/Core/Sources/HostApp/MCPSettings/MCPAppState.swift +++ /dev/null @@ -1,116 +0,0 @@ -import Persist -import GitHubCopilotService -import Foundation - -public let MCP_TOOLS_STATUS = "mcpToolsStatus" - -extension AppState { - public func getMCPToolsStatus() -> [UpdateMCPToolsStatusServerCollection]? { - guard let savedJSON = get(key: MCP_TOOLS_STATUS), - let data = try? JSONEncoder().encode(savedJSON), - let savedStatus = try? JSONDecoder().decode([UpdateMCPToolsStatusServerCollection].self, from: data) else { - return nil - } - return savedStatus - } - - public func updateMCPToolsStatus(_ servers: [UpdateMCPToolsStatusServerCollection]) { - var existingServers = getMCPToolsStatus() ?? [] - - // Update or add servers - for newServer in servers { - if let existingIndex = existingServers.firstIndex(where: { $0.name == newServer.name }) { - // Update existing server - let updatedTools = mergeTools(original: existingServers[existingIndex].tools, new: newServer.tools) - existingServers[existingIndex].tools = updatedTools - } else { - // Add new server - existingServers.append(newServer) - } - } - - update(key: MCP_TOOLS_STATUS, value: existingServers) - } - - private func mergeTools(original: [UpdatedMCPToolsStatus], new: [UpdatedMCPToolsStatus]) -> [UpdatedMCPToolsStatus] { - var result = original - - for newTool in new { - if let index = result.firstIndex(where: { $0.name == newTool.name }) { - result[index].status = newTool.status - } else { - result.append(newTool) - } - } - - return result - } - - public func createMCPToolsStatus(_ serverCollections: [MCPServerToolsCollection]) { - var existingServers = getMCPToolsStatus() ?? [] - var serversChanged = false - - for serverCollection in serverCollections { - // Find or create a server entry - let serverIndex = existingServers.firstIndex(where: { $0.name == serverCollection.name }) - var toolsToUpdate: [UpdatedMCPToolsStatus] - - if let index = serverIndex { - toolsToUpdate = existingServers[index].tools - } else { - toolsToUpdate = [] - serversChanged = true - } - - // Add new tools with default enabled status - let existingToolNames = Set(toolsToUpdate.map { $0.name }) - let newTools = serverCollection.tools - .filter { !existingToolNames.contains($0.name) } - .map { UpdatedMCPToolsStatus(name: $0.name, status: .enabled) } - - if !newTools.isEmpty { - serversChanged = true - toolsToUpdate.append(contentsOf: newTools) - } - - // Update or add the server - if let index = serverIndex { - existingServers[index].tools = toolsToUpdate - } else { - existingServers.append(UpdateMCPToolsStatusServerCollection( - name: serverCollection.name, - tools: toolsToUpdate - )) - } - } - - // Only update storage if changes were made - if serversChanged { - update(key: MCP_TOOLS_STATUS, value: existingServers) - } - } - - public func cleanupMCPToolsStatus(availableTools: [MCPServerToolsCollection]) { - guard var existingServers = getMCPToolsStatus() else { return } - - // Get all available server names and their respective tool names - let availableServerMap = Dictionary( - uniqueKeysWithValues: availableTools.map { collection in - (collection.name, Set(collection.tools.map { $0.name })) - } - ) - - // Remove servers that don't exist in available tools - existingServers.removeAll { !availableServerMap.keys.contains($0.name) } - - // For each remaining server, remove tools that don't exist in available tools - for i in 0..) { - self.isExpanded = isExpanded - self._isMCPFFEnabled = isMCPFFEnabled - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - if !isMCPFFEnabled { - GroupBox { - HStack(alignment: .top, spacing: 8) { - Image(systemName: "info.circle.fill") - .font(.body) - .foregroundColor(.gray) - Text( - "MCP servers are disabled by your organization’s policy. To enable them, please contact your administrator. [Get More Info about Copilot policies](https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies)" - ) - } - } - .groupBoxStyle( - CardGroupBoxStyle( - backgroundColor: Color(nsColor: .textBackgroundColor) - ) - ) - } - - GroupBox( - label: Text("Model Context Protocol (MCP) Configuration") - .fontWeight(.bold) - ) { - Text( - "MCP is an open standard that connects AI models to external tools. In Xcode, it enhances GitHub Copilot's agent mode by connecting to any MCP server and integrating its tools into your workflow. [Learn More](https://modelcontextprotocol.io/introduction)" - ) - }.groupBoxStyle(CardGroupBoxStyle()) - - if isMCPFFEnabled { - DisclosureGroup(isExpanded: $isExpanded) { - exampleConfigView() - } label: { - sectionHeader() - } - .padding(.horizontal, 0) - .padding(.vertical, 10) - - HStack(spacing: 8) { - Button { - openConfigFile() - } label: { - HStack(spacing: 0) { - Image(systemName: "square.and.pencil") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12, alignment: .center) - .padding(4) - Text("Edit Config") - } - .conditionalFontWeight(.semibold) - } - .buttonStyle(.borderedProminent) - .help("Configure your MCP server") - - Button { - openMCPRunTimeLogFolder() - } label: { - HStack(spacing: 0) { - Image(systemName: "folder") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12, alignment: .center) - .padding(4) - Text("Open MCP Log Folder") - } - .conditionalFontWeight(.semibold) - } - .buttonStyle(.borderedProminentWhite) - .help("Open MCP Runtime Log Folder") - } - } - } - - } - - @ViewBuilder - private func exampleConfigView() -> some View { - Text(exampleConfig) - .font(.system(.body, design: .monospaced)) - .padding(.horizontal, 16) - .padding(.top, 8) - .padding(.bottom, 6) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - Color(nsColor: .textBackgroundColor).opacity(0.5) - ) - .textSelection(.enabled) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .inset(by: 0.5) - .stroke(Color("GroupBoxStrokeColor"), lineWidth: 1) - ) - } - - @ViewBuilder - private func sectionHeader() -> some View { - HStack(spacing: 8) { - Text("Example Configuration").foregroundColor(.primary.opacity(0.85)) - - CopyButton( - copy: { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(exampleConfig, forType: .string) - }, - foregroundColor: .primary.opacity(0.85), - fontWeight: .semibold - ) - .frame(width: 10, height: 10) - } - .padding(.leading, 4) - } - - private func openConfigFile() { - let url = URL(fileURLWithPath: mcpConfigFilePath) - NSWorkspace.shared.open(url) - } - - private func openMCPRunTimeLogFolder() { - let url = URL( - fileURLWithPath: FileLoggingLocation.mcpRuntimeLogsPath.description, - isDirectory: true - ) - - // Create directory if it doesn't exist - if !FileManager.default.fileExists(atPath: url.path) { - do { - try FileManager.default.createDirectory( - atPath: url.path, - withIntermediateDirectories: true, - attributes: nil - ) - } catch { - Logger.client.error("Failed to create MCP runtime log folder: \(error)") - return - } - } - - NSWorkspace.shared.open(url) - } -} - -#Preview { - MCPIntroView(isExpanded: true, isMCPFFEnabled: .constant(true)) - .frame(width: 800) -} - -#Preview { - MCPIntroView(isExpanded: true, isMCPFFEnabled: .constant(false)) - .frame(width: 800) -} diff --git a/Core/Sources/HostApp/MCPSettings/MCPServerToolsSection.swift b/Core/Sources/HostApp/MCPSettings/MCPServerToolsSection.swift deleted file mode 100644 index 9641a45a..00000000 --- a/Core/Sources/HostApp/MCPSettings/MCPServerToolsSection.swift +++ /dev/null @@ -1,199 +0,0 @@ -import SwiftUI -import Persist -import GitHubCopilotService -import Client -import Logger - -/// Section for a single server's tools -struct MCPServerToolsSection: View { - let serverTools: MCPServerToolsCollection - @Binding var isServerEnabled: Bool - var forceExpand: Bool = false - @State private var toolEnabledStates: [String: Bool] = [:] - @State private var isExpanded: Bool = true - private var originalServerName: String { serverTools.name } - - private var serverToggleLabel: some View { - HStack(spacing: 8) { - Text("MCP Server: \(serverTools.name)").fontWeight(.medium) - if serverTools.status == .error { - let message = extractErrorMessage(serverTools.error?.description ?? "") - Badge(text: message, level: .danger, icon: "xmark.circle.fill") - } - Spacer() - } - } - - private var serverToggle: some View { - Toggle(isOn: Binding( - get: { isServerEnabled }, - set: { updateAllToolsStatus(enabled: $0) } - )) { - serverToggleLabel - } - .toggleStyle(.checkbox) - .padding(.leading, 4) - .disabled(serverTools.status == .error) - } - - private var divider: some View { - Divider() - .padding(.leading, 36) - .padding(.top, 2) - .padding(.bottom, 4) - } - - private var toolsList: some View { - VStack(spacing: 0) { - divider - ForEach(serverTools.tools, id: \.name) { tool in - MCPToolRow( - tool: tool, - isServerEnabled: isServerEnabled, - isToolEnabled: toolBindingFor(tool), - onToolToggleChanged: { handleToolToggleChange(tool: tool, isEnabled: $0) } - ) - } - } - .onChange(of: serverTools) { newValue in - initializeToolStates(server: newValue) - } - } - - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - // Conditional view rendering based on error state - if serverTools.status == .error { - // No disclosure group for error state - VStack(spacing: 0) { - serverToggle.padding(.leading, 12) - divider.padding(.top, 4) - } - } else { - // Regular DisclosureGroup for non-error state - DisclosureGroup(isExpanded: $isExpanded) { - toolsList - } label: { - serverToggle - } - .onAppear { - initializeToolStates(server: serverTools) - if forceExpand { - isExpanded = true - } - } - .onChange(of: forceExpand) { newForceExpand in - if newForceExpand { - isExpanded = true - } - } - - if !isExpanded { - divider - } - } - } - } - - private func extractErrorMessage(_ description: String) -> String { - guard let messageRange = description.range(of: "message:"), - let stackRange = description.range(of: "stack:") else { - return description - } - let start = description.index(messageRange.upperBound, offsetBy: 0) - let end = description.index(stackRange.lowerBound, offsetBy: 0) - return description[start.. Binding { - Binding( - get: { toolEnabledStates[tool.name] ?? (tool._status == .enabled) }, - set: { toolEnabledStates[tool.name] = $0 } - ) - } - - private func handleToolToggleChange(tool: MCPTool, isEnabled: Bool) { - toolEnabledStates[tool.name] = isEnabled - - // Update server state based on tool states - updateServerState() - - // Update only this specific tool status - updateToolStatus(tool: tool, isEnabled: isEnabled) - } - - private func updateServerState() { - // If any tool is enabled, server should be enabled - // If all tools are disabled, server should be disabled - let allToolsDisabled = serverTools.tools.allSatisfy { tool in - !(toolEnabledStates[tool.name] ?? (tool._status == .enabled)) - } - - isServerEnabled = !allToolsDisabled - } - - private func updateToolStatus(tool: MCPTool, isEnabled: Bool) { - let serverUpdate = UpdateMCPToolsStatusServerCollection( - name: serverTools.name, - tools: [UpdatedMCPToolsStatus(name: tool.name, status: isEnabled ? .enabled : .disabled)] - ) - - updateMCPStatus([serverUpdate]) - } - - private func updateAllToolsStatus(enabled: Bool) { - isServerEnabled = enabled - - // Get all tools for this server from the original collection - let allServerTools = CopilotMCPToolManagerObservable.shared.availableMCPServerTools - .first(where: { $0.name == originalServerName })?.tools ?? serverTools.tools - - // Update all tool states - includes both visible and filtered-out tools - for tool in allServerTools { - toolEnabledStates[tool.name] = enabled - } - - // Create status update for all tools - let serverUpdate = UpdateMCPToolsStatusServerCollection( - name: serverTools.name, - tools: allServerTools.map { - UpdatedMCPToolsStatus(name: $0.name, status: enabled ? .enabled : .disabled) - } - ) - - updateMCPStatus([serverUpdate]) - } - - private func updateMCPStatus(_ serverUpdates: [UpdateMCPToolsStatusServerCollection]) { - // Update status in AppState and CopilotMCPToolManager - AppState.shared.updateMCPToolsStatus(serverUpdates) - - Task { - do { - let service = try getService() - try await service.updateMCPServerToolsStatus(serverUpdates) - } catch { - Logger.client.error("Failed to update MCP status: \(error.localizedDescription)") - } - } - } -} diff --git a/Core/Sources/HostApp/MCPSettings/MCPToolRowView.swift b/Core/Sources/HostApp/MCPSettings/MCPToolRowView.swift deleted file mode 100644 index f6a8e20f..00000000 --- a/Core/Sources/HostApp/MCPSettings/MCPToolRowView.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI -import GitHubCopilotService - -/// Individual tool row -struct MCPToolRow: View { - let tool: MCPTool - let isServerEnabled: Bool - @Binding var isToolEnabled: Bool - let onToolToggleChanged: (Bool) -> Void - - var body: some View { - HStack(alignment: .center) { - Toggle(isOn: Binding( - get: { isToolEnabled }, - set: { onToolToggleChanged($0) } - )) { - VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .center, spacing: 8) { - Text(tool.name).fontWeight(.medium) - - if let description = tool.description { - Text(description) - .font(.system(size: 11)) - .foregroundColor(.secondary) - .lineLimit(1) - .help(description) - } - } - - Divider().padding(.vertical, 4) - } - } - } - .padding(.leading, 36) - .padding(.vertical, 0) - .onChange(of: tool._status) { isToolEnabled = $0 == .enabled } - .onChange(of: isServerEnabled) { if !$0 { isToolEnabled = false } } - } -} diff --git a/Core/Sources/HostApp/MCPSettings/MCPToolsListContainerView.swift b/Core/Sources/HostApp/MCPSettings/MCPToolsListContainerView.swift deleted file mode 100644 index 27f2d6cb..00000000 --- a/Core/Sources/HostApp/MCPSettings/MCPToolsListContainerView.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI -import GitHubCopilotService - -/// Main list view containing all the tools -struct MCPToolsListContainerView: View { - let mcpServerTools: [MCPServerToolsCollection] - @Binding var serverToggleStates: [String: Bool] - let searchKey: String - let expandedServerNames: Set - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(mcpServerTools, id: \.name) { serverTools in - MCPServerToolsSection( - serverTools: serverTools, - isServerEnabled: serverToggleBinding(for: serverTools.name), - forceExpand: expandedServerNames.contains(serverTools.name) && !searchKey.isEmpty - ) - } - } - .padding(.vertical, 4) - } - - private func serverToggleBinding(for serverName: String) -> Binding { - Binding( - get: { serverToggleStates[serverName] ?? true }, - set: { serverToggleStates[serverName] = $0 } - ) - } -} diff --git a/Core/Sources/HostApp/MCPSettings/MCPToolsListView.swift b/Core/Sources/HostApp/MCPSettings/MCPToolsListView.swift deleted file mode 100644 index c4f0f0f2..00000000 --- a/Core/Sources/HostApp/MCPSettings/MCPToolsListView.swift +++ /dev/null @@ -1,159 +0,0 @@ -import SwiftUI -import Combine -import GitHubCopilotService -import Persist - -struct MCPToolsListView: View { - @ObservedObject private var mcpToolManager = CopilotMCPToolManagerObservable.shared - @State private var serverToggleStates: [String: Bool] = [:] - @State private var isSearchBarVisible: Bool = false - @State private var searchText: String = "" - @FocusState private var isSearchFieldFocused: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - GroupBox( - label: - HStack(alignment: .center) { - Text("Available MCP Tools").fontWeight(.bold) - Spacer() - if isSearchBarVisible { - HStack(spacing: 5) { - Image(systemName: "magnifyingglass") - .foregroundColor(.secondary) - - TextField("Search tools...", text: $searchText) - .accessibilityIdentifier("searchTextField") - .accessibilityLabel("Search MCP tools") - .textFieldStyle(PlainTextFieldStyle()) - .focused($isSearchFieldFocused) - - if !searchText.isEmpty { - Button(action: { searchText = "" }) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.secondary) - } - .buttonStyle(PlainButtonStyle()) - } - } - .padding(.leading, 7) - .padding(.trailing, 3) - .padding(.vertical, 3) - .background( - RoundedRectangle(cornerRadius: 5) - .fill(Color(.textBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 5) - .stroke(isSearchFieldFocused ? - Color(red: 0, green: 0.48, blue: 1).opacity(0.5) : - Color.gray.opacity(0.4), lineWidth: isSearchFieldFocused ? 3 : 1 - ) - ) - .cornerRadius(5) - .frame(width: 212, height: 20, alignment: .leading) - .shadow(color: Color(red: 0, green: 0.48, blue: 1).opacity(0.5), radius: isSearchFieldFocused ? 1.25 : 0, x: 0, y: 0) - .shadow(color: .black.opacity(0.05), radius: 0, x: 0, y: 0) - .shadow(color: .black.opacity(0.3), radius: 1.25, x: 0, y: 0.5) - .padding(2) - .transition(.move(edge: .trailing).combined(with: .opacity)) - } else { - Button(action: { withAnimation(.easeInOut) { isSearchBarVisible = true } }) { - Image(systemName: "magnifyingglass") - .padding(.trailing, 2) - } - .buttonStyle(PlainButtonStyle()) - .frame(height: 24) - .transition(.move(edge: .trailing).combined(with: .opacity)) - } - } - .clipped() - ) { - let filteredServerTools = filteredMCPServerTools() - if filteredServerTools.isEmpty { - EmptyStateView() - } else { - ToolsListView( - mcpServerTools: filteredServerTools, - serverToggleStates: $serverToggleStates, - searchKey: searchText, - expandedServerNames: expandedServerNames(filteredServerTools: filteredServerTools) - ) - } - } - .groupBoxStyle(CardGroupBoxStyle()) - } - .contentShape(Rectangle()) // Allow the VStack to receive taps for dismissing focus - .onTapGesture { - if isSearchFieldFocused { // Only dismiss focus if the search field is currently focused - isSearchFieldFocused = false - } - } - .onAppear(perform: updateServerToggleStates) - .onChange(of: mcpToolManager.availableMCPServerTools) { _ in - updateServerToggleStates() - } - .onChange(of: isSearchFieldFocused) { focused in - if !focused && searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - withAnimation(.easeInOut) { - isSearchBarVisible = false - } - } - } - .onChange(of: isSearchBarVisible) { newIsVisible in - if newIsVisible { - // When isSearchBarVisible becomes true, schedule focusing the TextField. - // The delay helps ensure the TextField is rendered and ready. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - isSearchFieldFocused = true - } - } - } - } - - private func updateServerToggleStates() { - serverToggleStates = mcpToolManager.availableMCPServerTools.reduce(into: [:]) { result, server in - result[server.name] = !server.tools.isEmpty && !server.tools.allSatisfy{ $0._status != .enabled } - } - } - - private func filteredMCPServerTools() -> [MCPServerToolsCollection] { - let key = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !key.isEmpty else { return mcpToolManager.availableMCPServerTools } - return mcpToolManager.availableMCPServerTools.compactMap { server in - let filteredTools = server.tools.filter { tool in - tool.name.lowercased().contains(key) || (tool.description?.lowercased().contains(key) ?? false) - } - if filteredTools.isEmpty { return nil } - return MCPServerToolsCollection( - name: server.name, - status: server.status, - tools: filteredTools, - error: server.error - ) - } - } - - private func expandedServerNames(filteredServerTools: [MCPServerToolsCollection]) -> Set { - // Expand all groups that have at least one tool in the filtered list - Set(filteredServerTools.map { $0.name }) - } -} - -/// Empty state view when no tools are available -private struct EmptyStateView: View { - var body: some View { - Text("No MCP tools available. Make sure your MCP server is configured correctly and running.") - .foregroundColor(.secondary) - } -} - -// Private components now defined in separate files: -// MCPToolsListContainerView - in MCPToolsListContainerView.swift -// MCPServerToolsSection - in MCPServerToolsSection.swift -// MCPToolRow - in MCPToolRowView.swift - -/// Private alias for maintaining backward compatibility -private typealias ToolsListView = MCPToolsListContainerView -private typealias ServerToolsSection = MCPServerToolsSection -private typealias ToolRow = MCPToolRow diff --git a/Core/Sources/HostApp/SharedComponents/Badge.swift b/Core/Sources/HostApp/SharedComponents/Badge.swift deleted file mode 100644 index d3a9dd6e..00000000 --- a/Core/Sources/HostApp/SharedComponents/Badge.swift +++ /dev/null @@ -1,69 +0,0 @@ -import SwiftUI - -struct BadgeItem { - enum Level: String, Equatable { - case warning = "Warning" - case danger = "Danger" - } - let text: String - let level: Level - let icon: String? - - init(text: String, level: Level, icon: String? = nil) { - self.text = text - self.level = level - self.icon = icon - } -} - -struct Badge: View { - let text: String - let level: BadgeItem.Level - let icon: String? - - init(badgeItem: BadgeItem) { - self.text = badgeItem.text - self.level = badgeItem.level - self.icon = badgeItem.icon - } - - init(text: String, level: BadgeItem.Level, icon: String? = nil) { - self.text = text - self.level = level - self.icon = icon - } - - var body: some View { - HStack(spacing: 4) { - if let icon = icon { - Image(systemName: icon) - .resizable() - .scaledToFit() - .frame(width: 11, height: 11) - } - Text(text) - .fontWeight(.semibold) - .font(.system(size: 11)) - .lineLimit(1) - } - .padding(.vertical, 2) - .padding(.horizontal, 4) - .foregroundColor( - Color("\(level.rawValue)ForegroundColor") - ) - .background( - Color("\(level.rawValue)BackgroundColor"), - in: RoundedRectangle( - cornerRadius: 9999, - style: .circular - ) - ) - .overlay( - RoundedRectangle( - cornerRadius: 9999, - style: .circular - ) - .stroke(Color("\(level.rawValue)StrokeColor"), lineWidth: 1) - ) - } -} diff --git a/Core/Sources/HostApp/SharedComponents/BorderedProminentWhiteButtonStyle.swift b/Core/Sources/HostApp/SharedComponents/BorderedProminentWhiteButtonStyle.swift deleted file mode 100644 index 7cc5db2a..00000000 --- a/Core/Sources/HostApp/SharedComponents/BorderedProminentWhiteButtonStyle.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI - -extension ButtonStyle where Self == BorderedProminentWhiteButtonStyle { - static var borderedProminentWhite: BorderedProminentWhiteButtonStyle { - BorderedProminentWhiteButtonStyle() - } -} - -public struct BorderedProminentWhiteButtonStyle: ButtonStyle { - @Environment(\.colorScheme) var colorScheme - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.leading, 4) - .padding(.trailing, 8) - .padding(.vertical, 0) - .frame(height: 22, alignment: .leading) - .foregroundColor(colorScheme == .dark ? .white : .primary) - .background( - colorScheme == .dark ? Color(red: 0.43, green: 0.43, blue: 0.44) : .white - ) - .cornerRadius(5) - .overlay( - RoundedRectangle(cornerRadius: 5).stroke(.clear, lineWidth: 1) - ) - } -} - diff --git a/Core/Sources/HostApp/SharedComponents/CardGroupBoxStyle.swift b/Core/Sources/HostApp/SharedComponents/CardGroupBoxStyle.swift deleted file mode 100644 index 85205d04..00000000 --- a/Core/Sources/HostApp/SharedComponents/CardGroupBoxStyle.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -public struct CardGroupBoxStyle: GroupBoxStyle { - public var backgroundColor: Color - public init(backgroundColor: Color = Color("GroupBoxBackgroundColor")) { - self.backgroundColor = backgroundColor - } - public func makeBody(configuration: Configuration) -> some View { - VStack(alignment: .leading, spacing: 11) { - configuration.label.foregroundColor(.primary) - configuration.content.foregroundColor(.primary) - } - .padding(8) - .frame(maxWidth: .infinity, alignment: .topLeading) - .background(backgroundColor) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .inset(by: 0.5) - .stroke(Color("GroupBoxStrokeColor"), lineWidth: 1) - ) - } -} diff --git a/Core/Sources/HostApp/SharedComponents/SettingsButtonRow.swift b/Core/Sources/HostApp/SharedComponents/SettingsButtonRow.swift deleted file mode 100644 index 2b583302..00000000 --- a/Core/Sources/HostApp/SharedComponents/SettingsButtonRow.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI -import Perception - -struct SettingsButtonRow: View { - let title: String - let subtitle: String? - @ViewBuilder let content: () -> Content - - var body: some View { - WithPerceptionTracking{ - HStack(alignment: .center, spacing: 8) { - VStack(alignment: .leading) { - Text(title) - .font(.body) - if let subtitle = subtitle { - Text(subtitle) - .font(.footnote) - } - } - Spacer() - content() - } - .foregroundStyle(.primary) - .padding(10) - } - } -} - -#Preview { - SettingsButtonRow( - title: "Example", - subtitle: "This is an example" - ) { - Button("Button") { } - Button("Button") { } - } -} diff --git a/Core/Sources/HostApp/SharedComponents/SettingsLink.swift b/Core/Sources/HostApp/SharedComponents/SettingsLink.swift deleted file mode 100644 index 32fb296d..00000000 --- a/Core/Sources/HostApp/SharedComponents/SettingsLink.swift +++ /dev/null @@ -1,85 +0,0 @@ -import SwiftUI - -struct SettingsLink: View { - let action: ()->Void - let title: String - let subtitle: AnyView? - let badge: BadgeItem? - - init( - action: @escaping ()->Void, - title: String, - subtitle: Subtitle?, - badge: BadgeItem? = nil - ) { - self.action = action - self.title = title - self.subtitle = subtitle.map { AnyView($0) } - self.badge = badge - } - - init( - _ url: URL, - title: String, - subtitle: String? = nil, - badge: BadgeItem? = nil - ) { - self.init( - action: { NSWorkspace.shared.open(url) }, - title: title, - subtitle: subtitle.map { Text($0) }, - badge: badge - ) - } - - init(url: String, title: String, subtitle: String? = nil, badge: BadgeItem? = nil) { - self.init( - URL(string: url)!, - title: title, - subtitle: subtitle, - badge: badge - ) - } - - init(url: String, title: String, subtitle: Subtitle?, badge: BadgeItem? = nil) { - self.init( - action: { NSWorkspace.shared.open(URL(string: url)!) }, - title: title, - subtitle: subtitle, - badge: badge - ) - } - - var body: some View { - Button(action: action) { - HStack{ - VStack(alignment: .leading) { - HStack{ - Text(title).font(.body) - if let badge = self.badge { - Badge(badgeItem: badge) - } - } - if let subtitle = subtitle { - subtitle.font(.footnote) - } - } - Spacer() - Image(systemName: "chevron.right") - } - .contentShape(Rectangle()) // This makes the entire HStack clickable - } - .buttonStyle(.plain) - .foregroundStyle(.primary) - .padding(10) - } -} - -#Preview { - SettingsLink( - url: "https://example.com", - title: "Example", - subtitle: "This is an example", - badge: .init(text: "Not Granted", level: .danger) - ) -} diff --git a/Core/Sources/HostApp/SharedComponents/SettingsSection.swift b/Core/Sources/HostApp/SharedComponents/SettingsSection.swift deleted file mode 100644 index 007eeb15..00000000 --- a/Core/Sources/HostApp/SharedComponents/SettingsSection.swift +++ /dev/null @@ -1,73 +0,0 @@ -import SwiftUI -import Perception - -struct SettingsSection: View { - let title: String - let showWarning: Bool - @ViewBuilder let content: () -> Content - @ViewBuilder let footer: () -> Footer - - - init(title: String, showWarning: Bool = false, @ViewBuilder content: @escaping () -> Content, @ViewBuilder footer: @escaping () -> Footer) { - self.title = title - self.showWarning = showWarning - self.content = content - self.footer = footer - } - - var body: some View { - WithPerceptionTracking{ - VStack(alignment: .leading, spacing: 10) { - Text(title) - .bold() - .padding(.horizontal, 10) - if showWarning { - HStack{ - Text("GitHub Copilot features are disabled. Please [check your subscription](https://github.com/settings/copilot) to access them.") - .foregroundColor(Color("WarningForegroundColor")) - .padding(4) - Spacer() - } - .background(Color("WarningBackgroundColor")) - .overlay( - RoundedRectangle(cornerRadius: 3) - .stroke(Color("WarningStrokeColor"), lineWidth: 1) - ) - } - VStack(alignment: .leading, spacing: 0) { - content() - } - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - footer() - } - .frame(maxWidth: .infinity, alignment: .leading) - } - } -} - -extension SettingsSection where Footer == EmptyView { - init(title: String, showWarning: Bool = false, @ViewBuilder content: @escaping () -> Content) { - self.init(title: title, showWarning: showWarning, content: content, footer: { EmptyView() }) - } -} - -#Preview { - VStack(spacing: 20) { - SettingsSection(title: "General") { - SettingsLink( - url: "https://github.com", title: "GitHub", subtitle: "footnote") - Divider() - SettingsToggle(title: "Example", isOn: .constant(true)) - Divider() - SettingsLink(url: "https://example.com", title: "Example") - } - SettingsSection(title: "Advanced", showWarning: true) { - SettingsLink(url: "https://example.com", title: "Example") - } footer: { - Text("Footer") - } - } - .padding() - .frame(width: 300) -} diff --git a/Core/Sources/HostApp/SharedComponents/SettingsTextField.swift b/Core/Sources/HostApp/SharedComponents/SettingsTextField.swift deleted file mode 100644 index ae135ee5..00000000 --- a/Core/Sources/HostApp/SharedComponents/SettingsTextField.swift +++ /dev/null @@ -1,69 +0,0 @@ -import SwiftUI - -struct SettingsTextField: View { - let title: String - let prompt: String - @Binding var text: String - let isSecure: Bool - - @State private var localText: String = "" - @State private var debounceTimer: Timer? - - var onDebouncedChange: ((String) -> Void)? - - init(title: String, prompt: String, text: Binding, isSecure: Bool = false, onDebouncedChange: ((String) -> Void)? = nil) { - self.title = title - self.prompt = prompt - self._text = text - self.isSecure = isSecure - self.onDebouncedChange = onDebouncedChange - self._localText = State(initialValue: text.wrappedValue) - } - - var body: some View { - Form { - Group { - if isSecure { - SecureField(text: $localText, prompt: Text(prompt)) { - Text(title) - } - } else { - TextField(text: $localText, prompt: Text(prompt)) { - Text(title) - } - } - } - .textFieldStyle(.plain) - .multilineTextAlignment(.trailing) - .onChange(of: localText) { newValue in - text = newValue - debounceTimer?.invalidate() - debounceTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in - onDebouncedChange?(newValue) - } - } - .onAppear { - localText = text - } - } - .padding(10) - } -} - -#Preview { - VStack(spacing: 10) { - SettingsTextField( - title: "Username", - prompt: "user", - text: .constant("") - ) - Divider() - SettingsTextField( - title: "Password", - prompt: "pass", - text: .constant(""), - isSecure: true - ) - } - .padding(.vertical, 10) -} diff --git a/Core/Sources/HostApp/SharedComponents/SettingsToggle.swift b/Core/Sources/HostApp/SharedComponents/SettingsToggle.swift deleted file mode 100644 index 5c51d21f..00000000 --- a/Core/Sources/HostApp/SharedComponents/SettingsToggle.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI - -struct SettingsToggle: View { - static let defaultPadding: CGFloat = 10 - - let title: String - let isOn: Binding - - var body: some View { - HStack(alignment: .center) { - Text(title) - Spacer() - Toggle(isOn: isOn) {} - .toggleStyle(.switch) - } - .padding(SettingsToggle.defaultPadding) - } -} - -#Preview { - SettingsToggle(title: "Test", isOn: .constant(true)) -} diff --git a/Core/Sources/HostApp/SidebarTabView.swift b/Core/Sources/HostApp/SidebarTabView.swift deleted file mode 100644 index ae5b009e..00000000 --- a/Core/Sources/HostApp/SidebarTabView.swift +++ /dev/null @@ -1,154 +0,0 @@ -import SwiftUI - -private struct SidebarItem: Identifiable, Equatable { - var id: Int { tag } - var tag: Int - var title: String - var subtitle: String? = nil - var image: String? = nil -} - -private struct SidebarItemPreferenceKey: PreferenceKey { - static var defaultValue: [SidebarItem] = [] - static func reduce(value: inout [SidebarItem], nextValue: () -> [SidebarItem]) { - value.append(contentsOf: nextValue()) - } -} - -private struct SidebarTabTagKey: EnvironmentKey { - static var defaultValue: Int = 0 -} - -private extension EnvironmentValues { - var sidebarTabTag: Int { - get { self[SidebarTabTagKey.self] } - set { self[SidebarTabTagKey.self] = newValue } - } -} - -private struct SidebarTabViewWrapper: View { - @Environment(\.sidebarTabTag) var sidebarTabTag - var tag: Int - var title: String - var subtitle: String? = nil - var image: String? = nil - var content: () -> Content - - var body: some View { - Group { - if tag == sidebarTabTag { - content() - } else { - Color.clear - } - } - .preference( - key: SidebarItemPreferenceKey.self, - value: [.init(tag: tag, title: title, subtitle: subtitle, image: image)] - ) - } -} - -extension View { - func sidebarItem( - tag: Int, - title: String, - subtitle: String? = nil, - image: String? = nil - ) -> some View { - SidebarTabViewWrapper( - tag: tag, - title: title, - subtitle: subtitle, - image: image, - content: { self } - ) - } -} - -struct SidebarTabView: View { - @State private var sidebarItems = [SidebarItem]() - @Binding var tag: Int - @ViewBuilder var views: () -> Content - var body: some View { - HStack(spacing: 0) { - ScrollView { - VStack(alignment: .leading) { - ForEach(sidebarItems) { item in - Button(action: { - tag = item.tag - }) { - HStack { - if let image = item.image { - Image(systemName: image) - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - } - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .foregroundStyle(.primary) - if let subtitle = item.subtitle { - Text(subtitle) - .lineSpacing(0) - .font(.caption) - .foregroundStyle(.secondary) - .opacity(0.5) - .multilineTextAlignment(.leading) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background( - Color.primary.opacity(tag == item.tag ? 0.1 : 0), - in: RoundedRectangle(cornerRadius: 4) - ) - .padding(.horizontal, 8) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - } - .frame(width: 200) - .padding(.vertical, 8) - } - .background(Color.primary.opacity(0.05)) - - Divider() - - ZStack(alignment: .topLeading) { - views() - } - } - .environment(\.sidebarTabTag, tag) - .onPreferenceChange(SidebarItemPreferenceKey.self) { items in - sidebarItems = items - } - } -} - -struct SidebarTabView_Previews: PreviewProvider { - static var previews: some View { - SidebarTabView(tag: .constant(0)) { - Color.red.sidebarItem( - tag: 0, - title: "Hello", - subtitle: "Meow\nMeow", - image: "person.circle.fill" - ) - Color.blue.sidebarItem( - tag: 1, - title: "World", - image: "person.circle.fill" - ) - Color.blue.sidebarItem( - tag: 3, - title: "Pikachu", - image: "person.circle.fill" - ) - } - } -} - diff --git a/Core/Sources/HostApp/TabContainer.swift b/Core/Sources/HostApp/TabContainer.swift deleted file mode 100644 index 0aa3b008..00000000 --- a/Core/Sources/HostApp/TabContainer.swift +++ /dev/null @@ -1,300 +0,0 @@ -import ComposableArchitecture -import Dependencies -import Foundation -import LaunchAgentManager -import SwiftUI -import Toast -import UpdateChecker -import Client -import Logger -import Combine - -@MainActor -public let hostAppStore: StoreOf = .init(initialState: .init(), reducer: { HostApp() }) - -public struct TabContainer: View { - let store: StoreOf - @ObservedObject var toastController: ToastController - @State private var tabBarItems = [TabBarItem]() - @State private var isAgentModeFFEnabled = true - @Binding var tag: Int - - public init() { - toastController = ToastControllerDependencyKey.liveValue - store = hostAppStore - _tag = Binding( - get: { hostAppStore.state.activeTabIndex }, - set: { hostAppStore.send(.setActiveTab($0)) } - ) - } - - init(store: StoreOf, toastController: ToastController) { - self.store = store - self.toastController = toastController - _tag = Binding( - get: { store.state.activeTabIndex }, - set: { store.send(.setActiveTab($0)) } - ) - } - - private func updateAgentModeFeatureFlag() async { - do { - let service = try getService() - let featureFlags = try await service.getCopilotFeatureFlags() - isAgentModeFFEnabled = featureFlags?.agentMode ?? true - if hostAppStore.activeTabIndex == 2 && !isAgentModeFFEnabled { - hostAppStore.send(.setActiveTab(0)) - } - } catch { - Logger.client.error("Failed to get copilot feature flags: \(error)") - } - } - - public var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - TabBar(tag: $tag, tabBarItems: tabBarItems) - .padding(.bottom, 8) - ZStack(alignment: .center) { - GeneralView(store: store.scope(state: \.general, action: \.general)) - .tabBarItem( - tag: 0, - title: "General", - image: "CopilotLogo", - isSystemImage: false - ) - AdvancedSettings().tabBarItem( - tag: 1, - title: "Advanced", - image: "gearshape.2.fill" - ) - if isAgentModeFFEnabled { - MCPConfigView().tabBarItem( - tag: 2, - title: "MCP", - image: "wrench.and.screwdriver.fill" - ) - } - } - .environment(\.tabBarTabTag, tag) - .frame(minHeight: 400) - } - .focusable(false) - .padding(.top, 8) - .background(.ultraThinMaterial.opacity(0.01)) - .background(Color(nsColor: .controlBackgroundColor).opacity(0.4)) - .handleToast() - .onPreferenceChange(TabBarItemPreferenceKey.self) { items in - tabBarItems = items - } - .onAppear { - store.send(.appear) - Task { - await updateAgentModeFeatureFlag() - } - } - .onReceive(DistributedNotificationCenter.default() - .publisher(for: .gitHubCopilotFeatureFlagsDidChange)) { _ in - Task { - await updateAgentModeFeatureFlag() - } - } - } - } -} - -struct TabBar: View { - @Binding var tag: Int - fileprivate var tabBarItems: [TabBarItem] - - var body: some View { - HStack { - ForEach(tabBarItems) { tab in - TabBarButton( - currentTag: $tag, - tag: tab.tag, - title: tab.title, - image: tab.image, - isSystemImage: tab.isSystemImage - ) - } - } - } -} - -struct TabBarButton: View { - @Binding var currentTag: Int - @State var isHovered = false - var tag: Int - var title: String - var image: String - var isSystemImage: Bool = true - - private var tabImage: Image { - isSystemImage ? Image(systemName: image) : Image(image) - } - - private var isSelected: Bool { - tag == currentTag - } - - var body: some View { - Button(action: { - self.currentTag = tag - }) { - VStack(spacing: 2) { - tabImage - .renderingMode(.template) - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - Text(title) - } - .foregroundColor(isSelected ? .blue : .gray) - .font(.body) - .padding(.horizontal, 12) - .padding(.vertical, 4) - .padding(.top, 4) - .background( - tag == currentTag - ? Color(nsColor: .textColor).opacity(0.1) - : Color.clear, - in: RoundedRectangle(cornerRadius: 8) - ) - .background( - isHovered - ? Color(nsColor: .textColor).opacity(0.05) - : Color.clear, - in: RoundedRectangle(cornerRadius: 8) - ) - } - .onHover(perform: { yes in - isHovered = yes - }) - .buttonStyle(.borderless) - } -} - -private struct TabBarTabViewWrapper: View { - @Environment(\.tabBarTabTag) var tabBarTabTag - var tag: Int - var title: String - var image: String - var isSystemImage: Bool = true - var content: () -> Content - - var body: some View { - Group { - if tag == tabBarTabTag { - content() - } else { - Color.clear - } - } - .preference( - key: TabBarItemPreferenceKey.self, - value: [.init(tag: tag, title: title, image: image, isSystemImage: isSystemImage)] - ) - } -} - -private extension View { - func tabBarItem( - tag: Int, - title: String, - image: String, - isSystemImage: Bool = true - ) -> some View { - TabBarTabViewWrapper( - tag: tag, - title: title, - image: image, - isSystemImage: isSystemImage, - content: { self } - ) - } -} - -private struct TabBarItem: Identifiable, Equatable { - var id: Int { tag } - var tag: Int - var title: String - var image: String - var isSystemImage: Bool = true -} - -private struct TabBarItemPreferenceKey: PreferenceKey { - static var defaultValue: [TabBarItem] = [] - static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) { - value.append(contentsOf: nextValue()) - } -} - -private struct TabBarTabTagKey: EnvironmentKey { - static var defaultValue: Int = 0 -} - -private extension EnvironmentValues { - var tabBarTabTag: Int { - get { self[TabBarTabTagKey.self] } - set { self[TabBarTabTagKey.self] = newValue } - } -} - -struct UpdateCheckerKey: EnvironmentKey { - static var defaultValue: UpdateCheckerProtocol = NoopUpdateChecker() -} - -public extension EnvironmentValues { - var updateChecker: UpdateCheckerProtocol { - get { self[UpdateCheckerKey.self] } - set { self[UpdateCheckerKey.self] = newValue } - } -} - -// MARK: - Previews - -struct TabContainer_Previews: PreviewProvider { - static var previews: some View { - TabContainer() - .frame(width: 800) - } -} - -struct TabContainer_Toasts_Previews: PreviewProvider { - static var previews: some View { - TabContainer( - store: .init(initialState: .init(), reducer: { HostApp() }), - toastController: .init(messages: [ - .init(id: UUID(), level: .info, content: Text("info")), - .init(id: UUID(), level: .error, content: Text("error")), - .init(id: UUID(), level: .warning, content: Text("warning")), - ]) - ) - .frame(width: 800) - } -} - -@available(macOS 14.0, *) -@MainActor -public struct SettingsEnvironment: View { - @Environment(\.openSettings) public var openSettings: OpenSettingsAction - - public init() {} - - public var body: some View { - EmptyView().onAppear { - openSettings() - } - } - - public func open() { - let controller = NSHostingController(rootView: self) - let window = NSWindow(contentViewController: controller) - window.orderFront(nil) - // Close the temporary window after settings are opened - DispatchQueue.main.async { - window.close() - } - } -} diff --git a/Core/Sources/KeyBindingManager/KeyBindingManager.swift b/Core/Sources/KeyBindingManager/KeyBindingManager.swift deleted file mode 100644 index 2fcf67fa..00000000 --- a/Core/Sources/KeyBindingManager/KeyBindingManager.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import Workspace -public final class KeyBindingManager { - let tabToAcceptSuggestion: TabToAcceptSuggestion - public init( - workspacePool: WorkspacePool, - acceptSuggestion: @escaping () -> Void, - expandSuggestion: @escaping () -> Void, - collapseSuggestion: @escaping () -> Void, - dismissSuggestion: @escaping () -> Void - ) { - tabToAcceptSuggestion = .init( - workspacePool: workspacePool, - acceptSuggestion: acceptSuggestion, - dismissSuggestion: dismissSuggestion, - expandSuggestion: expandSuggestion, - collapseSuggestion: collapseSuggestion - ) - } - - public func start() { - tabToAcceptSuggestion.start() - } - - @MainActor - public func stopForExit() { - tabToAcceptSuggestion.stopForExit() - } -} diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift deleted file mode 100644 index f2d4c147..00000000 --- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift +++ /dev/null @@ -1,213 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import CGEventOverride -import Foundation -import Logger -import Preferences -import SuggestionBasic -import UserDefaultsObserver -import Workspace -import XcodeInspector - -final class TabToAcceptSuggestion { - let hook: CGEventHookType = CGEventHook(eventsOfInterest: [.keyDown]) { message in - Logger.service.debug("TabToAcceptSuggestion: \(message)") - } - - let workspacePool: WorkspacePool - let acceptSuggestion: () -> Void - let expandSuggestion: () -> Void - let collapseSuggestion: () -> Void - let dismissSuggestion: () -> Void - private var modifierEventMonitor: Any? - private let userDefaultsObserver = UserDefaultsObserver( - object: UserDefaults.shared, forKeyPaths: [ - UserDefaultPreferenceKeys().acceptSuggestionWithTab.key, - UserDefaultPreferenceKeys().dismissSuggestionWithEsc.key, - ], context: nil - ) - private var stoppedForExit = false - - struct ObservationKey: Hashable {} - - var canTapToAcceptSuggestion: Bool { - UserDefaults.shared.value(for: \.acceptSuggestionWithTab) - } - - var canEscToDismissSuggestion: Bool { - UserDefaults.shared.value(for: \.dismissSuggestionWithEsc) - } - - @MainActor - func stopForExit() { - stoppedForExit = true - stopObservation() - } - - init( - workspacePool: WorkspacePool, - acceptSuggestion: @escaping () -> Void, - dismissSuggestion: @escaping () -> Void, - expandSuggestion: @escaping () -> Void, - collapseSuggestion: @escaping () -> Void - ) { - _ = ThreadSafeAccessToXcodeInspector.shared - self.workspacePool = workspacePool - self.acceptSuggestion = acceptSuggestion - self.dismissSuggestion = dismissSuggestion - self.expandSuggestion = expandSuggestion - self.collapseSuggestion = collapseSuggestion - - hook.add( - .init( - eventsOfInterest: [.keyDown], - convert: { [weak self] _, _, event in - self?.handleEvent(event) ?? .unchanged - } - ), - forKey: ObservationKey() - ) - } - - func start() { - Task { [weak self] in - for await _ in ActiveApplicationMonitor.shared.createInfoStream() { - guard let self else { return } - try Task.checkCancellation() - Task { @MainActor in - if ActiveApplicationMonitor.shared.activeXcode != nil { - self.startObservation() - } else { - self.stopObservation() - } - } - } - } - - userDefaultsObserver.onChange = { [weak self] in - guard let self else { return } - Task { @MainActor in - if self.canTapToAcceptSuggestion { - self.startObservation() - } else { - self.stopObservation() - } - } - } - } - - @MainActor - func startObservation() { - guard !stoppedForExit else { return } - guard canTapToAcceptSuggestion else { return } - hook.activateIfPossible() - removeMonitor() - modifierEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in - self?.handleModifierEvents(event: event) - } - } - - @MainActor - func stopObservation() { - hook.deactivate() - removeMonitor() - } - - private func removeMonitor() { - if let monitor = modifierEventMonitor { - NSEvent.removeMonitor(monitor) - modifierEventMonitor = nil - } - } - - func handleEvent(_ event: CGEvent) -> CGEventManipulation.Result { - let (accept, reason) = Self.shouldAcceptSuggestion( - event: event, - workspacePool: workspacePool, - xcodeInspector: ThreadSafeAccessToXcodeInspector.shared - ) - if let reason = reason { - Logger.service.debug("TabToAcceptSuggestion: \(accept ? "" : "not") accepting due to: \(reason)") - } - if accept { - acceptSuggestion() - return .discarded - } - return .unchanged - } - - func handleModifierEvents(event: NSEvent) { - if event.modifierFlags.contains(NSEvent.ModifierFlags.option) { - expandSuggestion() - } else { - collapseSuggestion() - } - } -} - -extension TabToAcceptSuggestion { - /// Returns whether a given keyboard event should be intercepted and trigger - /// accepting a suggestion. - static func shouldAcceptSuggestion( - event: CGEvent, - workspacePool: WorkspacePool, - xcodeInspector: ThreadSafeAccessToXcodeInspectorProtocol - ) -> (accept: Bool, reason: String?) { - let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode)) - let tab = 48 - guard keycode == tab else { return (false, nil) } - if event.flags.contains(.maskHelp) { return (false, nil) } - if event.flags.contains(.maskShift) { return (false, nil) } - if event.flags.contains(.maskControl) { return (false, nil) } - if event.flags.contains(.maskCommand) { return (false, nil) } - guard xcodeInspector.hasActiveXcode else { - return (false, "No active Xcode") - } - guard xcodeInspector.hasFocusedEditor else { - return (false, "No focused editor") - } - guard let fileURL = xcodeInspector.activeDocumentURL else { - return (false, "No active document") - } - guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) else { - return (false, "No filespace") - } - if filespace.presentingSuggestion == nil { - return (false, "No suggestion") - } - return (true, nil) - } -} - -import Combine - -protocol ThreadSafeAccessToXcodeInspectorProtocol { - var activeDocumentURL: URL? {get} - var hasActiveXcode: Bool {get} - var hasFocusedEditor: Bool {get} -} - -private class ThreadSafeAccessToXcodeInspector: ThreadSafeAccessToXcodeInspectorProtocol { - static let shared = ThreadSafeAccessToXcodeInspector() - - private(set) var activeDocumentURL: URL? - private(set) var hasActiveXcode = false - private(set) var hasFocusedEditor = false - private var cancellable: Set = [] - - init() { - let inspector = XcodeInspector.shared - - inspector.$activeDocumentURL.receive(on: DispatchQueue.main).sink { [weak self] newValue in - self?.activeDocumentURL = newValue - }.store(in: &cancellable) - - inspector.$activeXcode.receive(on: DispatchQueue.main).sink { [weak self] newValue in - self?.hasActiveXcode = newValue != nil - }.store(in: &cancellable) - - inspector.$focusedEditor.receive(on: DispatchQueue.main).sink { [weak self] newValue in - self?.hasFocusedEditor = newValue != nil - }.store(in: &cancellable) - } -} diff --git a/Core/Sources/LaunchAgentManager/LaunchAgentManager.swift b/Core/Sources/LaunchAgentManager/LaunchAgentManager.swift deleted file mode 100644 index c311439d..00000000 --- a/Core/Sources/LaunchAgentManager/LaunchAgentManager.swift +++ /dev/null @@ -1,187 +0,0 @@ -import Foundation -import Logger -import ServiceManagement - -public struct LaunchAgentManager { - let lastLaunchAgentVersionKey = "LastLaunchAgentVersion" - let serviceIdentifier: String - let executablePath: String - let bundleIdentifier: String - - var launchAgentDirURL: URL { - FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/LaunchAgents") - } - - var launchAgentPath: String { - launchAgentDirURL.appendingPathComponent("\(serviceIdentifier).plist").path - } - - public init(serviceIdentifier: String, executablePath: String, bundleIdentifier: String) { - self.serviceIdentifier = serviceIdentifier - self.executablePath = executablePath - self.bundleIdentifier = bundleIdentifier - } - - public func setupLaunchAgentForTheFirstTimeIfNeeded() async throws { - if #available(macOS 13, *) { - await removeObsoleteLaunchAgent() - try await setupLaunchAgent() - } else { - guard !FileManager.default.fileExists(atPath: launchAgentPath) else { return } - try await setupLaunchAgent() - await removeObsoleteLaunchAgent() - } - } - - @available(macOS 13.0, *) - public func isBackgroundPermissionGranted() async -> Bool { - // On macOS 13+, check SMAppService status - let bridgeLaunchAgent = SMAppService.agent(plistName: "bridgeLaunchAgent.plist") - let status = bridgeLaunchAgent.status - return status != .requiresApproval - } - - public func setupLaunchAgent() async throws { - if #available(macOS 13, *) { - Logger.client.info("Registering bridge launch agent") - let bridgeLaunchAgent = SMAppService.agent(plistName: "bridgeLaunchAgent.plist") - try bridgeLaunchAgent.register() - } else { - Logger.client.info("Creating and loading bridge launch agent") - let content = """ - - - - - Label - \(serviceIdentifier) - Program - \(executablePath) - MachServices - - \(serviceIdentifier) - - - AssociatedBundleIdentifiers - - \(bundleIdentifier) - \(serviceIdentifier) - - - - """ - if !FileManager.default.fileExists(atPath: launchAgentDirURL.path) { - try FileManager.default.createDirectory( - at: launchAgentDirURL, - withIntermediateDirectories: false - ) - } - FileManager.default.createFile( - atPath: launchAgentPath, - contents: content.data(using: .utf8) - ) - try await launchctl("load", launchAgentPath) - } - - let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - UserDefaults.standard.set(buildNumber, forKey: lastLaunchAgentVersionKey) - } - - public func removeLaunchAgent() async throws { - if #available(macOS 13, *) { - Logger.client.info("Unregistering bridge launch agent") - let bridgeLaunchAgent = SMAppService.agent(plistName: "bridgeLaunchAgent.plist") - try await bridgeLaunchAgent.unregister() - } else { - Logger.client.info("Unloading and removing bridge launch agent") - try await launchctl("unload", launchAgentPath) - try FileManager.default.removeItem(atPath: launchAgentPath) - } - } - - public func reloadLaunchAgent() async throws { - if #unavailable(macOS 13) { - Logger.client.info("Reloading bridge launch agent") - try await helper("reload-launch-agent", "--service-identifier", serviceIdentifier) - } - } - - public func removeObsoleteLaunchAgent() async { - if #available(macOS 13, *) { - let path = launchAgentPath - if FileManager.default.fileExists(atPath: path) { - Logger.client.info("Unloading and removing old bridge launch agent") - try? await launchctl("unload", path) - try? FileManager.default.removeItem(atPath: path) - } - } else { - let path = launchAgentPath.replacingOccurrences( - of: "ExtensionService", - with: "XPCService" - ) - if FileManager.default.fileExists(atPath: path) { - Logger.client.info("Removing old bridge launch agent plist") - try? FileManager.default.removeItem(atPath: path) - } - } - } -} - -private func process(_ launchPath: String, _ args: [String]) async throws { - let task = Process() - task.launchPath = launchPath - task.arguments = args - task.environment = [ - "PATH": "/usr/bin", - ] - let outpipe = Pipe() - task.standardOutput = outpipe - - return try await withUnsafeThrowingContinuation { continuation in - do { - task.terminationHandler = { process in - do { - if process.terminationStatus == 0 { - continuation.resume(returning: ()) - } else { - if let data = try? outpipe.fileHandleForReading.readToEnd(), - let content = String(data: data, encoding: .utf8) - { - continuation.resume(throwing: E(errorDescription: content)) - } else { - continuation.resume( - throwing: E( - errorDescription: "Unknown error." - ) - ) - } - } - } - } - try task.run() - } catch { - continuation.resume(throwing: error) - } - } -} - -private func helper(_ args: String...) async throws { - // TODO: A more robust way to locate the executable. - guard let url = Bundle.main.executableURL? - .deletingLastPathComponent() - .deletingLastPathComponent() - .appendingPathComponent("Applications") - .appendingPathComponent("Helper") - else { throw E(errorDescription: "Unable to locate Helper.") } - return try await process(url.path, args) -} - -private func launchctl(_ args: String...) async throws { - return try await process("/bin/launchctl", args) -} - -struct E: Error, LocalizedError { - var errorDescription: String? -} - diff --git a/Core/Sources/PersistMiddleware/Extensions/ChatMessage+Storage.swift b/Core/Sources/PersistMiddleware/Extensions/ChatMessage+Storage.swift deleted file mode 100644 index 77d91bb0..00000000 --- a/Core/Sources/PersistMiddleware/Extensions/ChatMessage+Storage.swift +++ /dev/null @@ -1,117 +0,0 @@ -import Foundation -import ChatAPIService -import Persist -import Logger -import ConversationServiceProvider - -extension ChatMessage { - - struct TurnItemData: Codable { - var content: String - var rating: ConversationRating - var references: [ConversationReference] - var followUp: ConversationFollowUp? - var suggestedTitle: String? - var errorMessages: [String] = [] - var steps: [ConversationProgressStep] - var editAgentRounds: [AgentRound] - var panelMessages: [CopilotShowMessageParams] - - // Custom decoder to provide default value for steps - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - content = try container.decode(String.self, forKey: .content) - rating = try container.decode(ConversationRating.self, forKey: .rating) - references = try container.decode([ConversationReference].self, forKey: .references) - followUp = try container.decodeIfPresent(ConversationFollowUp.self, forKey: .followUp) - suggestedTitle = try container.decodeIfPresent(String.self, forKey: .suggestedTitle) - errorMessages = try container.decodeIfPresent([String].self, forKey: .errorMessages) ?? [] - steps = try container.decodeIfPresent([ConversationProgressStep].self, forKey: .steps) ?? [] - editAgentRounds = try container.decodeIfPresent([AgentRound].self, forKey: .editAgentRounds) ?? [] - panelMessages = try container.decodeIfPresent([CopilotShowMessageParams].self, forKey: .panelMessages) ?? [] - } - - // Default memberwise init for encoding - init( - content: String, - rating: ConversationRating, - references: [ConversationReference], - followUp: ConversationFollowUp?, - suggestedTitle: String?, - errorMessages: [String] = [], - steps: [ConversationProgressStep]?, - editAgentRounds: [AgentRound]? = nil, - panelMessages: [CopilotShowMessageParams]? = nil - ) { - self.content = content - self.rating = rating - self.references = references - self.followUp = followUp - self.suggestedTitle = suggestedTitle - self.errorMessages = errorMessages - self.steps = steps ?? [] - self.editAgentRounds = editAgentRounds ?? [] - self.panelMessages = panelMessages ?? [] - } - } - - func toTurnItem() -> TurnItem { - let turnItemData = TurnItemData( - content: self.content, - rating: self.rating, - references: self.references, - followUp: self.followUp, - suggestedTitle: self.suggestedTitle, - errorMessages: self.errorMessages, - steps: self.steps, - editAgentRounds: self.editAgentRounds, - panelMessages: self.panelMessages - ) - - // TODO: handle exception - let encoder = JSONEncoder() - let encodeData = (try? encoder.encode(turnItemData)) ?? Data() - let data = String(data: encodeData, encoding: .utf8) ?? "{}" - - return TurnItem(id: self.id, conversationID: self.chatTabID, CLSTurnID: self.clsTurnID, role: role.rawValue, data: data, createdAt: self.createdAt, updatedAt: self.updatedAt) - } - - static func from(_ turnItem: TurnItem) -> ChatMessage? { - var chatMessage: ChatMessage? = nil - - do { - if let jsonData = turnItem.data.data(using: .utf8) { - let decoder = JSONDecoder() - let turnItemData = try decoder.decode(TurnItemData.self, from: jsonData) - - chatMessage = .init( - id: turnItem.id, - chatTabID: turnItem.conversationID, - clsTurnID: turnItem.CLSTurnID, - role: ChatMessage.Role(rawValue: turnItem.role)!, - content: turnItemData.content, - references: turnItemData.references, - followUp: turnItemData.followUp, - suggestedTitle: turnItemData.suggestedTitle, - errorMessages: turnItemData.errorMessages, - rating: turnItemData.rating, - steps: turnItemData.steps, - editAgentRounds: turnItemData.editAgentRounds, - panelMessages: turnItemData.panelMessages, - createdAt: turnItem.createdAt, - updatedAt: turnItem.updatedAt - ) - } - } catch { - Logger.client.error("Failed to restore chat message: \(error)") - } - - return chatMessage - } -} - -extension Array where Element == ChatMessage { - func toTurnItems() -> [TurnItem] { - return self.map { $0.toTurnItem() } - } -} diff --git a/Core/Sources/PersistMiddleware/Extensions/ChatTabInfo+Storage.swift b/Core/Sources/PersistMiddleware/Extensions/ChatTabInfo+Storage.swift deleted file mode 100644 index f642cb71..00000000 --- a/Core/Sources/PersistMiddleware/Extensions/ChatTabInfo+Storage.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import ChatTab -import Persist -import Logger - -extension ChatTabInfo { - - func toConversationItem() -> ConversationItem { - // Currently, no additional data to store. - let data = "{}" - - return ConversationItem(id: self.id, title: self.title, isSelected: self.isSelected, CLSConversationID: self.CLSConversationID, data: data, createdAt: self.createdAt, updatedAt: self.updatedAt) - } - - static func from(_ conversationItem: ConversationItem, with metadata: StorageMetadata) -> ChatTabInfo? { - var chatTabInfo: ChatTabInfo? = nil - - chatTabInfo = .init( - id: conversationItem.id, - title: conversationItem.title, - isSelected: conversationItem.isSelected, - CLSConversationID: conversationItem.CLSConversationID, - createdAt: conversationItem.createdAt, - updatedAt: conversationItem.updatedAt, - workspacePath: metadata.workspacePath, - username: metadata.username) - - return chatTabInfo - } -} - - -extension Array where Element == ChatTabInfo { - func toConversationItems() -> [ConversationItem] { - return self.map { $0.toConversationItem() } - } -} - -extension ChatTabPreviewInfo { - static func from(_ conversationPreviewItem: ConversationPreviewItem) -> ChatTabPreviewInfo { - return .init( - id: conversationPreviewItem.id, - title: conversationPreviewItem.title, - isSelected: conversationPreviewItem.isSelected, - updatedAt: conversationPreviewItem.updatedAt - ) - } -} diff --git a/Core/Sources/PersistMiddleware/Stores/ChatMessageStore.swift b/Core/Sources/PersistMiddleware/Stores/ChatMessageStore.swift deleted file mode 100644 index f3061006..00000000 --- a/Core/Sources/PersistMiddleware/Stores/ChatMessageStore.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Persist -import ChatAPIService - -public struct ChatMessageStore { - public static func save(_ chatMessage: ChatMessage, with metadata: StorageMetadata) { - let turnItem = chatMessage.toTurnItem() - ConversationStorageService.shared.operate( - OperationRequest([.upsertTurn([turnItem])]), - metadata: metadata) - } - - public static func delete(by id: String, with metadata: StorageMetadata) { - ConversationStorageService.shared.operate( - OperationRequest([.delete([.turn(id: id)])]), metadata: metadata) - } - - public static func deleteAll(by ids: [String], with metadata: StorageMetadata) { - ConversationStorageService.shared.operate( - OperationRequest([.delete(ids.map { .turn(id: $0)})]), metadata: metadata) - } - - public static func getAll(by conversationID: String, metadata: StorageMetadata) -> [ChatMessage] { - var chatMessages: [ChatMessage] = [] - - let turnItems = ConversationStorageService.shared.fetchTurnItems(for: conversationID, metadata: metadata) - if turnItems.count > 0 { - chatMessages = turnItems.compactMap { ChatMessage.from($0) } - } - - return chatMessages - } -} diff --git a/Core/Sources/PersistMiddleware/Stores/ChatTabInfoStore.swift b/Core/Sources/PersistMiddleware/Stores/ChatTabInfoStore.swift deleted file mode 100644 index da9bccd3..00000000 --- a/Core/Sources/PersistMiddleware/Stores/ChatTabInfoStore.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Persist -import ChatTab - -public struct ChatTabInfoStore { - public static func saveAll(_ chatTabInfos: [ChatTabInfo], with metadata: StorageMetadata) { - let conversationItems = chatTabInfos.toConversationItems() - ConversationStorageService.shared.operate( - OperationRequest([.upsertConversation(conversationItems)]), metadata: metadata) - } - - public static func delete(by id: String, with metadata: StorageMetadata) { - ConversationStorageService.shared.operate( - OperationRequest( - [.delete([.conversation(id: id), .turnByConversationID(conversationID: id)])]), - metadata: metadata) - } - - public static func getAll(with metadata: StorageMetadata) -> [ChatTabInfo] { - return fetchChatTabInfos(.all, metadata: metadata) - } - - public static func getSelected(with metadata: StorageMetadata) -> ChatTabInfo? { - return fetchChatTabInfos(.selected, metadata: metadata).first - } - - public static func getLatest(with metadata: StorageMetadata) -> ChatTabInfo? { - return fetchChatTabInfos(.latest, metadata: metadata).first - } - - public static func getByID(_ id: String, with metadata: StorageMetadata) -> ChatTabInfo? { - return fetchChatTabInfos(.id(id), metadata: metadata).first - } - - private static func fetchChatTabInfos(_ type: ConversationFetchType, metadata: StorageMetadata) -> [ChatTabInfo] { - let items = ConversationStorageService.shared.fetchConversationItems(type, metadata: metadata) - - return items.compactMap { ChatTabInfo.from($0, with: metadata) } - } -} - -public struct ChatTabPreviewInfoStore { - public static func getAll(with metadata: StorageMetadata) -> [ChatTabPreviewInfo] { - var previewInfos: [ChatTabPreviewInfo] = [] - - let conversationPreviewItems = ConversationStorageService.shared.fetchConversationPreviewItems(metadata: metadata) - if conversationPreviewItems.count > 0 { - previewInfos = conversationPreviewItems.compactMap { ChatTabPreviewInfo.from($0) } - } - - return previewInfos - } -} diff --git a/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift b/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift deleted file mode 100644 index c6062ec7..00000000 --- a/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import SuggestionBasic - -public final class PreviewPromptToCodeService: PromptToCodeServiceType { - public init() {} - - public func modifyCode( - code: String, - requirement: String, - source: PromptToCodeSource, - isDetached: Bool, - extraSystemPrompt: String?, - generateDescriptionRequirement: Bool? - ) async throws -> AsyncThrowingStream<(code: String, description: String), Error> { - return AsyncThrowingStream { continuation in - Task { - let code = """ - struct Cat { - var name: String - } - - print("Hello world!") - """ - let description = "I have created a struct `Cat`." - var resultCode = "" - var resultDescription = "" - do { - for character in code { - try await Task.sleep(nanoseconds: 50_000_000) - resultCode.append(character) - continuation.yield((resultCode, resultDescription)) - } - for character in description { - try await Task.sleep(nanoseconds: 50_000_000) - resultDescription.append(character) - continuation.yield((resultCode, resultDescription)) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - } - } - - public func stopResponding() {} -} - diff --git a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift deleted file mode 100644 index 5967e318..00000000 --- a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Dependencies -import Foundation -import SuggestionBasic - -public protocol PromptToCodeServiceType { - func modifyCode( - code: String, - requirement: String, - source: PromptToCodeSource, - isDetached: Bool, - extraSystemPrompt: String?, - generateDescriptionRequirement: Bool? - ) async throws -> AsyncThrowingStream<(code: String, description: String), Error> - - func stopResponding() -} - -public struct PromptToCodeSource { - public var language: CodeLanguage - public var documentURL: URL - public var projectRootURL: URL - public var content: String - public var lines: [String] - public var range: CursorRange - - public init( - language: CodeLanguage, - documentURL: URL, - projectRootURL: URL, - content: String, - lines: [String], - range: CursorRange - ) { - self.language = language - self.documentURL = documentURL - self.projectRootURL = projectRootURL - self.content = content - self.lines = lines - self.range = range - } -} - -public struct PromptToCodeServiceDependencyKey: DependencyKey { - public static let liveValue: PromptToCodeServiceType = PreviewPromptToCodeService() - public static let previewValue: PromptToCodeServiceType = PreviewPromptToCodeService() -} - -public extension DependencyValues { - var promptToCodeService: PromptToCodeServiceType { - get { self[PromptToCodeServiceDependencyKey.self] } - set { self[PromptToCodeServiceDependencyKey.self] = newValue } - } - - var promptToCodeServiceFactory: () -> PromptToCodeServiceType { - get { self[PromptToCodeServiceFactoryDependencyKey.self] } - set { self[PromptToCodeServiceFactoryDependencyKey.self] = newValue } - } -} - -#if canImport(ContextAwarePromptToCodeService) - -import ContextAwarePromptToCodeService - -extension ContextAwarePromptToCodeService: PromptToCodeServiceType { - public func modifyCode( - code: String, - requirement: String, - source: PromptToCodeSource, - isDetached: Bool, - extraSystemPrompt: String?, - generateDescriptionRequirement: Bool? - ) async throws -> AsyncThrowingStream<(code: String, description: String), Error> { - try await modifyCode( - code: code, - requirement: requirement, - source: ContextAwarePromptToCodeService.Source( - language: source.language, - documentURL: source.documentURL, - projectRootURL: source.projectRootURL, - content: source.content, - lines: source.lines, - range: source.range - ), - isDetached: isDetached, - extraSystemPrompt: extraSystemPrompt, - generateDescriptionRequirement: generateDescriptionRequirement - ) - } -} - -public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { - public static let liveValue: () -> PromptToCodeServiceType = { - ContextAwarePromptToCodeService() - } - - public static let previewValue: () -> PromptToCodeServiceType = { - PreviewPromptToCodeService() - } -} - -#else - -public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { - public static let liveValue: () -> PromptToCodeServiceType = { - PreviewPromptToCodeService() - } - - public static let previewValue: () -> PromptToCodeServiceType = { - PreviewPromptToCodeService() - } -} - -#endif - diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift deleted file mode 100644 index 6a1ace89..00000000 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ /dev/null @@ -1,27 +0,0 @@ -import ConversationTab -import ChatService -import ChatTab -import Foundation -import PromptToCodeService -import SuggestionBasic -import SuggestionWidget -import XcodeInspector - -enum ChatTabFactory { - static func chatTabBuilderCollection() -> [ChatTabBuilderCollection] { - func folderIfNeeded( - _ builders: [any ChatTabBuilder], - title: String - ) -> ChatTabBuilderCollection? { - if builders.count > 1 { - return .folder(title: title, kinds: builders.map(ChatTabKind.init)) - } - if let first = builders.first { return .kind(ChatTabKind(first)) } - return nil - } - - return [ - folderIfNeeded(ConversationTab.chatBuilders(), title: ConversationTab.name), - ].compactMap { $0 } - } -} diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift deleted file mode 100644 index 117977b9..00000000 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ /dev/null @@ -1,516 +0,0 @@ -import ActiveApplicationMonitor -import AppActivator -import AppKit -import ConversationTab -import ChatTab -import ComposableArchitecture -import Dependencies -import Preferences -import SuggestionBasic -import SuggestionWidget -import PersistMiddleware -import ChatService -import Persist - -#if canImport(ChatTabPersistent) -import ChatTabPersistent -#endif - -@Reducer -struct GUI { - @ObservableState - struct State: Equatable { - var suggestionWidgetState = WidgetFeature.State() - - var chatHistory: ChatHistory { - get { suggestionWidgetState.chatPanelState.chatHistory } - set { suggestionWidgetState.chatPanelState.chatHistory = newValue } - } - - var promptToCodeGroup: PromptToCodeGroup.State { - get { suggestionWidgetState.panelState.content.promptToCodeGroup } - set { suggestionWidgetState.panelState.content.promptToCodeGroup = newValue } - } - } - - enum Action { - case start - case openChatPanel(forceDetach: Bool) - case createAndSwitchToChatTabIfNeeded -// case createAndSwitchToBrowserTabIfNeeded(url: URL) - case sendCustomCommandToActiveChat(CustomCommand) - case toggleWidgetsHotkeyPressed - - case suggestionWidget(WidgetFeature.Action) - case switchWorkspace(path: String, name: String, username: String) - case initWorkspaceChatTabIfNeeded(path: String, username: String) - - static func promptToCodeGroup(_ action: PromptToCodeGroup.Action) -> Self { - .suggestionWidget(.panel(.sharedPanel(.promptToCodeGroup(action)))) - } - - #if canImport(ChatTabPersistent) - case persistent(ChatTabPersistent.Action) - #endif - } - - @Dependency(\.chatTabPool) var chatTabPool - @Dependency(\.activateThisApp) var activateThisApp - - public enum Debounce: Hashable { - case updateChatTabOrder - } - - var body: some ReducerOf { - CombineReducers { - Scope(state: \.suggestionWidgetState, action: \.suggestionWidget) { - WidgetFeature() - } - - Scope( - state: \.chatHistory, - action: \.suggestionWidget.chatPanel - ) { - Reduce { state, action in - switch action { - case let .createNewTapButtonClicked(kind): -// return .run { send in -// if let (_, chatTabInfo) = await chatTabPool.createTab(for: kind) { -// await send(.createNewTab(chatTabInfo)) -// } -// } - // The chat workspace should exist before create tab - guard let currentChatWorkspace = state.currentChatWorkspace else { return .none } - - return .run { send in - if let (_, chatTabInfo) = await chatTabPool.createTab(for: kind, with: currentChatWorkspace) { - await send(.appendAndSelectTab(chatTabInfo)) - } - } - case .restoreTabByInfo(let info): - guard let currentChatWorkspace = state.currentChatWorkspace else { return .none } - - return .run { send in - if let _ = await chatTabPool.restoreTab(by: info, with: currentChatWorkspace) { - await send(.appendAndSelectTab(info)) - } - } - - case .createNewTabByID(let id): - guard let currentChatWorkspace = state.currentChatWorkspace else { return .none } - - return .run { send in - if let (_, info) = await chatTabPool.createTab(id: id, with: currentChatWorkspace) { - await send(.appendAndSelectTab(info)) - } - } - -// case let .closeTabButtonClicked(id): -// return .run { _ in -// chatTabPool.removeTab(of: id) -// } - - case let .chatTab(_, .openNewTab(builder)): - // The chat workspace should exist before create tab - guard let currentChatWorkspace = state.currentChatWorkspace else { return .none } - return .run { send in - if let (_, chatTabInfo) = await chatTabPool - .createTab(from: builder.chatTabBuilder, with: currentChatWorkspace) - { - await send(.appendAndSelectTab(chatTabInfo)) - } - } - - default: - return .none - } - } - } - - #if canImport(ChatTabPersistent) - Scope(state: \.persistentState, action: \.persistent) { - ChatTabPersistent() - } - #endif - - Reduce { state, action in - switch action { - case .start: - #if canImport(ChatTabPersistent) - return .run { send in - await send(.persistent(.restoreChatTabs)) - } - #else - return .none - #endif - - case let .openChatPanel(forceDetach): - return .run { send in - await send( - .suggestionWidget( - .chatPanel(.presentChatPanel(forceDetach: forceDetach)) - ) - ) - await send(.suggestionWidget(.updateKeyWindow(.chatPanel))) - - activateThisApp() - } - - case .createAndSwitchToChatTabIfNeeded: - // The chat workspace should exist before create tab - guard let currentChatWorkspace = state.chatHistory.currentChatWorkspace else { return .none } - - if let selectedTabInfo = currentChatWorkspace.selectedTabInfo, - chatTabPool.getTab(of: selectedTabInfo.id) is ConversationTab - { - // Already in Chat tab - return .none - } - - if let firstChatTabInfo = state.chatHistory.currentChatWorkspace?.tabInfo.first(where: { - chatTabPool.getTab(of: $0.id) is ConversationTab - }) { - return .run { send in - await send(.suggestionWidget(.chatPanel(.tabClicked( - id: firstChatTabInfo.id - )))) - } - } - return .run { send in - if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil, with: currentChatWorkspace) { - await send( - .suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo))) - ) - } - } - - case let .switchWorkspace(path, name, username): - return .run { send in - await send( - .suggestionWidget(.chatPanel(.switchWorkspace(path, name, username))) - ) - } - case let .initWorkspaceChatTabIfNeeded(path, username): - let identifier = WorkspaceIdentifier(path: path, username: username) - guard let chatWorkspace = state.chatHistory.workspaces[id: identifier], chatWorkspace.tabInfo.isEmpty - else { return .none } - return .run { send in - if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil, with: chatWorkspace) { - await send( - .suggestionWidget(.chatPanel(.appendTabToWorkspace(chatTabInfo, chatWorkspace))) - ) - } - } -// case let .createAndSwitchToBrowserTabIfNeeded(url): -// #if canImport(BrowserChatTab) -// func match(_ tabURL: URL?) -> Bool { -// guard let tabURL else { return false } -// return tabURL == url -// || tabURL.absoluteString.hasPrefix(url.absoluteString) -// } -// -// if let selectedTabInfo = state.chatTabGroup.selectedTabInfo, -// let tab = chatTabPool.getTab(of: selectedTabInfo.id) as? BrowserChatTab, -// match(tab.url) -// { -// // Already in the target Browser tab -// return .none -// } -// -// if let firstChatTabInfo = state.chatTabGroup.tabInfo.first(where: { -// guard let tab = chatTabPool.getTab(of: $0.id) as? BrowserChatTab, -// match(tab.url) -// else { return false } -// return true -// }) { -// return .run { send in -// await send(.suggestionWidget(.chatPanel(.tabClicked( -// id: firstChatTabInfo.id -// )))) -// } -// } -// -// return .run { send in -// if let (_, chatTabInfo) = await chatTabPool.createTab( -// for: .init(BrowserChatTab.urlChatBuilder( -// url: url, -// externalDependency: ChatTabFactory -// .externalDependenciesForBrowserChatTab() -// )) -// ) { -// await send( -// .suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo))) -// ) -// } -// } -// -// #else -// return .none -// #endif - - case let .sendCustomCommandToActiveChat(command): - @Sendable func stopAndHandleCommand(_ tab: ConversationTab) async { - if tab.service.isReceivingMessage { - await tab.service.stopReceivingMessage() - } - try? await tab.service.handleCustomCommand(command) - } - - guard var currentChatWorkspace = state.chatHistory.currentChatWorkspace else { return .none } - - if let info = currentChatWorkspace.selectedTabInfo, - let activeTab = chatTabPool.getTab(of: info.id) as? ConversationTab - { - return .run { send in - await send(.openChatPanel(forceDetach: false)) - await stopAndHandleCommand(activeTab) - } - } - - let chatWorkspace = currentChatWorkspace - if var info = currentChatWorkspace.tabInfo.first(where: { - chatTabPool.getTab(of: $0.id) is ConversationTab - }), - let chatTab = chatTabPool.getTab(of: info.id) as? ConversationTab - { - let (originalTab, currentTab) = currentChatWorkspace.switchTab(to: &info) - let updatedChatWorkspace = currentChatWorkspace - - return .run { send in - await send(.suggestionWidget(.chatPanel(.updateChatHistory(updatedChatWorkspace)))) - await send(.openChatPanel(forceDetach: false)) - await stopAndHandleCommand(chatTab) - await send(.suggestionWidget(.chatPanel(.saveChatTabInfo([originalTab, currentTab], chatWorkspace)))) - } - } - - return .run { send in - guard let (chatTab, chatTabInfo) = await chatTabPool.createTab(for: nil, with: chatWorkspace) - else { - return - } - await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo)))) - await send(.openChatPanel(forceDetach: false)) - if let chatTab = chatTab as? ConversationTab { - await stopAndHandleCommand(chatTab) - } - } - - case .toggleWidgetsHotkeyPressed: - return .run { send in - await send(.suggestionWidget(.circularWidget(.widgetClicked))) - } - - case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): - #if canImport(ChatTabPersistent) - // when a tab is updated, persist it. - return .run { send in - await send(.persistent(.chatTabUpdated(id: id))) - } - #else - return .none - #endif - -// case let .suggestionWidget(.chatPanel(.closeTabButtonClicked(id))): -// #if canImport(ChatTabPersistent) -// // when a tab is closed, remove it from persistence. -// return .run { send in -// await send(.persistent(.chatTabClosed(id: id))) -// } -// #else -// return .none -// #endif - - case .suggestionWidget: - return .none - - #if canImport(ChatTabPersistent) - case .persistent: - return .none - #endif - } - } - } -// .onChange(of: \.chatCollection.selectedChatGroup?.tabInfo) { old, new in -// Reduce { _, _ in -// guard old.map(\.id) != new.map(\.id) else { -// return .none -// } -// #if canImport(ChatTabPersistent) -// return .run { send in -// await send(.persistent(.chatOrderChanged)) -// }.debounce(id: Debounce.updateChatTabOrder, for: 1, scheduler: DispatchQueue.main) -// #else -// return .none -// #endif -// } -// } - } -} - -@MainActor -public final class GraphicalUserInterfaceController { - let store: StoreOf - let widgetController: SuggestionWidgetController - let widgetDataSource: WidgetDataSource - let chatTabPool: ChatTabPool - - // Used for restoring. Handle concurrency - var restoredChatHistory: Set = Set() - - class WeakStoreHolder { - weak var store: StoreOf? - } - - init() { - let chatTabPool = ChatTabPool() - let suggestionDependency = SuggestionWidgetControllerDependency() - let setupDependency: (inout DependencyValues) -> Void = { dependencies in - dependencies.suggestionWidgetControllerDependency = suggestionDependency - dependencies.suggestionWidgetUserDefaultsObservers = .init() - dependencies.chatTabPool = chatTabPool - dependencies.chatTabBuilderCollection = ChatTabFactory.chatTabBuilderCollection - dependencies.promptToCodeAcceptHandler = { promptToCode in - Task { - let handler = PseudoCommandHandler() - await handler.acceptPromptToCode() - if !promptToCode.isContinuous { - NSWorkspace.activatePreviousActiveXcode() - } else { - NSWorkspace.activateThisApp() - } - } - } - } - let store = StoreOf( - initialState: .init(), - reducer: { GUI() }, - withDependencies: setupDependency - ) - self.store = store - self.chatTabPool = chatTabPool - widgetDataSource = .init() - - widgetController = SuggestionWidgetController( - store: store.scope( - state: \.suggestionWidgetState, - action: \.suggestionWidget - ), - chatTabPool: chatTabPool, - dependency: suggestionDependency - ) - - chatTabPool.createStore = { info in - store.scope( - state: { state in - state.chatHistory.currentChatWorkspace?.tabInfo[id: info.id] ?? info - }, - action: { childAction in - .suggestionWidget(.chatPanel(.chatTab(id: info.id, action: childAction))) - } - ) - } - - suggestionDependency.suggestionWidgetDataSource = widgetDataSource - suggestionDependency.onOpenChatClicked = { [weak self] in - Task { [weak self] in - await self?.store.send(.createAndSwitchToChatTabIfNeeded).finish() - self?.store.send(.openChatPanel(forceDetach: false)) - } - } - suggestionDependency.onCustomCommandClicked = { command in - Task { - let commandHandler = PseudoCommandHandler() - await commandHandler.handleCustomCommand(command) - } - } - } - - func start() { - store.send(.start) - } - - public func openGlobalChat() { - PseudoCommandHandler().openChat(forceDetach: true) - } -} - -extension ChatTabPool { - @MainActor - func createTab( - id: String = UUID().uuidString, - from builder: ChatTabBuilder? = nil, - with chatWorkspace: ChatWorkspace - ) async -> (any ChatTab, ChatTabInfo)? { - let id = id - let info = ChatTabInfo(id: id, workspacePath: chatWorkspace.workspacePath, username: chatWorkspace.username) - guard let builder else { - let chatTab = ConversationTab(store: createStore(info), with: info) - setTab(chatTab) - return (chatTab, info) - } - - guard let chatTab = await builder.build(store: createStore(info)) else { return nil } - setTab(chatTab) - return (chatTab, info) - } - - @MainActor - func createTab( - for kind: ChatTabKind?, - with chatWorkspace: ChatWorkspace - ) async -> (any ChatTab, ChatTabInfo)? { - let id = UUID().uuidString - let info = ChatTabInfo(id: id, workspacePath: chatWorkspace.workspacePath, username: chatWorkspace.username) - guard let builder = kind?.builder else { - let chatTab = ConversationTab(store: createStore(info), with: info) - setTab(chatTab) - return (chatTab, info) - } - - guard let chatTab = await builder.build(store: createStore(info)) else { return nil } - setTab(chatTab) - return (chatTab, info) - } - - @MainActor - func restoreTab( - by info: ChatTabInfo, - with chaWorkspace: ChatWorkspace - ) async -> (any ChatTab)? { - let chatTab = ConversationTab.restoreConversation(by: info, store: createStore(info)) - setTab(chatTab) - return chatTab - } -} - - -extension GraphicalUserInterfaceController { - - @MainActor - public func restore(path workspacePath: String, name workspaceName: String, username: String) async -> Void { - let workspaceIdentifier = WorkspaceIdentifier(path: workspacePath, username: username) - guard !restoredChatHistory.contains(workspaceIdentifier) else { return } - - // only restore once regardless of success or fail - restoredChatHistory.insert(workspaceIdentifier) - - let metadata = StorageMetadata(workspacePath: workspacePath, username: username) - let selectedChatTabInfo = ChatTabInfoStore.getSelected(with: metadata) ?? ChatTabInfoStore.getLatest(with: metadata) - - if let selectedChatTabInfo { - let chatTab = ConversationTab.restoreConversation(by: selectedChatTabInfo, store: chatTabPool.createStore(selectedChatTabInfo)) - chatTabPool.setTab(chatTab) - - let chatWorkspace = ChatWorkspace( - id: .init(path: workspacePath, username: username), - tabInfo: [selectedChatTabInfo], - tabCollection: [], - selectedTabId: selectedChatTabInfo.id - ) { [weak self] in - self?.chatTabPool.removeTab(of: $0) - } - await self.store.send(.suggestionWidget(.chatPanel(.restoreWorkspace(chatWorkspace)))).finish() - } - } -} diff --git a/Core/Sources/Service/GUI/WidgetDataSource.swift b/Core/Sources/Service/GUI/WidgetDataSource.swift deleted file mode 100644 index 01611d11..00000000 --- a/Core/Sources/Service/GUI/WidgetDataSource.swift +++ /dev/null @@ -1,67 +0,0 @@ -import ActiveApplicationMonitor -import AppActivator -import AppKit -import ChatService -import ComposableArchitecture -import Foundation -import GitHubCopilotService -import ChatAPIService -import PromptToCodeService -import SuggestionBasic -import SuggestionWidget - -@MainActor -final class WidgetDataSource {} - -extension WidgetDataSource: SuggestionWidgetDataSource { - func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? { - for workspace in Service.shared.workspacePool.workspaces.values { - if let filespace = workspace.filespaces[url], - let suggestion = filespace.presentingSuggestion - { - return .init( - code: suggestion.text, - language: filespace.language.rawValue, - startLineIndex: suggestion.position.line, - suggestionCount: filespace.suggestions.count, - currentSuggestionIndex: filespace.suggestionIndex, - onSelectPreviousSuggestionTapped: { - Task { - let handler = PseudoCommandHandler() - await handler.presentPreviousSuggestion() - } - }, - onSelectNextSuggestionTapped: { - Task { - let handler = PseudoCommandHandler() - await handler.presentNextSuggestion() - } - }, - onRejectSuggestionTapped: { - Task { - let handler = PseudoCommandHandler() - await handler.rejectSuggestions() - NSWorkspace.activatePreviousActiveXcode() - } - }, - onAcceptSuggestionTapped: { - Task { - let handler = PseudoCommandHandler() - await handler.acceptSuggestion() - NSWorkspace.activatePreviousActiveXcode() - } - }, - onDismissSuggestionTapped: { - Task { - let handler = PseudoCommandHandler() - await handler.dismissSuggestion() - NSWorkspace.activatePreviousActiveXcode() - } - } - ) - } - } - return nil - } -} - diff --git a/Core/Sources/Service/GlobalShortcutManager.swift b/Core/Sources/Service/GlobalShortcutManager.swift deleted file mode 100644 index 9620f25a..00000000 --- a/Core/Sources/Service/GlobalShortcutManager.swift +++ /dev/null @@ -1,63 +0,0 @@ -import AppKit -import Combine -import Foundation -import KeyboardShortcuts -import XcodeInspector - -extension KeyboardShortcuts.Name { - static let showHideWidget = Self("ShowHideWidget") -} - -@MainActor -final class GlobalShortcutManager { - let guiController: GraphicalUserInterfaceController - private var cancellable = Set() - - nonisolated init(guiController: GraphicalUserInterfaceController) { - self.guiController = guiController - } - - func start() { - KeyboardShortcuts.userDefaults = .shared - setupShortcutIfNeeded() - - KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in - let isXCodeActive = XcodeInspector.shared.activeXcode != nil - - if !isXCodeActive, - !guiController.store.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, - UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) - { - guiController.store.send(.openChatPanel(forceDetach: true)) - } else { - guiController.store.send(.toggleWidgetsHotkeyPressed) - } - } - - XcodeInspector.shared.$activeApplication.sink { app in - if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { - let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { - true - } else { - false - } - if shouldBeEnabled { - self.setupShortcutIfNeeded() - } else { - self.removeShortcutIfNeeded() - } - } else { - self.setupShortcutIfNeeded() - } - }.store(in: &cancellable) - } - - func setupShortcutIfNeeded() { - KeyboardShortcuts.enable(.showHideWidget) - } - - func removeShortcutIfNeeded() { - KeyboardShortcuts.disable(.showHideWidget) - } -} - diff --git a/Core/Sources/Service/Helpers.swift b/Core/Sources/Service/Helpers.swift deleted file mode 100644 index 90ac6344..00000000 --- a/Core/Sources/Service/Helpers.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import GitHubCopilotService -import LanguageServerProtocol - -extension NSError { - static func from(_ error: Error) -> NSError { - if let error = error as? ServerError { - var message = "Unknown" - switch error { - case let .handlerUnavailable(handler): - message = "Handler unavailable: \(handler)." - case let .unhandledMethod(method): - message = "Methond unhandled: \(method)." - case let .notificationDispatchFailed(error): - message = "Notification dispatch failed: \(error.localizedDescription)." - case let .requestDispatchFailed(error): - message = "Request dispatch failed: \(error.localizedDescription)." - case let .clientDataUnavailable(error): - message = "Client data unavailable: \(error.localizedDescription)." - case .serverUnavailable: - message = "Server unavailable, please make sure you have installed Node." - case .missingExpectedParameter: - message = "Missing expected parameter." - case .missingExpectedResult: - message = "Missing expected result." - case let .unableToDecodeRequest(error): - message = "Unable to decode request: \(error.localizedDescription)." - case let .unableToSendRequest(error): - message = "Unable to send request: \(error.localizedDescription)." - case let .unableToSendNotification(error): - message = "Unable to send notification: \(error.localizedDescription)." - case let .serverError(code, m, _): - message = "Server error: (\(code)) \(m)." - case let .invalidRequest(error): - message = "Invalid request: \(error?.localizedDescription ?? "Unknown")." - case .timeout: - message = "Timeout." - case .unknownError: - message = "Unknown error: \(error.localizedDescription)." - } - return NSError(domain: "com.github.CopilotForXcode", code: -1, userInfo: [ - NSLocalizedDescriptionKey: message, - ]) - } - if let error = error as? CancellationError { - return NSError(domain: "com.github.CopilotForXcode", code: -100, userInfo: [ - NSLocalizedDescriptionKey: error.localizedDescription, - ]) - } - return NSError(domain: "com.github.CopilotForXcode", code: -1, userInfo: [ - NSLocalizedDescriptionKey: error.localizedDescription, - ]) - } -} diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift deleted file mode 100644 index 899865f1..00000000 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ /dev/null @@ -1,206 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import AsyncAlgorithms -import AXExtension -import Combine -import Foundation -import Logger -import Preferences -import Status -import QuartzCore -import Workspace -import XcodeInspector - -public actor RealtimeSuggestionController { - private var cancellable: Set = [] - private var inflightPrefetchTask: Task? - private var editorObservationTask: Task? - private var sourceEditor: SourceEditor? - - init() {} - - deinit { - cancellable.forEach { $0.cancel() } - inflightPrefetchTask?.cancel() - editorObservationTask?.cancel() - } - - nonisolated - func start() { - Task { await observeXcodeChange() } - } - - private func observeXcodeChange() { - cancellable.forEach { $0.cancel() } - - XcodeInspector.shared.$focusedEditor - .sink { [weak self] editor in - guard let self else { return } - Task { - guard let editor else { return } - await self.handleFocusElementChange(editor) - } - }.store(in: &cancellable) - } - - private func handleFocusElementChange(_ sourceEditor: SourceEditor) { - self.sourceEditor = sourceEditor - - let notificationsFromEditor = sourceEditor.axNotifications - - editorObservationTask?.cancel() - editorObservationTask = nil - - editorObservationTask = Task { [weak self] in - if let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL { - await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded( - fileURL: fileURL, - sourceEditor: sourceEditor - ) - } - - let valueChange = await notificationsFromEditor.notifications() - .filter { $0.kind == .valueChanged } - let selectedTextChanged = await notificationsFromEditor.notifications() - .filter { $0.kind == .selectedTextChanged } - - await withTaskGroup(of: Void.self) { [weak self] group in - group.addTask { [weak self] in - let handler = { [weak self] in - guard let self else { return } - await cancelInFlightTasks() - await self.triggerPrefetchDebounced() - await self.notifyEditingFileChange(editor: sourceEditor.element) - } - - if #available(macOS 13.0, *) { - for await _ in valueChange._throttle(for: .milliseconds(200)) { - if Task.isCancelled { return } - await handler() - } - } else { - for await _ in valueChange { - if Task.isCancelled { return } - await handler() - } - } - } - group.addTask { - let handler = { - guard let fileURL = await XcodeInspector.shared.safe.activeDocumentURL - else { return } - await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded( - fileURL: fileURL, - sourceEditor: sourceEditor - ) - } - - if #available(macOS 13.0, *) { - for await _ in selectedTextChanged._throttle(for: .milliseconds(200)) { - if Task.isCancelled { return } - await handler() - } - } else { - for await _ in selectedTextChanged { - if Task.isCancelled { return } - await handler() - } - } - } - - await group.waitForAll() - } - } - - Task { @WorkspaceActor in // Get cache ready for real-time suggestions. - guard UserDefaults.shared.value(for: \.preCacheOnFileOpen) else { return } - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - let (_, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - - if filespace.codeMetadata.uti == nil { - // avoid the command get called twice - filespace.codeMetadata.uti = "" - do { - try await XcodeInspector.shared.safe.latestActiveXcode? - .triggerCopilotCommand(name: "Sync Text Settings") - await Status.shared.updateExtensionStatus(.granted) - } catch { - if filespace.codeMetadata.uti?.isEmpty ?? true { - filespace.codeMetadata.uti = nil - } - if let cantRunError = error as? AppInstanceInspector.CantRunCommand { - if cantRunError.errorDescription.contains("No bundle found") { - await Status.shared.updateExtensionStatus(.notGranted) - } else if cantRunError.errorDescription.contains("found but disabled") { - await Status.shared.updateExtensionStatus(.disabled) - } - } - } - } - } - } - - func triggerPrefetchDebounced(force: Bool = false) { - inflightPrefetchTask = Task(priority: .utility) { @WorkspaceActor in - try? await Task.sleep(nanoseconds: UInt64( - max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.15) - * 1_000_000_000 - )) - - if Task.isCancelled { return } - - // check if user loggin - let authStatus = await Status.shared.getAuthStatus() - guard authStatus.status == .loggedIn else { return } - - guard UserDefaults.shared.value(for: \.realtimeSuggestionToggle) - else { return } - - if UserDefaults.shared.value(for: \.disableSuggestionFeatureGlobally), - let fileURL = await XcodeInspector.shared.safe.activeDocumentURL, - let (workspace, _) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - { - let isEnabled = workspace.isSuggestionFeatureEnabled - if !isEnabled { return } - } - if Task.isCancelled { return } - - // So the editor won't be blocked (after information are cached)! - await PseudoCommandHandler().generateRealtimeSuggestions(sourceEditor: sourceEditor) - } - } - - func cancelInFlightTasks(excluding: Task? = nil) async { - inflightPrefetchTask?.cancel() - - // cancel in-flight tasks - await withTaskGroup(of: Void.self) { group in - for (_, workspace) in Service.shared.workspacePool.workspaces { - group.addTask { - await workspace.cancelInFlightRealtimeSuggestionRequests() - } - } - } - } - - /// This method will still return true if the completion panel is hidden by esc. - /// Looks like the Xcode will keep the panel around until content is changed, - /// not sure how to observe that it's hidden. - func isCompletionPanelPresenting() -> Bool { - guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode else { return false } - let application = AXUIElementCreateApplication(activeXcode.processIdentifier) - return application.focusedWindow?.child(identifier: "_XC_COMPLETION_TABLE_") != nil - } - - func notifyEditingFileChange(editor: AXUIElement) async { - guard let fileURL = await XcodeInspector.shared.safe.activeDocumentURL, - let (workspace, _) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - else { return } - await workspace.didUpdateFilespace(fileURL: fileURL, content: editor.value) - } -} - diff --git a/Core/Sources/Service/ScheduledCleaner.swift b/Core/Sources/Service/ScheduledCleaner.swift deleted file mode 100644 index 2178ba50..00000000 --- a/Core/Sources/Service/ScheduledCleaner.swift +++ /dev/null @@ -1,92 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import AXExtension -import BuiltinExtension -import Foundation -import Logger -import Workspace -import XcodeInspector - -public final class ScheduledCleaner { - weak var service: Service? - - init() {} - - func start() { - Task { @ServiceActor in - while !Task.isCancelled { - try await Task.sleep(nanoseconds: 10 * 60 * 1_000_000_000) - await cleanUp() - } - } - - Task { @ServiceActor in - for await app in ActiveApplicationMonitor.shared.createInfoStream() { - try Task.checkCancellation() - if let app, !app.isXcode { - await cleanUp() - } - } - } - } - - @ServiceActor - func cleanUp() async { - guard let service else { return } - - let workspaceInfos = XcodeInspector.shared.xcodes.reduce( - into: [ - XcodeAppInstanceInspector.WorkspaceIdentifier: - XcodeAppInstanceInspector.WorkspaceInfo - ]() - ) { result, xcode in - let infos = xcode.realtimeWorkspaces - for (id, info) in infos { - if let existed = result[id] { - result[id] = existed.combined(with: info) - } else { - result[id] = info - } - } - } - for (url, workspace) in service.workspacePool.workspaces { - if workspace.isExpired, workspaceInfos[.url(url)] == nil { - Logger.service.info("Remove idle workspace") - _ = await Task { @MainActor in - service.guiController.store.send( - .promptToCodeGroup(.discardExpiredPromptToCode(documentURLs: Array( - workspace.filespaces.keys - ))) - ) - }.result - await workspace.cleanUp(availableTabs: []) - await service.workspacePool.removeWorkspace(url: url) - } else { - let tabs = (workspaceInfos[.url(url)]?.tabs ?? []) - .union(workspaceInfos[.unknown]?.tabs ?? []) - // cleanup chats for unused files - let filespaces = workspace.filespaces - for (url, _) in filespaces { - if workspace.isFilespaceExpired( - fileURL: url, - availableTabs: tabs - ) { - _ = await Task { @MainActor in - service.guiController.store.send( - .promptToCodeGroup(.discardExpiredPromptToCode(documentURLs: [url])) - ) - }.result - } - } - // cleanup workspace - await workspace.cleanUp(availableTabs: tabs) - } - } - } - - @ServiceActor - public func closeAllChildProcesses() async { - BuiltinExtensionManager.shared.terminate() - } -} - diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift deleted file mode 100644 index 8072778a..00000000 --- a/Core/Sources/Service/Service.swift +++ /dev/null @@ -1,220 +0,0 @@ -import BuiltinExtension -import Combine -import Dependencies -import Foundation -import GitHubCopilotService -import KeyBindingManager -import Logger -import SuggestionService -import Toast -import Workspace -import WorkspaceSuggestionService -import XcodeInspector -import XcodeThemeController -import XPCShared -import SuggestionWidget -import Status -import ChatService -import Persist -import PersistMiddleware - -@globalActor public enum ServiceActor { - public actor TheActor {} - public static let shared = TheActor() -} - -/// The running extension service. -public final class Service { - public static let shared = Service() - - @WorkspaceActor - let workspacePool: WorkspacePool - @MainActor - public let guiController = GraphicalUserInterfaceController() - public let realtimeSuggestionController = RealtimeSuggestionController() - public let scheduledCleaner: ScheduledCleaner - let globalShortcutManager: GlobalShortcutManager - let keyBindingManager: KeyBindingManager - let xcodeThemeController: XcodeThemeController = .init() - - @Dependency(\.toast) var toast - var cancellable = Set() - - private init() { - @Dependency(\.workspacePool) var workspacePool - - BuiltinExtensionManager.shared.setupExtensions([ - GitHubCopilotExtension(workspacePool: workspacePool) - ]) - scheduledCleaner = .init() - workspacePool.registerPlugin { - SuggestionServiceWorkspacePlugin(workspace: $0) { SuggestionService.service() } - } - workspacePool.registerPlugin { - GitHubCopilotWorkspacePlugin(workspace: $0) - } - workspacePool.registerPlugin { - BuiltinExtensionWorkspacePlugin(workspace: $0) - } - self.workspacePool = workspacePool - - globalShortcutManager = .init(guiController: guiController) - keyBindingManager = .init( - workspacePool: workspacePool, - acceptSuggestion: { - Task { await PseudoCommandHandler().acceptSuggestion() } - }, - expandSuggestion: { - if !ExpandableSuggestionService.shared.isSuggestionExpanded { - ExpandableSuggestionService.shared.isSuggestionExpanded = true - } - }, - collapseSuggestion: { - if ExpandableSuggestionService.shared.isSuggestionExpanded { - ExpandableSuggestionService.shared.isSuggestionExpanded = false - } - }, - dismissSuggestion: { - Task { await PseudoCommandHandler().dismissSuggestion() } - } - ) - let scheduledCleaner = ScheduledCleaner() - - scheduledCleaner.service = self - Logger.telemetryLogger = TelemetryLogger() - } - - @MainActor - public func start() { - scheduledCleaner.start() - realtimeSuggestionController.start() - guiController.start() - xcodeThemeController.start() - globalShortcutManager.start() - keyBindingManager.start() - - Task { - await Publishers.CombineLatest( - XcodeInspector.shared.safe.$activeDocumentURL - .removeDuplicates(), - XcodeInspector.shared.safe.$latestActiveXcode - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] documentURL, latestXcode in - Task { - let fileURL = documentURL ?? latestXcode?.realtimeDocumentURL - guard fileURL != nil, fileURL != .init(fileURLWithPath: "/") else { - return - } - do { - let _ = try await self?.workspacePool - .fetchOrCreateWorkspaceAndFilespace( - fileURL: fileURL! - ) - } catch let error as Workspace.WorkspaceFileError { - Logger.workspacePool - .info(error.localizedDescription) - } - catch { - Logger.workspacePool.error(error) - } - } - }.store(in: &cancellable) - - // Combine both workspace and auth status changes into a single stream - await Publishers.CombineLatest3( - XcodeInspector.shared.safe.$latestActiveXcode, - XcodeInspector.shared.safe.$activeWorkspaceURL - .removeDuplicates(), - StatusObserver.shared.$authStatus - .removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] newXcode, newURL, newStatus in - // First check for realtimeWorkspaceURL if activeWorkspaceURL is nil - if let realtimeURL = newXcode?.realtimeWorkspaceURL, newURL == nil { - self?.onNewActiveWorkspaceURLOrAuthStatus( - newURL: realtimeURL, - newStatus: newStatus - ) - } else if let newURL = newURL { - // Then use activeWorkspaceURL if available - self?.onNewActiveWorkspaceURLOrAuthStatus( - newURL: newURL, - newStatus: newStatus - ) - } - } - .store(in: &cancellable) - } - } - - @MainActor - public func prepareForExit() async { - Logger.service.info("Prepare for exit.") - keyBindingManager.stopForExit() - await scheduledCleaner.closeAllChildProcesses() - } - - private func getDisplayNameOfXcodeWorkspace(url: URL) -> String { - var name = url.lastPathComponent - let suffixes = [".xcworkspace", ".xcodeproj", ".playground"] - for suffix in suffixes { - if name.hasSuffix(suffix) { - name = String(name.dropLast(suffix.count)) - break - } - } - return name - } -} - -public extension Service { - func handleXPCServiceRequests( - endpoint: String, - requestBody: Data, - reply: @escaping (Data?, Error?) -> Void - ) { - reply(nil, XPCRequestNotHandledError()) - } -} - -// internal extension -extension Service { - - func onNewActiveWorkspaceURLOrAuthStatus(newURL: URL?, newStatus: AuthStatus) { - Task { @MainActor in - // check path - guard let path = newURL?.path, path != "/", - // check auth status - newStatus.status == .loggedIn, - let username = newStatus.username, !username.isEmpty, - // Switch workspace only when the `workspace` or `username` is not the same as the current one - ( - self.guiController.store.chatHistory.selectedWorkspacePath != path || - self.guiController.store.chatHistory.currentUsername != username - ) - else { return } - - await self.doSwitchWorkspace(workspaceURL: newURL!, username: username) - } - } - - /// - Parameters: - /// - workspaceURL: The active workspace URL that need switch to - /// - path: Path of the workspace URL - /// - username: Curent github username - @MainActor - func doSwitchWorkspace(workspaceURL: URL, username: String) async { - // get workspace display name - let name = self.getDisplayNameOfXcodeWorkspace(url: workspaceURL) - let path = workspaceURL.path - - // switch workspace and username and wait for it to complete - await self.guiController.store.send(.switchWorkspace(path: path, name: name, username: username)).finish() - // restore if needed - await self.guiController.restore(path: path, name: name, username: username) - // init chat tab if no history tab (only after workspace is fully switched and restored) - await self.guiController.store.send(.initWorkspaceChatTabIfNeeded(path: path, username: username)).finish() - } -} diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift deleted file mode 100644 index 2ad3e765..00000000 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ /dev/null @@ -1,440 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import Dependencies -import Preferences -import SuggestionInjector -import SuggestionBasic -import Toast -import Workspace -import WorkspaceSuggestionService -import XcodeInspector -import XPCShared -import AXHelper - -/// It's used to run some commands without really triggering the menu bar item. -/// -/// For example, we can use it to generate real-time suggestions without Apple Scripts. -struct PseudoCommandHandler { - static var lastTimeCommandFailedToTriggerWithAccessibilityAPI = Date(timeIntervalSince1970: 0) - static var lastBundleNotFoundTime = Date(timeIntervalSince1970: 0) - static var lastBundleDisabledTime = Date(timeIntervalSince1970: 0) - private var toast: ToastController { ToastControllerDependencyKey.liveValue } - - func presentPreviousSuggestion() async { - let handler = WindowBaseCommandHandler() - _ = try? await handler.presentPreviousSuggestion(editor: .init( - content: "", - lines: [], - uti: "", - cursorPosition: .outOfScope, - cursorOffset: -1, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - )) - } - - func presentNextSuggestion() async { - let handler = WindowBaseCommandHandler() - _ = try? await handler.presentNextSuggestion(editor: .init( - content: "", - lines: [], - uti: "", - cursorPosition: .outOfScope, - cursorOffset: -1, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - )) - } - - @WorkspaceActor - func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async { - guard let filespace = await getFilespace(), - let (workspace, _) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return } - - if Task.isCancelled { return } - - // Can't use handler if content is not available. - guard let editor = await getEditorContent(sourceEditor: sourceEditor) - else { return } - - let fileURL = filespace.fileURL - let presenter = PresentInWindowSuggestionPresenter() - - presenter.markAsProcessing(true) - defer { presenter.markAsProcessing(false) } - - if filespace.presentingSuggestion != nil { - // Check if the current suggestion is still valid. - if filespace.validateSuggestions( - lines: editor.lines, - cursorPosition: editor.cursorPosition - ) { - return - } else { - presenter.discardSuggestion(fileURL: filespace.fileURL) - } - } - - do { - try await workspace.generateSuggestions( - forFileAt: fileURL, - editor: editor - ) - if let sourceEditor { - let editorContent = sourceEditor.getContent() - _ = filespace.validateSuggestions( - lines: editorContent.lines, - cursorPosition: editorContent.cursorPosition - ) - } - if !filespace.errorMessage.isEmpty { - presenter - .presentWarningMessage( - filespace.errorMessage, - url: "https://github.com/github-copilot/signup/copilot_individual" - ) - } - if filespace.presentingSuggestion != nil { - presenter.presentSuggestion(fileURL: fileURL) - workspace.notifySuggestionShown(fileFileAt: fileURL) - } else { - presenter.discardSuggestion(fileURL: fileURL) - } - } catch { - return - } - } - - @WorkspaceActor - func invalidateRealtimeSuggestionsIfNeeded(fileURL: URL, sourceEditor: SourceEditor) async { - guard let (_, filespace) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return } - - if filespace.presentingSuggestion == nil { - return // skip if there's no suggestion presented. - } - - let content = sourceEditor.getContent() - if !filespace.validateSuggestions( - lines: content.lines, - cursorPosition: content.cursorPosition - ) { - PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: fileURL) - } - } - - func rejectSuggestions() async { - let handler = WindowBaseCommandHandler() - _ = try? await handler.rejectSuggestion(editor: .init( - content: "", - lines: [], - uti: "", - cursorPosition: .outOfScope, - cursorOffset: -1, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - )) - } - - func handleCustomCommand(_ command: CustomCommand) async { - guard let editor = await { - if let it = await getEditorContent(sourceEditor: nil) { - return it - } - switch command.feature { - // editor content is not required. - case .customChat, .chatWithSelection, .singleRoundDialog: - return .init( - content: "", - lines: [], - uti: "", - cursorPosition: .outOfScope, - cursorOffset: -1, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - ) - // editor content is required. - case .promptToCode: - return nil - } - }() else { - do { - try await XcodeInspector.shared.safe.latestActiveXcode? - .triggerCopilotCommand(name: command.name) - } catch { - let presenter = PresentInWindowSuggestionPresenter() - presenter.presentError(error) - } - return - } - - let handler = WindowBaseCommandHandler() - do { - try await handler.handleCustomCommand(id: command.id, editor: editor) - } catch { - let presenter = PresentInWindowSuggestionPresenter() - presenter.presentError(error) - } - } - - func acceptPromptToCode() async { - do { - if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { - throw CancellationError() - } - do { - try await XcodeInspector.shared.safe.latestActiveXcode? - .triggerCopilotCommand(name: "Accept Prompt to Code") - } catch { - let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI - let now = Date() - if now.timeIntervalSince(last) > 60 * 60 { - Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now - toast.toast(content: """ - The app is using a fallback solution to accept suggestions. \ - For better experience, please restart Xcode to re-activate the Copilot \ - menu item. - """, level: .warning) - } - - throw error - } - } catch { - guard let xcode = ActiveApplicationMonitor.shared.activeXcode - ?? ActiveApplicationMonitor.shared.latestXcode else { return } - let application = AXUIElementCreateApplication(xcode.processIdentifier) - guard let focusElement = application.focusedElement, - focusElement.description == "Source Editor" - else { return } - guard let ( - content, - lines, - _, - cursorPosition, - cursorOffset - ) = await getFileContent(sourceEditor: nil) - else { - PresentInWindowSuggestionPresenter() - .presentErrorMessage("Unable to get file content.") - return - } - let handler = WindowBaseCommandHandler() - do { - guard let result = try await handler.acceptPromptToCode(editor: .init( - content: content, - lines: lines, - uti: "", - cursorPosition: cursorPosition, - cursorOffset: cursorOffset, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - )) else { return } - - try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement) - } catch { - PresentInWindowSuggestionPresenter().presentError(error) - } - } - } - - func acceptSuggestion() async { - do { - if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { - throw CancellationError() - } - do { - try await XcodeInspector.shared.safe.latestActiveXcode? - .triggerCopilotCommand(name: "Accept Suggestion") - } catch { - let lastBundleNotFoundTime = Self.lastBundleNotFoundTime - let lastBundleDisabledTime = Self.lastBundleDisabledTime - let now = Date() - if let cantRunError = error as? AppInstanceInspector.CantRunCommand { - if cantRunError.errorDescription.contains("No bundle found") { - // Extension permission not granted - if now.timeIntervalSince(lastBundleNotFoundTime) > 60 * 60 { - Self.lastBundleNotFoundTime = now - toast.toast( - title: "GitHub Copilot Extension Permission Not Granted", - content: """ - Enable Extensions → Xcode Source Editor → GitHub Copilot \ - for Xcode for faster and full-featured code completion. \ - [View How-to Guide](https://github.com/github/CopilotForXcode/blob/main/TROUBLESHOOTING.md#extension-permission) - """, - level: .warning, - button: .init( - title: "Enable", - action: { NSWorkspace.openXcodeExtensionsPreferences() } - ) - ) - } - } else if cantRunError.errorDescription.contains("found but disabled") { - if now.timeIntervalSince(lastBundleDisabledTime) > 60 * 60 { - Self.lastBundleDisabledTime = now - toast.toast( - title: "GitHub Copilot Extension Disabled", - content: "Quit and restart Xcode to enable extension.", - level: .warning, - button: .init( - title: "Restart Xcode", - action: { NSWorkspace.restartXcode() } - ) - ) - } - } - } - - throw error - } - } catch { - guard let xcode = ActiveApplicationMonitor.shared.activeXcode - ?? ActiveApplicationMonitor.shared.latestXcode else { return } - let application = AXUIElementCreateApplication(xcode.processIdentifier) - guard let focusElement = application.focusedElement, - focusElement.description == "Source Editor" - else { return } - guard let ( - content, - lines, - _, - cursorPosition, - cursorOffset - ) = await getFileContent(sourceEditor: nil) - else { - PresentInWindowSuggestionPresenter() - .presentErrorMessage("Unable to get file content.") - return - } - let handler = WindowBaseCommandHandler() - do { - guard let result = try await handler.acceptSuggestion(editor: .init( - content: content, - lines: lines, - uti: "", - cursorPosition: cursorPosition, - cursorOffset: cursorOffset, - selections: [], - tabSize: 0, - indentSize: 0, - usesTabsForIndentation: false - )) else { return } - - try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement) - } catch { - PresentInWindowSuggestionPresenter().presentError(error) - } - } - } - - func dismissSuggestion() async { - guard let documentURL = await XcodeInspector.shared.safe.activeDocumentURL else { return } - guard let (_, filespace) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: documentURL) else { return } - - await filespace.reset() - PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: documentURL) - } - - func openChat(forceDetach: Bool) { - let store = Service.shared.guiController.store - Task { @MainActor in - await store.send(.createAndSwitchToChatTabIfNeeded).finish() - store.send(.openChatPanel(forceDetach: forceDetach)) - } - } -} - -extension PseudoCommandHandler { - /// When Xcode commands are not available, we can fallback to directly - /// set the value of the editor with Accessibility API. - func injectUpdatedCodeWithAccessibilityAPI( - _ result: UpdatedContent, - focusElement: AXUIElement - ) throws { - try AXHelper().injectUpdatedCodeWithAccessibilityAPI( - result, - focusElement: focusElement, - onError: { - PresentInWindowSuggestionPresenter() - .presentErrorMessage("Fail to set editor content.") - } - ) - } - - func getFileContent(sourceEditor: AXUIElement?) async - -> ( - content: String, - lines: [String], - selections: [CursorRange], - cursorPosition: CursorPosition, - cursorOffset: Int - )? - { - guard let xcode = ActiveApplicationMonitor.shared.activeXcode - ?? ActiveApplicationMonitor.shared.latestXcode else { return nil } - let application = AXUIElementCreateApplication(xcode.processIdentifier) - guard let focusElement = sourceEditor ?? application.focusedElement, - focusElement.description == "Source Editor" - else { return nil } - guard let selectionRange = focusElement.selectedTextRange else { return nil } - let content = focusElement.value - let split = content.breakLines(appendLineBreakToLastLine: false) - let range = SourceEditor.convertRangeToCursorRange(selectionRange, in: content) - return (content, split, [range], range.start, selectionRange.lowerBound) - } - - func getFileURL() async -> URL? { - await XcodeInspector.shared.safe.realtimeActiveDocumentURL - } - - @WorkspaceActor - func getFilespace() async -> Filespace? { - guard - let fileURL = await getFileURL(), - let (_, filespace) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - else { return nil } - return filespace - } - - @WorkspaceActor - func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? { - guard let filespace = await getFilespace(), - let sourceEditor = await { - if let sourceEditor { sourceEditor } - else { await XcodeInspector.shared.safe.focusedEditor } - }() - else { return nil } - if Task.isCancelled { return nil } - let content = sourceEditor.getContent() - let uti = filespace.codeMetadata.uti ?? "" - let tabSize = filespace.codeMetadata.tabSize ?? 4 - let indentSize = filespace.codeMetadata.indentSize ?? 4 - let usesTabsForIndentation = filespace.codeMetadata.usesTabsForIndentation ?? false - return .init( - content: content.content, - lines: content.lines, - uti: uti, - cursorPosition: content.cursorPosition, - cursorOffset: content.cursorOffset, - selections: content.selections.map { - .init(start: $0.start, end: $0.end) - }, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation - ) - } -} - diff --git a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift deleted file mode 100644 index 3d612e82..00000000 --- a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SuggestionBasic -import XPCShared - -protocol SuggestionCommandHandler { - @ServiceActor - func presentSuggestions(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func presentNextSuggestion(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func presentPreviousSuggestion(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func promptToCode(editor: EditorContent) async throws -> UpdatedContent? - @ServiceActor - func customCommand(id: String, editor: EditorContent) async throws -> UpdatedContent? -} diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift deleted file mode 100644 index 694bff25..00000000 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ /dev/null @@ -1,475 +0,0 @@ -import AppKit -import ChatService -import Foundation -import GitHubCopilotService -import LanguageServerProtocol -import Logger -import ChatAPIService -import SuggestionInjector -import SuggestionBasic -import SuggestionWidget -import UserNotifications -import Workspace -import WorkspaceSuggestionService -import XcodeInspector -import XPCShared -import ChatService - -struct WindowBaseCommandHandler: SuggestionCommandHandler { - nonisolated init() {} - - let presenter = PresentInWindowSuggestionPresenter() - - func presentSuggestions(editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await _presentSuggestions(editor: editor) - } catch let error as ServerError { - Logger.service.error(error) - } catch { - presenter.presentError(error) - Logger.service.error(error) - } - } - return nil - } - - @WorkspaceActor - private func _presentSuggestions(editor: EditorContent) async throws { - presenter.markAsProcessing(true) - defer { - presenter.markAsProcessing(false) - } - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - let (workspace, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - - try Task.checkCancellation() - - try await workspace.generateSuggestions( - forFileAt: fileURL, - editor: editor - ) - - try Task.checkCancellation() - - if filespace.presentingSuggestion != nil { - presenter.presentSuggestion(fileURL: fileURL) - workspace.notifySuggestionShown(fileFileAt: fileURL) - } else { - presenter.discardSuggestion(fileURL: fileURL) - } - } - - func presentNextSuggestion(editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await _presentNextSuggestion(editor: editor) - } catch { - presenter.presentError(error) - } - } - return nil - } - - @WorkspaceActor - private func _presentNextSuggestion(editor: EditorContent) async throws { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - let (workspace, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - workspace.selectNextSuggestion(forFileAt: fileURL) - - if filespace.presentingSuggestion != nil { - presenter.presentSuggestion(fileURL: fileURL) - workspace.notifySuggestionShown(fileFileAt: fileURL) - } else { - presenter.discardSuggestion(fileURL: fileURL) - } - } - - func presentPreviousSuggestion(editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await _presentPreviousSuggestion(editor: editor) - } catch { - presenter.presentError(error) - } - } - return nil - } - - @WorkspaceActor - private func _presentPreviousSuggestion(editor: EditorContent) async throws { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - let (workspace, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - workspace.selectPreviousSuggestion(forFileAt: fileURL) - - if filespace.presentingSuggestion != nil { - presenter.presentSuggestion(fileURL: fileURL) - workspace.notifySuggestionShown(fileFileAt: fileURL) - } else { - presenter.discardSuggestion(fileURL: fileURL) - } - } - - func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await _rejectSuggestion(editor: editor) - } catch { - presenter.presentError(error) - } - } - return nil - } - - @WorkspaceActor - private func _rejectSuggestion(editor: EditorContent) async throws { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - - let (workspace, _) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - workspace.rejectSuggestion(forFileAt: fileURL, editor: editor) - presenter.discardSuggestion(fileURL: fileURL) - } - - @WorkspaceActor - func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return nil } - let (workspace, _) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - - let injector = SuggestionInjector() - var lines = editor.lines - var cursorPosition = editor.cursorPosition - var extraInfo = SuggestionInjector.ExtraInfo() - - if let acceptedSuggestion = workspace.acceptSuggestion( - forFileAt: fileURL, - editor: editor, - suggestionLineLimit: ExpandableSuggestionService.shared.isSuggestionExpanded ? nil : 1 - ) { - injector.acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursorPosition, - completion: acceptedSuggestion, - extraInfo: &extraInfo, - suggestionLineLimit: ExpandableSuggestionService.shared.isSuggestionExpanded ? nil : 1 - ) - - presenter.discardSuggestion(fileURL: fileURL) - - return .init( - content: String(lines.joined(separator: "")), - newSelection: .cursor(cursorPosition), - modifications: extraInfo.modifications - ) - } - - return nil - } - - func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return nil } - - let injector = SuggestionInjector() - var lines = editor.lines - var cursorPosition = editor.cursorPosition - var extraInfo = SuggestionInjector.ExtraInfo() - - let store = Service.shared.guiController.store - - if let promptToCode = store.state.promptToCodeGroup.activePromptToCode { - if promptToCode.isAttachedToSelectionRange, promptToCode.documentURL != fileURL { - return nil - } - - let range = { - if promptToCode.isAttachedToSelectionRange, - let range = promptToCode.selectionRange - { - return range - } - return editor.selections.first.map { - CursorRange(start: $0.start, end: $0.end) - } ?? CursorRange( - start: editor.cursorPosition, - end: editor.cursorPosition - ) - }() - - let suggestion = CodeSuggestion( - id: UUID().uuidString, - text: promptToCode.code, - position: range.start, - range: range - ) - - injector.acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursorPosition, - completion: suggestion, - extraInfo: &extraInfo - ) - - _ = await Task { @MainActor [cursorPosition] in - store.send( - .promptToCodeGroup(.updatePromptToCodeRange( - id: promptToCode.id, - range: .init(start: range.start, end: cursorPosition) - )) - ) - store.send( - .promptToCodeGroup(.discardAcceptedPromptToCodeIfNotContinuous( - id: promptToCode.id - )) - ) - }.result - - return .init( - content: String(lines.joined(separator: "")), - newSelection: .init(start: range.start, end: cursorPosition), - modifications: extraInfo.modifications - ) - } - - return nil - } - - func presentRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? { - Task { - try? await prepareCache(editor: editor) - } - return nil - } - - @WorkspaceActor - func prepareCache(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return nil } - let (_, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - filespace.codeMetadata.uti = editor.uti - filespace.codeMetadata.tabSize = editor.tabSize - filespace.codeMetadata.indentSize = editor.indentSize - filespace.codeMetadata.usesTabsForIndentation = editor.usesTabsForIndentation - filespace.codeMetadata.guessLineEnding(from: editor.lines.first) - return nil - } - - func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent? { - return try await presentSuggestions(editor: editor) - } - - func promptToCode(editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await presentPromptToCode( - editor: editor, - extraSystemPrompt: nil, - prompt: nil, - isContinuous: false, - generateDescription: nil, - name: nil - ) - } catch { - presenter.presentError(error) - } - } - return nil - } - - func customCommand(id: String, editor: EditorContent) async throws -> UpdatedContent? { - Task { - do { - try await handleCustomCommand(id: id, editor: editor) - } catch { - presenter.presentError(error) - } - } - return nil - } -} - -extension WindowBaseCommandHandler { - func handleCustomCommand(id: String, editor: EditorContent) async throws { - struct CommandNotFoundError: Error, LocalizedError { - var errorDescription: String? { "Command not found" } - } - - let availableCommands = UserDefaults.shared.value(for: \.customCommands) - guard let command = availableCommands.first(where: { $0.id == id }) - else { throw CommandNotFoundError() } - - switch command.feature { - case .chatWithSelection, .customChat: - Task { @MainActor in - Service.shared.guiController.store - .send(.sendCustomCommandToActiveChat(command)) - } - case let .promptToCode(extraSystemPrompt, prompt, continuousMode, generateDescription): - try await presentPromptToCode( - editor: editor, - extraSystemPrompt: extraSystemPrompt, - prompt: prompt, - isContinuous: continuousMode ?? false, - generateDescription: generateDescription, - name: command.name - ) - case let .singleRoundDialog( - systemPrompt, - overwriteSystemPrompt, - prompt, - receiveReplyInNotification - ): - try await executeSingleRoundDialog( - systemPrompt: systemPrompt, - overwriteSystemPrompt: overwriteSystemPrompt ?? false, - prompt: prompt ?? "", - receiveReplyInNotification: receiveReplyInNotification ?? false - ) - } - } - - @WorkspaceActor - func presentPromptToCode( - editor: EditorContent, - extraSystemPrompt: String?, - prompt: String?, - isContinuous: Bool, - generateDescription: Bool?, - name: String? - ) async throws { - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - let (workspace, filespace) = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - guard workspace.suggestionPlugin?.isSuggestionFeatureEnabled ?? false else { - presenter.presentErrorMessage("Prompt to code is disabled for this project") - return - } - - let codeLanguage = languageIdentifierFromFileURL(fileURL) - - let (code, selection) = { - guard var selection = editor.selections.last, - selection.start != selection.end - else { return ("", .cursor(editor.cursorPosition)) } - - let isMultipleLine = selection.start.line != selection.end.line - let isSpaceOnlyBeforeStartPositionOnTheSameLine = { - guard selection.start.line >= 0, selection.start.line < editor.lines.count else { - return false - } - let line = editor.lines[selection.start.line] - guard selection.start.character > 0, - selection.start.character < line.utf16.count - else { return false } - let substring = line[line.utf16.startIndex..<(line.index( - line.utf16.startIndex, - offsetBy: selection.start.character, - limitedBy: line.utf16.endIndex - ) ?? line.utf16.endIndex)] - return substring.allSatisfy { $0.isWhitespace } - }() - - if isMultipleLine || isSpaceOnlyBeforeStartPositionOnTheSameLine { - // when there are multiple lines start from char 0 so that it can keep the - // indentation. - selection.start = .init(line: selection.start.line, character: 0) - } - return ( - editor.selectedCode(in: selection), - .init( - start: .init(line: selection.start.line, character: selection.start.character), - end: .init(line: selection.end.line, character: selection.end.character) - ) - ) - }() as (String, CursorRange) - - let store = Service.shared.guiController.store - - let customCommandTemplateProcessor = CustomCommandTemplateProcessor() - - let newExtraSystemPrompt: String? = if let extraSystemPrompt { - await customCommandTemplateProcessor.process(extraSystemPrompt) - } else { - nil - } - - let newPrompt: String? = if let prompt { - await customCommandTemplateProcessor.process(prompt) - } else { - nil - } - - _ = await Task { @MainActor in - // if there is already a prompt to code presenting, we should not present another one - store.send(.promptToCodeGroup(.activateOrCreatePromptToCode(.init( - code: code, - selectionRange: selection, - language: codeLanguage, - identSize: filespace.codeMetadata.indentSize ?? 4, - usesTabsForIndentation: filespace.codeMetadata.usesTabsForIndentation ?? false, - documentURL: fileURL, - projectRootURL: workspace.projectRootURL, - allCode: editor.content, - allLines: editor.lines, - isContinuous: isContinuous, - commandName: name, - defaultPrompt: newPrompt ?? "", - extraSystemPrompt: newExtraSystemPrompt, - generateDescriptionRequirement: generateDescription - )))) - }.result - } - - // not used feature - // commit it to avoid init error for ChatService - func executeSingleRoundDialog( - systemPrompt: String?, - overwriteSystemPrompt: Bool, - prompt: String, - receiveReplyInNotification: Bool - ) async throws { -// guard !prompt.isEmpty else { return } -// let service = ChatService.service() -// -// let result = try await service.handleSingleRoundDialogCommand( -// systemPrompt: systemPrompt, -// overwriteSystemPrompt: overwriteSystemPrompt, -// prompt: prompt -// ) -// -// guard receiveReplyInNotification else { return } -// -// let granted = try await UNUserNotificationCenter.current() -// .requestAuthorization(options: [.alert]) -// -// if granted { -// let content = UNMutableNotificationContent() -// content.title = "Reply" -// content.body = result -// let request = UNNotificationRequest( -// identifier: "reply", -// content: content, -// trigger: nil -// ) -// do { -// try await UNUserNotificationCenter.current().add(request) -// } catch { -// presenter.presentError(error) -// } -// } else { -// presenter.presentErrorMessage("Notification permission is not granted.") -// } - } -} - diff --git a/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift b/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift deleted file mode 100644 index 4007a06c..00000000 --- a/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift +++ /dev/null @@ -1,80 +0,0 @@ -import ChatService -import Foundation -import ChatAPIService -import SuggestionBasic -import SuggestionWidget - -struct PresentInWindowSuggestionPresenter { - func presentSuggestion(fileURL: URL) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.suggestCode() - } - } - - func expandSuggestion(fileURL: URL) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.expandSuggestion() - } - } - - func discardSuggestion(fileURL: URL) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.discardSuggestion() - } - } - - func markAsProcessing(_ isProcessing: Bool) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.markAsProcessing(isProcessing) - } - } - - func presentError(_ error: Error) { - if error is CancellationError { return } - if let urlError = error as? URLError, urlError.code == URLError.cancelled { return } - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.presentError(error.localizedDescription) - } - } - - func presentErrorMessage(_ message: String) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.presentError(message) - } - } - - func presentWarningMessage(_ message: String, url: String?) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.presentWarning(message: message, url: url) - } - } - - func dismissWarning() { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.dismissWarning() - } - } - - func closeChatRoom(fileURL: URL) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.closeChatRoom() - } - } - - func presentChatRoom(fileURL: URL) { - Task { @MainActor in - let controller = Service.shared.guiController.widgetController - controller.presentChatRoom() - } - } -} - diff --git a/Core/Sources/Service/TelemetryLogger.swift b/Core/Sources/Service/TelemetryLogger.swift deleted file mode 100644 index 1bdf3181..00000000 --- a/Core/Sources/Service/TelemetryLogger.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Logger -import Foundation -import TelemetryService - -public class TelemetryLogger: TelemetryLoggerProvider { - public func sendError( - error: any Error, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - callStackSymbols: [String] - ) { - TelemetryService.shared.sendError( - error, - category: category, - file: file, - line: line, - function: function, - from: callStackSymbols - ) - } - - public func sendError( - message: String, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - callStackSymbols: [String] - ) { - TelemetryService.shared - .sendError( - message, - category: category, - file: file, - line: line, - function: function, - from: callStackSymbols - ) - } -} diff --git a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift b/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift deleted file mode 100644 index d154aade..00000000 --- a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SuggestionProvider -import Workspace -import WorkspaceSuggestionService - -extension Workspace { - @WorkspaceActor - func cleanUp(availableTabs: Set) { - for (fileURL, _) in filespaces { - if isFilespaceExpired(fileURL: fileURL, availableTabs: availableTabs) { - openedFileRecoverableStorage.closeFile(fileURL: fileURL) - closeFilespace(fileURL: fileURL) - } - } - } - - func isFilespaceExpired(fileURL: URL, availableTabs: Set) -> Bool { - let filename = fileURL.lastPathComponent - if availableTabs.contains(filename) { return false } - guard let filespace = filespaces[fileURL] else { return true } - return filespace.isExpired - } - - func cancelInFlightRealtimeSuggestionRequests() async { - guard let suggestionService else { return } - await suggestionService.cancelRequest(workspaceInfo: .init( - workspaceURL: workspaceURL, - projectURL: projectRootURL - )) - } -} - diff --git a/Core/Sources/Service/XPCService.swift b/Core/Sources/Service/XPCService.swift deleted file mode 100644 index 0297224a..00000000 --- a/Core/Sources/Service/XPCService.swift +++ /dev/null @@ -1,348 +0,0 @@ -import AppKit -import Foundation -import GitHubCopilotService -import LanguageServerProtocol -import Logger -import Preferences -import Status -import XPCShared -import HostAppActivator -import XcodeInspector -import GitHubCopilotViewModel - -public class XPCService: NSObject, XPCServiceProtocol { - // MARK: - Service - - public func getXPCServiceVersion(withReply reply: @escaping (String, String) -> Void) { - reply( - Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A", - Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "N/A" - ) - } - - public func getXPCCLSVersion(withReply reply: @escaping (String?) -> Void) { - Task { @MainActor in - do { - let service = try GitHubCopilotViewModel.shared.getGitHubCopilotAuthService() - let version = try await service.version() - reply(version) - } catch { - Logger.service.error("Failed to get CLS version: \(error.localizedDescription)") - reply(nil) - } - } - } - - public func getXPCServiceAccessibilityPermission(withReply reply: @escaping (ObservedAXStatus) -> Void) { - Task { - reply(await Status.shared.getAXStatus()) - } - } - - public func getXPCServiceExtensionPermission( - withReply reply: @escaping (ExtensionPermissionStatus) -> Void - ) { - Task { - reply(await Status.shared.getExtensionStatus()) - } - } - - // MARK: - Suggestion - - @discardableResult - private func replyWithUpdatedContent( - editorContent: Data, - file: StaticString = #file, - line: UInt = #line, - isRealtimeSuggestionRelatedCommand: Bool = false, - withReply reply: @escaping (Data?, Error?) -> Void, - getUpdatedContent: @escaping @ServiceActor ( - SuggestionCommandHandler, - EditorContent - ) async throws -> UpdatedContent? - ) -> Task { - let task = Task { - do { - let editor = try JSONDecoder().decode(EditorContent.self, from: editorContent) - let handler: SuggestionCommandHandler = WindowBaseCommandHandler() - try Task.checkCancellation() - guard let updatedContent = try await getUpdatedContent(handler, editor) else { - reply(nil, nil) - return - } - try Task.checkCancellation() - try reply(JSONEncoder().encode(updatedContent), nil) - } catch { - Logger.service.error("\(file):\(line) \(error.localizedDescription)") - reply(nil, NSError.from(error)) - } - } - - Task { - await Service.shared.realtimeSuggestionController.cancelInFlightTasks(excluding: task) - } - return task - } - - public func getSuggestedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.presentSuggestions(editor: editor) - } - } - - public func getNextSuggestedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.presentNextSuggestion(editor: editor) - } - } - - public func getPreviousSuggestedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.presentPreviousSuggestion(editor: editor) - } - } - - public func getSuggestionRejectedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.rejectSuggestion(editor: editor) - } - } - - public func getSuggestionAcceptedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.acceptSuggestion(editor: editor) - } - } - - public func getPromptToCodeAcceptedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.acceptPromptToCode(editor: editor) - } - } - - public func getRealtimeSuggestedCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent( - editorContent: editorContent, - isRealtimeSuggestionRelatedCommand: true, - withReply: reply - ) { handler, editor in - try await handler.presentRealtimeSuggestions(editor: editor) - } - } - - public func prefetchRealtimeSuggestions( - editorContent: Data, - withReply reply: @escaping () -> Void - ) { - // We don't need to wait for this. - reply() - - replyWithUpdatedContent( - editorContent: editorContent, - isRealtimeSuggestionRelatedCommand: true, - withReply: { _, _ in } - ) { handler, editor in - try await handler.generateRealtimeSuggestions(editor: editor) - } - } - - public func openChat( - withReply reply: @escaping (Error?) -> Void - ) { - Task { - do { - // Check if app is already running - if let _ = getRunningHostApp() { - // App is already running, use the chat service - let handler = PseudoCommandHandler() - handler.openChat(forceDetach: true) - } else { - try launchHostAppDefault() - } - reply(nil) - } catch { - reply(error) - } - } - } - - public func promptToCode( - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.promptToCode(editor: editor) - } - } - - public func customCommand( - id: String, - editorContent: Data, - withReply reply: @escaping (Data?, Error?) -> Void - ) { - replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in - try await handler.customCommand(id: id, editor: editor) - } - } - - // MARK: - Settings - - public func toggleRealtimeSuggestion(withReply reply: @escaping (Error?) -> Void) { - guard AXIsProcessTrusted() else { - reply(NoAccessToAccessibilityAPIError()) - return - } - Task { @ServiceActor in - await Service.shared.realtimeSuggestionController.cancelInFlightTasks() - let on = !UserDefaults.shared.value(for: \.realtimeSuggestionToggle) - UserDefaults.shared.set(on, for: \.realtimeSuggestionToggle) - Task { @MainActor in - Service.shared.guiController.store - .send(.suggestionWidget(.toastPanel(.toast(.toast( - "Real-time suggestion is turned \(on ? "on" : "off")", - .info, - nil - ))))) - } - reply(nil) - } - } - - public func postNotification(name: String, withReply reply: @escaping () -> Void) { - reply() - NotificationCenter.default.post(name: .init(name), object: nil) - } - - public func quit(reply: @escaping () -> Void) { - Task { - await Service.shared.prepareForExit() - reply() - } - } - - // MARK: - Requests - - public func send( - endpoint: String, - requestBody: Data, - reply: @escaping (Data?, Error?) -> Void - ) { - Service.shared.handleXPCServiceRequests( - endpoint: endpoint, - requestBody: requestBody, - reply: reply - ) - } - - // MARK: - XcodeInspector - - public func getXcodeInspectorData(withReply reply: @escaping (Data?, Error?) -> Void) { - do { - // Capture current XcodeInspector data - let inspectorData = XcodeInspectorData( - activeWorkspaceURL: XcodeInspector.shared.activeWorkspaceURL?.absoluteString, - activeProjectRootURL: XcodeInspector.shared.activeProjectRootURL?.absoluteString, - realtimeActiveWorkspaceURL: XcodeInspector.shared.realtimeActiveWorkspaceURL?.absoluteString, - realtimeActiveProjectURL: XcodeInspector.shared.realtimeActiveProjectURL?.absoluteString, - latestNonRootWorkspaceURL: XcodeInspector.shared.latestNonRootWorkspaceURL?.absoluteString - ) - - // Encode and send the data - let data = try JSONEncoder().encode(inspectorData) - reply(data, nil) - } catch { - Logger.service.error("Failed to encode XcodeInspector data: \(error.localizedDescription)") - reply(nil, error) - } - } - - // MARK: - MCP Server Tools - public func getAvailableMCPServerToolsCollections(withReply reply: @escaping (Data?) -> Void) { - let availableMCPServerTools = CopilotMCPToolManager.getAvailableMCPServerToolsCollections() - if let availableMCPServerTools = availableMCPServerTools { - // Encode and send the data - let data = try? JSONEncoder().encode(availableMCPServerTools) - reply(data) - } else { - reply(nil) - } - } - - public func updateMCPServerToolsStatus(tools: Data) { - // Decode the data - let decoder = JSONDecoder() - var collections: [UpdateMCPToolsStatusServerCollection] = [] - do { - collections = try decoder.decode([UpdateMCPToolsStatusServerCollection].self, from: tools) - if collections.isEmpty { - return - } - } catch { - Logger.service.error("Failed to decode MCP server collections: \(error)") - return - } - - Task { @MainActor in - await GitHubCopilotService.updateAllClsMCP(collections: collections) - } - } - - // MARK: - FeatureFlags - public func getCopilotFeatureFlags( - withReply reply: @escaping (Data?) -> Void - ) { - let featureFlags = FeatureFlagNotifierImpl.shared.featureFlags - let data = try? JSONEncoder().encode(featureFlags) - reply(data) - } - - // MARK: - Auth - public func signOutAllGitHubCopilotService() { - Task { @MainActor in - do { - try await GitHubCopilotService.signOutAll() - } catch { - Logger.service.error("Failed to sign out all: \(error)") - } - } - } - - public func getXPCServiceAuthStatus(withReply reply: @escaping (Data?) -> Void) { - Task { @MainActor in - let service = try GitHubCopilotViewModel.shared.getGitHubCopilotAuthService() - _ = try await service.checkStatus() - let authStatus = await Status.shared.getAuthStatus() - let data = try? JSONEncoder().encode(authStatus) - reply(data) - } - } -} - -struct NoAccessToAccessibilityAPIError: Error, LocalizedError { - var errorDescription: String? { - "Accessibility API permission is not granted. Please enable in System Settings.app." - } - - init() {} -} diff --git a/Core/Sources/SuggestionInjector/SuggestionInjector.swift b/Core/Sources/SuggestionInjector/SuggestionInjector.swift deleted file mode 100644 index df78acf5..00000000 --- a/Core/Sources/SuggestionInjector/SuggestionInjector.swift +++ /dev/null @@ -1,234 +0,0 @@ -import Foundation -import SuggestionBasic - -// NOTE: Every lines from Xcode Extension has a line break at its end, even the last line. -// NOTE: Copilot's completion always start at character 0, no matter where the cursor is. - -public struct SuggestionInjector { - public init() {} - - public struct ExtraInfo { - public var didChangeContent = false - public var didChangeCursorPosition = false - public var suggestionRange: ClosedRange? - public var modifications: [Modification] = [] - public init() {} - } - - public func acceptSuggestion( - intoContentWithoutSuggestion content: inout [String], - cursorPosition: inout CursorPosition, - completion: CodeSuggestion, - extraInfo: inout ExtraInfo, - suggestionLineLimit: Int? = nil - ) { - extraInfo.didChangeContent = true - extraInfo.didChangeCursorPosition = true - extraInfo.suggestionRange = nil - let start = completion.range.start - let end = completion.range.end - let suggestionContent = completion.text - let lineEnding = if let ending = content.first?.last, ending.isNewline { - String(ending) - } else { - "\n" - } - - let firstRemovedLine = content[safe: start.line] - let lastRemovedLine = content[safe: end.line] - let startLine = max(0, start.line) - let endLine = max(start.line, min(end.line, content.endIndex - 1)) - if startLine < content.endIndex { - extraInfo.modifications.append(.deleted(startLine...endLine)) - content.removeSubrange(startLine...endLine) - } - - var toBeInserted = suggestionContent.breakLines( - proposedLineEnding: lineEnding, - appendLineBreakToLastLine: true - ) - - if let suggestionLineLimit { - let allLines = toBeInserted - toBeInserted = Array(toBeInserted.prefix(suggestionLineLimit)) - if suggestionLineLimit < allLines.count { - // advance to the next line when accepting part of a multi-line suggestion - toBeInserted.append(startOfLine(line: allLines[suggestionLineLimit], usingEnding: lineEnding)) - } - } - // prepending prefix text not in range if needed. - if let firstRemovedLine, - !firstRemovedLine.isEmptyOrNewLine, - start.character > 0, - start.character < firstRemovedLine.count, - !toBeInserted.isEmpty - { - let leftoverRange = firstRemovedLine.utf16.startIndex..<(firstRemovedLine.utf16.index( - firstRemovedLine.utf16.startIndex, - offsetBy: start.character, - limitedBy: firstRemovedLine.utf16.endIndex - ) ?? firstRemovedLine.utf16.endIndex) - var leftover = String(firstRemovedLine[leftoverRange]) - if leftover.last?.isNewline ?? false { - leftover.removeLast(1) - } - toBeInserted[0].insert( - contentsOf: leftover, - at: toBeInserted[0].startIndex - ) - } - - let recoveredSuffixLength = recoverSuffixIfNeeded( - endOfReplacedContent: end, - toBeInserted: &toBeInserted, - lastRemovedLine: lastRemovedLine, - lineEnding: lineEnding - ) - - let cursorCol = toBeInserted[toBeInserted.endIndex - 1].utf16.count - - 1 - recoveredSuffixLength - let insertingIndex = min(start.line, content.endIndex) - content.insert(contentsOf: toBeInserted, at: insertingIndex) - extraInfo.modifications.append(.inserted(insertingIndex, toBeInserted)) - cursorPosition = .init( - line: startLine + toBeInserted.count - 1, - character: max(0, cursorCol) - ) - } - - func startOfLine(line: String, usingEnding lineEnding: String) -> String { - return line.prefix(while: { $0.isWhitespace }).appending(lineEnding) - } - - func recoverSuffixIfNeeded( - endOfReplacedContent end: CursorPosition, - toBeInserted: inout [String], - lastRemovedLine: String?, - lineEnding: String - ) -> Int { - // If there is no line removed, there is no need to recover anything. - guard let lastRemovedLine, !lastRemovedLine.isEmptyOrNewLine else { return 0 } - - let lastRemovedLineCleaned = lastRemovedLine.droppedLineBreak() - - // If the replaced range covers the whole line, return immediately. - guard end.character >= 0, end.character - 1 < lastRemovedLineCleaned.utf16.count - else { return 0 } - - // if we are not inserting anything, return immediately. - guard !toBeInserted.isEmpty, - let first = toBeInserted.first?.droppedLineBreak(), !first.isEmpty, - let last = toBeInserted.last?.droppedLineBreak(), !last.isEmpty - else { return 0 } - - // case 1: user keeps typing as the suggestion suggests. - - if first.hasPrefix(lastRemovedLineCleaned) { - return 0 - } - - // case 2: user also typed the suffix of the suggestion (or auto-completed by Xcode) - - // locate the split index, the prefix of which matches the suggestion prefix. - var splitIndex: String.Index? - - for offset in end.character..` - - let regex = try! NSRegularExpression(pattern: "\\s*?<#.*?#>") - - if let firstPlaceholderRange = regex.firstMatch( - in: suffix, - options: [], - range: NSRange(suffix.startIndex..., in: suffix) - )?.range, - firstPlaceholderRange.location == 0, - let r = Range(firstPlaceholderRange, in: suffix) - { - suffix.removeSubrange(r) - } - - let lastInsertingLine = toBeInserted[toBeInserted.endIndex - 1] - .droppedLineBreak() - .appending(suffix) - .recoveredLineBreak(lineEnding: lineEnding) - - toBeInserted[toBeInserted.endIndex - 1] = lastInsertingLine - - return suffix.utf16.count - } -} - -public struct SuggestionAnalyzer { - struct Result { - enum InsertPostion { - case currentLine - case nextLine - } - - var insertPosition: InsertPostion - var commonPrefix: String? - } - - func analyze() -> Result { - fatalError() - } -} - -extension String { - var isEmptyOrNewLine: Bool { - isEmpty || self == "\n" || self == "\r\n" || self == "\r" - } - - func droppedLineBreak() -> String { - if last?.isNewline ?? false { - return String(dropLast(1)) - } - return self - } - - func recoveredLineBreak(lineEnding: String) -> String { - if hasSuffix(lineEnding) { - return self - } - return self + lineEnding - } -} - -func longestCommonPrefix(of a: String, and b: String) -> String { - let length = min(a.count, b.count) - - var prefix = "" - for i in 0.. Element? { - indices.contains(index) ? self[index] : nil - } -} - diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift deleted file mode 100644 index 2802d787..00000000 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ /dev/null @@ -1,86 +0,0 @@ -import BuiltinExtension -import struct CopilotForXcodeKit.WorkspaceInfo -import Foundation -import GitHubCopilotService -import Preferences -import SuggestionBasic -import SuggestionProvider -import UserDefaultsObserver -import Workspace - -public protocol SuggestionServiceType: SuggestionServiceProvider {} - -public actor SuggestionService: SuggestionServiceType { - public var configuration: SuggestionProvider.SuggestionServiceConfiguration { - get async { await suggestionProvider.configuration } - } - - let middlewares: [SuggestionServiceMiddleware] - - let suggestionProvider: SuggestionServiceProvider - - public init( - provider: any SuggestionServiceProvider, - middlewares: [SuggestionServiceMiddleware] = SuggestionServiceMiddlewareContainer - .middlewares - ) { - suggestionProvider = provider - self.middlewares = middlewares - } - - public static func service( - for serviceType: SuggestionFeatureProvider = UserDefaults.shared - .value(for: \.suggestionFeatureProvider) - ) -> SuggestionService { - switch serviceType { - case .builtIn(.gitHubCopilot), .extension: - let provider = BuiltinExtensionSuggestionServiceProvider( - extension: GitHubCopilotExtension.self - ) - return SuggestionService(provider: provider) - } - } -} - -public extension SuggestionService { - func getSuggestions( - _ request: SuggestionRequest, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async throws -> [SuggestionBasic.CodeSuggestion] { - var getSuggestion = suggestionProvider.getSuggestions(_:workspaceInfo:) - let configuration = await configuration - - for middleware in middlewares.reversed() { - getSuggestion = { [getSuggestion] request, workspaceInfo in - try await middleware.getSuggestion( - request, - configuration: configuration, - next: { [getSuggestion] request in - try await getSuggestion(request, workspaceInfo) - } - ) - } - } - - return try await getSuggestion(request, workspaceInfo) - } - - func notifyAccepted( - _ suggestion: SuggestionBasic.CodeSuggestion, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async { - await suggestionProvider.notifyAccepted(suggestion, workspaceInfo: workspaceInfo) - } - - func notifyRejected( - _ suggestions: [SuggestionBasic.CodeSuggestion], - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async { - await suggestionProvider.notifyRejected(suggestions, workspaceInfo: workspaceInfo) - } - - func cancelRequest(workspaceInfo: CopilotForXcodeKit.WorkspaceInfo) async { - await suggestionProvider.cancelRequest(workspaceInfo: workspaceInfo) - } -} - diff --git a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift deleted file mode 100644 index d6cf456d..00000000 --- a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift +++ /dev/null @@ -1,124 +0,0 @@ -import AppKit -import ChatTab -import ComposableArchitecture -import Foundation -import SwiftUI -import ConversationTab - -final class ChatPanelWindow: NSWindow { - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } - - private let storeObserver = NSObject() - - var minimizeWindow: () -> Void = {} - - init( - store: StoreOf, - chatTabPool: ChatTabPool, - minimizeWindow: @escaping () -> Void - ) { - self.minimizeWindow = minimizeWindow - // Initialize with zero rect initially to prevent flashing - super.init( - contentRect: .zero, - styleMask: [.resizable, .titled, .miniaturizable, .fullSizeContentView, .closable], - backing: .buffered, - defer: true // Use defer to prevent window from appearing immediately - ) - - titleVisibility = .hidden - addTitlebarAccessoryViewController({ - let controller = NSTitlebarAccessoryViewController() - let view = NSHostingView(rootView: ChatTitleBar(store: store)) - controller.view = view - view.frame = .init(x: 0, y: 0, width: 100, height: 40) - controller.layoutAttribute = .right - return controller - }()) - titlebarAppearsTransparent = true - isReleasedWhenClosed = false - isOpaque = false - backgroundColor = .clear - level = widgetLevel(1) - collectionBehavior = [ - .fullScreenAuxiliary, -// .transient, - .fullScreenPrimary, - .fullScreenAllowsTiling, - ] - hasShadow = true - - // Set contentView after basic configuration - contentView = NSHostingView( - rootView: ChatWindowView( - store: store, - toggleVisibility: { [weak self] isDisplayed in - guard let self else { return } - self.isPanelDisplayed = isDisplayed - } - ) - .environment(\.chatTabPool, chatTabPool) - ) - - // Initialize as invisible first - alphaValue = 0 - isPanelDisplayed = false - setIsVisible(true) - - storeObserver.observe { [weak self] in - guard let self else { return } - let isDetached = store.isDetached - Task { @MainActor in - if UserDefaults.shared.value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) { - self.setFloatOnTop(!isDetached) - } else { - self.setFloatOnTop(true) - } - } - } - - setInitialFrame() - } - - private func setInitialFrame() { - let frame = UpdateLocationStrategy.getChatPanelFrame() - setFrame(frame, display: false, animate: true) - } - - func setFloatOnTop(_ isFloatOnTop: Bool) { - let targetLevel: NSWindow.Level = isFloatOnTop - ? .init(NSWindow.Level.floating.rawValue + 1) - : .normal - - if targetLevel != level { - level = targetLevel - } - } - - var isWindowHidden: Bool = false { - didSet { - alphaValue = isPanelDisplayed && !isWindowHidden ? 1 : 0 - } - } - - var isPanelDisplayed: Bool = false { - didSet { - alphaValue = isPanelDisplayed && !isWindowHidden ? 1 : 0 - } - } - - override var alphaValue: CGFloat { - didSet { - ignoresMouseEvents = alphaValue <= 0 - } - } - - override func miniaturize(_: Any?) { - minimizeWindow() - } - - override func close() { - minimizeWindow() - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/ChatHistoryView.swift b/Core/Sources/SuggestionWidget/ChatWindow/ChatHistoryView.swift deleted file mode 100644 index 3817c812..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/ChatHistoryView.swift +++ /dev/null @@ -1,279 +0,0 @@ -import ActiveApplicationMonitor -import ConversationTab -import AppKit -import ComposableArchitecture -import SwiftUI -import ChatTab -import SharedUIComponents -import PersistMiddleware - - -struct ChatHistoryView: View { - let store: StoreOf - @Environment(\.chatTabPool) var chatTabPool - @Binding var isChatHistoryVisible: Bool - @State private var searchText = "" - - var body: some View { - WithPerceptionTracking { - - VStack(alignment: .center, spacing: 0) { - Header(isChatHistoryVisible: $isChatHistoryVisible) - .frame(height: 32) - .padding(.leading, 16) - .padding(.trailing, 12) - - Divider() - - ChatHistorySearchBarView(searchText: $searchText) - .padding(.horizontal, 16) - .padding(.vertical, 4) - - ItemView(store: store, searchText: $searchText, isChatHistoryVisible: $isChatHistoryVisible) - .padding(.horizontal, 16) - } - } - } - - struct Header: View { - @Binding var isChatHistoryVisible: Bool - @AppStorage(\.chatFontSize) var chatFontSize - - var body: some View { - HStack { - Text("Chat History") - .font(.system(size: 13, weight: .bold)) - .lineLimit(nil) - - Spacer() - - Button(action: { - isChatHistoryVisible = false - }) { - Image(systemName: "xmark") - } - .buttonStyle(HoverButtonStyle()) - .help("Close") - } - } - } - - struct ItemView: View { - let store: StoreOf - @Binding var searchText: String - @Binding var isChatHistoryVisible: Bool - @State private var storedChatTabPreviewInfos: [ChatTabPreviewInfo] = [] - - @Environment(\.chatTabPool) var chatTabPool - - var body: some View { - WithPerceptionTracking { - ScrollView { - LazyVStack(alignment: .leading, spacing: 0) { - ForEach(filteredTabInfo, id: \.id) { previewInfo in - ChatHistoryItemView( - store: store, - previewInfo: previewInfo, - isChatHistoryVisible: $isChatHistoryVisible - ) { - refreshStoredChatTabInfos() - } - .id(previewInfo.id) - .frame(height: 61) - } - } - } - .onAppear { refreshStoredChatTabInfos() } - } - } - - func refreshStoredChatTabInfos() -> Void { - Task { - if let workspacePath = store.chatHistory.selectedWorkspacePath, - let username = store.chatHistory.currentUsername - { - storedChatTabPreviewInfos = ChatTabPreviewInfoStore.getAll(with: .init(workspacePath: workspacePath, username: username)) - } - } - } - - var filteredTabInfo: IdentifiedArray { - // Only compute when view is visible to prevent unnecessary computation - if !isChatHistoryVisible { - return IdentifiedArray(uniqueElements: []) - } - - guard !searchText.isEmpty else { return IdentifiedArray(uniqueElements: storedChatTabPreviewInfos) } - - let result = storedChatTabPreviewInfos.filter { info in - return (info.title ?? "New Chat").localizedCaseInsensitiveContains(searchText) - } - - return IdentifiedArray(uniqueElements: result) - } - } -} - - -struct ChatHistorySearchBarView: View { - @Binding var searchText: String - @FocusState private var isSearchBarFocused: Bool - - var body: some View { - HStack(spacing: 5) { - Image(systemName: "magnifyingglass") - .foregroundColor(.secondary) - - TextField("Search", text: $searchText) - .textFieldStyle(PlainTextFieldStyle()) - .focused($isSearchBarFocused) - .foregroundColor(searchText.isEmpty ? Color(nsColor: .placeholderTextColor) : Color(nsColor: .textColor)) - } - .cornerRadius(10) - .padding(.vertical, 8) - .padding(.horizontal, 12) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.1)) - ) - .onAppear { - isSearchBarFocused = true - } - } -} - -struct ChatHistoryItemView: View { - let store: StoreOf - let previewInfo: ChatTabPreviewInfo - @Binding var isChatHistoryVisible: Bool - @State private var isHovered = false - - let onDelete: () -> Void - - func isTabSelected() -> Bool { - return store.state.currentChatWorkspace?.selectedTabId == previewInfo.id - } - - func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "MMMM d, yyyy, h:mm a" - return formatter.string(from: date) - } - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - HStack(alignment: .center, spacing: 0) { - VStack(spacing: 4) { - HStack(spacing: 8) { - // Do not use the `ChatConversationItemView` any more - // directly get title from chat tab info - Text(previewInfo.title ?? "New Chat") - .frame(alignment: .leading) - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(.primary) - .lineLimit(1) - - if isTabSelected() { - Text("Current") - .foregroundStyle(.secondary) - } - - Spacer() - } - - HStack(spacing: 0) { - Text(formatDate(previewInfo.updatedAt)) - .frame(alignment: .leading) - .font(.system(size: 13, weight: .regular)) - .foregroundColor(.secondary) - .lineLimit(1) - - Spacer() - } - } - - Spacer() - - if !isTabSelected() { - Button(action: { - Task { @MainActor in - await store.send(.chatHistoryDeleteButtonClicked(id: previewInfo.id)).finish() - onDelete() - } - }) { - Image(systemName: "trash") - .foregroundColor(.primary) - .opacity(isHovered ? 1 : 0) - } - .buttonStyle(HoverButtonStyle()) - .help("Delete") - .allowsHitTesting(isHovered) - } - } - .padding(.horizontal, 12) - } - .frame(maxHeight: .infinity) - .onHover(perform: { - isHovered = $0 - }) - .hoverRadiusBackground( - isHovered: isHovered, - hoverColor: Color(nsColor: .textBackgroundColor.withAlphaComponent(0.55)), - cornerRadius: 4, - showBorder: isHovered, - borderColor: Color(nsColor: .separatorColor) - ) - .onTapGesture { - Task { @MainActor in - await store.send(.chatHistoryItemClicked(id: previewInfo.id)).finish() - isChatHistoryVisible = false - } - } - } - } -} - -struct ChatHistoryView_Previews: PreviewProvider { - static let pool = ChatTabPool([ - "2": EmptyChatTab(id: "2"), - "3": EmptyChatTab(id: "3"), - "4": EmptyChatTab(id: "4"), - "5": EmptyChatTab(id: "5"), - "6": EmptyChatTab(id: "6") - ]) - - static func createStore() -> StoreOf { - StoreOf( - initialState: .init( - chatHistory: .init( - workspaces: [.init( - id: .init(path: "p", username: "u"), - tabInfo: [ - .init(id: "2", title: "Empty-2", workspacePath: "path", username: "username"), - .init(id: "3", title: "Empty-3", workspacePath: "path", username: "username"), - .init(id: "4", title: "Empty-4", workspacePath: "path", username: "username"), - .init(id: "5", title: "Empty-5", workspacePath: "path", username: "username"), - .init(id: "6", title: "Empty-6", workspacePath: "path", username: "username") - ] as IdentifiedArray, - selectedTabId: "2" - ) { _ in }] as IdentifiedArray, - selectedWorkspacePath: "activeWorkspacePath", - selectedWorkspaceName: "activeWorkspacePath" - ), - isPanelDisplayed: true - ), - reducer: { ChatPanelFeature() } - ) - } - - static var previews: some View { - ChatHistoryView( - store: createStore(), - isChatHistoryVisible: .constant(true) - ) - .xcodeStyleFrame() - .padding() - .environment(\.chatTabPool, pool) - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/ChatLoginView.swift b/Core/Sources/SuggestionWidget/ChatWindow/ChatLoginView.swift deleted file mode 100644 index 871dd24e..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/ChatLoginView.swift +++ /dev/null @@ -1,91 +0,0 @@ -import SwiftUI -import Perception -import GitHubCopilotViewModel -import SharedUIComponents - -struct ChatLoginView: View { - @StateObject var viewModel: GitHubCopilotViewModel - @Environment(\.openURL) private var openURL - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0){ - VStack(spacing: 24) { - Spacer() - VStack(spacing: 8) { - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 60.0, height: 60.0) - .foregroundColor(.secondary) - - Text("Welcome to Copilot") - .font(.largeTitle) - .multilineTextAlignment(.center) - - Text("Your AI-powered coding assistant") - .font(.body) - .multilineTextAlignment(.center) - } - - CopilotIntroView() - - VStack(spacing: 8) { - Button("Sign Up for Copilot Free") { - if let url = URL(string: "https://github.com/features/copilot/plans") { - openURL(url) - } - } - .buttonStyle(.borderedProminent) - - HStack{ - Text("Already have an account?") - Button("Sign In") { viewModel.signIn() } - .buttonStyle(.borderless) - .foregroundColor(Color("TextLinkForegroundColor")) - - if viewModel.isRunningAction || viewModel.waitingForSignIn { - ProgressView() - .controlSize(.small) - } - } - } - .padding(.top, 16) - - Spacer() - Text("Copilot Free and Copilot Pro may show [public code](https://aka.ms/github-copilot-match-public-code) suggestions and collect telemetry. You can change these [GitHub settings](https://aka.ms/github-copilot-settings) at any time. By continuing, you agree to our [terms](https://github.com/customer-terms/github-copilot-product-specific-terms) and [privacy policy](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement).") - .font(.system(size: 12)) - } - .padding() - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - .alert( - viewModel.signInResponse?.userCode ?? "", - isPresented: $viewModel.isSignInAlertPresented, - presenting: viewModel.signInResponse - ) { _ in - Button("Cancel", role: .cancel, action: {}) - Button("Copy Code and Open", action: viewModel.copyAndOpen) - } message: { response in - Text(""" - Please enter the above code in the GitHub website \ - to authorize your GitHub account with Copilot for Xcode. - - \(response?.verificationURL.absoluteString ?? "") - """) - } - } - } -} - -struct ChatLoginView_Previews: PreviewProvider { - static var previews: some View { - ChatLoginView(viewModel: GitHubCopilotViewModel.shared) - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoAXPermissionView.swift b/Core/Sources/SuggestionWidget/ChatWindow/ChatNoAXPermissionView.swift deleted file mode 100644 index 299c46cc..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoAXPermissionView.swift +++ /dev/null @@ -1,55 +0,0 @@ -import SwiftUI -import Perception -import SharedUIComponents - -struct ChatNoAXPermissionView: View { - @Environment(\.openURL) private var openURL - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - VStack(alignment: .center, spacing: 20) { - Spacer() - Image("CopilotError") - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 64.0, height: 64.0) - .foregroundColor(.primary) - - Text("Accessibility Permission Required") - .font(.largeTitle) - .multilineTextAlignment(.center) - - Text("Please grant accessibility permission for Github Copilot to work with Xcode.") - .font(.body) - .multilineTextAlignment(.center) - - HStack{ - Button("Open Permission Settings") { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { - openURL(url) - } - } - .buttonStyle(.borderedProminent) - } - - Spacer() - } - .padding() - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - } - } -} - -struct ChatNoAXPermission_Previews: PreviewProvider { - static var previews: some View { - ChatNoAXPermissionView() - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoSubscriptionView.swift b/Core/Sources/SuggestionWidget/ChatWindow/ChatNoSubscriptionView.swift deleted file mode 100644 index 5c052411..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoSubscriptionView.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI -import Perception -import GitHubCopilotViewModel -import SharedUIComponents - -struct ChatNoSubscriptionView: View { - @StateObject var viewModel: GitHubCopilotViewModel - @Environment(\.openURL) private var openURL - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - VStack(alignment: .center, spacing: 20) { - Spacer() - Image("CopilotIssue") - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 60.0, height: 60.0) - .foregroundColor(.primary) - - Text("No Copilot Subscription Found") - .font(.system(size: 24)) - .multilineTextAlignment(.center) - - Text("Request a license from your organization manager \nor start a 30-day [free trial](https://github.com/github-copilot/signup/copilot_individual) to explore Copilot") - .font(.system(size: 12)) - .multilineTextAlignment(.center) - - HStack{ - Button("Check Subscription Plans") { - if let url = URL(string: "https://github.com/settings/copilot") { - openURL(url) - } - } - .buttonStyle(.borderedProminent) - - Button("Retry") { viewModel.checkStatus() } - .buttonStyle(.bordered) - - if viewModel.isRunningAction || viewModel.waitingForSignIn { - ProgressView() - .controlSize(.small) - } - } - - Spacer() - - Text("Copilot Free and Copilot Pro may show [public code](https://aka.ms/github-copilot-match-public-code) suggestions and collect telemetry. You can change these [GitHub settings](https://aka.ms/github-copilot-settings) at any time. By continuing, you agree to our [terms](https://github.com/customer-terms/github-copilot-product-specific-terms) and [privacy policy](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement).") - .font(.system(size: 12)) - } - .padding() - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - } - } -} - -struct ChatNoSubcription_Previews: PreviewProvider { - static var previews: some View { - ChatNoSubscriptionView(viewModel: GitHubCopilotViewModel.shared) - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoWorkspaceView.swift b/Core/Sources/SuggestionWidget/ChatWindow/ChatNoWorkspaceView.swift deleted file mode 100644 index 8d7cbf60..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/ChatNoWorkspaceView.swift +++ /dev/null @@ -1,48 +0,0 @@ -import SwiftUI -import Perception -import SharedUIComponents - -struct ChatNoWorkspaceView: View { - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - VStack(alignment: .center, spacing: 32) { - Spacer() - VStack (alignment: .center, spacing: 8) { - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 64.0, height: 64.0) - .foregroundColor(.secondary) - - Text("No Active Xcode Workspace") - .font(.largeTitle) - .multilineTextAlignment(.center) - - Text("To use Copilot, open Xcode with an active workspace in focus") - .font(.body) - .multilineTextAlignment(.center) - } - - CopilotIntroView() - - Spacer() - } - .padding() - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - } - } -} - -struct ChatNoWorkspace_Previews: PreviewProvider { - static var previews: some View { - ChatNoWorkspaceView() - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindow/CopilotIntroView.swift b/Core/Sources/SuggestionWidget/ChatWindow/CopilotIntroView.swift deleted file mode 100644 index b3d5eb5b..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindow/CopilotIntroView.swift +++ /dev/null @@ -1,110 +0,0 @@ -import SwiftUI -import Perception -import SharedUIComponents - -struct CopilotIntroView: View { - var body: some View { - WithPerceptionTracking { - VStack(alignment: .center, spacing: 8) { - CopilotIntroItemView( - imageName: "CopilotLogo", - title: "Agent Mode", - description: "Activate Agent Mode to handle multi-step coding tasks with Copilot." - ) - - CopilotIntroItemView( - systemImage: "wrench.and.screwdriver", - title: "MCP Support", - description: "Connect to MCP to extend your Copilot with custom tools and services for advanced workflows." - ) - - CopilotIntroItemView( - imageName: "ChatIcon", - title: "Ask Mode", - description: "Use Ask Mode to chat with Copilot to understand, debug, or improve your code." - ) - - CopilotIntroItemView( - systemImage: "option", - title: "Code Suggestions", - description: "Get smart code suggestions in Xcode. Press Tab ⇥ to accept a code suggestion, or Option ⌥ to see more alternatives." - ) - } - .padding(0) - .frame(maxWidth: .infinity, alignment: .center) - } - } -} - -struct CopilotIntroItemView: View { - let image: Image - let title: String - let description: String - - public init(imageName: String, title: String, description: String) { - self.init( - imageObject: Image(imageName), - title: title, - description: description - ) - } - - public init(systemImage: String, title: String, description: String) { - self.init( - imageObject: Image(systemName: systemImage), - title: title, - description: description - ) - } - - public init(imageObject: Image, title: String, description: String) { - self.image = imageObject - self.title = title - self.description = description - } - - var body: some View { - WithPerceptionTracking { - VStack(alignment: .leading, spacing: 0){ - HStack(alignment: .center, spacing: 8) { - image - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 12, height: 12) - .foregroundColor(.primary) - .padding(.leading, 8) - - Text(title) - .font(.body) - .kerning(0.096) - .multilineTextAlignment(.center) - .foregroundColor(.primary) - } - .frame(maxWidth: .infinity, alignment: .leading) - - Text(description) - .font(.body) - .foregroundColor(.secondary) - .padding(.leading, 28) - .padding(.top, 4) - .frame(maxWidth: .infinity, alignment: .topLeading) - } - .padding(8) - .frame(maxWidth: 360, alignment: .top) - .background(.primary.opacity(0.1)) - .cornerRadius(2) - .overlay( - RoundedRectangle(cornerRadius: 2) - .inset(by: 0.5) - .stroke(lineWidth: 0) - ) - } - } -} - -struct CopilotIntroView_Previews: PreviewProvider { - static var previews: some View { - CopilotIntroView() - } -} diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift deleted file mode 100644 index cc4a82a8..00000000 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ /dev/null @@ -1,578 +0,0 @@ -import ActiveApplicationMonitor -import ConversationTab -import AppKit -import ChatTab -import ComposableArchitecture -import SwiftUI -import SharedUIComponents -import GitHubCopilotViewModel -import Status -import ChatService -import Workspace - -private let r: Double = 8 - -struct ChatWindowView: View { - let store: StoreOf - let toggleVisibility: (Bool) -> Void - @State private var isChatHistoryVisible: Bool = false - @ObservedObject private var statusObserver = StatusObserver.shared - - var body: some View { - WithPerceptionTracking { - // Force re-evaluation when workspace state changes - let currentWorkspace = store.currentChatWorkspace - let _ = currentWorkspace?.selectedTabId - ZStack { - if statusObserver.observedAXStatus == .notGranted { - ChatNoAXPermissionView() - } else { - switch statusObserver.authStatus.status { - case .loggedIn: - if currentWorkspace == nil || (currentWorkspace?.tabInfo.isEmpty ?? true) { - ChatNoWorkspaceView() - } else if isChatHistoryVisible { - ChatHistoryViewWrapper(store: store, isChatHistoryVisible: $isChatHistoryVisible) - } else { - ChatView(store: store, isChatHistoryVisible: $isChatHistoryVisible) - } - case .notLoggedIn: - ChatLoginView(viewModel: GitHubCopilotViewModel.shared) - case .notAuthorized: - ChatNoSubscriptionView(viewModel: GitHubCopilotViewModel.shared) - case .unknown: - ChatLoginView(viewModel: GitHubCopilotViewModel.shared) - } - } - } - .onChange(of: store.isPanelDisplayed) { isDisplayed in - toggleVisibility(isDisplayed) - } - .preferredColorScheme(store.colorScheme) - } - } -} - -struct ChatView: View { - let store: StoreOf - @Binding var isChatHistoryVisible: Bool - - var body: some View { - VStack(spacing: 0) { - Rectangle().fill(.regularMaterial).frame(height: 28) - - Divider() - - ZStack { - VStack(spacing: 0) { - ChatBar(store: store, isChatHistoryVisible: $isChatHistoryVisible) - .frame(height: 32) - .background(Color(nsColor: .windowBackgroundColor)) - - Divider() - - ChatTabContainer(store: store) - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - } -} - -struct ChatHistoryViewWrapper: View { - let store: StoreOf - @Binding var isChatHistoryVisible: Bool - - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - Rectangle().fill(.regularMaterial).frame(height: 28) - - Divider() - - ChatHistoryView( - store: store, - isChatHistoryVisible: $isChatHistoryVisible - ) - .background(Color(nsColor: .windowBackgroundColor)) - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - .preferredColorScheme(store.colorScheme) - .focusable() - .onExitCommand(perform: { - isChatHistoryVisible = false - }) - } - } -} - -struct ChatLoadingView: View { - var body: some View { - VStack(alignment: .center) { - - Spacer() - - VStack(spacing: 24) { - Instruction(isAgentMode: .constant(false)) - - ProgressView("Loading...") - - } - .frame(maxWidth: .infinity, alignment: .center) - // keep same as chat view - .padding(.top, 20) // chat bar - - Spacer() - - } - .xcodeStyleFrame(cornerRadius: 10) - .ignoresSafeArea(edges: .top) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(nsColor: .windowBackgroundColor)) - } -} - -struct ChatTitleBar: View { - let store: StoreOf - @State var isHovering = false - @AppStorage(\.autoAttachChatToXcode) var autoAttachChatToXcode - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 6) { - Button(action: { - store.send(.closeActiveTabClicked) - }) { - EmptyView() - } - .opacity(0) - .keyboardShortcut("w", modifiers: [.command]) - - Button( - action: { - store.send(.hideButtonClicked) - } - ) { - Image(systemName: "minus") - .foregroundStyle(.black.opacity(0.5)) - .font(Font.system(size: 8).weight(.heavy)) - } - .opacity(0) - .keyboardShortcut("m", modifiers: [.command]) - - Spacer() - - if !autoAttachChatToXcode { - TrafficLightButton( - isHovering: isHovering, - isActive: store.isDetached, - color: Color(nsColor: .systemCyan), - action: { - store.send(.toggleChatPanelDetachedButtonClicked) - } - ) { - Image(systemName: "pin.fill") - .foregroundStyle(.black.opacity(0.5)) - .font(Font.system(size: 6).weight(.black)) - .transformEffect(.init(translationX: 0, y: 0.5)) - } - } - } - .buttonStyle(.plain) - .padding(.trailing, 8) - .onHover(perform: { hovering in - isHovering = hovering - }) - } - } - - struct TrafficLightButton: View { - let isHovering: Bool - let isActive: Bool - let color: Color - let action: () -> Void - let icon: () -> Icon - - @Environment(\.controlActiveState) var controlActiveState - - var body: some View { - Button(action: { - action() - }) { - Circle() - .fill( - controlActiveState == .key && isActive - ? color - : Color(nsColor: .separatorColor) - ) - .frame( - width: Style.trafficLightButtonSize, - height: Style.trafficLightButtonSize - ) - .overlay { - Circle().stroke(lineWidth: 0.5).foregroundColor(.black.opacity(0.2)) - } - .overlay { - if isHovering { - icon() - } - } - } - .focusable(false) - } - } -} - -private extension View { - func hideScrollIndicator() -> some View { - if #available(macOS 13.0, *) { - return scrollIndicators(.hidden) - } else { - return self - } - } -} - -struct ChatBar: View { - let store: StoreOf - @Binding var isChatHistoryVisible: Bool - - struct TabBarState: Equatable { - var tabInfo: IdentifiedArray - var selectedTabId: String - } - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 0) { - if store.chatHistory.selectedWorkspaceName != nil { - ChatWindowHeader(store: store) - } - - Spacer() - - CreateButton(store: store) - - ChatHistoryButton(store: store, isChatHistoryVisible: $isChatHistoryVisible) - - SettingsButton(store: store) - } - .padding(.horizontal, 12) - } - } - - struct Tabs: View { - let store: StoreOf - @Environment(\.chatTabPool) var chatTabPool - - var body: some View { - WithPerceptionTracking { - let tabInfo = store.currentChatWorkspace?.tabInfo - let selectedTabId = store.currentChatWorkspace?.selectedTabId - ?? store.currentChatWorkspace?.tabInfo.first?.id - ?? "" - ScrollViewReader { proxy in - ScrollView(.horizontal) { - HStack(spacing: 0) { - ForEach(tabInfo!, id: \.id) { info in - if let tab = chatTabPool.getTab(of: info.id) { - ChatTabBarButton( - store: store, - info: info, - content: { tab.tabItem }, - icon: { tab.icon }, - isSelected: info.id == selectedTabId - ) - .contextMenu { - tab.menu - } - .id(info.id) - } else { - EmptyView() - } - } - } - } - .hideScrollIndicator() - .onChange(of: selectedTabId) { id in - withAnimation(.easeInOut(duration: 0.2)) { - proxy.scrollTo(id) - } - } - } - } - } - } - - struct ChatWindowHeader: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - HStack(spacing: 0) { - Image("XcodeIcon") - .resizable() - .renderingMode(.original) - .scaledToFit() - .frame(width: 24, height: 24) - - Text(store.chatHistory.selectedWorkspaceName!) - .font(.system(size: 13, weight: .bold)) - .padding(.leading, 4) - .truncationMode(.tail) - .frame(maxWidth: 192, alignment: .leading) - .help(store.chatHistory.selectedWorkspacePath!) - } - } - } - } - - struct CreateButton: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - Button(action: { - store.send(.createNewTapButtonClicked(kind: nil)) - }) { - Image(systemName: "plus.bubble") - } - .buttonStyle(HoverButtonStyle()) - .padding(.horizontal, 4) - .help("New Chat") - .accessibilityLabel("New Chat") - } - } - } - - struct ChatHistoryButton: View { - let store: StoreOf - @Binding var isChatHistoryVisible: Bool - - var body: some View { - WithPerceptionTracking { - Button(action: { - isChatHistoryVisible = true - }) { - if #available(macOS 15.0, *) { - Image(systemName: "clock.arrow.trianglehead.counterclockwise.rotate.90") - } else { - Image(systemName: "clock.arrow.circlepath") - } - } - .buttonStyle(HoverButtonStyle()) - .padding(.horizontal, 4) - .help("Show Chats...") - .accessibilityLabel("Show Chats...") - } - } - } - - struct SettingsButton: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - Button(action: { - store.send(.openSettings) - }) { - Image(systemName: "gearshape") - } - .buttonStyle(HoverButtonStyle()) - .padding(.horizontal, 4) - .help("Open Settings") - .accessibilityLabel("Open Settings") - } - } - } -} - -struct ChatTabBarButton: View { - let store: StoreOf - let info: ChatTabInfo - let content: () -> Content - let icon: () -> Icon - let isSelected: Bool - @State var isHovered: Bool = false - - var body: some View { - if self.isSelected { - HStack(spacing: 0) { - HStack(spacing: 0) { - icon() - .buttonStyle(.plain) - } - .font(.callout) - .lineLimit(1) - } - .frame(maxHeight: .infinity) - } - } -} - -struct ChatTabContainer: View { - let store: StoreOf - @Environment(\.chatTabPool) var chatTabPool - @State private var pasteMonitor: Any? - - var body: some View { - WithPerceptionTracking { - let tabInfoArray = store.currentChatWorkspace?.tabInfo - let selectedTabId = store.currentChatWorkspace?.selectedTabId - ?? store.currentChatWorkspace?.tabInfo.first?.id - ?? "" - - if let tabInfoArray = tabInfoArray, !tabInfoArray.isEmpty { - activeTabsView( - tabInfoArray: tabInfoArray, - selectedTabId: selectedTabId - ) - } else { - // Fallback view for empty state (rarely seen in practice) - EmptyView().frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - .onAppear { - setupPasteMonitor() - } - .onDisappear { - removePasteMonitor() - } - } - - // View displayed when there are active tabs - private func activeTabsView( - tabInfoArray: IdentifiedArray, - selectedTabId: String - ) -> some View { - GeometryReader { geometry in - ZStack { - ForEach(tabInfoArray) { tabInfo in - if let tab = chatTabPool.getTab(of: tabInfo.id) { - let isActive = tab.id == selectedTabId - - if isActive { - // Only render the active tab with full layout - tab.body - .frame( - width: geometry.size.width, - height: geometry.size.height - ) - } else { - // Render inactive tabs with minimal footprint to avoid layout conflicts - tab.body - .frame(width: 1, height: 1) - .opacity(0) - .allowsHitTesting(false) - .clipped() - } - } - } - } - } - } - - private func setupPasteMonitor() { - pasteMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - guard event.modifierFlags.contains(.command), - event.charactersIgnoringModifiers?.lowercased() == "v" else { - return event - } - - // Find the active chat tab and forward paste event to it - if let activeConversationTab = getActiveConversationTab() { - if !activeConversationTab.handlePasteEvent() { - return event - } - } - - return nil - } - } - - private func removePasteMonitor() { - if let monitor = pasteMonitor { - NSEvent.removeMonitor(monitor) - pasteMonitor = nil - } - } - - private func getActiveConversationTab() -> ConversationTab? { - guard let selectedTabId = store.currentChatWorkspace?.selectedTabId, - let chatTab = chatTabPool.getTab(of: selectedTabId) as? ConversationTab else { - return nil - } - return chatTab - } -} - -struct CreateOtherChatTabMenuStyle: MenuStyle { - func makeBody(configuration: Configuration) -> some View { - Image(systemName: "chevron.down") - .resizable() - .frame(width: 7, height: 4) - .frame(maxHeight: .infinity) - .padding(.leading, 4) - .padding(.trailing, 8) - .foregroundColor(.secondary) - } -} - -struct ChatWindowView_Previews: PreviewProvider { - static let pool = ChatTabPool([ - "2": EmptyChatTab(id: "2"), - "3": EmptyChatTab(id: "3"), - "4": EmptyChatTab(id: "4"), - "5": EmptyChatTab(id: "5"), - "6": EmptyChatTab(id: "6"), - "7": EmptyChatTab(id: "7"), - ]) - - static func createStore() -> StoreOf { - StoreOf( - initialState: .init( - chatHistory: .init( - workspaces: [ - .init( - id: .init(path: "p", username: "u"), - tabInfo: [ - .init(id: "2", title: "Empty-2", workspacePath: "path", username: "username"), - .init(id: "3", title: "Empty-3", workspacePath: "path", username: "username"), - .init(id: "4", title: "Empty-4", workspacePath: "path", username: "username"), - .init(id: "5", title: "Empty-5", workspacePath: "path", username: "username"), - .init(id: "6", title: "Empty-6", workspacePath: "path", username: "username"), - .init(id: "7", title: "Empty-7", workspacePath: "path", username: "username"), - ] as IdentifiedArray, - selectedTabId: "2" - ) { _ in } - ] as IdentifiedArray, - selectedWorkspacePath: "activeWorkspacePath", - selectedWorkspaceName: "activeWorkspacePath" - ), - isPanelDisplayed: true - ), - reducer: { ChatPanelFeature() } - ) - } - - static var previews: some View { - ChatWindowView(store: createStore(), toggleVisibility: { _ in }) - .xcodeStyleFrame() - .padding() - .environment(\.chatTabPool, pool) - } -} - -struct ChatLoadingView_Previews: PreviewProvider { - static var previews: some View { - ChatLoadingView() - } -} diff --git a/Core/Sources/SuggestionWidget/CodeReviewPanelView.swift b/Core/Sources/SuggestionWidget/CodeReviewPanelView.swift deleted file mode 100644 index 1a64a7dc..00000000 --- a/Core/Sources/SuggestionWidget/CodeReviewPanelView.swift +++ /dev/null @@ -1,448 +0,0 @@ -import SwiftUI -import Combine -import XcodeInspector -import ComposableArchitecture -import ConversationServiceProvider -import LanguageServerProtocol -import ChatService -import SharedUIComponents -import ConversationTab - -private typealias CodeReviewPanelViewStore = ViewStore - -private struct ViewState: Equatable { - let reviewComments: [ReviewComment] - let currentSelectedComment: ReviewComment? - let currentIndex: Int - let operatedCommentIds: Set - var hasNextComment: Bool - var hasPreviousComment: Bool - - var commentsCount: Int { reviewComments.count } - - init(state: CodeReviewPanelFeature.State) { - self.reviewComments = state.currentDocumentReview?.comments ?? [] - self.currentSelectedComment = state.currentSelectedComment - self.currentIndex = state.currentIndex - self.operatedCommentIds = state.operatedCommentIds - self.hasNextComment = state.hasNextComment - self.hasPreviousComment = state.hasPreviousComment - } -} - -struct CodeReviewPanelView: View { - let store: StoreOf - - var body: some View { - WithViewStore(self.store, observe: ViewState.init) { viewStore in - WithPerceptionTracking { - VStack(spacing: 0) { - VStack(spacing: 0) { - HeaderView(viewStore: viewStore) - .padding(.bottom, 4) - - Divider() - - ContentView( - comment: viewStore.currentSelectedComment, - viewStore: viewStore - ) - .padding(.top, 16) - } - .padding(.vertical, 10) - .padding(.horizontal, 20) - .frame(maxWidth: .infinity, maxHeight: Style.codeReviewPanelHeight, alignment: .top) - .fixedSize(horizontal: false, vertical: true) - .xcodeStyleFrame(cornerRadius: 10) - .onAppear { viewStore.send(.appear) } - - Spacer() - } - } - } - } -} - -// MARK: - Header View -private struct HeaderView: View { - let viewStore: CodeReviewPanelViewStore - - var body: some View { - HStack(alignment: .center, spacing: 8) { - ZStack { - Circle() - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - .frame(width: 24, height: 24) - - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFit() - .frame(width: 12, height: 12) - } - - Text("Code Review Comment") - .font(.system(size: 13, weight: .semibold)) - .lineLimit(1) - - if viewStore.commentsCount > 0 { - Text("(\(viewStore.currentIndex + 1) of \(viewStore.commentsCount))") - .font(.caption) - .foregroundColor(.secondary) - .lineLimit(1) - } - - Spacer() - - NavigationControls(viewStore: viewStore) - } - .fixedSize(horizontal: false, vertical: true) - } -} - -// MARK: - Navigation Controls -private struct NavigationControls: View { - let viewStore: CodeReviewPanelViewStore - - var body: some View { - HStack(spacing: 4) { - if viewStore.hasPreviousComment { - Button(action: { - viewStore.send(.previous) - }) { - Image(systemName: "arrow.up") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13, height: 13) - } - .buttonStyle(HoverButtonStyle()) - .buttonStyle(PlainButtonStyle()) - .help("Previous") - } - - if viewStore.hasNextComment { - Button(action: { - viewStore.send(.next) - }) { - Image(systemName: "arrow.down") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13, height: 13) - } - .buttonStyle(HoverButtonStyle()) - .buttonStyle(PlainButtonStyle()) - .help("Next") - } - - Button(action: { - if let id = viewStore.currentSelectedComment?.id { - viewStore.send(.close(commentId: id)) - } - }) { - Image(systemName: "xmark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13, height: 13) - } - .buttonStyle(HoverButtonStyle()) - .buttonStyle(PlainButtonStyle()) - .help("Close") - } - } -} - -// MARK: - Content View -private struct ContentView: View { - let comment: ReviewComment? - let viewStore: CodeReviewPanelViewStore - - var body: some View { - if let comment = comment { - CommentDetailView(comment: comment, viewStore: viewStore) - } else { - EmptyView() - } - } -} - -// MARK: - Comment Detail View -private struct CommentDetailView: View { - let comment: ReviewComment - let viewStore: CodeReviewPanelViewStore - @AppStorage(\.chatFontSize) var chatFontSize - - var lineInfoContent: String { - let displayStartLine = comment.range.start.line + 1 - let displayEndLine = comment.range.end.line + 1 - - if displayStartLine == displayEndLine { - return "Line \(displayStartLine)" - } else { - return "Line \(displayStartLine)-\(displayEndLine)" - } - } - - var lineInfoView: some View { - Text(lineInfoContent) - .font(.system(size: chatFontSize)) - } - - var kindView: some View { - Text(comment.kind) - .font(.system(size: chatFontSize)) - .padding(.horizontal, 6) - .frame(maxHeight: 20) - .background( - RoundedRectangle(cornerRadius: 4) - .foregroundColor(.hoverColor) - ) - } - - var messageView: some View { - ScrollView { - ThemedMarkdownText( - text: comment.message, - context: .init(supportInsert: false) - ) - } - } - - var dismissButton: some View { - Button(action: { - viewStore.send(.dismiss(commentId: comment.id)) - }) { - Text("Dismiss") - } - .buttonStyle(.bordered) - .foregroundColor(.primary) - .help("Dismiss") - } - - var acceptButton: some View { - Button(action: { - viewStore.send(.accept(commentId: comment.id)) - }) { - Text("Accept") - } - .buttonStyle(.borderedProminent) - .help("Accept") - } - - private var fileURL: URL? { - URL(string: comment.uri) - } - - var fileNameView: some View { - HStack(spacing: 8) { - drawFileIcon(fileURL) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - - Text(fileURL?.lastPathComponent ?? comment.uri) - .fontWeight(.semibold) - .lineLimit(1) - .truncationMode(.middle) - } - } - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - // Compact header with range info and badges in one line - HStack(alignment: .center, spacing: 8) { - fileNameView - - Spacer() - - lineInfoView - - kindView - } - - messageView - .frame(maxHeight: 100) - .fixedSize(horizontal: false, vertical: true) - - // Add suggested change view if suggestion exists - if let suggestion = comment.suggestion, - !suggestion.isEmpty, - let fileUrl = URL(string: comment.uri), - let content = try? String(contentsOf: fileUrl) - { - SuggestedChangeView( - suggestion: suggestion, - content: content, - range: comment.range, - chatFontSize: chatFontSize - ) - - if !viewStore.operatedCommentIds.contains(comment.id) { - HStack(spacing: 9) { - Spacer() - - dismissButton - - acceptButton - } - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -// MARK: - Suggested Change View -private struct SuggestedChangeView: View { - let suggestion: String - let content: String - let range: LSPRange - let chatFontSize: CGFloat - - struct DiffLine { - let content: String - let lineNumber: Int - let type: DiffLineType - } - - enum DiffLineType { - case removed - case added - } - - var diffLines: [DiffLine] { - var lines: [DiffLine] = [] - - // Add removed lines - let contentLines = content.components(separatedBy: .newlines) - if range.start.line >= 0 && range.end.line < contentLines.count { - let removedLines = Array(contentLines[range.start.line...range.end.line]) - for (index, lineContent) in removedLines.enumerated() { - lines.append(DiffLine( - content: lineContent, - lineNumber: range.start.line + index + 1, - type: .removed - )) - } - } - - // Add suggested lines - let suggestionLines = suggestion.components(separatedBy: .newlines) - for (index, lineContent) in suggestionLines.enumerated() { - lines.append(DiffLine( - content: lineContent, - lineNumber: range.start.line + index + 1, - type: .added - )) - } - - return lines - } - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - HStack { - Text("Suggested change") - .font(.system(size: chatFontSize, weight: .regular)) - .foregroundColor(.secondary) - - Spacer() - } - .padding(.leading, 8) - .padding(.vertical, 6) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color(NSColor.separatorColor), lineWidth: 0.5) - ) - - Rectangle() - .fill(.ultraThickMaterial) - .frame(height: 1) - - ScrollView { - LazyVStack(spacing: 0) { - ForEach(diffLines.indices, id: \.self) { index in - DiffLineView( - line: diffLines[index], - chatFontSize: chatFontSize - ) - } - } - } - .frame(maxHeight: 150) - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: 4) - .fill(.ultraThickMaterial) - ) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } -} - -// MARK: - Diff Line View -private struct DiffLineView: View { - let line: SuggestedChangeView.DiffLine - let chatFontSize: CGFloat - @State private var contentHeight: CGFloat = 0 - - private var backgroundColor: SwiftUICore.Color { - switch line.type { - case .removed: - return Color("editorOverviewRuler.inlineChatRemoved") - case .added: - return Color("editor.focusedStackFrameHighlightBackground") - } - } - - private var lineNumberBackgroundColor: SwiftUICore.Color { - switch line.type { - case .removed: - return Color("gitDecoration.deletedResourceForeground") - case .added: - return Color("gitDecoration.addedResourceForeground") - } - } - - private var prefix: String { - switch line.type { - case .removed: - return "-" - case .added: - return "+" - } - } - - var body: some View { - HStack(spacing: 0) { - HStack(alignment: .top, spacing: 0) { - HStack(spacing: 4) { - Text("\(line.lineNumber)") - Text(prefix) - } - } - .font(.system(size: chatFontSize)) - .foregroundColor(.white) - .frame(width: 60, height: contentHeight) // TODO: dynamic set height by font size - .background(lineNumberBackgroundColor) - - // Content section with text wrapping - VStack(alignment: .leading) { - Text(line.content) - .font(.system(size: chatFontSize)) - .lineLimit(nil) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - } - .padding(.vertical, 4) - .padding(.leading, 8) - .background(backgroundColor) - .background( - GeometryReader { geometry in - Color.clear - .onAppear { contentHeight = geometry.size.height } - } - ) - } - } -} diff --git a/Core/Sources/SuggestionWidget/CursorPositionTracker.swift b/Core/Sources/SuggestionWidget/CursorPositionTracker.swift deleted file mode 100644 index 35f74326..00000000 --- a/Core/Sources/SuggestionWidget/CursorPositionTracker.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Combine -import Foundation -import Perception -import SuggestionBasic -import XcodeInspector - -@Perceptible -final class CursorPositionTracker { - @MainActor - var cursorPosition: CursorPosition = .zero - - @PerceptionIgnored var editorObservationTask: Set = [] - @PerceptionIgnored var eventObservationTask: Task? - - init() { - observeAppChange() - } - - deinit { - eventObservationTask?.cancel() - } - - private func observeAppChange() { - editorObservationTask = [] - Task { - await XcodeInspector.shared.safe.$focusedEditor.sink { [weak self] editor in - guard let editor, let self else { return } - Task { @MainActor in - self.observeAXNotifications(editor) - } - }.store(in: &editorObservationTask) - } - } - - private func observeAXNotifications(_ editor: SourceEditor) { - eventObservationTask?.cancel() - let content = editor.getLatestEvaluatedContent() - Task { @MainActor in - self.cursorPosition = content.cursorPosition - } - eventObservationTask = Task { [weak self] in - for await event in await editor.axNotifications.notifications() { - guard let self else { return } - guard event.kind == .evaluatedContentChanged else { continue } - let content = editor.getLatestEvaluatedContent() - Task { @MainActor in - self.cursorPosition = content.cursorPosition - } - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift deleted file mode 100644 index d22b6024..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ /dev/null @@ -1,674 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import ChatTab -import ComposableArchitecture -import GitHubCopilotService -import SwiftUI -import PersistMiddleware -import ConversationTab -import HostAppActivator - -public enum ChatTabBuilderCollection: Equatable { - case folder(title: String, kinds: [ChatTabKind]) - case kind(ChatTabKind) -} - -public struct ChatTabKind: Equatable { - public var builder: any ChatTabBuilder - var title: String { builder.title } - - public init(_ builder: any ChatTabBuilder) { - self.builder = builder - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.title == rhs.title - } -} - -public struct WorkspaceIdentifier: Hashable, Codable { - public let path: String - public let username: String - - public init(path: String, username: String) { - self.path = path - self.username = username - } -} - -@ObservableState -public struct ChatHistory: Equatable { - public var workspaces: IdentifiedArray - public var selectedWorkspacePath: String? - public var selectedWorkspaceName: String? - public var currentUsername: String? - - public var currentChatWorkspace: ChatWorkspace? { - guard let id = selectedWorkspacePath, - let username = currentUsername - else { return workspaces.first } - let identifier = WorkspaceIdentifier(path: id, username: username) - return workspaces[id: identifier] - } - - init(workspaces: IdentifiedArray = [], - selectedWorkspacePath: String? = nil, - selectedWorkspaceName: String? = nil, - currentUsername: String? = nil) { - self.workspaces = workspaces - self.selectedWorkspacePath = selectedWorkspacePath - self.selectedWorkspaceName = selectedWorkspaceName - self.currentUsername = currentUsername - } - - mutating func updateHistory(_ workspace: ChatWorkspace) { - if let index = workspaces.firstIndex(where: { $0.id == workspace.id }) { - workspaces[index] = workspace - } - } - - mutating func addWorkspace(_ workspace: ChatWorkspace) { - guard !workspaces.contains(where: { $0.id == workspace.id }) else { return } - workspaces[id: workspace.id] = workspace - } -} - -@ObservableState -public struct ChatWorkspace: Identifiable, Equatable { - public var id: WorkspaceIdentifier - public var tabInfo: IdentifiedArray - public var tabCollection: [ChatTabBuilderCollection] - public var selectedTabId: String? - - public var selectedTabInfo: ChatTabInfo? { - guard let tabId = selectedTabId else { return tabInfo.first } - return tabInfo[id: tabId] - } - - public var workspacePath: String { get { id.path} } - public var username: String { get { id.username } } - - private var onTabInfoDeleted: (String) -> Void - - public init( - id: WorkspaceIdentifier, - tabInfo: IdentifiedArray = [], - tabCollection: [ChatTabBuilderCollection] = [], - selectedTabId: String? = nil, - onTabInfoDeleted: @escaping (String) -> Void - ) { - self.id = id - self.tabInfo = tabInfo - self.tabCollection = tabCollection - self.selectedTabId = selectedTabId - self.onTabInfoDeleted = onTabInfoDeleted - } - - /// Walkaround `Equatable` error for `onTabInfoDeleted` - public static func == (lhs: ChatWorkspace, rhs: ChatWorkspace) -> Bool { - lhs.id == rhs.id && - lhs.tabInfo == rhs.tabInfo && - lhs.tabCollection == rhs.tabCollection && - lhs.selectedTabId == rhs.selectedTabId - } - - public mutating func applyLRULimit(maxSize: Int = 5) { - guard tabInfo.count > maxSize else { return } - - // Tabs not selected - let nonSelectedTabs = Array(tabInfo.filter { $0.id != selectedTabId }) - let sortedByUpdatedAt = nonSelectedTabs.sorted { $0.updatedAt < $1.updatedAt } - - let tabsToRemove = Array(sortedByUpdatedAt.prefix(tabInfo.count - maxSize)) - - // Remove Tabs - for tab in tabsToRemove { - // destroy tab - onTabInfoDeleted(tab.id) - - // remove from workspace - tabInfo.remove(id: tab.id) - } - } -} - -@Reducer -public struct ChatPanelFeature { - @ObservableState - public struct State: Equatable { - public var chatHistory = ChatHistory() - public var currentChatWorkspace: ChatWorkspace? { - return chatHistory.currentChatWorkspace - } - - var colorScheme: ColorScheme = .light - public internal(set) var isPanelDisplayed = false - var isDetached = false - var isFullScreen = false - } - - public enum Action: Equatable { - // Window - case hideButtonClicked - case closeActiveTabClicked - case toggleChatPanelDetachedButtonClicked - case detachChatPanel - case attachChatPanel - case enterFullScreen - case exitFullScreen - case presentChatPanel(forceDetach: Bool) - case switchWorkspace(String, String, String) - case openSettings - - // Tabs - case updateChatHistory(ChatWorkspace) -// case updateChatTabInfo(IdentifiedArray) -// case createNewTapButtonHovered - case closeTabButtonClicked(id: String) - case createNewTapButtonClicked(kind: ChatTabKind?) - case restoreTabByInfo(info: ChatTabInfo) - case createNewTabByID(id: String) - case tabClicked(id: String) - case appendAndSelectTab(ChatTabInfo) - case appendTabToWorkspace(ChatTabInfo, ChatWorkspace) -// case switchToNextTab -// case switchToPreviousTab -// case moveChatTab(from: Int, to: Int) - case focusActiveChatTab - - // Chat History - case chatHistoryItemClicked(id: String) - case chatHistoryDeleteButtonClicked(id: String) - case chatTab(id: String, action: ChatTabItem.Action) - - // persist - case saveChatTabInfo([ChatTabInfo?], ChatWorkspace) - case deleteChatTabInfo(id: String, ChatWorkspace) - case restoreWorkspace(ChatWorkspace) - - // ChatWorkspace cleanup - case scheduleLRUCleanup(ChatWorkspace) - case performLRUCleanup(ChatWorkspace) - } - - @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency - @Dependency(\.xcodeInspector) var xcodeInspector - @Dependency(\.activatePreviousActiveXcode) var activatePreviouslyActiveXcode - @Dependency(\.activateThisApp) var activateExtensionService - @Dependency(\.chatTabBuilderCollection) var chatTabBuilderCollection - @Dependency(\.chatTabPool) var chatTabPool - - @MainActor func toggleFullScreen() { - let window = suggestionWidgetControllerDependency.windowsController?.windows - .chatPanelWindow - window?.toggleFullScreen(nil) - } - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .hideButtonClicked: - state.isPanelDisplayed = false - - if state.isFullScreen { - return .run { _ in - await MainActor.run { toggleFullScreen() } - activatePreviouslyActiveXcode() - } - } - - return .run { _ in - activatePreviouslyActiveXcode() - } - - case .closeActiveTabClicked: - if let id = state.currentChatWorkspace?.selectedTabId { - return .run { send in - await send(.closeTabButtonClicked(id: id)) - } - } - - state.isPanelDisplayed = false - return .none - - case .toggleChatPanelDetachedButtonClicked: - if state.isFullScreen, state.isDetached { - return .run { send in - await send(.attachChatPanel) - } - } - - state.isDetached.toggle() - return .none - - case .detachChatPanel: - state.isDetached = true - return .none - - case .attachChatPanel: - if state.isFullScreen { - return .run { send in - await MainActor.run { toggleFullScreen() } - try await Task.sleep(nanoseconds: 1_000_000_000) - await send(.attachChatPanel) - } - } - - state.isDetached = false - return .none - - case .enterFullScreen: - state.isFullScreen = true - return .run { send in - await send(.detachChatPanel) - } - - case .exitFullScreen: - state.isFullScreen = false - return .none - - case let .presentChatPanel(forceDetach): - if forceDetach { - state.isDetached = true - } - state.isPanelDisplayed = true - return .run { send in - activateExtensionService() - await send(.focusActiveChatTab) - } - case let .switchWorkspace(path, name, username): - state.chatHistory.selectedWorkspacePath = path - state.chatHistory.selectedWorkspaceName = name - state.chatHistory.currentUsername = username - if state.chatHistory.currentChatWorkspace == nil { - let identifier = WorkspaceIdentifier(path: path, username: username) - state.chatHistory.addWorkspace( - ChatWorkspace(id: identifier) { chatTabPool.removeTab(of: $0) } - ) - } - return .none - case .openSettings: - try? launchHostAppSettings() - return .none - case let .updateChatHistory(chatWorkspace): - state.chatHistory.updateHistory(chatWorkspace) - return .none -// case let .updateChatTabInfo(chatTabInfo): -// let previousSelectedIndex = state.chatTabGroup.tabInfo -// .firstIndex(where: { $0.id == state.chatTabGroup.selectedTabId }) -// state.chatTabGroup.tabInfo = chatTabInfo -// if !chatTabInfo.contains(where: { $0.id == state.chatTabGroup.selectedTabId }) { -// if let previousSelectedIndex { -// let proposedSelectedIndex = previousSelectedIndex - 1 -// if proposedSelectedIndex >= 0, -// proposedSelectedIndex < chatTabInfo.endIndex -// { -// state.chatTabGroup.selectedTabId = chatTabInfo[proposedSelectedIndex].id -// } else { -// state.chatTabGroup.selectedTabId = chatTabInfo.first?.id -// } -// } else { -// state.chatTabGroup.selectedTabId = nil -// } -// } -// return .none - - case let .closeTabButtonClicked(id): - guard var currentChatWorkspace = state.currentChatWorkspace else { - return .none - } - let firstIndex = currentChatWorkspace.tabInfo.firstIndex { $0.id == id } - let nextIndex = { - guard let firstIndex else { return 0 } - let nextIndex = firstIndex - 1 - return max(nextIndex, 0) - }() - currentChatWorkspace.tabInfo.removeAll { $0.id == id } - if currentChatWorkspace.tabInfo.isEmpty { - state.isPanelDisplayed = false - } - if nextIndex < currentChatWorkspace.tabInfo.count { - currentChatWorkspace.selectedTabId = currentChatWorkspace.tabInfo[nextIndex].id - } else { - currentChatWorkspace.selectedTabId = nil - } - state.chatHistory.updateHistory(currentChatWorkspace) - return .none - - case let .chatHistoryDeleteButtonClicked(id): - // the current chat should not be deleted - guard var currentChatWorkspace = state.currentChatWorkspace, id != currentChatWorkspace.selectedTabId else { - return .none - } - currentChatWorkspace.tabInfo.removeAll { $0.id == id } - state.chatHistory.updateHistory(currentChatWorkspace) - - let chatWorkspace = currentChatWorkspace - return .run { send in - await send(.deleteChatTabInfo(id: id, chatWorkspace)) - } - -// case .createNewTapButtonHovered: -// state.chatTabGroup.tabCollection = chatTabBuilderCollection() -// return .none - - case .createNewTapButtonClicked: - return .none // handled in GUI Reducer - - case .restoreTabByInfo(_): - return .none // handled in GUI Reducer - - case .createNewTabByID(_): - return .none // handled in GUI Reducer - - case let .tabClicked(id): - guard var currentChatWorkspace = state.currentChatWorkspace, - var chatTabInfo = currentChatWorkspace.tabInfo.first(where: { $0.id == id }) else { -// chatTabGroup.selectedTabId = nil - return .none - } - - let (originalTab, currentTab) = currentChatWorkspace.switchTab(to: &chatTabInfo) - state.chatHistory.updateHistory(currentChatWorkspace) - - let workspace = currentChatWorkspace - return .run { send in - await send(.focusActiveChatTab) - await send(.saveChatTabInfo([originalTab, currentTab], workspace)) - } - - case let .chatHistoryItemClicked(id): - guard var chatWorkspace = state.currentChatWorkspace, - // No Need to swicth selected Tab when already selected - id != chatWorkspace.selectedTabId - else { return .none } - - // Try to find the tab in three places: - // 1. In current workspace's open tabs - let existingTab = chatWorkspace.tabInfo.first(where: { $0.id == id }) - - // 2. In persistent storage - let storedTab = existingTab == nil - ? ChatTabInfoStore.getByID(id, with: .init(workspacePath: chatWorkspace.workspacePath, username: chatWorkspace.username)) - : nil - - if var tabInfo = existingTab ?? storedTab { - // Tab found in workspace or storage - switch to it - let (originalTab, currentTab) = chatWorkspace.switchTab(to: &tabInfo) - state.chatHistory.updateHistory(chatWorkspace) - - let workspace = chatWorkspace - let info = tabInfo - return .run { send in - // For stored tabs that aren't in the workspace yet, restore them first - if storedTab != nil { - await send(.restoreTabByInfo(info: info)) - } - - // as converstaion tab is lazy restore - // should restore tab when switching - if let chatTab = chatTabPool.getTab(of: id), - let conversationTab = chatTab as? ConversationTab { - await conversationTab.restoreIfNeeded() - } - - await send(.saveChatTabInfo([originalTab, currentTab], workspace)) - } - } - - // 3. Tab not found - create a new one - return .run { send in - await send(.createNewTabByID(id: id)) - } - - case var .appendAndSelectTab(tab): - guard var chatWorkspace = state.currentChatWorkspace, - !chatWorkspace.tabInfo.contains(where: { $0.id == tab.id }) - else { return .none } - - chatWorkspace.tabInfo.append(tab) - let (originalTab, currentTab) = chatWorkspace.switchTab(to: &tab) - state.chatHistory.updateHistory(chatWorkspace) - - let currentChatWorkspace = chatWorkspace - return .run { send in - await send(.focusActiveChatTab) - await send(.saveChatTabInfo([originalTab, currentTab], currentChatWorkspace)) - await send(.scheduleLRUCleanup(currentChatWorkspace)) - } - case .appendTabToWorkspace(var tab, let chatWorkspace): - guard !chatWorkspace.tabInfo.contains(where: { $0.id == tab.id }) - else { return .none } - var targetWorkspace = chatWorkspace - targetWorkspace.tabInfo.append(tab) - let (originalTab, currentTab) = targetWorkspace.switchTab(to: &tab) - state.chatHistory.updateHistory(targetWorkspace) - - let currentChatWorkspace = targetWorkspace - return .run { send in - await send(.saveChatTabInfo([originalTab, currentTab], currentChatWorkspace)) - await send(.scheduleLRUCleanup(currentChatWorkspace)) - } - -// case .switchToNextTab: -// let selectedId = state.chatTabGroup.selectedTabId -// guard let index = state.chatTabGroup.tabInfo -// .firstIndex(where: { $0.id == selectedId }) -// else { return .none } -// let nextIndex = index + 1 -// if nextIndex >= state.chatTabGroup.tabInfo.endIndex { -// return .none -// } -// let targetId = state.chatTabGroup.tabInfo[nextIndex].id -// state.chatTabGroup.selectedTabId = targetId -// return .run { send in -// await send(.focusActiveChatTab) -// } - -// case .switchToPreviousTab: -// let selectedId = state.chatTabGroup.selectedTabId -// guard let index = state.chatTabGroup.tabInfo -// .firstIndex(where: { $0.id == selectedId }) -// else { return .none } -// let previousIndex = index - 1 -// if previousIndex < 0 || previousIndex >= state.chatTabGroup.tabInfo.endIndex { -// return .none -// } -// let targetId = state.chatTabGroup.tabInfo[previousIndex].id -// state.chatTabGroup.selectedTabId = targetId -// return .run { send in -// await send(.focusActiveChatTab) -// } - -// case let .moveChatTab(from, to): -// guard from >= 0, from < state.chatTabGroup.tabInfo.endIndex, to >= 0, -// to <= state.chatTabGroup.tabInfo.endIndex -// else { -// return .none -// } -// let tab = state.chatTabGroup.tabInfo[from] -// state.chatTabGroup.tabInfo.remove(at: from) -// state.chatTabGroup.tabInfo.insert(tab, at: to) -// return .none - - case .focusActiveChatTab: - guard FeatureFlagNotifierImpl.shared.featureFlags.chat else { - return .none - } - let id = state.currentChatWorkspace?.selectedTabInfo?.id - guard let id else { return .none } - return .run { send in - await send(.chatTab(id: id, action: .focus)) - } - -// case let .chatTab(id, .close): -// return .run { send in -// await send(.closeTabButtonClicked(id: id)) -// } - - // MARK: - ChatTabItem action - - case let .chatTab(id, .tabContentUpdated): - guard var currentChatWorkspace = state.currentChatWorkspace, - var info = state.currentChatWorkspace?.tabInfo[id: id] - else { return .none } - - info.updatedAt = .now - currentChatWorkspace.tabInfo[id: id] = info - state.chatHistory.updateHistory(currentChatWorkspace) - - let chatTabInfo = info - let chatWorkspace = currentChatWorkspace - return .run { send in - await send(.saveChatTabInfo([chatTabInfo], chatWorkspace)) - } - - case let .chatTab(id, .setCLSConversationID(CID)): - guard var currentChatWorkspace = state.currentChatWorkspace, - var info = state.currentChatWorkspace?.tabInfo[id: id] - else { return .none } - - info.CLSConversationID = CID - currentChatWorkspace.tabInfo[id: id] = info - state.chatHistory.updateHistory(currentChatWorkspace) - - let chatTabInfo = info - let chatWorkspace = currentChatWorkspace - return .run { send in - await send(.saveChatTabInfo([chatTabInfo], chatWorkspace)) - } - - case let .chatTab(id, .updateTitle(title)): - guard var currentChatWorkspace = state.currentChatWorkspace, - var info = state.currentChatWorkspace?.tabInfo[id: id], - !info.isTitleSet - else { return .none } - - info.title = title - info.updatedAt = .now - currentChatWorkspace.tabInfo[id: id] = info - state.chatHistory.updateHistory(currentChatWorkspace) - - let chatTabInfo = info - let chatWorkspace = currentChatWorkspace - return .run { send in - await send(.saveChatTabInfo([chatTabInfo], chatWorkspace)) - } - - case .chatTab: - return .none - - // MARK: - Persist - case let .saveChatTabInfo(chatTabInfos, chatWorkspace): - let toSaveInfo = chatTabInfos.compactMap { $0 } - guard toSaveInfo.count > 0 else { return .none } - let workspacePath = chatWorkspace.workspacePath - let username = chatWorkspace.username - - return .run { _ in - Task(priority: .background) { - ChatTabInfoStore.saveAll(toSaveInfo, with: .init(workspacePath: workspacePath, username: username)) - } - } - - case let .deleteChatTabInfo(id, chatWorkspace): - let workspacePath = chatWorkspace.workspacePath - let username = chatWorkspace.username - - ChatTabInfoStore.delete(by: id, with: .init(workspacePath: workspacePath, username: username)) - return .none - case var .restoreWorkspace(chatWorkspace): - // chat opened before finishing restoration - if var existChatWorkspace = state.chatHistory.workspaces[id: chatWorkspace.id] { - - if var selectedChatTabInfo = chatWorkspace.tabInfo.first(where: { $0.id == chatWorkspace.selectedTabId }) { - // Keep the selection state when restoring - selectedChatTabInfo.isSelected = true - chatWorkspace.tabInfo[id: selectedChatTabInfo.id] = selectedChatTabInfo - - // Update the existing workspace's selected tab to match - existChatWorkspace.selectedTabId = selectedChatTabInfo.id - - // merge tab info - existChatWorkspace.tabInfo.append(contentsOf: chatWorkspace.tabInfo) - state.chatHistory.updateHistory(existChatWorkspace) - - let chatTabInfo = selectedChatTabInfo - let workspace = existChatWorkspace - return .run { send in - // update chat tab info - await send(.saveChatTabInfo([chatTabInfo], workspace)) - await send(.scheduleLRUCleanup(workspace)) - } - } - - // merge tab info - existChatWorkspace.tabInfo.append(contentsOf: chatWorkspace.tabInfo) - state.chatHistory.updateHistory(existChatWorkspace) - - let workspace = existChatWorkspace - return .run { send in - await send(.scheduleLRUCleanup(workspace)) - } - } - - state.chatHistory.addWorkspace(chatWorkspace) - return .none - - // MARK: - Clean up ChatWorkspace - case .scheduleLRUCleanup(let chatWorkspace): - return .run { send in - await send(.performLRUCleanup(chatWorkspace)) - }.cancellable(id: "lru-cleanup-\(chatWorkspace.id)", cancelInFlight: true) // apply built-in race condition prevention - - case .performLRUCleanup(var chatWorkspace): - chatWorkspace.applyLRULimit() - state.chatHistory.updateHistory(chatWorkspace) - return .none - } - } -// .forEach(\.chatGroupCollection.selectedChatGroup?.tabInfo, action: /Action.chatTab) { -// ChatTabItem() -// } - } -} - -extension ChatPanelFeature { - - func restoreConversationTabIfNeeded(_ id: String) async { - if let chatTab = chatTabPool.getTab(of: id), - let conversationTab = chatTab as? ConversationTab { - await conversationTab.restoreIfNeeded() - } - } -} - -extension ChatWorkspace { - public mutating func switchTab(to chatTabInfo: inout ChatTabInfo) -> (originalTab: ChatTabInfo?, currentTab: ChatTabInfo) { - guard self.selectedTabId != chatTabInfo.id else { return (nil, chatTabInfo) } - - // get original selected tab info to update its isSelected - var originalTabInfo: ChatTabInfo? = nil - if self.selectedTabId != nil { - originalTabInfo = self.tabInfo[id: self.selectedTabId!] - } - - // fresh selected info in chatWorksapce and tabInfo - self.selectedTabId = chatTabInfo.id - originalTabInfo?.isSelected = false - chatTabInfo.isSelected = true - - // update tab back to chatWorkspace - let isNewTab = self.tabInfo[id: chatTabInfo.id] == nil - self.tabInfo[id: chatTabInfo.id] = chatTabInfo - if isNewTab { - applyLRULimit() - } - - if let originalTabInfo { - self.tabInfo[id: originalTabInfo.id] = originalTabInfo - } - - return (originalTabInfo, chatTabInfo) - } -} diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift deleted file mode 100644 index 51b7d918..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift +++ /dev/null @@ -1,83 +0,0 @@ -import ActiveApplicationMonitor -import ComposableArchitecture -import Preferences -import SuggestionBasic -import SwiftUI - -@Reducer -public struct CircularWidgetFeature { - public struct IsProcessingCounter: Equatable { - var expirationDate: TimeInterval - } - - @ObservableState - public struct State: Equatable { - var isProcessingCounters = [IsProcessingCounter]() - var isProcessing: Bool - var isDisplayingContent: Bool - var isContentEmpty: Bool - var isChatPanelDetached: Bool - var isChatOpen: Bool - } - - public enum Action: Equatable { - case widgetClicked - case detachChatPanelToggleClicked - case openChatButtonClicked - case runCustomCommandButtonClicked(CustomCommand) - case markIsProcessing - case endIsProcessing - case _forceEndIsProcessing - } - - struct CancelAutoEndIsProcessKey: Hashable {} - - @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .detachChatPanelToggleClicked: - return .none // handled elsewhere - - case .openChatButtonClicked: - return .run { _ in - suggestionWidgetControllerDependency.onOpenChatClicked() - } - - case let .runCustomCommandButtonClicked(command): - return .run { _ in - suggestionWidgetControllerDependency.onCustomCommandClicked(command) - } - - case .widgetClicked: - return .none // handled elsewhere - - case .markIsProcessing: - let deadline = Date().timeIntervalSince1970 + 20 - state.isProcessingCounters.append(IsProcessingCounter(expirationDate: deadline)) - state.isProcessing = true - return .run { send in - try await Task.sleep(nanoseconds: 20 * 1_000_000_000) - try Task.checkCancellation() - await send(._forceEndIsProcessing) - }.cancellable(id: CancelAutoEndIsProcessKey(), cancelInFlight: true) - - case .endIsProcessing: - if !state.isProcessingCounters.isEmpty { - state.isProcessingCounters.removeFirst() - } - state.isProcessingCounters - .removeAll(where: { $0.expirationDate < Date().timeIntervalSince1970 }) - state.isProcessing = !state.isProcessingCounters.isEmpty - return .none - - case ._forceEndIsProcessing: - state.isProcessingCounters.removeAll() - state.isProcessing = false - return .none - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/CodeReviewFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/CodeReviewFeature.swift deleted file mode 100644 index ed7b4375..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/CodeReviewFeature.swift +++ /dev/null @@ -1,356 +0,0 @@ -import ChatService -import ComposableArchitecture -import AppKit -import AXHelper -import ConversationServiceProvider -import Foundation -import LanguageServerProtocol -import Logger -import Terminal -import XcodeInspector -import SuggestionBasic -import ConversationTab - -@Reducer -public struct CodeReviewPanelFeature { - @ObservableState - public struct State: Equatable { - public fileprivate(set) var documentReviews: DocumentReviewsByUri = [:] - public var operatedCommentIds: Set = [] - public var currentIndex: Int = 0 - public var activeDocumentURL: URL? = nil - public var isPanelDisplayed: Bool = false - public var closedByUser: Bool = false - - public var currentDocumentReview: DocumentReview? { - if let url = activeDocumentURL, - let result = documentReviews[url.absoluteString] - { - return result - } - return nil - } - - public var currentSelectedComment: ReviewComment? { - guard let currentDocumentReview = currentDocumentReview else { return nil } - guard currentIndex >= 0 && currentIndex < currentDocumentReview.comments.count - else { return nil } - - return currentDocumentReview.comments[currentIndex] - } - - public var originalContent: String? { currentDocumentReview?.originalContent } - - public var documentUris: [DocumentUri] { Array(documentReviews.keys) } - - public var pendingNavigation: PendingNavigation? = nil - - public func getCommentById(id: String) -> ReviewComment? { - // Check current selected comment first for efficiency - if let currentSelectedComment = currentSelectedComment, - currentSelectedComment.id == id { - return currentSelectedComment - } - - // Search through all document reviews - for documentReview in documentReviews.values { - for comment in documentReview.comments { - if comment.id == id { - return comment - } - } - } - - return nil - } - - public func getOriginalContentByUri(_ uri: DocumentUri) -> String? { - documentReviews[uri]?.originalContent - } - - public var hasNextComment: Bool { hasComment(of: .next) } - public var hasPreviousComment: Bool { hasComment(of: .previous) } - - public init() {} - } - - public struct PendingNavigation: Equatable { - public let url: URL - public let index: Int - - public init(url: URL, index: Int) { - self.url = url - self.index = index - } - } - - public enum Action: Equatable { - case next - case previous - case close(commentId: String) - case dismiss(commentId: String) - case accept(commentId: String) - - case onActiveDocumentURLChanged(URL?) - - case appear - case onCodeReviewResultsChanged(DocumentReviewsByUri) - case observeDocumentReviews - case observeReviewedFileClicked - - case checkDisplay - case reviewedfileClicked - } - - public init() {} - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .next: - let nextIndex = state.currentIndex + 1 - if let reviewComments = state.currentDocumentReview?.comments, - reviewComments.count > nextIndex { - state.currentIndex = nextIndex - return .none - } - - if let result = state.getDocumentNavigation(.next) { - state.navigateToDocument(uri: result.documentUri, index: result.commentIndex) - } - - return .none - - case .previous: - let previousIndex = state.currentIndex - 1 - if let reviewComments = state.currentDocumentReview?.comments, - reviewComments.count > previousIndex && previousIndex >= 0 { - state.currentIndex = previousIndex - return .none - } - - if let result = state.getDocumentNavigation(.previous) { - state.navigateToDocument(uri: result.documentUri, index: result.commentIndex) - } - - return .none - - case let .close(id): - state.isPanelDisplayed = false - state.closedByUser = true - - return .none - - case let .dismiss(id): - state.operatedCommentIds.insert(id) - return .run { send in - await send(.checkDisplay) - await send(.next) - } - - case let .accept(id): - guard !state.operatedCommentIds.contains(id), - let comment = state.getCommentById(id: id), - let suggestion = comment.suggestion, - let url = URL(string: comment.uri), - let currentContent = try? String(contentsOf: url), - let originalContent = state.getOriginalContentByUri(comment.uri) - else { return .none } - - let currentLines = currentContent.components(separatedBy: .newlines) - - let currentEndLineNumber = CodeReviewLocationStrategy.calculateCurrentLineNumber( - for: comment.range.end.line, - originalLines: originalContent.components(separatedBy: .newlines), - currentLines: currentLines - ) - - let range: CursorRange = .init( - start: .init( - line: currentEndLineNumber - (comment.range.end.line - comment.range.start.line), - character: comment.range.start.character - ), - end: .init(line: currentEndLineNumber, character: comment.range.end.character) - ) - - ChatInjector.insertSuggestion( - suggestion: suggestion, - range: range, - lines: currentLines - ) - - state.operatedCommentIds.insert(id) - - return .none - - case let .onActiveDocumentURLChanged(url): - if url != state.activeDocumentURL { - if let pendingNavigation = state.pendingNavigation, - pendingNavigation.url == url { - state.activeDocumentURL = url - state.currentIndex = pendingNavigation.index - } else { - state.activeDocumentURL = url - state.currentIndex = 0 - } - } - return .run { send in await send(.checkDisplay) } - - case .appear: - return .run { send in - await send(.observeDocumentReviews) - await send(.observeReviewedFileClicked) - } - - case .observeDocumentReviews: - return .run { send in - for await documentReviews in await CodeReviewService.shared.$documentReviews.values { - await send(.onCodeReviewResultsChanged(documentReviews)) - } - } - - case .observeReviewedFileClicked: - return .run { send in - for await _ in await CodeReviewStateService.shared.fileClickedEvent.values { - await send(.reviewedfileClicked) - } - } - - case let .onCodeReviewResultsChanged(newCodeReviewResults): - state.documentReviews = newCodeReviewResults - - return .run { send in await send(.checkDisplay) } - - case .checkDisplay: - guard !state.closedByUser else { - state.isPanelDisplayed = false - return .none - } - - if let currentDocumentReview = state.currentDocumentReview, - currentDocumentReview.comments.count > 0 { - state.isPanelDisplayed = true - } else { - state.isPanelDisplayed = false - } - - return .none - - case .reviewedfileClicked: - state.isPanelDisplayed = true - state.closedByUser = false - - return .none - } - } - } -} - -enum NavigationDirection { - case previous, next -} - -extension CodeReviewPanelFeature.State { - func getDocumentNavigation(_ direction: NavigationDirection) -> (documentUri: String, commentIndex: Int)? { - let documentUris = documentUris - let documentUrisCount = documentUris.count - - guard documentUrisCount > 1, - let activeDocumentURL = activeDocumentURL, - let documentIndex = documentUris.firstIndex(where: { $0 == activeDocumentURL.absoluteString }) - else { return nil } - - var offSet = 1 - // Iter documentUris to find valid next/previous document and comment - while offSet < documentUrisCount { - let targetDocumentIndex: Int = { - switch direction { - case .previous: (documentIndex - offSet + documentUrisCount) % documentUrisCount - case .next: (documentIndex + offSet) % documentUrisCount - } - }() - - let targetDocumentUri = documentUris[targetDocumentIndex] - if let targetComments = documentReviews[targetDocumentUri]?.comments, - !targetComments.isEmpty { - let targetCommentIndex: Int = { - switch direction { - case .previous: targetComments.count - 1 - case .next: 0 - } - }() - - return (targetDocumentUri, targetCommentIndex) - } - - offSet += 1 - } - - return nil - } - - mutating func navigateToDocument(uri: String, index: Int) { - let url = URL(fileURLWithPath: uri) - let originalContent = documentReviews[uri]!.originalContent - let comment = documentReviews[uri]!.comments[index] - - openFileInXcode(fileURL: url, originalContent: originalContent, range: comment.range) - - pendingNavigation = .init(url: url, index: index) - } - - func hasComment(of direction: NavigationDirection) -> Bool { - // Has next comment against current document - switch direction { - case .next: - if currentDocumentReview?.comments.count ?? 0 > currentIndex + 1 { - return true - } - case .previous: - if currentIndex > 0 { - return true - } - } - - // Has next comment against next document - if getDocumentNavigation(direction) != nil { - return true - } - - return false - } -} - -private func openFileInXcode( - fileURL: URL, - originalContent: String, - range: LSPRange -) { - NSWorkspace.openFileInXcode(fileURL: fileURL) { app, error in - guard error == nil else { - Logger.client.error("Failed to open file in xcode: \(error!.localizedDescription)") - return - } - - guard let app = app else { return } - - let appInstanceInspector = AppInstanceInspector(runningApplication: app) - guard appInstanceInspector.isXcode, - let focusedElement = appInstanceInspector.appElement.focusedElement, - let content = try? String(contentsOf: fileURL) - else { return } - - let currentLineNumber = CodeReviewLocationStrategy.calculateCurrentLineNumber( - for: range.end.line, - originalLines: originalContent.components(separatedBy: .newlines), - currentLines: content.components(separatedBy: .newlines) - ) - - - AXHelper.scrollSourceEditorToLine( - currentLineNumber, - content: content, - focusedElement: focusedElement - ) - } -} diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift deleted file mode 100644 index e76afbc0..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ /dev/null @@ -1,176 +0,0 @@ -import AppKit -import ComposableArchitecture -import Foundation - -@Reducer -public struct PanelFeature { - @ObservableState - public struct State: Equatable { - public var content: SharedPanelFeature.Content { - get { sharedPanelState.content } - set { - sharedPanelState.content = newValue - suggestionPanelState.content = newValue.suggestion - } - } - - // MARK: SharedPanel - - var sharedPanelState = SharedPanelFeature.State() - - // MARK: SuggestionPanel - - var suggestionPanelState = SuggestionPanelFeature.State() - - var warningMessage: String? - var warningURL: String? - } - - public enum Action: Equatable { - case presentSuggestion - case presentSuggestionProvider(CodeSuggestionProvider, displayContent: Bool) - case presentError(String) - case presentPromptToCode(PromptToCodeGroup.PromptToCodeInitialState) - case displayPanelContent - case expandSuggestion - case discardSuggestion - case removeDisplayedContent - case switchToAnotherEditorAndUpdateContent - case hidePanel - case showPanel - - case sharedPanel(SharedPanelFeature.Action) - case suggestionPanel(SuggestionPanelFeature.Action) - - case presentWarning(message: String, url: String?) - case dismissWarning - } - - @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency - @Dependency(\.xcodeInspector) var xcodeInspector - @Dependency(\.activateThisApp) var activateThisApp - var windows: WidgetWindows? { suggestionWidgetControllerDependency.windowsController?.windows } - - public var body: some ReducerOf { - Scope(state: \.suggestionPanelState, action: \.suggestionPanel) { - SuggestionPanelFeature() - } - - Scope(state: \.sharedPanelState, action: \.sharedPanel) { - SharedPanelFeature() - } - - Reduce { state, action in - switch action { - case .presentSuggestion: - return .run { send in - guard let fileURL = await xcodeInspector.safe.activeDocumentURL, - let provider = await fetchSuggestionProvider(fileURL: fileURL) - else { return } - await send(.presentSuggestionProvider(provider, displayContent: true)) - } - - case let .presentSuggestionProvider(provider, displayContent): - state.content.suggestion = provider - if displayContent { - return .run { send in - await send(.displayPanelContent) - }.animation(.easeInOut(duration: 0.2)) - } - return .none - - case let .presentError(errorDescription): - state.content.error = errorDescription - return .run { send in - await send(.displayPanelContent) - }.animation(.easeInOut(duration: 0.2)) - - case let .presentPromptToCode(initialState): - return .run { send in - await send(.sharedPanel(.promptToCodeGroup(.createPromptToCode(initialState)))) - } - - case .displayPanelContent: - if !state.sharedPanelState.isEmpty { - state.sharedPanelState.isPanelDisplayed = true - } - - if state.suggestionPanelState.content != nil { - state.suggestionPanelState.isPanelDisplayed = true - } - - return .none - - case .discardSuggestion: - state.content.suggestion = nil - return .none - case .expandSuggestion: - state.content.isExpanded = true - return .none - case .switchToAnotherEditorAndUpdateContent: - return .run { send in - guard let fileURL = await xcodeInspector.safe.realtimeActiveDocumentURL - else { return } - - await send(.sharedPanel( - .promptToCodeGroup( - .updateActivePromptToCode(documentURL: fileURL) - ) - )) - } - case .hidePanel: - state.suggestionPanelState.isPanelDisplayed = false - return .none - case .showPanel: - state.suggestionPanelState.isPanelDisplayed = true - return .none - case .removeDisplayedContent: - state.content.error = nil - state.content.suggestion = nil - return .none - - case .sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode)), - .sharedPanel(.promptToCodeGroup(.createPromptToCode)): - let hasPromptToCode = state.content.promptToCode != nil - return .run { send in - await send(.displayPanelContent) - - if hasPromptToCode { - activateThisApp() - await MainActor.run { - windows?.sharedPanelWindow.makeKey() - } - } - }.animation(.easeInOut(duration: 0.2)) - - case .sharedPanel: - return .none - - case .suggestionPanel: - return .none - - case .presentWarning(let message, let url): - state.warningMessage = message - state.warningURL = url - state.suggestionPanelState.warningMessage = message - state.suggestionPanelState.warningURL = url - return .none - - case .dismissWarning: - state.warningMessage = nil - state.warningURL = nil - state.suggestionPanelState.warningMessage = nil - state.suggestionPanelState.warningURL = nil - return .none - } - } - } - - func fetchSuggestionProvider(fileURL: URL) async -> CodeSuggestionProvider? { - guard let provider = await suggestionWidgetControllerDependency - .suggestionWidgetDataSource? - .suggestionForFile(at: fileURL) else { return nil } - return provider - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift deleted file mode 100644 index 9ba5cad3..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift +++ /dev/null @@ -1,276 +0,0 @@ -import AppKit -import ComposableArchitecture -import CustomAsyncAlgorithms -import Dependencies -import Foundation -import PromptToCodeService -import SuggestionBasic - -public struct PromptToCodeAcceptHandlerDependencyKey: DependencyKey { - public static let liveValue: (PromptToCode.State) -> Void = { _ in - assertionFailure("Please provide a handler") - } - - public static let previewValue: (PromptToCode.State) -> Void = { _ in - print("Accept Prompt to Code") - } -} - -public extension DependencyValues { - var promptToCodeAcceptHandler: (PromptToCode.State) -> Void { - get { self[PromptToCodeAcceptHandlerDependencyKey.self] } - set { self[PromptToCodeAcceptHandlerDependencyKey.self] = newValue } - } -} - -@Reducer -public struct PromptToCode { - @ObservableState - public struct State: Equatable, Identifiable { - public indirect enum HistoryNode: Equatable { - case empty - case node(code: String, description: String, previous: HistoryNode) - - mutating func enqueue(code: String, description: String) { - let current = self - self = .node(code: code, description: description, previous: current) - } - - mutating func pop() -> (code: String, description: String)? { - switch self { - case .empty: - return nil - case let .node(code, description, previous): - self = previous - return (code, description) - } - } - } - - public enum FocusField: Equatable { - case textField - } - - public var id: URL { documentURL } - public var history: HistoryNode - public var code: String - public var isResponding: Bool - public var description: String - public var error: String? - public var selectionRange: CursorRange? - public var language: CodeLanguage - public var indentSize: Int - public var usesTabsForIndentation: Bool - public var projectRootURL: URL - public var documentURL: URL - public var allCode: String - public var allLines: [String] - public var extraSystemPrompt: String? - public var generateDescriptionRequirement: Bool? - public var commandName: String? - public var prompt: String - public var isContinuous: Bool - public var isAttachedToSelectionRange: Bool - public var focusedField: FocusField? = .textField - - public var filename: String { documentURL.lastPathComponent } - public var canRevert: Bool { history != .empty } - - public init( - code: String, - prompt: String, - language: CodeLanguage, - indentSize: Int, - usesTabsForIndentation: Bool, - projectRootURL: URL, - documentURL: URL, - allCode: String, - allLines: [String], - commandName: String? = nil, - description: String = "", - isResponding: Bool = false, - isAttachedToSelectionRange: Bool = true, - error: String? = nil, - history: HistoryNode = .empty, - isContinuous: Bool = false, - selectionRange: CursorRange? = nil, - extraSystemPrompt: String? = nil, - generateDescriptionRequirement: Bool? = nil - ) { - self.history = history - self.code = code - self.prompt = prompt - self.isResponding = isResponding - self.description = description - self.error = error - self.isContinuous = isContinuous - self.selectionRange = selectionRange - self.language = language - self.indentSize = indentSize - self.usesTabsForIndentation = usesTabsForIndentation - self.projectRootURL = projectRootURL - self.documentURL = documentURL - self.allCode = allCode - self.allLines = allLines - self.extraSystemPrompt = extraSystemPrompt - self.generateDescriptionRequirement = generateDescriptionRequirement - self.isAttachedToSelectionRange = isAttachedToSelectionRange - self.commandName = commandName - - if selectionRange?.isEmpty ?? true { - self.isAttachedToSelectionRange = false - } - } - } - - public enum Action: Equatable, BindableAction { - case binding(BindingAction) - case focusOnTextField - case selectionRangeToggleTapped - case modifyCodeButtonTapped - case revertButtonTapped - case stopRespondingButtonTapped - case modifyCodeFinished - case modifyCodeChunkReceived(code: String, description: String) - case modifyCodeFailed(error: String) - case modifyCodeCancelled - case cancelButtonTapped - case acceptButtonTapped - case copyCodeButtonTapped - case appendNewLineToPromptButtonTapped - } - - @Dependency(\.promptToCodeService) var promptToCodeService - @Dependency(\.promptToCodeAcceptHandler) var promptToCodeAcceptHandler - - enum CancellationKey: Hashable { - case modifyCode(State.ID) - } - - public var body: some ReducerOf { - BindingReducer() - - Reduce { state, action in - switch action { - case .binding: - return .none - - case .focusOnTextField: - state.focusedField = .textField - return .none - - case .selectionRangeToggleTapped: - state.isAttachedToSelectionRange.toggle() - return .none - - case .modifyCodeButtonTapped: - guard !state.isResponding else { return .none } - let copiedState = state - state.history.enqueue(code: state.code, description: state.description) - state.isResponding = true - state.code = "" - state.description = "" - state.error = nil - - return .run { send in - do { - let stream = try await promptToCodeService.modifyCode( - code: copiedState.code, - requirement: copiedState.prompt, - source: .init( - language: copiedState.language, - documentURL: copiedState.documentURL, - projectRootURL: copiedState.projectRootURL, - content: copiedState.allCode, - lines: copiedState.allLines, - range: copiedState.selectionRange ?? .outOfScope - ), - isDetached: !copiedState.isAttachedToSelectionRange, - extraSystemPrompt: copiedState.extraSystemPrompt, - generateDescriptionRequirement: copiedState - .generateDescriptionRequirement - ).timedDebounce(for: 0.2) - - for try await fragment in stream { - try Task.checkCancellation() - await send(.modifyCodeChunkReceived( - code: fragment.code, - description: fragment.description - )) - } - try Task.checkCancellation() - await send(.modifyCodeFinished) - } catch is CancellationError { - try Task.checkCancellation() - await send(.modifyCodeCancelled) - } catch { - try Task.checkCancellation() - if (error as NSError).code == NSURLErrorCancelled { - await send(.modifyCodeCancelled) - return - } - - await send(.modifyCodeFailed(error: error.localizedDescription)) - } - }.cancellable(id: CancellationKey.modifyCode(state.id), cancelInFlight: true) - - case .revertButtonTapped: - guard let (code, description) = state.history.pop() else { return .none } - state.code = code - state.description = description - return .none - - case .stopRespondingButtonTapped: - state.isResponding = false - promptToCodeService.stopResponding() - return .cancel(id: CancellationKey.modifyCode(state.id)) - - case let .modifyCodeChunkReceived(code, description): - state.code = code - state.description = description - return .none - - case .modifyCodeFinished: - state.prompt = "" - state.isResponding = false - if state.code.isEmpty, state.description.isEmpty { - // if both code and description are empty, we treat it as failed - return .run { send in - await send(.revertButtonTapped) - } - } - - return .none - - case let .modifyCodeFailed(error): - state.error = error - state.isResponding = false - return .run { send in - await send(.revertButtonTapped) - } - - case .modifyCodeCancelled: - state.isResponding = false - return .none - - case .cancelButtonTapped: - promptToCodeService.stopResponding() - return .none - - case .acceptButtonTapped: - promptToCodeAcceptHandler(state) - return .none - - case .copyCodeButtonTapped: - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(state.code, forType: .string) - return .none - - case .appendNewLineToPromptButtonTapped: - state.prompt += "\n" - return .none - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift deleted file mode 100644 index b9617798..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ /dev/null @@ -1,188 +0,0 @@ -import ComposableArchitecture -import Foundation -import PromptToCodeService -import SuggestionBasic -import XcodeInspector - -@Reducer -public struct PromptToCodeGroup { - @ObservableState - public struct State: Equatable { - public var promptToCodes: IdentifiedArrayOf = [] - public var activeDocumentURL: PromptToCode.State.ID? = XcodeInspector.shared - .realtimeActiveDocumentURL - public var activePromptToCode: PromptToCode.State? { - get { - if let detached = promptToCodes.first(where: { !$0.isAttachedToSelectionRange }) { - return detached - } - guard let id = activeDocumentURL else { return nil } - return promptToCodes[id: id] - } - set { - if let id = newValue?.id { - promptToCodes[id: id] = newValue - } - } - } - } - - public struct PromptToCodeInitialState: Equatable { - public var code: String - public var selectionRange: CursorRange? - public var language: CodeLanguage - public var identSize: Int - public var usesTabsForIndentation: Bool - public var documentURL: URL - public var projectRootURL: URL - public var allCode: String - public var allLines: [String] - public var isContinuous: Bool - public var commandName: String? - public var defaultPrompt: String - public var extraSystemPrompt: String? - public var generateDescriptionRequirement: Bool? - - public init( - code: String, - selectionRange: CursorRange?, - language: CodeLanguage, - identSize: Int, - usesTabsForIndentation: Bool, - documentURL: URL, - projectRootURL: URL, - allCode: String, - allLines: [String], - isContinuous: Bool, - commandName: String?, - defaultPrompt: String, - extraSystemPrompt: String?, - generateDescriptionRequirement: Bool? - ) { - self.code = code - self.selectionRange = selectionRange - self.language = language - self.identSize = identSize - self.usesTabsForIndentation = usesTabsForIndentation - self.documentURL = documentURL - self.projectRootURL = projectRootURL - self.allCode = allCode - self.allLines = allLines - self.isContinuous = isContinuous - self.commandName = commandName - self.defaultPrompt = defaultPrompt - self.extraSystemPrompt = extraSystemPrompt - self.generateDescriptionRequirement = generateDescriptionRequirement - } - } - - public enum Action: Equatable { - /// Activate the prompt to code if it exists or create it if it doesn't - case activateOrCreatePromptToCode(PromptToCodeInitialState) - case createPromptToCode(PromptToCodeInitialState) - case updatePromptToCodeRange(id: PromptToCode.State.ID, range: CursorRange) - case discardAcceptedPromptToCodeIfNotContinuous(id: PromptToCode.State.ID) - case updateActivePromptToCode(documentURL: URL) - case discardExpiredPromptToCode(documentURLs: [URL]) - case promptToCode(PromptToCode.State.ID, PromptToCode.Action) - case activePromptToCode(PromptToCode.Action) - } - - @Dependency(\.promptToCodeServiceFactory) var promptToCodeServiceFactory - @Dependency(\.activatePreviousActiveXcode) var activatePreviousActiveXcode - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case let .activateOrCreatePromptToCode(s): - if let promptToCode = state.activePromptToCode { - return .run { send in - await send(.promptToCode(promptToCode.id, .focusOnTextField)) - } - } - return .run { send in - await send(.createPromptToCode(s)) - } - case let .createPromptToCode(s): - let newPromptToCode = PromptToCode.State( - code: s.code, - prompt: s.defaultPrompt, - language: s.language, - indentSize: s.identSize, - usesTabsForIndentation: s.usesTabsForIndentation, - projectRootURL: s.projectRootURL, - documentURL: s.documentURL, - allCode: s.allCode, - allLines: s.allLines, - commandName: s.commandName, - isContinuous: s.isContinuous, - selectionRange: s.selectionRange, - extraSystemPrompt: s.extraSystemPrompt, - generateDescriptionRequirement: s.generateDescriptionRequirement - ) - // insert at 0 so it has high priority then the other detached prompt to codes - state.promptToCodes.insert(newPromptToCode, at: 0) - return .run { send in - if !newPromptToCode.prompt.isEmpty { - await send(.promptToCode(newPromptToCode.id, .modifyCodeButtonTapped)) - } - }.cancellable( - id: PromptToCode.CancellationKey.modifyCode(newPromptToCode.id), - cancelInFlight: true - ) - - case let .updatePromptToCodeRange(id, range): - if let p = state.promptToCodes[id: id], p.isAttachedToSelectionRange { - state.promptToCodes[id: id]?.selectionRange = range - } - return .none - - case let .discardAcceptedPromptToCodeIfNotContinuous(id): - state.promptToCodes.removeAll { $0.id == id && !$0.isContinuous } - return .none - - case let .updateActivePromptToCode(documentURL): - state.activeDocumentURL = documentURL - return .none - - case let .discardExpiredPromptToCode(documentURLs): - for url in documentURLs { - state.promptToCodes.remove(id: url) - } - return .none - - case .promptToCode: - return .none - - case .activePromptToCode: - return .none - } - } - .ifLet(\.activePromptToCode, action: \.activePromptToCode) { - PromptToCode() - .dependency(\.promptToCodeService, promptToCodeServiceFactory()) - } - .forEach(\.promptToCodes, action: /Action.promptToCode, element: { - PromptToCode() - .dependency(\.promptToCodeService, promptToCodeServiceFactory()) - }) - - Reduce { state, action in - switch action { - case let .promptToCode(id, .cancelButtonTapped): - state.promptToCodes.remove(id: id) - return .run { _ in - activatePreviousActiveXcode() - } - case .activePromptToCode(.cancelButtonTapped): - guard let id = state.activePromptToCode?.id else { return .none } - state.promptToCodes.remove(id: id) - return .run { _ in - activatePreviousActiveXcode() - } - default: return .none - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift deleted file mode 100644 index a3a22842..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift +++ /dev/null @@ -1,58 +0,0 @@ -import ComposableArchitecture -import Preferences -import SwiftUI - -@Reducer -public struct SharedPanelFeature { - public struct Content: Equatable { - public var promptToCodeGroup = PromptToCodeGroup.State() - var suggestion: CodeSuggestionProvider? - var isExpanded: Bool = false - public var promptToCode: PromptToCode.State? { promptToCodeGroup.activePromptToCode } - var error: String? - } - - @ObservableState - public struct State: Equatable { - var content: Content = .init() - var colorScheme: ColorScheme = .light - var alignTopToAnchor = false - var isPanelDisplayed: Bool = false - var isEmpty: Bool { - if content.error != nil { return false } - if content.promptToCode != nil { return false } - if content.suggestion != nil, - UserDefaults.shared - .value(for: \.suggestionPresentationMode) == .floatingWidget { return false } - return true - } - - var opacity: Double { - guard isPanelDisplayed else { return 0 } - guard !isEmpty else { return 0 } - return 1 - } - } - - public enum Action: Equatable { - case errorMessageCloseButtonTapped - case promptToCodeGroup(PromptToCodeGroup.Action) - } - - public var body: some ReducerOf { - Scope(state: \.content.promptToCodeGroup, action: \.promptToCodeGroup) { - PromptToCodeGroup() - } - - Reduce { state, action in - switch action { - case .errorMessageCloseButtonTapped: - state.content.error = nil - return .none - case .promptToCodeGroup: - return .none - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift deleted file mode 100644 index 028ae777..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift +++ /dev/null @@ -1,44 +0,0 @@ -import ComposableArchitecture -import Foundation -import SwiftUI - -@Reducer -public struct SuggestionPanelFeature { - @ObservableState - public struct State: Equatable { - var content: CodeSuggestionProvider? - var isExpanded: Bool = false - var colorScheme: ColorScheme = .light - var alignTopToAnchor = false - var firstLineIndent: Double = 0 - var lineHeight: Double = 17 - var isPanelDisplayed: Bool = false - var isPanelOutOfFrame: Bool = false - var warningMessage: String? - var warningURL: String? - var opacity: Double { - guard isPanelDisplayed else { return 0 } - if isPanelOutOfFrame { return 0 } - guard content != nil else { return 0 } - return 1 - } - } - - public enum Action: Equatable { - case noAction - case dismissWarning - } - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .dismissWarning: - state.warningMessage = nil - state.warningURL = nil - return .none - default: - return .none - } - } - } -} diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift deleted file mode 100644 index 14ac9d4b..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift +++ /dev/null @@ -1,36 +0,0 @@ -import ComposableArchitecture -import Preferences -import SwiftUI -import Toast - -@Reducer -public struct ToastPanel { - @ObservableState - public struct State: Equatable { - var toast: Toast.State = .init() - var colorScheme: ColorScheme = .light - var alignTopToAnchor = false - } - - public enum Action: Equatable { - case start - case toast(Toast.Action) - } - - public var body: some ReducerOf { - Scope(state: \.toast, action: \.toast) { - Toast() - } - - Reduce { state, action in - switch action { - case .start: - return .run { send in - await send(.toast(.start)) - } - case .toast: - return .none - } - } - } -} diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift deleted file mode 100644 index 83b516d5..00000000 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ /dev/null @@ -1,418 +0,0 @@ -import ActiveApplicationMonitor -import AppActivator -import AsyncAlgorithms -import ComposableArchitecture -import Foundation -import GitHubCopilotService -import Logger -import Preferences -import SwiftUI -import Toast -import XcodeInspector - -@Reducer -public struct WidgetFeature { - public struct WindowState: Equatable { - var alphaValue: Double = 0 - var frame: CGRect = .zero - } - - public enum WindowCanBecomeKey: Equatable { - case sharedPanel - case chatPanel - } - - @ObservableState - public struct State: Equatable { - var focusingDocumentURL: URL? - public var colorScheme: ColorScheme = .light - - var toastPanel = ToastPanel.State() - - // MARK: Panels - - public var panelState = PanelFeature.State() - - // MARK: ChatPanel - - public var chatPanelState = ChatPanelFeature.State() - - // MARK: CodeReview - - public var codeReviewPanelState = CodeReviewPanelFeature.State() - - // MARK: CircularWidget - - public struct CircularWidgetState: Equatable { - var isProcessingCounters = [CircularWidgetFeature.IsProcessingCounter]() - var isProcessing: Bool = false - } - - public var circularWidgetState = CircularWidgetState() - var _internalCircularWidgetState: CircularWidgetFeature.State { - get { - .init( - isProcessingCounters: circularWidgetState.isProcessingCounters, - isProcessing: circularWidgetState.isProcessing, - isDisplayingContent: { - if chatPanelState.isPanelDisplayed { - return true - } - if panelState.sharedPanelState.isPanelDisplayed, - !panelState.sharedPanelState.isEmpty - { - return true - } - if panelState.suggestionPanelState.isPanelDisplayed, - panelState.suggestionPanelState.content != nil - { - return true - } - return false - }(), - isContentEmpty: chatPanelState.currentChatWorkspace == nil - || (chatPanelState.currentChatWorkspace!.tabInfo.isEmpty - && panelState.sharedPanelState.isEmpty), - isChatPanelDetached: chatPanelState.isDetached, - isChatOpen: chatPanelState.isPanelDisplayed - ) - } - set { - circularWidgetState = .init( - isProcessingCounters: newValue.isProcessingCounters, - isProcessing: newValue.isProcessing - ) - } - } - - public init() {} - } - - private enum CancelID { - case observeActiveApplicationChange - case observeCompletionPanelChange - case observeFullscreenChange - case observeWindowChange - case observeEditorChange - case observeUserDefaults - } - - public enum Action: Equatable { - case startup - case observeActiveApplicationChange - case observeFullscreenChange - case observeColorSchemeChange - - case updateActiveApplication - case updateColorScheme - - case updatePanelStateToMatch(WidgetLocation) - case updateFocusingDocumentURL - case setFocusingDocumentURL(to: URL?) - case updateKeyWindow(WindowCanBecomeKey) - - case toastPanel(ToastPanel.Action) - case panel(PanelFeature.Action) - case chatPanel(ChatPanelFeature.Action) - case circularWidget(CircularWidgetFeature.Action) - case codeReviewPanel(CodeReviewPanelFeature.Action) - } - - var windowsController: WidgetWindowsController? { - suggestionWidgetControllerDependency.windowsController - } - - @Dependency(\.suggestionWidgetUserDefaultsObservers) var userDefaultsObservers - @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency - @Dependency(\.xcodeInspector) var xcodeInspector - @Dependency(\.mainQueue) var mainQueue - @Dependency(\.activateThisApp) var activateThisApp - @Dependency(\.activatePreviousActiveApp) var activatePreviousActiveApp - - public enum DebounceKey: Hashable { - case updateWindowOpacity - } - - public init() {} - - public var body: some ReducerOf { - Scope(state: \.toastPanel, action: \.toastPanel) { - ToastPanel() - } - - Scope(state: \._internalCircularWidgetState, action: \.circularWidget) { - CircularWidgetFeature() - } - - Scope(state: \.codeReviewPanelState, action: \.codeReviewPanel) { - CodeReviewPanelFeature() - } - - Reduce { state, action in - switch action { - case .circularWidget(.detachChatPanelToggleClicked): - return .run { send in - await send(.chatPanel(.toggleChatPanelDetachedButtonClicked)) - } - - case .circularWidget(.widgetClicked): - guard FeatureFlagNotifierImpl.shared.featureFlags.chat else { - return .none - } - - let wasDisplayingContent = state._internalCircularWidgetState.isDisplayingContent - if wasDisplayingContent { - state.panelState.sharedPanelState.isPanelDisplayed = false - state.panelState.suggestionPanelState.isPanelDisplayed = false - state.chatPanelState.isPanelDisplayed = false - } else { - state.panelState.sharedPanelState.isPanelDisplayed = true - state.panelState.suggestionPanelState.isPanelDisplayed = true - state.chatPanelState.isPanelDisplayed = true - } - - let isDisplayingContent = state._internalCircularWidgetState.isDisplayingContent - let hasChat = state.chatPanelState.currentChatWorkspace?.selectedTabInfo != nil - let hasPromptToCode = state.panelState.sharedPanelState.content - .promptToCodeGroup.activePromptToCode != nil - - return .run { send in - if isDisplayingContent { - if hasPromptToCode { - await send(.updateKeyWindow(.sharedPanel)) - } else if hasChat { - await send(.updateKeyWindow(.chatPanel)) - } - await send(.chatPanel(.focusActiveChatTab)) - } - - if isDisplayingContent, !(await NSApplication.shared.isActive) { - activateThisApp() - } else if !isDisplayingContent { - activatePreviousActiveApp() - } - } - - default: return .none - } - } - - Scope(state: \.panelState, action: \.panel) { - PanelFeature() - } - - Scope(state: \.chatPanelState, action: \.chatPanel) { - ChatPanelFeature() - } - - Reduce { state, action in - switch action { - case .chatPanel(.presentChatPanel): - let isDetached = state.chatPanelState.isDetached - return .run { _ in - await windowsController?.updateWindowLocation( - animated: false, - immediately: false - ) - await windowsController?.updateWindowOpacity(immediately: false) - if isDetached { - Task { @MainActor in - windowsController?.windows.chatPanelWindow.isWindowHidden = false - } - } - } - - case .chatPanel(.toggleChatPanelDetachedButtonClicked): - let isDetached = state.chatPanelState.isDetached - return .run { _ in - await windowsController?.updateWindowLocation( - animated: !isDetached, - immediately: false - ) - await windowsController?.updateWindowOpacity(immediately: false) - } - default: return .none - } - } - - Reduce { state, action in - switch action { - case .startup: - return .merge( - .run { send in - await send(.toastPanel(.start)) - await send(.observeActiveApplicationChange) - await send(.observeFullscreenChange) - await send(.observeColorSchemeChange) - } - ) - - case .observeActiveApplicationChange: - return .run { send in - let stream = AsyncStream { continuation in - let cancellable = xcodeInspector.$activeApplication.sink { newValue in - guard let newValue else { return } - continuation.yield(newValue) - } - continuation.onTermination = { _ in - cancellable.cancel() - } - } - - var previousAppIdentifier: pid_t? - for await app in stream { - try Task.checkCancellation() - if app.processIdentifier != previousAppIdentifier { - await send(.updateActiveApplication) - } - previousAppIdentifier = app.processIdentifier - } - }.cancellable(id: CancelID.observeActiveApplicationChange, cancelInFlight: true) - - case .observeFullscreenChange: - return .run { _ in - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.activeSpaceDidChangeNotification) - for await _ in sequence { - try Task.checkCancellation() - guard let activeXcode = await xcodeInspector.safe.activeXcode - else { continue } - guard let windowsController, - await windowsController.windows.fullscreenDetector.isOnActiveSpace - else { continue } - let app = activeXcode.appElement - if let _ = app.focusedWindow { - await windowsController.windows.orderFront() - } - } - }.cancellable(id: CancelID.observeFullscreenChange, cancelInFlight: true) - - case .observeColorSchemeChange: - return .run { send in - await send(.updateColorScheme) - let stream = AsyncStream { continuation in - userDefaultsObservers.xcodeColorSchemeChangeObserver.onChange = { - continuation.yield() - } - - userDefaultsObservers.systemColorSchemeChangeObserver.onChange = { - continuation.yield() - } - - Task { @MainActor in - Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in - continuation.yield() - } - } - - continuation.onTermination = { _ in - userDefaultsObservers.xcodeColorSchemeChangeObserver.onChange = {} - userDefaultsObservers.systemColorSchemeChangeObserver.onChange = {} - } - } - - for await _ in stream { - try Task.checkCancellation() - await send(.updateColorScheme) - } - }.cancellable(id: CancelID.observeUserDefaults, cancelInFlight: true) - - - case .updateActiveApplication: - return .none - - case .updateColorScheme: - let xcodePref = UserDefaults(suiteName: "com.apple.dt.Xcode")! - .value(forKey: "IDEAppearance") as? Int ?? 0 - let xcodeColorScheme: XcodeColorScheme = .init(rawValue: xcodePref) ?? .system - let systemColorScheme: ColorScheme = NSApp.effectiveAppearance.name == .darkAqua - ? .dark - : .light - - let scheme: ColorScheme = { - switch (xcodeColorScheme, systemColorScheme) { - case (.system, .dark), (.dark, _): - return .dark - case (.system, .light), (.light, _): - return .light - case (.system, _): - return .light - } - }() - - state.colorScheme = scheme - state.toastPanel.colorScheme = scheme - state.panelState.sharedPanelState.colorScheme = scheme - state.panelState.suggestionPanelState.colorScheme = scheme - state.chatPanelState.colorScheme = scheme - return .none - - case .updateFocusingDocumentURL: - return .run { send in - await send(.setFocusingDocumentURL( - to: await xcodeInspector.safe - .realtimeActiveDocumentURL - )) - } - - case let .setFocusingDocumentURL(url): - state.focusingDocumentURL = url - return .none - - case let .updatePanelStateToMatch(widgetLocation): - state.panelState.sharedPanelState.alignTopToAnchor = widgetLocation - .defaultPanelLocation - .alignPanelTop - - if let suggestionPanelLocation = widgetLocation.suggestionPanelLocation { - state.panelState.suggestionPanelState.isPanelOutOfFrame = false - state.panelState.suggestionPanelState - .alignTopToAnchor = suggestionPanelLocation - .alignPanelTop - state.panelState.suggestionPanelState.firstLineIndent = suggestionPanelLocation.firstLineIndent ?? 0 - if let lineHeight = suggestionPanelLocation.lineHeight { - state.panelState.suggestionPanelState.lineHeight = lineHeight - } - } else { - state.panelState.suggestionPanelState.isPanelOutOfFrame = true - } - - state.toastPanel.alignTopToAnchor = widgetLocation - .defaultPanelLocation - .alignPanelTop - - return .none - - case let .updateKeyWindow(window): - return .run { _ in - await MainActor.run { - switch window { - case .chatPanel: - windowsController?.windows.chatPanelWindow - .makeKeyAndOrderFront(nil) - case .sharedPanel: - windowsController?.windows.sharedPanelWindow - .makeKeyAndOrderFront(nil) - } - } - } - - case .toastPanel: - return .none - - case .circularWidget: - return .none - - case .panel: - return .none - - case .chatPanel: - return .none - - case .codeReviewPanel: - return .none - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/ModuleDependency.swift b/Core/Sources/SuggestionWidget/ModuleDependency.swift deleted file mode 100644 index fb0652b2..00000000 --- a/Core/Sources/SuggestionWidget/ModuleDependency.swift +++ /dev/null @@ -1,90 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import ChatTab -import ComposableArchitecture -import Dependencies -import Foundation -import Preferences -import SwiftUI -import UserDefaultsObserver -import XcodeInspector - -public final class SuggestionWidgetControllerDependency { - public var suggestionWidgetDataSource: SuggestionWidgetDataSource? - public var onOpenChatClicked: () -> Void = {} - public var onCustomCommandClicked: (CustomCommand) -> Void = { _ in } - var windowsController: WidgetWindowsController? - - public init() {} -} - -public final class WidgetUserDefaultsObservers { - let presentationModeChangeObserver = UserDefaultsObserver( - object: UserDefaults.shared, - forKeyPaths: [ - UserDefaultPreferenceKeys().suggestionPresentationMode.key, - ], context: nil - ) - let xcodeColorSchemeChangeObserver = UserDefaultsObserver( - object: UserDefaults(suiteName: "com.apple.dt.Xcode")!, - forKeyPaths: ["xcodeColorScheme"], - context: nil - ) - let systemColorSchemeChangeObserver = UserDefaultsObserver( - object: UserDefaults.standard, - forKeyPaths: ["AppleInterfaceStyle"], - context: nil - ) - - public init() {} -} - -struct SuggestionWidgetControllerDependencyKey: DependencyKey { - static let liveValue = SuggestionWidgetControllerDependency() -} - -struct UserDefaultsDependencyKey: DependencyKey { - static let liveValue = WidgetUserDefaultsObservers() -} - -struct XcodeInspectorKey: DependencyKey { - static let liveValue = XcodeInspector.shared -} - -struct ActiveApplicationMonitorKey: DependencyKey { - static let liveValue = ActiveApplicationMonitor.shared -} - -struct ChatTabBuilderCollectionKey: DependencyKey { - static let liveValue: () -> [ChatTabBuilderCollection] = { [] } -} - -public extension DependencyValues { - var suggestionWidgetControllerDependency: SuggestionWidgetControllerDependency { - get { self[SuggestionWidgetControllerDependencyKey.self] } - set { self[SuggestionWidgetControllerDependencyKey.self] = newValue } - } - - var suggestionWidgetUserDefaultsObservers: WidgetUserDefaultsObservers { - get { self[UserDefaultsDependencyKey.self] } - set { self[UserDefaultsDependencyKey.self] = newValue } - } - - var chatTabBuilderCollection: () -> [ChatTabBuilderCollection] { - get { self[ChatTabBuilderCollectionKey.self] } - set { self[ChatTabBuilderCollectionKey.self] = newValue } - } -} - -extension DependencyValues { - var xcodeInspector: XcodeInspector { - get { self[XcodeInspectorKey.self] } - set { self[XcodeInspectorKey.self] = newValue } - } - - var activeApplicationMonitor: ActiveApplicationMonitor { - get { self[ActiveApplicationMonitorKey.self] } - set { self[ActiveApplicationMonitorKey.self] = newValue } - } -} - diff --git a/Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift b/Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift deleted file mode 100644 index dd50233f..00000000 --- a/Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Combine -import Foundation -import Perception -import SharedUIComponents -import SwiftUI -import XcodeInspector - -@Perceptible -public final class CodeSuggestionProvider: Equatable { - public static func == (lhs: CodeSuggestionProvider, rhs: CodeSuggestionProvider) -> Bool { - lhs.code == rhs.code && lhs.language == rhs.language - } - - public var code: String = "" - public var language: String = "" - public var startLineIndex: Int = 0 - public var suggestionCount: Int = 0 - public var currentSuggestionIndex: Int = 0 - public var extraInformation: String = "" - - @PerceptionIgnored public var onSelectPreviousSuggestionTapped: () -> Void - @PerceptionIgnored public var onSelectNextSuggestionTapped: () -> Void - @PerceptionIgnored public var onRejectSuggestionTapped: () -> Void - @PerceptionIgnored public var onAcceptSuggestionTapped: () -> Void - @PerceptionIgnored public var onDismissSuggestionTapped: () -> Void - - public init( - code: String = "", - language: String = "", - startLineIndex: Int = 0, - startCharacerIndex: Int = 0, - suggestionCount: Int = 0, - currentSuggestionIndex: Int = 0, - onSelectPreviousSuggestionTapped: @escaping () -> Void = {}, - onSelectNextSuggestionTapped: @escaping () -> Void = {}, - onRejectSuggestionTapped: @escaping () -> Void = {}, - onAcceptSuggestionTapped: @escaping () -> Void = {}, - onDismissSuggestionTapped: @escaping () -> Void = {} - ) { - self.code = code - self.language = language - self.startLineIndex = startLineIndex - self.suggestionCount = suggestionCount - self.currentSuggestionIndex = currentSuggestionIndex - self.onSelectPreviousSuggestionTapped = onSelectPreviousSuggestionTapped - self.onSelectNextSuggestionTapped = onSelectNextSuggestionTapped - self.onRejectSuggestionTapped = onRejectSuggestionTapped - self.onAcceptSuggestionTapped = onAcceptSuggestionTapped - self.onDismissSuggestionTapped = onDismissSuggestionTapped - } - - func selectPreviousSuggestion() { onSelectPreviousSuggestionTapped() } - func selectNextSuggestion() { onSelectNextSuggestionTapped() } - func rejectSuggestion() { onRejectSuggestionTapped() } - func acceptSuggestion() { onAcceptSuggestionTapped() } - func dismissSuggestion() { onDismissSuggestionTapped() } - - -} - diff --git a/Core/Sources/SuggestionWidget/SharedPanelView.swift b/Core/Sources/SuggestionWidget/SharedPanelView.swift deleted file mode 100644 index 6fed9f13..00000000 --- a/Core/Sources/SuggestionWidget/SharedPanelView.swift +++ /dev/null @@ -1,184 +0,0 @@ -import ComposableArchitecture -import Preferences -import SwiftUI - -extension View { - @ViewBuilder - func animation( - featureFlag: KeyPath, - _ animation: Animation?, - value: V - ) -> some View { - let isOn = UserDefaults.shared.value(for: featureFlag) - if isOn { - self.animation(animation, value: value) - } else { - self - } - } -} - -struct SharedPanelView: View { - var store: StoreOf - - struct OverallState: Equatable { - var isPanelDisplayed: Bool - var opacity: Double - var colorScheme: ColorScheme - var alignTopToAnchor: Bool - } - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - if !store.alignTopToAnchor { - Spacer() - .frame(minHeight: 0, maxHeight: .infinity) - .allowsHitTesting(false) - } - - DynamicContent(store: store) - - .frame(maxWidth: .infinity, maxHeight: Style.panelHeight) - .fixedSize(horizontal: false, vertical: true) - .allowsHitTesting(store.isPanelDisplayed) - .frame(maxWidth: .infinity) - - if store.alignTopToAnchor { - Spacer() - .frame(minHeight: 0, maxHeight: .infinity) - .allowsHitTesting(false) - } - } - .preferredColorScheme(store.colorScheme) - .opacity(store.opacity) - .animation( - featureFlag: \.animationBCrashSuggestion, - .easeInOut(duration: 0.2), - value: store.isPanelDisplayed - ) - .frame(maxWidth: Style.panelWidth, maxHeight: Style.panelHeight) - } - } - - struct DynamicContent: View { - let store: StoreOf - - @AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode - - var body: some View { - WithPerceptionTracking { - ZStack(alignment: .topLeading) { - if let errorMessage = store.content.error { - error(errorMessage) - } else if let _ = store.content.promptToCode { - promptToCode() - } else if let suggestionProvider = store.content.suggestion { - suggestion(suggestionProvider) - } - } - } - } - - @ViewBuilder - func error(_ error: String) -> some View { - ErrorPanel(description: error) { - store.send( - .errorMessageCloseButtonTapped, - animation: .easeInOut(duration: 0.2) - ) - } - } - - @ViewBuilder - func promptToCode() -> some View { - if let store = store.scope( - state: \.content.promptToCodeGroup.activePromptToCode, - action: \.promptToCodeGroup.activePromptToCode - ) { - PromptToCodePanel(store: store) - } - } - - @ViewBuilder - func suggestion(_ suggestion: CodeSuggestionProvider) -> some View { - switch suggestionPresentationMode { - case .nearbyTextCursor: - EmptyView() - case .floatingWidget: - CodeBlockSuggestionPanel(suggestion: suggestion, firstLineIndent: 0, lineHeight: 12, isPanelDisplayed: true) - } - } - } -} - -struct CommandButtonStyle: ButtonStyle { - var color: Color - var cornerRadius: Double = 4 - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.vertical, 4) - .padding(.horizontal, 8) - .foregroundColor(.white) - .background( - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .fill(color.opacity(configuration.isPressed ? 0.8 : 1)) - .animation(.easeOut(duration: 0.1), value: configuration.isPressed) - ) - .overlay { - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .stroke(Color.white.opacity(0.2), style: .init(lineWidth: 1)) - } - } -} - -// MARK: - Previews - -struct SharedPanelView_Error_Preview: PreviewProvider { - static var previews: some View { - SharedPanelView(store: .init( - initialState: .init( - content: .init(error: "This is an error\nerror"), - colorScheme: .light, - isPanelDisplayed: true - ), - reducer: { SharedPanelFeature() } - )) - .frame(width: 450, height: 200) - } -} - -struct SharedPanelView_Both_DisplayingSuggestion_Preview: PreviewProvider { - static var previews: some View { - SharedPanelView(store: .init( - initialState: .init( - content: .init( - suggestion: .init( - code: """ - - (void)addSubview:(UIView *)view { - [self addSubview:view]; - } - """, - language: "objective-c", - startLineIndex: 8, - suggestionCount: 2, - currentSuggestionIndex: 0 - ) - ), - colorScheme: .dark, - isPanelDisplayed: true - ), - reducer: { SharedPanelFeature() } - )) - .frame(width: 450, height: 200) - .background { - HStack { - Color.red - Color.green - Color.blue - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/Styles.swift b/Core/Sources/SuggestionWidget/Styles.swift deleted file mode 100644 index 6a7ea438..00000000 --- a/Core/Sources/SuggestionWidget/Styles.swift +++ /dev/null @@ -1,144 +0,0 @@ -import AppKit -import MarkdownUI -import SharedUIComponents -import SwiftUI - -enum Style { - static let panelHeight: Double = 560 - static let panelWidth: Double = 504 - static let minChatPanelWidth: Double = 242 // Following the minimal width of Navigator in Xcode - static let inlineSuggestionMaxHeight: Double = 400 - static let inlineSuggestionPadding: Double = 25 - static let widgetHeight: Double = 20 - static var widgetWidth: Double { widgetHeight } - static let widgetPadding: Double = 4 - static let chatWindowTitleBarHeight: Double = 24 - static let trafficLightButtonSize: Double = 12 - static let codeReviewPanelWidth: Double = 550 - static let codeReviewPanelHeight: Double = 450 -} - -extension Color { - static var contentBackground: Color { - Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in - if appearance.isDarkMode { - return #colorLiteral(red: 0.1580096483, green: 0.1730263829, blue: 0.2026666105, alpha: 1) - } - return .white - })) - } - - static var userChatContentBackground: Color { - Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in - if appearance.isDarkMode { - return #colorLiteral(red: 0.2284317913, green: 0.2145925438, blue: 0.3214019983, alpha: 1) - } - return #colorLiteral(red: 0.9458052187, green: 0.9311983998, blue: 0.9906365955, alpha: 1) - })) - } -} - -extension NSAppearance { - var isDarkMode: Bool { - if bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { - return true - } else { - return false - } - } -} - -struct XcodeLikeFrame: View { - @Environment(\.colorScheme) var colorScheme - let content: Content - let cornerRadius: Double - - var body: some View { - content.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) - .background( - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .fill(Material.bar) - ) - .overlay( - RoundedRectangle(cornerRadius: max(0, cornerRadius), style: .continuous) - .stroke(Color.black.opacity(0.1), style: .init(lineWidth: 1)) - ) // Add an extra border just incase the background is not displayed. - .overlay( - RoundedRectangle(cornerRadius: max(0, cornerRadius - 1), style: .continuous) - .stroke(Color.white.opacity(0.2), style: .init(lineWidth: 1)) - .padding(1) - ) - } -} - -extension View { - func xcodeStyleFrame(cornerRadius: Double? = nil) -> some View { - XcodeLikeFrame(content: self, cornerRadius: cornerRadius ?? 10) - } -} - -extension MarkdownUI.Theme { - static func custom(fontSize: Double) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.primary) - BackgroundColor(Color.clear) - FontSize(fontSize) - } - .codeBlock { configuration in - configuration.label - .relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(16) - .padding(.top, 14) - .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay(alignment: .top) { - HStack(alignment: .center) { - Text(configuration.language ?? "code") - .foregroundStyle(.tertiary) - .font(.callout) - .padding(.leading, 8) - .lineLimit(1) - Spacer() - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(configuration.content, forType: .string) - } - } - } - .markdownMargin(top: 4, bottom: 16) - } - } - - static func functionCall(fontSize: Double) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.secondary) - BackgroundColor(Color.clear) - FontSize(fontSize - 1) - } - .list { configuration in - configuration.label - .markdownMargin(top: 4, bottom: 4) - } - .paragraph { configuration in - configuration.label - .markdownMargin(top: 0, bottom: 4) - } - .codeBlock { configuration in - configuration.label - .relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(16) - .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .markdownMargin(top: 4, bottom: 4) - } - } -} - diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift deleted file mode 100644 index db037302..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ /dev/null @@ -1,140 +0,0 @@ -import Combine -import Perception -import SharedUIComponents -import SuggestionBasic -import SwiftUI -import XcodeInspector -import ChatService -import Foundation -import SuggestionBasic - -public final class ExpandableSuggestionService: ObservableObject { - public static let shared = ExpandableSuggestionService() - @Published public var isSuggestionExpanded: Bool = false - - private init() {} -} - -struct CodeBlockSuggestionPanel: View { - let suggestion: CodeSuggestionProvider - let firstLineIndent: Double - let lineHeight: Double - let isPanelDisplayed: Bool - @Environment(CursorPositionTracker.self) var cursorPositionTracker - @Environment(\.colorScheme) var colorScheme - @AppStorage(\.suggestionCodeFont) var codeFont - /// <#Description#> - @AppStorage(\.suggestionDisplayCompactMode) var suggestionDisplayCompactMode - @AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode - @AppStorage(\.hideCommonPrecedingSpacesInSuggestion) var hideCommonPrecedingSpaces - @AppStorage(\.syncSuggestionHighlightTheme) var syncHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - @AppStorage(\.currentLineBackgroundColorLight) var currentLineBackgroundColorLight - @AppStorage(\.currentLineBackgroundColorDark) var currentLineBackgroundColorDark - @AppStorage(\.codeFontLight) var codeFontLight - @AppStorage(\.codeFontDark) var codeFontDark - - @ObservedObject var object = ExpandableSuggestionService.shared - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - WithPerceptionTracking { - AsyncCodeBlock( - code: suggestion.code, - language: suggestion.language, - startLineIndex: suggestion.startLineIndex, - scenario: "suggestion", - firstLineIndent: firstLineIndent, - lineHeight: lineHeight, - font: { - if syncHighlightTheme { - return colorScheme == .light ? codeFontLight.value.nsFont : codeFontDark.value.nsFont - } - return codeFont.value.nsFont - }(), - droppingLeadingSpaces: hideCommonPrecedingSpaces, - proposedForegroundColor: { - if syncHighlightTheme { - if colorScheme == .light, - let color = codeForegroundColorLight.value?.swiftUIColor - { - return color - } else if let color = codeForegroundColorDark.value? - .swiftUIColor - { - return color - } - } - return nil - }(), - proposedBackgroundColor: { - if syncHighlightTheme { - if colorScheme == .light, - let color = codeBackgroundColorLight.value?.swiftUIColor - { - return color - } else if let color = codeBackgroundColorDark.value?.swiftUIColor - { - return color - } - } - return nil - }(), - currentLineBackgroundColor: { - if colorScheme == .light, - let color = currentLineBackgroundColorLight.value?.swiftUIColor { - return color - } else if let color = currentLineBackgroundColorDark.value?.swiftUIColor { - return color - } - return nil - }(), - dimmedCharacterCount: suggestion.startLineIndex - == cursorPositionTracker.cursorPosition.line - ? cursorPositionTracker.cursorPosition.character - : 0, - isExpanded: $object.isSuggestionExpanded, - isPanelDisplayed: isPanelDisplayed - ) - .frame(maxWidth: .infinity) - .padding(Style.inlineSuggestionPadding) - } - } - } - .background(Color.clear) - } - } - -// MARK: - Previews - -#Preview("Code Block Suggestion Panel") { - CodeBlockSuggestionPanel(suggestion: CodeSuggestionProvider( - code: """ - LazyVGrid(columns: [GridItem(.fixed(30)), GridItem(.flexible())]) { - ForEach(0.. Void - - var body: some View { - ZStack(alignment: .topTrailing) { - Text(description) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundColor(.white) - .padding() - .background(Color.red) - - // close button - Button(action: onCloseButtonTap) { - Image(systemName: "xmark") - .padding([.leading, .bottom], 16) - .padding([.top, .trailing], 8) - .foregroundColor(.white) - } - .buttonStyle(.plain) - } - .xcodeStyleFrame() - } -} diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift deleted file mode 100644 index 682d9c79..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ /dev/null @@ -1,580 +0,0 @@ -import ComposableArchitecture -import MarkdownUI -import SharedUIComponents -import SuggestionBasic -import SwiftUI - -struct PromptToCodePanel: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 0) { - TopBar(store: store) - - Content(store: store) - .overlay(alignment: .bottom) { - ActionBar(store: store) - .padding(.bottom, 8) - } - - Divider() - - Toolbar(store: store) - } - .background(.ultraThickMaterial) - .xcodeStyleFrame() - } - } -} - -extension PromptToCodePanel { - struct TopBar: View { - let store: StoreOf - - var body: some View { - HStack { - SelectionRangeButton(store: store) - Spacer() - CopyCodeButton(store: store) - } - .padding(2) - } - - struct SelectionRangeButton: View { - let store: StoreOf - var body: some View { - WithPerceptionTracking { - Button(action: { - store.send(.selectionRangeToggleTapped, animation: .linear(duration: 0.1)) - }) { - let attachedToFilename = store.filename - let isAttached = store.isAttachedToSelectionRange - let selectionRange = store.selectionRange - let color: Color = isAttached ? .accentColor : .secondary.opacity(0.6) - HStack(spacing: 4) { - Image( - systemName: isAttached ? "link" : "character.cursor.ibeam" - ) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.white) - .background( - color, - in: RoundedRectangle( - cornerRadius: 4, - style: .continuous - ) - ) - - if isAttached { - HStack(spacing: 4) { - Text(attachedToFilename) - .lineLimit(1) - .truncationMode(.middle) - if let range = selectionRange { - Text(range.description) - } - }.foregroundColor(.primary) - } else { - Text("current selection").foregroundColor(.secondary) - } - } - .padding(2) - .padding(.trailing, 4) - .overlay { - RoundedRectangle(cornerRadius: 4, style: .continuous) - .stroke(color, lineWidth: 1) - } - .background { - RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(color.opacity(0.2)) - } - .padding(2) - } - .keyboardShortcut("j", modifiers: [.command]) - .buttonStyle(.plain) - } - } - } - - struct CopyCodeButton: View { - let store: StoreOf - var body: some View { - WithPerceptionTracking { - if !store.code.isEmpty { - CopyButton { - store.send(.copyCodeButtonTapped) - } - } - } - } - } - } - - struct ActionBar: View { - let store: StoreOf - - var body: some View { - HStack { - StopRespondingButton(store: store) - ActionButtons(store: store) - } - } - - struct StopRespondingButton: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - if store.isResponding { - Button(action: { - store.send(.stopRespondingButtonTapped) - }) { - HStack(spacing: 4) { - Image(systemName: "stop.fill") - Text("Stop") - } - .padding(8) - .background( - .regularMaterial, - in: RoundedRectangle(cornerRadius: 6, style: .continuous) - ) - .overlay { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - } - .buttonStyle(.plain) - } - } - } - } - - struct ActionButtons: View { - @Perception.Bindable var store: StoreOf - - var body: some View { - WithPerceptionTracking { - let isResponding = store.isResponding - let isCodeEmpty = store.code.isEmpty - let isDescriptionEmpty = store.description.isEmpty - var isRespondingButCodeIsReady: Bool { - isResponding - && !isCodeEmpty - && !isDescriptionEmpty - } - if !isResponding || isRespondingButCodeIsReady { - HStack { - Toggle("Continuous Mode", isOn: $store.isContinuous) - .toggleStyle(.checkbox) - - Button(action: { - store.send(.cancelButtonTapped) - }) { - Text("Cancel") - } - .buttonStyle(CommandButtonStyle(color: .gray)) - .keyboardShortcut("w", modifiers: [.command]) - - if !isCodeEmpty { - Button(action: { - store.send(.acceptButtonTapped) - }) { - Text("Accept(⌘ + ⏎)") - } - .buttonStyle(CommandButtonStyle(color: .accentColor)) - .keyboardShortcut(KeyEquivalent.return, modifiers: [.command]) - } - } - .padding(8) - .background( - .regularMaterial, - in: RoundedRectangle(cornerRadius: 6, style: .continuous) - ) - .overlay { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - } - } - } - } - } - - struct Content: View { - let store: StoreOf - @Environment(\.colorScheme) var colorScheme - @AppStorage(\.syncPromptToCodeHighlightTheme) var syncHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - - var codeForegroundColor: Color? { - if syncHighlightTheme { - if colorScheme == .light, - let color = codeForegroundColorLight.value?.swiftUIColor - { - return color - } else if let color = codeForegroundColorDark.value?.swiftUIColor { - return color - } - } - return nil - } - - var codeBackgroundColor: Color { - if syncHighlightTheme { - if colorScheme == .light, - let color = codeBackgroundColorLight.value?.swiftUIColor - { - return color - } else if let color = codeBackgroundColorDark.value?.swiftUIColor { - return color - } - } - return Color.contentBackground - } - - var body: some View { - WithPerceptionTracking { - ScrollView { - VStack(spacing: 0) { - Spacer(minLength: 60) - ErrorMessage(store: store) - DescriptionContent(store: store, codeForegroundColor: codeForegroundColor) - CodeContent(store: store, codeForegroundColor: codeForegroundColor) - } - } - .background(codeBackgroundColor) - .scaleEffect(x: 1, y: -1, anchor: .center) - } - } - - struct ErrorMessage: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - if let errorMessage = store.error, !errorMessage.isEmpty { - Text(errorMessage) - .multilineTextAlignment(.leading) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background( - Color.red, - in: RoundedRectangle(cornerRadius: 4, style: .continuous) - ) - .overlay { - RoundedRectangle(cornerRadius: 4, style: .continuous) - .stroke(Color.primary.opacity(0.2), lineWidth: 1) - } - .scaleEffect(x: 1, y: -1, anchor: .center) - } - } - } - } - - struct DescriptionContent: View { - let store: StoreOf - let codeForegroundColor: Color? - - var body: some View { - WithPerceptionTracking { - if !store.description.isEmpty { - Markdown(store.description) - .textSelection(.enabled) - .markdownTheme(.gitHub.text { - BackgroundColor(Color.clear) - ForegroundColor(codeForegroundColor) - }) - .padding() - .frame(maxWidth: .infinity) - .scaleEffect(x: 1, y: -1, anchor: .center) - } - } - } - } - - struct CodeContent: View { - let store: StoreOf - let codeForegroundColor: Color? - - @AppStorage(\.wrapCodeInPromptToCode) var wrapCode - - var body: some View { - WithPerceptionTracking { - if store.code.isEmpty { - Text( - store.isResponding - ? "Thinking..." - : "Enter your requirement to generate code." - ) - .foregroundColor(codeForegroundColor?.opacity(0.7) ?? .secondary) - .padding() - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .scaleEffect(x: 1, y: -1, anchor: .center) - } else { - if wrapCode { - CodeBlockInContent( - store: store, - codeForegroundColor: codeForegroundColor - ) - } else { - ScrollView(.horizontal) { - CodeBlockInContent( - store: store, - codeForegroundColor: codeForegroundColor - ) - } - .modify { - if #available(macOS 13.0, *) { - $0.scrollIndicators(.hidden) - } else { - $0 - } - } - } - } - } - } - - struct CodeBlockInContent: View { - let store: StoreOf - let codeForegroundColor: Color? - - @Environment(\.colorScheme) var colorScheme - @AppStorage(\.promptToCodeCodeFont) var codeFont - @AppStorage(\.hideCommonPrecedingSpacesInPromptToCode) var hideCommonPrecedingSpaces - - var body: some View { - WithPerceptionTracking { - let startLineIndex = store.selectionRange?.start.line ?? 0 - let firstLinePrecedingSpaceCount = store.selectionRange?.start - .character ?? 0 - CodeBlock( - code: store.code, - language: store.language.rawValue, - startLineIndex: startLineIndex, - scenario: "promptToCode", - colorScheme: colorScheme, - firstLinePrecedingSpaceCount: firstLinePrecedingSpaceCount, - font: codeFont.value.nsFont, - droppingLeadingSpaces: hideCommonPrecedingSpaces, - proposedForegroundColor: codeForegroundColor - ) - .frame(maxWidth: .infinity) - .scaleEffect(x: 1, y: -1, anchor: .center) - } - } - } - } - } - - struct Toolbar: View { - let store: StoreOf - @FocusState var focusField: PromptToCode.State.FocusField? - - struct RevertButtonState: Equatable { - var isResponding: Bool - var canRevert: Bool - } - - var body: some View { - HStack { - RevertButton(store: store) - - HStack(spacing: 0) { - InputField(store: store, focusField: $focusField) - SendButton(store: store) - } - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: 6) - .fill(Color(nsColor: .controlBackgroundColor)) - } - .overlay { - RoundedRectangle(cornerRadius: 6) - .stroke(Color(nsColor: .controlColor), lineWidth: 1) - } - .background { - Button(action: { store.send(.appendNewLineToPromptButtonTapped) }) { - EmptyView() - } - .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) - } - .background { - Button(action: { focusField = .textField }) { - EmptyView() - } - .keyboardShortcut("l", modifiers: [.command]) - } - } - .padding(8) - .background(.ultraThickMaterial) - } - - struct RevertButton: View { - let store: StoreOf - var body: some View { - WithPerceptionTracking { - Button(action: { - store.send(.revertButtonTapped) - }) { - Group { - Image(systemName: "arrow.uturn.backward") - } - .padding(6) - .background { - Circle().fill(Color(nsColor: .controlBackgroundColor)) - } - .overlay { - Circle() - .stroke(Color(nsColor: .controlColor), lineWidth: 1) - } - } - .buttonStyle(.plain) - .disabled(store.isResponding || !store.canRevert) - } - } - } - - struct InputField: View { - @Perception.Bindable var store: StoreOf - var focusField: FocusState.Binding - - var body: some View { - WithPerceptionTracking { - AutoresizingCustomTextEditor( - text: $store.prompt, - font: .systemFont(ofSize: 14), - isEditable: !store.isResponding, - maxHeight: 400, - onSubmit: { store.send(.modifyCodeButtonTapped) } - ) - .opacity(store.isResponding ? 0.5 : 1) - .disabled(store.isResponding) - .focused(focusField, equals: PromptToCode.State.FocusField.textField) - .bind($store.focusedField, to: focusField) - } - .padding(8) - .fixedSize(horizontal: false, vertical: true) - } - } - - struct SendButton: View { - let store: StoreOf - var body: some View { - WithPerceptionTracking { - Button(action: { - store.send(.modifyCodeButtonTapped) - }) { - Image(systemName: "paperplane.fill") - .padding(8) - } - .buttonStyle(.plain) - .disabled(store.isResponding) - .keyboardShortcut(KeyEquivalent.return, modifiers: []) - } - } - } - } -} - -// MARK: - Previews - -#Preview("Default") { - PromptToCodePanel(store: .init(initialState: .init( - code: """ - ForEach(0.. Bool - - func body(content: Content) -> some View { - WithPerceptionTracking { - content.allowsHitTesting(hitTestPredicate()) - } - } -} - -struct ToastPanelView: View { - let store: StoreOf - @Dependency(\.toastController) var toastController - - var body: some View { - WithPerceptionTracking { - VStack(spacing: 4) { - if !store.alignTopToAnchor { - Spacer() - .allowsHitTesting(false) - } - - ForEach(store.toast.messages) { message in - NotificationView( - message: message, - onDismiss: { toastController.dismissMessage(withId: message.id) } - ) - .frame(maxWidth: 450) - // Allow hit testing for notification views - .allowsHitTesting(true) - } - - if store.alignTopToAnchor { - Spacer() - .allowsHitTesting(false) - } - } - .colorScheme(store.colorScheme) - .background(Color.clear) - // Only allow hit testing when there are messages - // to prevent the view from blocking the mouse events - .modifier(HitTestConfiguration(hitTestPredicate: { !store.toast.messages.isEmpty })) - } - } -} diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/WarningPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/WarningPanel.swift deleted file mode 100644 index c06a915a..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/WarningPanel.swift +++ /dev/null @@ -1,81 +0,0 @@ -import SwiftUI -import SharedUIComponents -import XcodeInspector -import ComposableArchitecture - -struct WarningPanel: View { - let message: String - let url: String? - let firstLineIndent: Double - let onDismiss: () -> Void - - @Environment(\.colorScheme) var colorScheme - @Environment(CursorPositionTracker.self) var cursorPositionTracker - @AppStorage(\.clsWarningDismissedUntilRelaunch) var isDismissedUntilRelaunch - - var foregroundColor: Color { - return colorScheme == .light ? .black.opacity(0.85) : .white.opacity(0.85) - } - - var body: some View { - WithPerceptionTracking { - if !isDismissedUntilRelaunch { - HStack(spacing: 12) { - HStack(spacing: 8) { - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFit() - .foregroundColor(.primary) - .frame(width: 14, height: 14) - - Text("Monthly completion limit reached.") - .font(.system(size: 12)) - .foregroundColor(.primary) - .lineLimit(1) - } - .padding(.horizontal, 9) - .background( - Capsule() - .fill(foregroundColor.opacity(0.1)) - .frame(height: 17) - ) - .fixedSize() - - HStack(spacing: 8) { - if let url = url { - Button("Upgrade Now") { - NSWorkspace.shared.open(URL(string: url)!) - } - .buttonStyle(.plain) - .padding(.horizontal, 8) - .padding(.vertical, 2) - .background(Color(nsColor: .controlAccentColor)) - .foregroundColor(Color(nsColor: .white)) - .cornerRadius(6) - .font(.system(size: 12)) - .fixedSize() - } - - Button("Dismiss") { - isDismissedUntilRelaunch = true - onDismiss() - } - .buttonStyle(.bordered) - .font(.system(size: 12)) - .keyboardShortcut(.escape, modifiers: []) - .fixedSize() - } - } - .padding(.top, 24) - .padding( - .leading, - firstLineIndent + 20 + CGFloat( - cursorPositionTracker.cursorPosition.character - ) - ) - .background(.clear) - } - } - } -} diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelView.swift deleted file mode 100644 index a0469859..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionPanelView.swift +++ /dev/null @@ -1,68 +0,0 @@ -import ComposableArchitecture -import Foundation -import SwiftUI - -struct SuggestionPanelView: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - Group { - if let message = store.warningMessage { - WarningPanel( - message: message, - url: store.warningURL, - firstLineIndent: store.firstLineIndent - ) { - store.send(.dismissWarning) - } - } else { - VStack(spacing: 0) { - Content(store: store) - .allowsHitTesting( - store.isPanelDisplayed && !store.isPanelOutOfFrame - ) - .frame(maxWidth: .infinity) - } - .preferredColorScheme(store.colorScheme) - .opacity(store.opacity) - .animation( - featureFlag: \.animationBCrashSuggestion, - .easeInOut(duration: 0.2), - value: store.isPanelDisplayed - ) - .animation( - featureFlag: \.animationBCrashSuggestion, - .easeInOut(duration: 0.2), - value: store.isPanelOutOfFrame - ) - .frame( - maxWidth: .infinity, - maxHeight: Style.inlineSuggestionMaxHeight, - alignment: .top - ) - } - } - } - } - - struct Content: View { - let store: StoreOf - - var body: some View { - WithPerceptionTracking { - if let content = store.content { - CodeBlockSuggestionPanel( - suggestion: content, - firstLineIndent: store.firstLineIndent, - lineHeight: store.lineHeight, - isPanelDisplayed: store.isPanelDisplayed - ) - .frame(maxWidth: .infinity, maxHeight: Style.inlineSuggestionMaxHeight) - .fixedSize(horizontal: false, vertical: true) - } - } - } - } -} - diff --git a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift deleted file mode 100644 index 06adce2f..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift +++ /dev/null @@ -1,104 +0,0 @@ -import ActiveApplicationMonitor -import AppKit -import AsyncAlgorithms -import ChatTab -import Combine -import ComposableArchitecture -import Preferences -import SwiftUI -import UserDefaultsObserver -import XcodeInspector - -@MainActor -public final class SuggestionWidgetController: NSObject { - let store: StoreOf - let chatTabPool: ChatTabPool - let windowsController: WidgetWindowsController - private var cancellable = Set() - - public let dependency: SuggestionWidgetControllerDependency - - public init( - store: StoreOf, - chatTabPool: ChatTabPool, - dependency: SuggestionWidgetControllerDependency - ) { - self.dependency = dependency - self.store = store - self.chatTabPool = chatTabPool - windowsController = .init(store: store, chatTabPool: chatTabPool) - - super.init() - - if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return } - - dependency.windowsController = windowsController - - store.send(.startup) - Task { - await windowsController.start() - } - } -} - -// MARK: - Handle Events - -public extension SuggestionWidgetController { - func suggestCode() { - store.send(.panel(.presentSuggestion)) - } - - func expandSuggestion() { - store.withState { state in - if state.panelState.content.suggestion != nil { - store.send(.panel(.expandSuggestion)) - } - } - } - - func discardSuggestion() { - store.withState { state in - if state.panelState.content.suggestion != nil { - store.send(.panel(.discardSuggestion)) - } - } - } - - #warning("TODO: Make a progress controller that doesn't use TCA.") - func markAsProcessing(_ isProcessing: Bool) { - store.withState { state in - if isProcessing, !state.circularWidgetState.isProcessing { - store.send(.circularWidget(.markIsProcessing)) - } else if !isProcessing, state.circularWidgetState.isProcessing { - store.send(.circularWidget(.endIsProcessing)) - } - } - } - - func presentError(_ errorDescription: String) { - store.send(.toastPanel(.toast(.toast(errorDescription, .error, nil)))) - } - - func presentChatRoom() { - store.send(.chatPanel(.presentChatPanel(forceDetach: false))) - } - - func presentDetachedGlobalChat() { - store.send(.chatPanel(.presentChatPanel(forceDetach: true))) - } - - func closeChatRoom() { -// store.send(.chatPanel(.closeChatPanel)) - } -} - -extension SuggestionWidgetController { - public func presentWarning(message: String, url: String?) { - store.send(.panel(.presentWarning(message: message, url: url))) - } - - public func dismissWarning() { - store.send(.panel(.dismissWarning)) - } -} - diff --git a/Core/Sources/SuggestionWidget/SuggestionWidgetDataSource.swift b/Core/Sources/SuggestionWidget/SuggestionWidgetDataSource.swift deleted file mode 100644 index f7ad662a..00000000 --- a/Core/Sources/SuggestionWidget/SuggestionWidgetDataSource.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -public protocol SuggestionWidgetDataSource { - func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? -} - -struct MockWidgetDataSource: SuggestionWidgetDataSource { - func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? { - return CodeSuggestionProvider( - code: """ - func test() { - let x = 1 - let y = 2 - let z = x + y - } - """, - language: "swift", - startLineIndex: 1, - suggestionCount: 3, - currentSuggestionIndex: 0 - ) - } -} - diff --git a/Core/Sources/SuggestionWidget/WidgetPositionStrategy.swift b/Core/Sources/SuggestionWidget/WidgetPositionStrategy.swift deleted file mode 100644 index 6ad035fb..00000000 --- a/Core/Sources/SuggestionWidget/WidgetPositionStrategy.swift +++ /dev/null @@ -1,443 +0,0 @@ -import AppKit -import Foundation -import XcodeInspector -import ConversationServiceProvider - -public struct WidgetLocation: Equatable { - struct PanelLocation: Equatable { - var frame: CGRect - var alignPanelTop: Bool - var firstLineIndent: Double? - var lineHeight: Double? - } - - var widgetFrame: CGRect - var tabFrame: CGRect - var defaultPanelLocation: PanelLocation - var suggestionPanelLocation: PanelLocation? -} - -enum UpdateLocationStrategy { - struct AlignToTextCursor { - func framesForWindows( - editorFrame: CGRect, - mainScreen: NSScreen, - activeScreen: NSScreen, - editor: AXUIElement, - hideCircularWidget: Bool = UserDefaults.shared.value(for: \.hideCircularWidget), - preferredInsideEditorMinWidth: Double = UserDefaults.shared - .value(for: \.preferWidgetToStayInsideEditorWhenWidthGreaterThan) - ) -> WidgetLocation { - guard let selectedRange: AXValue = try? editor - .copyValue(key: kAXSelectedTextRangeAttribute), - let rect: AXValue = try? editor.copyParameterizedValue( - key: kAXBoundsForRangeParameterizedAttribute, - parameters: selectedRange - ) - else { - return FixedToBottom().framesForWindows( - editorFrame: editorFrame, - mainScreen: mainScreen, - activeScreen: activeScreen, - hideCircularWidget: hideCircularWidget - ) - } - var frame: CGRect = .zero - let found = AXValueGetValue(rect, .cgRect, &frame) - guard found else { - return FixedToBottom().framesForWindows( - editorFrame: editorFrame, - mainScreen: mainScreen, - activeScreen: activeScreen, - hideCircularWidget: hideCircularWidget - ) - } - return HorizontalMovable().framesForWindows( - y: mainScreen.frame.height - frame.maxY, - alignPanelTopToAnchor: nil, - editorFrame: editorFrame, - mainScreen: mainScreen, - activeScreen: activeScreen, - preferredInsideEditorMinWidth: preferredInsideEditorMinWidth, - hideCircularWidget: hideCircularWidget - ) - } - } - - struct FixedToBottom { - func framesForWindows( - editorFrame: CGRect, - mainScreen: NSScreen, - activeScreen: NSScreen, - hideCircularWidget: Bool = UserDefaults.shared.value(for: \.hideCircularWidget), - preferredInsideEditorMinWidth: Double = UserDefaults.shared - .value(for: \.preferWidgetToStayInsideEditorWhenWidthGreaterThan), - editorFrameExpendedSize: CGSize = .zero - ) -> WidgetLocation { - return HorizontalMovable().framesForWindows( - y: mainScreen.frame.height - editorFrame.maxY + Style.widgetPadding, - alignPanelTopToAnchor: false, - editorFrame: editorFrame, - mainScreen: mainScreen, - activeScreen: activeScreen, - preferredInsideEditorMinWidth: preferredInsideEditorMinWidth, - hideCircularWidget: hideCircularWidget, - editorFrameExpendedSize: editorFrameExpendedSize - ) - } - } - - struct HorizontalMovable { - func framesForWindows( - y: CGFloat, - alignPanelTopToAnchor fixedAlignment: Bool?, - editorFrame: CGRect, - mainScreen: NSScreen, - activeScreen: NSScreen, - preferredInsideEditorMinWidth: Double, - hideCircularWidget: Bool = UserDefaults.shared.value(for: \.hideCircularWidget), - editorFrameExpendedSize: CGSize = .zero - ) -> WidgetLocation { - let maxY = max( - y, - mainScreen.frame.height - editorFrame.maxY + Style.widgetPadding, - 4 + activeScreen.frame.minY - ) - let y = min( - maxY, - activeScreen.frame.maxY - 4, - mainScreen.frame.height - editorFrame.minY - Style.widgetHeight - Style - .widgetPadding - ) - - var proposedAnchorFrameOnTheRightSide = CGRect( - x: editorFrame.maxX - Style.widgetPadding, - y: y, - width: 0, - height: 0 - ) - - let widgetFrameOnTheRightSide = CGRect( - x: editorFrame.maxX - Style.widgetPadding - Style.widgetWidth, - y: y, - width: Style.widgetWidth, - height: Style.widgetHeight - ) - - if !hideCircularWidget { - proposedAnchorFrameOnTheRightSide = widgetFrameOnTheRightSide - } - - let proposedPanelX = proposedAnchorFrameOnTheRightSide.maxX - + Style.widgetPadding * 2 - - editorFrameExpendedSize.width - let putPanelToTheRight = { - if editorFrame.size.width >= preferredInsideEditorMinWidth { return false } - return activeScreen.frame.maxX > proposedPanelX + Style.panelWidth - }() - let alignPanelTopToAnchor = fixedAlignment ?? (y > activeScreen.frame.midY) - - let chatPanelFrame = getChatPanelFrame(mainScreen) - - if putPanelToTheRight { - let anchorFrame = proposedAnchorFrameOnTheRightSide - let tabFrame = CGRect( - x: anchorFrame.origin.x, - y: alignPanelTopToAnchor - ? anchorFrame.minY - Style.widgetHeight - Style.widgetPadding - : anchorFrame.maxY + Style.widgetPadding, - width: Style.widgetWidth, - height: Style.widgetHeight - ) - - return .init( - widgetFrame: widgetFrameOnTheRightSide, - tabFrame: tabFrame, - defaultPanelLocation: .init( - frame: chatPanelFrame, - alignPanelTop: alignPanelTopToAnchor - ), - suggestionPanelLocation: nil - ) - } else { - var proposedAnchorFrameOnTheLeftSide = CGRect( - x: editorFrame.minX + Style.widgetPadding, - y: proposedAnchorFrameOnTheRightSide.origin.y, - width: 0, - height: 0 - ) - - let widgetFrameOnTheLeftSide = CGRect( - x: editorFrame.minX + Style.widgetPadding, - y: proposedAnchorFrameOnTheRightSide.origin.y, - width: Style.widgetWidth, - height: Style.widgetHeight - ) - - if !hideCircularWidget { - proposedAnchorFrameOnTheLeftSide = widgetFrameOnTheLeftSide - } - - let proposedPanelX = proposedAnchorFrameOnTheLeftSide.minX - - Style.widgetPadding * 2 - - Style.panelWidth - + editorFrameExpendedSize.width - let putAnchorToTheLeft = { - if editorFrame.size.width >= preferredInsideEditorMinWidth { - if editorFrame.maxX <= activeScreen.frame.maxX { - return false - } - } - return proposedPanelX > activeScreen.frame.minX - }() - - if putAnchorToTheLeft { - let anchorFrame = proposedAnchorFrameOnTheLeftSide - let tabFrame = CGRect( - x: anchorFrame.origin.x, - y: alignPanelTopToAnchor - ? anchorFrame.minY - Style.widgetHeight - Style.widgetPadding - : anchorFrame.maxY + Style.widgetPadding, - width: Style.widgetWidth, - height: Style.widgetHeight - ) - return .init( - widgetFrame: widgetFrameOnTheLeftSide, - tabFrame: tabFrame, - defaultPanelLocation: .init( - frame: chatPanelFrame, - alignPanelTop: alignPanelTopToAnchor - ), - suggestionPanelLocation: nil - ) - } else { - let anchorFrame = proposedAnchorFrameOnTheRightSide - let tabFrame = CGRect( - x: anchorFrame.minX - Style.widgetPadding - Style.widgetWidth, - y: anchorFrame.origin.y, - width: Style.widgetWidth, - height: Style.widgetHeight - ) - return .init( - widgetFrame: widgetFrameOnTheRightSide, - tabFrame: tabFrame, - defaultPanelLocation: .init( - frame: chatPanelFrame, - alignPanelTop: alignPanelTopToAnchor - ), - suggestionPanelLocation: nil - ) - } - } - } - } - - struct NearbyTextCursor { - func framesForSuggestionWindow( - editorFrame: CGRect, - mainScreen: NSScreen, - activeScreen: NSScreen, - editor: AXUIElement, - completionPanel: AXUIElement? - ) -> WidgetLocation.PanelLocation? { - guard let selectionFrame = UpdateLocationStrategy - .getSelectionFirstLineFrame(editor: editor) else { return nil } - - // hide it when the line of code is outside of the editor visible rect - if selectionFrame.maxY < editorFrame.minY || selectionFrame.minY > editorFrame.maxY { - return nil - } - - // Always place suggestion window at cursor position. - return .init( - frame: .init( - x: editorFrame.minX, - y: mainScreen.frame.height - selectionFrame.minY - Style.inlineSuggestionMaxHeight + Style.inlineSuggestionPadding, - width: editorFrame.width, - height: Style.inlineSuggestionMaxHeight - ), - alignPanelTop: true, - firstLineIndent: selectionFrame.maxX - editorFrame.minX - Style.inlineSuggestionPadding, - lineHeight: selectionFrame.height - ) - } - } - - /// Get the frame of the selection. - static func getSelectionFrame(editor: AXUIElement) -> CGRect? { - guard let selectedRange: AXValue = try? editor - .copyValue(key: kAXSelectedTextRangeAttribute), - let rect: AXValue = try? editor.copyParameterizedValue( - key: kAXBoundsForRangeParameterizedAttribute, - parameters: selectedRange - ) - else { - return nil - } - var selectionFrame: CGRect = .zero - let found = AXValueGetValue(rect, .cgRect, &selectionFrame) - guard found else { return nil } - return selectionFrame - } - - /// Get the frame of the first line of the selection. - static func getSelectionFirstLineFrame(editor: AXUIElement) -> CGRect? { - // Find selection range rect - guard let selectedRange: AXValue = try? editor - .copyValue(key: kAXSelectedTextRangeAttribute), - let rect: AXValue = try? editor.copyParameterizedValue( - key: kAXBoundsForRangeParameterizedAttribute, - parameters: selectedRange - ) - else { - return nil - } - var selectionFrame: CGRect = .zero - let found = AXValueGetValue(rect, .cgRect, &selectionFrame) - guard found else { return nil } - - var firstLineRange: CFRange = .init() - let foundFirstLine = AXValueGetValue(selectedRange, .cfRange, &firstLineRange) - firstLineRange.length = 0 - - #warning( - "FIXME: When selection is too low and out of the screen, the selection range becomes something else." - ) - - if foundFirstLine, - let firstLineSelectionRange = AXValueCreate(.cfRange, &firstLineRange), - let firstLineRect: AXValue = try? editor.copyParameterizedValue( - key: kAXBoundsForRangeParameterizedAttribute, - parameters: firstLineSelectionRange - ) - { - var firstLineFrame: CGRect = .zero - let foundFirstLineFrame = AXValueGetValue(firstLineRect, .cgRect, &firstLineFrame) - if foundFirstLineFrame { - selectionFrame = firstLineFrame - } - } - - return selectionFrame - } - - static func getChatPanelFrame(_ screen: NSScreen? = nil) -> CGRect { - let screen = screen ?? NSScreen.main ?? NSScreen.screens.first! - - let visibleScreenFrame = screen.visibleFrame - - // Default Frame - let width = min(Style.panelWidth, visibleScreenFrame.width * 0.3) - let height = visibleScreenFrame.height - let x = visibleScreenFrame.maxX - width - let y = visibleScreenFrame.minY - - return CGRect(x: x, y: y, width: width, height: height) - } - - static func getAttachedChatPanelFrame(_ screen: NSScreen, workspaceWindowElement: AXUIElement) -> CGRect { - guard let xcodeScreen = workspaceWindowElement.maxIntersectionScreen, - let xcodeRect = workspaceWindowElement.rect, - let mainDisplayScreen = NSScreen.screens.first(where: { $0.frame.origin == .zero }) - else { - return getChatPanelFrame() - } - - let minWidth = Style.minChatPanelWidth - let visibleXcodeScreenFrame = xcodeScreen.visibleFrame - - let width = max(visibleXcodeScreenFrame.maxX - xcodeRect.maxX, minWidth) - let height = xcodeRect.height - let x = visibleXcodeScreenFrame.maxX - width - - // AXUIElement coordinates: Y=0 at top-left - // NSWindow coordinates: Y=0 at bottom-left - let y = mainDisplayScreen.frame.maxY - xcodeRect.maxY + mainDisplayScreen.frame.minY - - return CGRect(x: x, y: y, width: width, height: height) - } -} - -public struct CodeReviewLocationStrategy { - static func calculateCurrentLineNumber( - for originalLineNumber: Int, // 1-based - originalLines: [String], - currentLines: [String] - ) -> Int { - let difference = currentLines.difference(from: originalLines) - - let targetIndex = originalLineNumber - var adjustment = 0 - - for change in difference { - switch change { - case .insert(let offset, _, _): - // Inserted at or before target line - if offset <= targetIndex + adjustment { - adjustment += 1 - } - case .remove(let offset, _, _): - // Deleted at or before target line - if offset <= targetIndex + adjustment { - adjustment -= 1 - } - } - } - - return targetIndex + adjustment - } - - static func getCurrentLineFrame( - editor: AXUIElement, - currentContent: String, - comment: ReviewComment, - originalContent: String - ) -> (lineNumber: Int?, lineFrame: CGRect?) { - let originalLines = originalContent.components(separatedBy: .newlines) - let currentLines = currentContent.components(separatedBy: .newlines) - - let originalLineNumber = comment.range.end.line - let currentLineNumber = calculateCurrentLineNumber( - for: originalLineNumber, - originalLines: originalLines, - currentLines: currentLines - ) // 1-based - // Calculate the character position for the start of the target line - var characterPosition = 0 - for i in 0 ..< currentLineNumber { - characterPosition += currentLines[i].count + 1 // +1 for newline character - } - - var range = CFRange(location: characterPosition, length: currentLines[currentLineNumber].count) - let rangeValue = AXValueCreate(AXValueType.cfRange, &range) - - var boundsValue: CFTypeRef? - let result = AXUIElementCopyParameterizedAttributeValue( - editor, - kAXBoundsForRangeParameterizedAttribute as CFString, - rangeValue!, - &boundsValue - ) - - if result == .success, - let bounds = boundsValue - { - var rect = CGRect.zero - let success = AXValueGetValue(bounds as! AXValue, AXValueType.cgRect, &rect) - - if success == true { - return ( - currentLineNumber, - CGRect( - x: rect.minX, - y: rect.minY, - width: rect.width, - height: rect.height - ) - ) - } - } - - return (nil, nil) - } -} diff --git a/Core/Sources/SuggestionWidget/WidgetView.swift b/Core/Sources/SuggestionWidget/WidgetView.swift deleted file mode 100644 index 04368aeb..00000000 --- a/Core/Sources/SuggestionWidget/WidgetView.swift +++ /dev/null @@ -1,324 +0,0 @@ -import ActiveApplicationMonitor -import ComposableArchitecture -import GitHubCopilotService -import Preferences -import SuggestionBasic -import SwiftUI - -struct WidgetView: View { - let store: StoreOf - @State var isHovering: Bool = false - var onOpenChatClicked: () -> Void = {} - var onCustomCommandClicked: (CustomCommand) -> Void = { _ in } - - @AppStorage(\.hideCircularWidget) var hideCircularWidget - - var body: some View { - WithPerceptionTracking { - Circle() - .fill(isHovering ? .white.opacity(0.5) : .white.opacity(0.15)) - .onTapGesture { - store.send(.widgetClicked, animation: .easeInOut(duration: 0.2)) - } - .overlay { - Group { - if !hideCircularWidget { - WidgetAnimatedCircle(store: store) - } - } - } - .onHover { yes in - withAnimation(.easeInOut(duration: 0.2)) { - isHovering = yes - } - }.contextMenu { - WidgetContextMenu(store: store) - } - .opacity({ - if !hideCircularWidget { return 1 } - return 0 - }()) - .animation( - featureFlag: \.animationCCrashSuggestion, - .easeInOut(duration: 0.2), - value: store.isProcessing - ) - } - } -} - -struct WidgetAnimatedCircle: View { - let store: StoreOf - @State var processingProgress: Double = 0 - - struct OverlayCircleState: Equatable { - var isProcessing: Bool - var isContentEmpty: Bool - } - - var body: some View { - WithPerceptionTracking { - let minimumLineWidth: Double = 3 - let lineWidth = (1 - processingProgress) * - (Style.widgetWidth - minimumLineWidth / 2) + minimumLineWidth - let scale = max(processingProgress * 1, 0.0001) - ZStack { - Circle() - .stroke( - Color(nsColor: .darkGray), - style: .init(lineWidth: minimumLineWidth) - ) - .padding(minimumLineWidth / 2) - - // how do I stop the repeatForever animation without removing the view? - // I tried many solutions found on stackoverflow but non of them works. - Group { - if store.isProcessing { - Circle() - .stroke( - Color.accentColor, - style: .init(lineWidth: lineWidth) - ) - .padding(minimumLineWidth / 2) - .scaleEffect(x: scale, y: scale) - .opacity( - !store.isContentEmpty || store.isProcessing ? 1 : 0 - ) - .animation( - featureFlag: \.animationCCrashSuggestion, - .easeInOut(duration: 1) - .repeatForever(autoreverses: true), - value: processingProgress - ) - } else { - Circle() - .stroke( - Color.accentColor, - style: .init(lineWidth: lineWidth) - ) - .padding(minimumLineWidth / 2) - .scaleEffect(x: scale, y: scale) - .opacity( - !store.isContentEmpty || store.isProcessing ? 1 : 0 - ) - .animation( - featureFlag: \.animationCCrashSuggestion, - .easeInOut(duration: 1), - value: processingProgress - ) - } - } - .onChange(of: store.isProcessing) { _ in - refreshRing( - isProcessing: store.isProcessing, - isContentEmpty: store.isContentEmpty - ) - } - .onChange(of: store.isContentEmpty) { _ in - refreshRing( - isProcessing: store.isProcessing, - isContentEmpty: store.isContentEmpty - ) - } - } - } - } - - func refreshRing(isProcessing: Bool, isContentEmpty: Bool) { - if isProcessing { - processingProgress = 1 - processingProgress - } else { - processingProgress = isContentEmpty ? 0 : 1 - } - } -} - -struct WidgetContextMenu: View { - @AppStorage(\.useGlobalChat) var useGlobalChat - @AppStorage(\.realtimeSuggestionToggle) var realtimeSuggestionToggle - @AppStorage(\.disableSuggestionFeatureGlobally) var disableSuggestionFeatureGlobally - @AppStorage(\.suggestionFeatureEnabledProjectList) var suggestionFeatureEnabledProjectList - @AppStorage(\.suggestionFeatureDisabledLanguageList) var suggestionFeatureDisabledLanguageList - @AppStorage(\.customCommands) var customCommands - let store: StoreOf - - @Dependency(\.xcodeInspector) var xcodeInspector - - var body: some View { - WithPerceptionTracking { - Group { // Commands - if !store.isChatOpen && FeatureFlagNotifierImpl.shared.featureFlags.chat { - Button(action: { - store.send(.openChatButtonClicked) - }) { - Text("Open Chat") - } - } - - if FeatureFlagNotifierImpl.shared.featureFlags.chat { - customCommandMenu() - } - } - - Divider() - - Group { - enableSuggestionForProject - - disableSuggestionForLanguage - } - - Divider() - - Group { // Settings - if FeatureFlagNotifierImpl.shared.featureFlags.chat { - Button(action: { - store.send(.detachChatPanelToggleClicked) - }) { - Text("Detach Chat Panel") - if store.isChatPanelDetached { - Image(systemName: "checkmark") - } - } - } - - Button(action: { - realtimeSuggestionToggle.toggle() - }) { - Text("Realtime Suggestion") - if realtimeSuggestionToggle { - Image(systemName: "checkmark") - } - } - } - - Divider() - } - } - - func customCommandMenu() -> some View { - Menu("Custom Commands") { - ForEach(customCommands, id: \.name) { command in - Button(action: { - store.send(.runCustomCommandButtonClicked(command)) - }) { - Text(command.name) - } - } - } - } -} - -extension WidgetContextMenu { - @ViewBuilder - var enableSuggestionForProject: some View { - if let projectPath = xcodeInspector.activeProjectRootURL?.path, - disableSuggestionFeatureGlobally - { - let matchedPath = suggestionFeatureEnabledProjectList.first { path in - projectPath.hasPrefix(path) - } - Button(action: { - if matchedPath != nil { - suggestionFeatureEnabledProjectList - .removeAll { path in path == matchedPath } - } else { - suggestionFeatureEnabledProjectList.append(projectPath) - } - }) { - if matchedPath == nil { - Text("Add to Suggestion-Enabled Project List") - } else { - Text("Remove from Suggestion-Enabled Project List") - } - } - } - } - - @ViewBuilder - var disableSuggestionForLanguage: some View { - let fileURL = xcodeInspector.activeDocumentURL - let fileLanguage = fileURL.map(languageIdentifierFromFileURL) ?? .plaintext - let matched = suggestionFeatureDisabledLanguageList.first { rawValue in - fileLanguage.rawValue == rawValue - } - Button(action: { - if let matched { - suggestionFeatureDisabledLanguageList.removeAll { $0 == matched } - } else { - suggestionFeatureDisabledLanguageList.append(fileLanguage.rawValue) - } - }) { - if matched == nil { - Text("Disable Suggestion for \"\(fileLanguage.rawValue.capitalized)\"") - } else { - Text("Enable Suggestion for \"\(fileLanguage.rawValue.capitalized)\"") - } - } - } -} - -struct WidgetView_Preview: PreviewProvider { - static var previews: some View { - VStack { - WidgetView( - store: Store( - initialState: .init( - isProcessing: false, - isDisplayingContent: false, - isContentEmpty: true, - isChatPanelDetached: false, - isChatOpen: false - ), - reducer: { CircularWidgetFeature() } - ), - isHovering: false - ) - - WidgetView( - store: Store( - initialState: .init( - isProcessing: false, - isDisplayingContent: false, - isContentEmpty: true, - isChatPanelDetached: false, - isChatOpen: false - ), - reducer: { CircularWidgetFeature() } - ), - isHovering: true - ) - - WidgetView( - store: Store( - initialState: .init( - isProcessing: true, - isDisplayingContent: false, - isContentEmpty: true, - isChatPanelDetached: false, - isChatOpen: false - ), - reducer: { CircularWidgetFeature() } - ), - isHovering: false - ) - - WidgetView( - store: Store( - initialState: .init( - isProcessing: false, - isDisplayingContent: true, - isContentEmpty: true, - isChatPanelDetached: false, - isChatOpen: false - ), - reducer: { CircularWidgetFeature() } - ), - isHovering: false - ) - } - .frame(width: 30) - .background(Color.black) - } -} - diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift deleted file mode 100644 index ca2e52f4..00000000 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ /dev/null @@ -1,1093 +0,0 @@ -import AppKit -import AsyncAlgorithms -import ChatTab -import Combine -import ComposableArchitecture -import Dependencies -import Foundation -import SwiftUI -import XcodeInspector -import AXHelper - -actor WidgetWindowsController: NSObject { - let userDefaultsObservers = WidgetUserDefaultsObservers() - var xcodeInspector: XcodeInspector { .shared } - - nonisolated let windows: WidgetWindows - nonisolated let store: StoreOf - nonisolated let chatTabPool: ChatTabPool - - var currentApplicationProcessIdentifier: pid_t? - - weak var currentXcodeApp: XcodeAppInstanceInspector? - weak var previousXcodeApp: XcodeAppInstanceInspector? - - var cancellable: Set = [] - var observeToAppTask: Task? - var observeToFocusedEditorTask: Task? - - var updateWindowOpacityTask: Task? - var lastUpdateWindowOpacityTime = Date(timeIntervalSince1970: 0) - - var updateWindowLocationTask: Task? - var lastUpdateWindowLocationTime = Date(timeIntervalSince1970: 0) - - var beatingCompletionPanelTask: Task? - - deinit { - userDefaultsObservers.presentationModeChangeObserver.onChange = {} - observeToAppTask?.cancel() - observeToFocusedEditorTask?.cancel() - } - - init(store: StoreOf, chatTabPool: ChatTabPool) { - self.store = store - self.chatTabPool = chatTabPool - windows = .init(store: store, chatTabPool: chatTabPool) - super.init() - windows.controller = self - } - - @MainActor func send(_ action: WidgetFeature.Action) { - store.send(action) - } - - func start() { - cancellable.removeAll() - - xcodeInspector.$activeApplication.sink { [weak self] app in - guard let app else { return } - Task { [weak self] in await self?.activate(app) } - }.store(in: &cancellable) - - xcodeInspector.$focusedEditor.sink { [weak self] editor in - guard let editor else { return } - Task { [weak self] in await self?.observe(toEditor: editor) } - }.store(in: &cancellable) - - xcodeInspector.$completionPanel.sink { [weak self] newValue in - Task { [weak self] in - await self?.handleCompletionPanelChange(isDisplaying: newValue != nil) - } - }.store(in: &cancellable) - - xcodeInspector.$activeDocumentURL.sink { [weak self] url in - Task { [weak self] in - await self?.updateCodeReviewWindowLocation(.onActiveDocumentURLChanged) - _ = await MainActor.run { [weak self] in - self?.store.send(.codeReviewPanel(.onActiveDocumentURLChanged(url))) - } - } - }.store(in: &cancellable) - - userDefaultsObservers.presentationModeChangeObserver.onChange = { [weak self] in - Task { [weak self] in - await self?.updateWindowLocation(animated: false, immediately: false) - await self?.send(.updateColorScheme) - } - } - - // Observe state change of code review - setupCodeReviewPanelObservers() - } - - private func setupCodeReviewPanelObservers() { - store.publisher - .map(\.codeReviewPanelState.currentIndex) - .removeDuplicates() - .sink { [weak self] _ in - Task { [weak self] in - await self?.updateCodeReviewWindowLocation(.onCurrentReviewIndexChanged) - } - }.store(in: &cancellable) - - store.publisher - .map(\.codeReviewPanelState.isPanelDisplayed) - .removeDuplicates() - .sink { [weak self] isPanelDisplayed in - Task { [weak self] in - await self?.updateCodeReviewWindowLocation(.onIsPanelDisplayedChanged(isPanelDisplayed)) - } - }.store(in: &cancellable) - } -} - -// MARK: - Observation - -private extension WidgetWindowsController { - func activate(_ app: AppInstanceInspector) { - Task { - if app.isXcode { - updateWindowLocation(animated: false, immediately: true) - updateWindowOpacity(immediately: false) - - if let xcodeApp = app as? XcodeAppInstanceInspector { - previousXcodeApp = currentXcodeApp ?? xcodeApp - currentXcodeApp = xcodeApp - } - - } else { - updateWindowOpacity(immediately: true) - updateWindowLocation(animated: false, immediately: false) - await hideSuggestionPanelWindow() - } - await adjustChatPanelWindowLevel() - } - guard currentApplicationProcessIdentifier != app.processIdentifier else { return } - currentApplicationProcessIdentifier = app.processIdentifier - observe(toApp: app) - } - - func observe(toApp app: AppInstanceInspector) { - guard let app = app as? XcodeAppInstanceInspector else { return } - let notifications = app.axNotifications - observeToAppTask?.cancel() - observeToAppTask = Task { - await windows.orderFront() - - for await notification in await notifications.notifications() { - try Task.checkCancellation() - - /// Hide the widgets before switching to another window/editor - /// so the transition looks better. - func hideWidgetForTransitions() async { - let newDocumentURL = await xcodeInspector.safe.realtimeActiveDocumentURL - let documentURL = await MainActor - .run { store.withState { $0.focusingDocumentURL } } - if documentURL != newDocumentURL { - await send(.panel(.removeDisplayedContent)) - await hidePanelWindows() - } - await send(.updateFocusingDocumentURL) - } - - func removeContent() async { - await send(.panel(.removeDisplayedContent)) - } - - func updateWidgetsAndNotifyChangeOfEditor(immediately: Bool) async { - await send(.panel(.switchToAnotherEditorAndUpdateContent)) - updateWindowLocation(animated: false, immediately: immediately) - updateWindowOpacity(immediately: immediately) - } - - func updateWidgets(immediately: Bool) async { - updateWindowLocation(animated: false, immediately: immediately) - updateWindowOpacity(immediately: immediately) - } - - switch notification.kind { - case .focusedWindowChanged, .focusedUIElementChanged: - await hideWidgetForTransitions() - await updateWidgetsAndNotifyChangeOfEditor(immediately: true) - case .applicationActivated: - await updateWidgetsAndNotifyChangeOfEditor(immediately: false) - case .mainWindowChanged: - await updateWidgetsAndNotifyChangeOfEditor(immediately: false) - case .windowMiniaturized, .windowDeminiaturized: - await updateWidgets(immediately: false) - await updateCodeReviewWindowLocation(.onXcodeAppNotification(notification)) - case .resized, - .moved, - .windowMoved, - .windowResized: - await updateWidgets(immediately: false) - await updateAttachedChatWindowLocation(notification) - await updateCodeReviewWindowLocation(.onXcodeAppNotification(notification)) - case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged, - .applicationDeactivated: - continue - case .titleChanged: - continue - } - } - } - } - - func observe(toEditor editor: SourceEditor) { - observeToFocusedEditorTask?.cancel() - observeToFocusedEditorTask = Task { - let selectionRangeChange = await editor.axNotifications.notifications() - .filter { $0.kind == .selectedTextChanged } - let scroll = await editor.axNotifications.notifications() - .filter { $0.kind == .scrollPositionChanged } - let valueChange = await editor.axNotifications.notifications() - .filter { $0.kind == .valueChanged } - - if #available(macOS 13.0, *) { - for await notification in merge( - scroll, - selectionRangeChange.debounce(for: Duration.milliseconds(0)), - valueChange.debounce(for: Duration.milliseconds(100)) - ) { - guard await xcodeInspector.safe.latestActiveXcode != nil else { return } - try Task.checkCancellation() - - // for better looking - if notification.kind == .scrollPositionChanged { - await hideSuggestionPanelWindow() - } - - updateWindowLocation(animated: false, immediately: false) - updateWindowOpacity(immediately: false) - await updateCodeReviewWindowLocation(.onSourceEditorNotification(notification)) - } - } else { - for await notification in merge(selectionRangeChange, scroll, valueChange) { - guard await xcodeInspector.safe.latestActiveXcode != nil else { return } - try Task.checkCancellation() - - // for better looking - if notification.kind == .scrollPositionChanged { - await hideSuggestionPanelWindow() - } - - updateWindowLocation(animated: false, immediately: false) - updateWindowOpacity(immediately: false) - await updateCodeReviewWindowLocation(.onSourceEditorNotification(notification)) - } - } - } - } - - func handleCompletionPanelChange(isDisplaying: Bool) { - beatingCompletionPanelTask?.cancel() - beatingCompletionPanelTask = Task { - if !isDisplaying { - // so that the buttons on the suggestion panel could be - // clicked - // before the completion panel updates the location of the - // suggestion panel - try await Task.sleep(nanoseconds: 400_000_000) - } - - updateWindowLocation(animated: false, immediately: false) - updateWindowOpacity(immediately: false) - } - } -} - -// MARK: - Window Updating - -extension WidgetWindowsController { - @MainActor - func hidePanelWindows() { - windows.sharedPanelWindow.alphaValue = 0 - windows.suggestionPanelWindow.alphaValue = 0 - } - - @MainActor - func hideSuggestionPanelWindow() { - windows.suggestionPanelWindow.alphaValue = 0 - send(.panel(.hidePanel)) - } - - @MainActor - func hideCodeReviewWindow() { - windows.codeReviewPanelWindow.alphaValue = 0 - windows.codeReviewPanelWindow.setIsVisible(false) - } - - @MainActor - func displayCodeReviewWindow() { - windows.codeReviewPanelWindow.setIsVisible(true) - windows.codeReviewPanelWindow.alphaValue = 1 - windows.codeReviewPanelWindow.orderFrontRegardless() - } - - func generateWidgetLocation() -> WidgetLocation? { - // Default location when no active application/window - let defaultLocation = generateDefaultLocation() - - if let application = xcodeInspector.latestActiveXcode?.appElement { - if let focusElement = xcodeInspector.focusedEditor?.element, - let parent = focusElement.parent, - let frame = parent.rect, - let screen = NSScreen.screens.first(where: { $0.frame.origin == .zero }), - let firstScreen = NSScreen.main - { - let positionMode = UserDefaults.shared - .value(for: \.suggestionWidgetPositionMode) - let suggestionMode = UserDefaults.shared - .value(for: \.suggestionPresentationMode) - - switch positionMode { - case .fixedToBottom: - var result = UpdateLocationStrategy.FixedToBottom().framesForWindows( - editorFrame: frame, - mainScreen: screen, - activeScreen: firstScreen - ) - switch suggestionMode { - case .nearbyTextCursor: - result.suggestionPanelLocation = UpdateLocationStrategy - .NearbyTextCursor() - .framesForSuggestionWindow( - editorFrame: frame, mainScreen: screen, - activeScreen: firstScreen, - editor: focusElement, - completionPanel: xcodeInspector.completionPanel - ) - default: - break - } - return result - case .alignToTextCursor: - var result = UpdateLocationStrategy.AlignToTextCursor().framesForWindows( - editorFrame: frame, - mainScreen: screen, - activeScreen: firstScreen, - editor: focusElement - ) - switch suggestionMode { - case .nearbyTextCursor: - result.suggestionPanelLocation = UpdateLocationStrategy - .NearbyTextCursor() - .framesForSuggestionWindow( - editorFrame: frame, mainScreen: screen, - activeScreen: firstScreen, - editor: focusElement, - completionPanel: xcodeInspector.completionPanel - ) - default: - break - } - return result - } - } else if var window = application.focusedWindow, - var frame = application.focusedWindow?.rect, - !["menu bar", "menu bar item"].contains(window.description), - frame.size.height > 300, - let screen = NSScreen.screens.first(where: { $0.frame.origin == .zero }), - let firstScreen = NSScreen.main - { - if ["open_quickly"].contains(window.identifier) - || ["alert"].contains(window.label) - { - // fallback to use workspace window - guard let workspaceWindow = application.windows - .first(where: { $0.identifier == "Xcode.WorkspaceWindow" }), - let rect = workspaceWindow.rect - else { - return defaultLocation - } - - window = workspaceWindow - frame = rect - } - - var expendedSize = CGSize.zero - if ["Xcode.WorkspaceWindow"].contains(window.identifier) { - // extra padding to bottom so buttons won't be covered - frame.size.height -= 40 - } else { - // move a bit away from the window so buttons won't be covered - frame.origin.x -= Style.widgetPadding + Style.widgetWidth / 2 - frame.size.width += Style.widgetPadding * 2 + Style.widgetWidth - expendedSize.width = (Style.widgetPadding * 2 + Style.widgetWidth) / 2 - expendedSize.height += Style.widgetPadding - } - - return UpdateLocationStrategy.FixedToBottom().framesForWindows( - editorFrame: frame, - mainScreen: screen, - activeScreen: firstScreen, - preferredInsideEditorMinWidth: 9_999_999_999, // never - editorFrameExpendedSize: expendedSize - ) - } - } - return defaultLocation - } - - // Generate a default location when no workspace is opened - private func generateDefaultLocation() -> WidgetLocation { - let chatPanelFrame = UpdateLocationStrategy.getChatPanelFrame() - - return WidgetLocation( - widgetFrame: .zero, - tabFrame: .zero, - defaultPanelLocation: .init( - frame: chatPanelFrame, - alignPanelTop: false - ), - suggestionPanelLocation: nil - ) - } - - func updatePanelState(_ location: WidgetLocation) async { - await send(.updatePanelStateToMatch(location)) - } - - func updateWindowOpacity(immediately: Bool) { - let shouldDebounce = !immediately && - !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 3) - lastUpdateWindowOpacityTime = Date() - updateWindowOpacityTask?.cancel() - - let task = Task { - if shouldDebounce { - try await Task.sleep(nanoseconds: 200_000_000) - } - try Task.checkCancellation() - let xcodeInspector = self.xcodeInspector - let activeApp = await xcodeInspector.safe.activeApplication - let latestActiveXcode = await xcodeInspector.safe.latestActiveXcode - let previousActiveApplication = xcodeInspector.previousActiveApplication - await MainActor.run { - let state = store.withState { $0 } - let isChatPanelDetached = state.chatPanelState.isDetached - // Check if the user has requested to display the panel, regardless of workspace state - let isPanelDisplayed = state.chatPanelState.isPanelDisplayed - - // Keep the chat panel visible even when there's no workspace/tabs if it's explicitly displayed - // This ensures the login screen remains visible - let shouldShowChatPanel = isPanelDisplayed || ( - state.chatPanelState.currentChatWorkspace != nil && - !state.chatPanelState.currentChatWorkspace!.tabInfo.isEmpty - ) - - if let activeApp, activeApp.isXcode { - let application = activeApp.appElement - /// We need this to hide the windows when Xcode is minimized. - let noFocus = application.focusedWindow == nil - windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 - send(.panel(noFocus ? .hidePanel : .showPanel)) - windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 - windows.widgetWindow.alphaValue = noFocus ? 0 : 1 - windows.toastWindow.alphaValue = noFocus ? 0 : 1 - - if isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = !shouldShowChatPanel - } else { - windows.chatPanelWindow.isWindowHidden = noFocus - } - } else if let activeApp, activeApp.isExtensionService { - let noFocus = { - guard let xcode = latestActiveXcode else { return true } - if let window = xcode.appElement.focusedWindow, - window.role == "AXWindow" - { - return false - } - return true - }() - - let previousAppIsXcode = previousActiveApplication?.isXcode ?? false - - send(.panel(noFocus ? .hidePanel : .showPanel)) - windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 - windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 - windows.widgetWindow.alphaValue = if noFocus { - 0 - } else if previousAppIsXcode { - 1 - } else { - 0 - } - windows.toastWindow.alphaValue = noFocus ? 0 : 1 - if isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = !shouldShowChatPanel - } else { - windows.chatPanelWindow.isWindowHidden = noFocus && !windows - .chatPanelWindow.isKeyWindow - } - } else { - windows.sharedPanelWindow.alphaValue = 0 - windows.suggestionPanelWindow.alphaValue = 0 - windows.widgetWindow.alphaValue = 0 - windows.toastWindow.alphaValue = 0 - if !isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = true - } - } - } - } - - updateWindowOpacityTask = task - } - - @MainActor - func updateAttachedChatWindowLocation(_ notif: XcodeAppInstanceInspector.AXNotification? = nil) async { - guard let currentXcodeApp = (await currentXcodeApp), - let currentFocusedWindow = currentXcodeApp.appElement.focusedWindow, - let currentXcodeScreen = currentXcodeApp.appScreen, - let currentXcodeRect = currentFocusedWindow.rect, - let notif = notif - else { return } - - if let previousXcodeApp = (await previousXcodeApp), - currentXcodeApp.processIdentifier == previousXcodeApp.processIdentifier { - if currentFocusedWindow.isFullScreen == true { - return - } - } - - let isAttachedToXcodeEnabled = UserDefaults.shared.value(for: \.autoAttachChatToXcode) - guard isAttachedToXcodeEnabled else { return } - - guard notif.element.isXcodeWorkspaceWindow else { return } - - let state = store.withState { $0 } - if state.chatPanelState.isPanelDisplayed && !windows.chatPanelWindow.isWindowHidden { - var frame = UpdateLocationStrategy.getAttachedChatPanelFrame( - NSScreen.main ?? NSScreen.screens.first!, - workspaceWindowElement: notif.element - ) - - let screenMaxX = currentXcodeScreen.visibleFrame.maxX - if screenMaxX - currentXcodeRect.maxX < Style.minChatPanelWidth - { - if let previousXcodeRect = (await previousXcodeApp?.appElement.focusedWindow?.rect), - screenMaxX - previousXcodeRect.maxX < Style.minChatPanelWidth - { - let isSameScreen = currentXcodeScreen.visibleFrame.intersects(windows.chatPanelWindow.frame) - // Only update y and height - frame = .init( - x: isSameScreen ? windows.chatPanelWindow.frame.minX : frame.minX, - y: frame.minY, - width: isSameScreen ? windows.chatPanelWindow.frame.width : frame.width, - height: frame.height - ) - } - } - - windows.chatPanelWindow.setFrame(frame, display: true, animate: true) - - await adjustChatPanelWindowLevel() - } - } - - func updateWindowLocation( - animated: Bool, - immediately: Bool, - function: StaticString = #function, - line: UInt = #line - ) { - @Sendable @MainActor - func update() async { - let state = store.withState { $0 } - let isChatPanelDetached = state.chatPanelState.isDetached - guard let widgetLocation = await generateWidgetLocation() else { return } - await updatePanelState(widgetLocation) - - windows.widgetWindow.setFrame( - widgetLocation.widgetFrame, - display: false, - animate: animated - ) - windows.toastWindow.setFrame( - widgetLocation.defaultPanelLocation.frame, - display: false, - animate: animated - ) - windows.sharedPanelWindow.setFrame( - widgetLocation.defaultPanelLocation.frame, - display: false, - animate: animated - ) - - if let suggestionPanelLocation = widgetLocation.suggestionPanelLocation { - windows.suggestionPanelWindow.setFrame( - suggestionPanelLocation.frame, - display: false, - animate: animated - ) - } - - let isAttachedToXcodeEnabled = UserDefaults.shared.value(for: \.autoAttachChatToXcode) - if isAttachedToXcodeEnabled { - // update in `updateAttachedChatWindowLocation` - } else if isChatPanelDetached { - // don't update it! - } else { - windows.chatPanelWindow.setFrame( - widgetLocation.defaultPanelLocation.frame, - display: false, - animate: animated - ) - } - - await adjustChatPanelWindowLevel() - } - - let now = Date() - let shouldThrottle = !immediately && - !(now.timeIntervalSince(lastUpdateWindowLocationTime) > 3) - - updateWindowLocationTask?.cancel() - let interval: TimeInterval = 0.05 - - if shouldThrottle { - let delay = max( - 0, - interval - now.timeIntervalSince(lastUpdateWindowLocationTime) - ) - - updateWindowLocationTask = Task { - try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - try Task.checkCancellation() - await update() - } - } else { - Task { - await update() - } - } - lastUpdateWindowLocationTime = Date() - } - - @MainActor - func adjustChatPanelWindowLevel() async { - let window = windows.chatPanelWindow - - let disableFloatOnTopWhenTheChatPanelIsDetached = UserDefaults.shared - .value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) - guard disableFloatOnTopWhenTheChatPanelIsDetached else { - window.setFloatOnTop(true) - return - } - - let state = store.withState { $0 } - let isChatPanelDetached = state.chatPanelState.isDetached - - guard isChatPanelDetached else { - window.setFloatOnTop(true) - return - } - - let floatOnTopWhenOverlapsXcode = UserDefaults.shared - .value(for: \.keepFloatOnTopIfChatPanelAndXcodeOverlaps) - - let latestApp = await xcodeInspector.safe.activeApplication - let latestAppIsXcodeOrExtension = if let latestApp { - latestApp.isXcode || latestApp.isExtensionService - } else { - false - } - - if !floatOnTopWhenOverlapsXcode || !latestAppIsXcodeOrExtension { - window.setFloatOnTop(false) - } else { - guard let xcode = await xcodeInspector.safe.latestActiveXcode else { return } - let windowElements = xcode.appElement.windows - let overlap = windowElements.contains { - if let position = $0.position, let size = $0.size { - let rect = CGRect( - x: position.x, - y: position.y, - width: size.width, - height: size.height - ) - return rect.intersects(window.frame) - } - return false - } - - window.setFloatOnTop(overlap) - } - } -} - -// MARK: - Code Review -extension WidgetWindowsController { - - enum CodeReviewLocationTrigger { - case onXcodeAppNotification(XcodeAppInstanceInspector.AXNotification) // resized, moved - case onSourceEditorNotification(SourceEditor.AXNotification) // scroll, valueChange - case onActiveDocumentURLChanged - case onCurrentReviewIndexChanged - case onIsPanelDisplayedChanged(Bool) - - static let relevantXcodeAppNotificationKind: [XcodeAppInstanceInspector.AXNotificationKind] = - [ - .windowMiniaturized, - .windowDeminiaturized, - .resized, - .moved, - .windowMoved, - .windowResized - ] - - static let relevantSourceEditorNotificationKind: [SourceEditor.AXNotificationKind] = - [.scrollPositionChanged, .valueChanged] - - var isRelevant: Bool { - switch self { - case .onActiveDocumentURLChanged, .onCurrentReviewIndexChanged, .onIsPanelDisplayedChanged: return true - case let .onSourceEditorNotification(notif): - return Self.relevantSourceEditorNotificationKind.contains(where: { $0 == notif.kind }) - case let .onXcodeAppNotification(notif): - return Self.relevantXcodeAppNotificationKind.contains(where: { $0 == notif.kind }) - } - } - - var shouldScroll: Bool { - switch self { - case .onCurrentReviewIndexChanged: return true - default: return false - } - } - } - - @MainActor - func updateCodeReviewWindowLocation(_ trigger: CodeReviewLocationTrigger) async { - guard trigger.isRelevant else { return } - if case .onIsPanelDisplayedChanged(let isPanelDisplayed) = trigger, !isPanelDisplayed { - hideCodeReviewWindow() - return - } - - var sourceEditorElement: AXUIElement? - - switch trigger { - case .onXcodeAppNotification(let notif): - sourceEditorElement = notif.element.retrieveSourceEditor() - case .onSourceEditorNotification(_), - .onActiveDocumentURLChanged, - .onCurrentReviewIndexChanged, - .onIsPanelDisplayedChanged: - sourceEditorElement = await xcodeInspector.safe.focusedEditor?.element - } - - guard let sourceEditorElement = sourceEditorElement - else { - hideCodeReviewWindow() - return - } - - await _updateCodeReviewWindowLocation( - sourceEditorElement, - shouldScroll: trigger.shouldScroll - ) - } - - @MainActor - func _updateCodeReviewWindowLocation(_ sourceEditorElement: AXUIElement, shouldScroll: Bool = false) async { - // Get the current index and comment from the store state - let state = store.withState { $0.codeReviewPanelState } - - guard state.isPanelDisplayed, - let comment = state.currentSelectedComment, - await currentXcodeApp?.realtimeDocumentURL?.absoluteString == comment.uri, - let reviewWindowFittingSize = windows.codeReviewPanelWindow.contentView?.fittingSize - else { - hideCodeReviewWindow() - return - } - - guard let originalContent = state.originalContent, - let screen = NSScreen.screens.first(where: { $0.frame.origin == .zero }), - let scrollViewRect = sourceEditorElement.parent?.rect, - let scrollScreenFrame = sourceEditorElement.parent?.maxIntersectionScreen?.frame, - let currentContent: String = try? sourceEditorElement.copyValue(key: kAXValueAttribute) - else { return } - - let result = CodeReviewLocationStrategy.getCurrentLineFrame( - editor: sourceEditorElement, - currentContent: currentContent, - comment: comment, - originalContent: originalContent) - guard let lineNumber = result.lineNumber, let lineFrame = result.lineFrame - else { return } - - // The line should be visible - guard lineFrame.width > 0, lineFrame.height > 0, - scrollViewRect.contains(lineFrame) - else { - if shouldScroll { - AXHelper - .scrollSourceEditorToLine( - lineNumber, - content: currentContent, - focusedElement: sourceEditorElement - ) - } else { - hideCodeReviewWindow() - } - return - } - - // Position the code review window near the target line - var reviewWindowFrame = windows.codeReviewPanelWindow.frame - reviewWindowFrame.origin.x = scrollViewRect.maxX - reviewWindowFrame.width - reviewWindowFrame.origin.y = screen.frame.maxY - lineFrame.maxY + screen.frame.minY - reviewWindowFrame.height - - windows.codeReviewPanelWindow.setFrame(reviewWindowFrame, display: true, animate: true) - displayCodeReviewWindow() - } -} - -// MARK: - NSWindowDelegate - -extension WidgetWindowsController: NSWindowDelegate { - nonisolated - func windowWillMove(_ notification: Notification) { - guard let window = notification.object as? NSWindow else { return } - Task { @MainActor in - guard window === windows.chatPanelWindow else { return } - await Task.yield() - store.send(.chatPanel(.detachChatPanel)) - } - } - - nonisolated - func windowDidMove(_ notification: Notification) { - guard let window = notification.object as? NSWindow else { return } - Task { @MainActor in - guard window === windows.chatPanelWindow else { return } - await Task.yield() - await adjustChatPanelWindowLevel() - } - } - - nonisolated - func windowWillEnterFullScreen(_ notification: Notification) { - guard let window = notification.object as? NSWindow else { return } - Task { @MainActor in - guard window === windows.chatPanelWindow else { return } - await Task.yield() - store.send(.chatPanel(.enterFullScreen)) - } - } - - nonisolated - func windowWillExitFullScreen(_ notification: Notification) { - guard let window = notification.object as? NSWindow else { return } - Task { @MainActor in - guard window === windows.chatPanelWindow else { return } - await Task.yield() - store.send(.chatPanel(.exitFullScreen)) - } - } -} - -// MARK: - Windows - -public final class WidgetWindows { - let store: StoreOf - let chatTabPool: ChatTabPool - weak var controller: WidgetWindowsController? - let cursorPositionTracker = CursorPositionTracker() - - // you should make these window `.transient` so they never show up in the mission control. - - @MainActor - lazy var fullscreenDetector = { - let it = CanBecomeKeyWindow( - contentRect: .zero, - styleMask: .borderless, - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient] - it.hasShadow = false - it.setIsVisible(false) - it.canBecomeKeyChecker = { false } - return it - }() - - @MainActor - lazy var widgetWindow = { - let it = CanBecomeKeyWindow( - contentRect: .zero, - styleMask: .borderless, - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.level = .floating - it.collectionBehavior = [.fullScreenAuxiliary, .transient, .canJoinAllSpaces] - it.hasShadow = true - it.contentView = NSHostingView( - rootView: WidgetView( - store: store.scope( - state: \._internalCircularWidgetState, - action: \.circularWidget - ) - ) - ) - it.setIsVisible(true) - it.canBecomeKeyChecker = { false } - return it - }() - - @MainActor - lazy var sharedPanelWindow = { - let it = CanBecomeKeyWindow( - contentRect: .init(x: 0, y: 0, width: Style.panelWidth, height: Style.panelHeight), - styleMask: .borderless, - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.level = widgetLevel(2) - it.collectionBehavior = [.fullScreenAuxiliary, .transient, .canJoinAllSpaces] - it.hasShadow = true - it.contentView = NSHostingView( - rootView: SharedPanelView( - store: store.scope( - state: \.panelState, - action: \.panel - ).scope( - state: \.sharedPanelState, - action: \.sharedPanel - ) - ).environment(cursorPositionTracker) - ) - it.setIsVisible(true) - it.canBecomeKeyChecker = { [store] in - store.withState { state in - state.panelState.sharedPanelState.content.promptToCode != nil - } - } - return it - }() - - @MainActor - lazy var suggestionPanelWindow = { - let it = CanBecomeKeyWindow( - contentRect: .init(x: 0, y: 0, width: Style.panelWidth, height: Style.panelHeight), - styleMask: .borderless, - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.level = widgetLevel(2) - it.collectionBehavior = [.fullScreenAuxiliary, .transient, .canJoinAllSpaces] - it.hasShadow = false - it.contentView = NSHostingView( - rootView: SuggestionPanelView( - store: store.scope( - state: \.panelState, - action: \.panel - ).scope( - state: \.suggestionPanelState, - action: \.suggestionPanel - ) - ).environment(cursorPositionTracker) - ) - it.canBecomeKeyChecker = { false } - it.setIsVisible(true) - return it - }() - - @MainActor - lazy var codeReviewPanelWindow = { - let it = CanBecomeKeyWindow( - contentRect: .init( - x: 0, - y: 0, - width: Style.codeReviewPanelWidth, - height: Style.codeReviewPanelHeight - ), - styleMask: .borderless, - backing: .buffered, - defer: true - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.collectionBehavior = [.fullScreenAuxiliary, .transient, .canJoinAllSpaces] - it.hasShadow = true - it.level = widgetLevel(2) - it.contentView = NSHostingView( - rootView: CodeReviewPanelView( - store: store.scope( - state: \.codeReviewPanelState, - action: \.codeReviewPanel - ) - ) - ) - it.canBecomeKeyChecker = { true } - it.alphaValue = 0 - it.setIsVisible(false) - return it - }() - - @MainActor - lazy var chatPanelWindow = { - let it = ChatPanelWindow( - store: store.scope( - state: \.chatPanelState, - action: \.chatPanel - ), - chatTabPool: chatTabPool, - minimizeWindow: { [weak self] in - self?.store.send(.chatPanel(.hideButtonClicked)) - } - ) - it.delegate = controller - it.isWindowHidden = true - return it - }() - - @MainActor - // The toast window area is now capturing mouse events - // Even in the transparent parts where there's no visible content. - lazy var toastWindow = { - let it = CanBecomeKeyWindow( - contentRect: .zero, - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.level = widgetLevel(2) - it.collectionBehavior = [.fullScreenAuxiliary, .transient, .canJoinAllSpaces] - it.hasShadow = false - it.contentView = NSHostingView( - rootView: ToastPanelView(store: store.scope( - state: \.toastPanel, - action: \.toastPanel - )) - ) - it.setIsVisible(true) - it.canBecomeKeyChecker = { false } - return it - }() - - init( - store: StoreOf, - chatTabPool: ChatTabPool - ) { - self.store = store - self.chatTabPool = chatTabPool - } - - @MainActor - func orderFront() { - widgetWindow.orderFrontRegardless() - toastWindow.orderFrontRegardless() - sharedPanelWindow.orderFrontRegardless() - suggestionPanelWindow.orderFrontRegardless() - if chatPanelWindow.level.rawValue > NSWindow.Level.normal.rawValue { - chatPanelWindow.orderFrontRegardless() - } - } -} - -// MARK: - Window Subclasses - -class CanBecomeKeyWindow: NSWindow { - var canBecomeKeyChecker: () -> Bool = { true } - override var canBecomeKey: Bool { canBecomeKeyChecker() } - override var canBecomeMain: Bool { canBecomeKeyChecker() } -} - -func widgetLevel(_ addition: Int) -> NSWindow.Level { - let minimumWidgetLevel: Int - minimumWidgetLevel = NSWindow.Level.floating.rawValue - return .init(minimumWidgetLevel + addition) -} diff --git a/Core/Sources/UpdateChecker/UpdateChecker.swift b/Core/Sources/UpdateChecker/UpdateChecker.swift deleted file mode 100644 index e477817d..00000000 --- a/Core/Sources/UpdateChecker/UpdateChecker.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Logger -import Preferences -import Sparkle - -public protocol UpdateCheckerProtocol { - func checkForUpdates() - func getAutomaticallyChecksForUpdates() -> Bool - func setAutomaticallyChecksForUpdates(_ value: Bool) -} - -public protocol UpdateCheckerDelegate: AnyObject { - func prepareForRelaunch(finish: @escaping () -> Void) -} - -public final class NoopUpdateChecker: UpdateCheckerProtocol { - public init() {} - public func checkForUpdates() {} - public func getAutomaticallyChecksForUpdates() -> Bool { false } - public func setAutomaticallyChecksForUpdates(_ value: Bool) {} -} - -public final class UpdateChecker: UpdateCheckerProtocol { - let updater: SPUUpdater - let delegate = UpdaterDelegate() - - public init(hostBundle: Bundle, checkerDelegate: UpdateCheckerDelegate) { - updater = SPUUpdater( - hostBundle: hostBundle, - applicationBundle: Bundle.main, - userDriver: SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil), - delegate: delegate - ) - delegate.updateCheckerDelegate = checkerDelegate - do { - try updater.start() - } catch { - Logger.updateChecker.error(error.localizedDescription) - } - } - - public convenience init?(hostBundle: Bundle?, checkerDelegate: UpdateCheckerDelegate) { - guard let hostBundle = hostBundle else { return nil } - self.init(hostBundle: hostBundle, checkerDelegate: checkerDelegate) - } - - public func checkForUpdates() { - updater.checkForUpdates() - } - - public func getAutomaticallyChecksForUpdates() -> Bool { - updater.automaticallyChecksForUpdates - } - - public func setAutomaticallyChecksForUpdates(_ value: Bool) { - updater.automaticallyChecksForUpdates = value - } -} - -class UpdaterDelegate: NSObject, SPUUpdaterDelegate { - weak var updateCheckerDelegate: UpdateCheckerDelegate? - - func updater( - _ updater: SPUUpdater, - shouldPostponeRelaunchForUpdate item: SUAppcastItem, - untilInvokingBlock installHandler: @escaping () -> Void) -> Bool { - if let updateCheckerDelegate { - updateCheckerDelegate.prepareForRelaunch(finish: installHandler) - return true - } - return false - } - - func allowedChannels(for updater: SPUUpdater) -> Set { - if UserDefaults.shared.value(for: \.installPrereleases) { - Set(["prerelease"]) - } else { - [] - } - } -} - diff --git a/Core/Sources/UserDefaultsObserver/UserDefaultsObserver.swift b/Core/Sources/UserDefaultsObserver/UserDefaultsObserver.swift deleted file mode 100644 index 62ecce3f..00000000 --- a/Core/Sources/UserDefaultsObserver/UserDefaultsObserver.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -public final class UserDefaultsObserver: NSObject { - public var onChange: (() -> Void)? - private weak var object: NSObject? - private let keyPaths: [String] - - public init( - object: NSObject, - forKeyPaths keyPaths: [String], - context: UnsafeMutableRawPointer? - ) { - self.object = object - self.keyPaths = keyPaths - super.init() - for keyPath in keyPaths { - object.addObserver(self, forKeyPath: keyPath, options: .new, context: context) - } - } - - deinit { - for keyPath in keyPaths { - object?.removeObserver(self, forKeyPath: keyPath) - } - } - - public override func observeValue( - forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer? - ) { - onChange?() - } -} - diff --git a/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift b/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift deleted file mode 100644 index 40e14b66..00000000 --- a/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation - -func buildHighlightJSTheme(_ theme: XcodeTheme) -> String { - /// The source value is an `r g b a` string, for example: `0.5 0.5 0.2 1` - - return """ - .hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - background: \(theme.backgroundColor.hexString); - color: \(theme.plainTextColor.hexString); - } - .xml .hljs-meta { - color: \(theme.marksColor.hexString); - } - .hljs-comment, - .hljs-quote { - color: \(theme.commentColor.hexString); - } - .hljs-tag, - .hljs-keyword, - .hljs-selector-tag, - .hljs-literal, - .hljs-name { - color: \(theme.keywordsColor.hexString); - } - .hljs-attribute { - color: \(theme.attributesColor.hexString); - } - .hljs-variable, - .hljs-template-variable { - color: \(theme.otherPropertiesAndGlobalsColor.hexString); - } - .hljs-code, - .hljs-string, - .hljs-meta-string { - color: \(theme.stringsColor.hexString); - } - .hljs-regexp { - color: \(theme.regexLiteralsColor.hexString); - } - .hljs-link { - color: \(theme.urlsColor.hexString); - } - .hljs-title { - color: \(theme.headingColor.hexString); - } - .hljs-symbol, - .hljs-bullet { - color: \(theme.attributesColor.hexString); - } - .hljs-number { - color: \(theme.numbersColor.hexString); - } - .hljs-section { - color: \(theme.marksColor.hexString); - } - .hljs-meta { - color: \(theme.keywordsColor.hexString); - } - .hljs-type, - .hljs-built_in, - .hljs-builtin-name { - color: \(theme.otherTypeNamesColor.hexString); - } - .hljs-class .hljs-title, - .hljs-title .class_ { - color: \(theme.typeDeclarationsColor.hexString); - } - .hljs-function .hljs-title, - .hljs-title .function_ { - color: \(theme.otherDeclarationsColor.hexString); - } - .hljs-params { - color: \(theme.otherDeclarationsColor.hexString); - } - .hljs-attr { - color: \(theme.attributesColor.hexString); - } - .hljs-subst { - color: \(theme.plainTextColor.hexString); - } - .hljs-formula { - background-color: \(theme.selectionColor.hexString); - font-style: italic; - } - .hljs-addition { - background-color: #baeeba; - } - .hljs-deletion { - background-color: #ffc8bd; - } - .hljs-selector-id, - .hljs-selector-class { - color: \(theme.plainTextColor.hexString); - } - .hljs-doctag, - .hljs-strong { - font-weight: bold; - } - .hljs-emphasis { - font-style: italic; - } - """ -} - diff --git a/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift b/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift deleted file mode 100644 index f5536e3c..00000000 --- a/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import Highlightr -import Preferences - -public class HighlightrThemeManager: ThemeManager { - let defaultManager: ThemeManager - - weak var controller: XcodeThemeController? - - public init(defaultManager: ThemeManager, controller: XcodeThemeController) { - self.defaultManager = defaultManager - self.controller = controller - } - - public func theme(for name: String) -> Theme? { - let syncSuggestionTheme = UserDefaults.shared.value(for: \.syncSuggestionHighlightTheme) - let syncPromptToCodeTheme = UserDefaults.shared.value(for: \.syncPromptToCodeHighlightTheme) - let syncChatTheme = UserDefaults.shared.value(for: \.syncChatCodeHighlightTheme) - - lazy var defaultLight = Theme(themeString: defaultLightTheme) - lazy var defaultDark = Theme(themeString: defaultDarkTheme) - - switch name { - case "suggestion-light": - guard syncSuggestionTheme, let theme = theme(lightMode: true) else { - return defaultLight - } - return theme - case "suggestion-dark": - guard syncSuggestionTheme, let theme = theme(lightMode: false) else { - return defaultDark - } - return theme - case "promptToCode-light": - guard syncPromptToCodeTheme, let theme = theme(lightMode: true) else { - return defaultLight - } - return theme - case "promptToCode-dark": - guard syncPromptToCodeTheme, let theme = theme(lightMode: false) else { - return defaultDark - } - return theme - case "chat-light": - guard syncChatTheme, let theme = theme(lightMode: true) else { - return defaultLight - } - return theme - case "chat-dark": - guard syncChatTheme, let theme = theme(lightMode: false) else { - return defaultDark - } - return theme - case "light": - return defaultLight - case "dark": - return defaultDark - default: - return defaultLight - } - } - - func theme(lightMode: Bool) -> Theme? { - guard let controller else { return nil } - guard let directories = controller.createSupportDirectoriesIfNeeded() else { return nil } - - let themeURL: URL = if lightMode { - directories.themeDirectory.appendingPathComponent("highlightjs-light") - } else { - directories.themeDirectory.appendingPathComponent("highlightjs-dark") - } - - if let themeString = try? String(contentsOf: themeURL) { - return Theme(themeString: themeString) - } - - controller.syncXcodeThemeIfNeeded() - - if let themeString = try? String(contentsOf: themeURL) { - return Theme(themeString: themeString) - } - - return nil - } -} - -let defaultLightTheme = ".hljs{display:block;overflow-x:auto;padding:0.5em;background:#FFFFFFFF;color:#000000D8}.xml .hljs-meta{color:#495460FF}.hljs-comment,.hljs-quote{color:#5D6B79FF}.hljs-tag,.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-name{color:#9A2393FF}.hljs-attribute{color:#805E03FF}.hljs-variable,.hljs-template-variable{color:#6B36A9FF}.hljs-code,.hljs-string,.hljs-meta-string{color:#C31A15FF}.hljs-regexp{color:#000000D8}.hljs-link{color:#0E0EFFFF}.hljs-title{color:#000000FF}.hljs-symbol,.hljs-bullet{color:#805E03FF}.hljs-number{color:#1C00CFFF}.hljs-section{color:#495460FF}.hljs-meta{color:#9A2393FF}.hljs-type,.hljs-built_in,.hljs-builtin-name{color:#3900A0FF}.hljs-class .hljs-title,.hljs-title .class_{color:#0B4F79FF}.hljs-function .hljs-title,.hljs-title .function_{color:#0E67A0FF}.hljs-params{color:#0E67A0FF}.hljs-attr{color:#805E03FF}.hljs-subst{color:#000000D8}.hljs-formula{background-color:#A3CCFEFF;font-style:italic}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-selector-id,.hljs-selector-class{color:#000000D8}.hljs-doctag,.hljs-strong{font-weight:bold}.hljs-emphasis{font-style:italic}" - -let defaultDarkTheme = ".hljs{display:block;overflow-x:auto;padding:0.5em;background:#1F1F23FF;color:#FFFFFFD8}.xml .hljs-meta{color:#91A1B1FF}.hljs-comment,.hljs-quote{color:#6B7985FF}.hljs-tag,.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-name{color:#FC5FA2FF}.hljs-attribute{color:#BF8554FF}.hljs-variable,.hljs-template-variable{color:#A166E5FF}.hljs-code,.hljs-string,.hljs-meta-string{color:#FC695DFF}.hljs-regexp{color:#FFFFFFD8}.hljs-link{color:#5482FEFF}.hljs-title{color:#FFFFFFFF}.hljs-symbol,.hljs-bullet{color:#BF8554FF}.hljs-number{color:#CFBF69FF}.hljs-section{color:#91A1B1FF}.hljs-meta{color:#FC5FA2FF}.hljs-type,.hljs-built_in,.hljs-builtin-name{color:#D0A7FEFF}.hljs-class .hljs-title,.hljs-title .class_{color:#5CD7FEFF}.hljs-function .hljs-title,.hljs-title .function_{color:#41A1BFFF}.hljs-params{color:#41A1BFFF}.hljs-attr{color:#BF8554FF}.hljs-subst{color:#FFFFFFD8}.hljs-formula{background-color:#505A6FFF;font-style:italic}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-selector-id,.hljs-selector-class{color:#FFFFFFD8}.hljs-doctag,.hljs-strong{font-weight:bold}.hljs-emphasis{font-style:italic}" diff --git a/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift b/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift deleted file mode 100644 index 0c696384..00000000 --- a/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import Preferences - -// MARK: - Theming - -public extension UserDefaultPreferenceKeys { - var lightXcodeThemeName: PreferenceKey { - .init(defaultValue: "", key: "LightXcodeThemeName") - } - - var lightXcodeTheme: PreferenceKey> { - .init(defaultValue: .init(nil), key: "LightXcodeTheme") - } - - var darkXcodeThemeName: PreferenceKey { - .init(defaultValue: "", key: "DarkXcodeThemeName") - } - - var darkXcodeTheme: PreferenceKey> { - .init(defaultValue: .init(nil), key: "DarkXcodeTheme") - } - - var lastSyncedHighlightJSThemeCreatedAt: PreferenceKey { - .init(defaultValue: 0, key: "LastSyncedHighlightJSThemeCreatedAt") - } -} - diff --git a/Core/Sources/XcodeThemeController/XcodeThemeController.swift b/Core/Sources/XcodeThemeController/XcodeThemeController.swift deleted file mode 100644 index 2f4f0fc1..00000000 --- a/Core/Sources/XcodeThemeController/XcodeThemeController.swift +++ /dev/null @@ -1,286 +0,0 @@ -import AppKit -import Foundation -import Highlightr -import Logger -import XcodeInspector - -public class XcodeThemeController { - var syncTriggerTask: Task? // to be removed - - public init() { - } - - public func start() { - let defaultHighlightrThemeManager = Highlightr.themeManager - Highlightr.themeManager = HighlightrThemeManager( - defaultManager: defaultHighlightrThemeManager, - controller: self - ) - - syncXcodeThemeIfNeeded(forceRefresh: true) - - guard syncTriggerTask == nil else { - Logger.service.error("XcodeThemeController.start() invoked multiple times.") - return - } - - syncTriggerTask = Task { [weak self] in - let notifications = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didActivateApplicationNotification) - for await notification in notifications { - try Task.checkCancellation() - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication - else { continue } - guard app.isCopilotForXcodeExtensionService || app.isXcode else { continue } - guard let self else { return } - self.syncXcodeThemeIfNeeded() - } - } - - Timer.scheduledTimer( - withTimeInterval: 60, - repeats: true - ) { [weak self] _ in - guard XcodeInspector.shared.activeApplication?.isXcode == true else { return } - self?.syncXcodeThemeIfNeeded() - } - } -} - -extension XcodeThemeController { - func syncXcodeThemeIfNeeded(forceRefresh: Bool = false) { - guard UserDefaults.shared.value(for: \.syncSuggestionHighlightTheme) - || UserDefaults.shared.value(for: \.syncPromptToCodeHighlightTheme) - || UserDefaults.shared.value(for: \.syncChatCodeHighlightTheme) - else { return } - guard let directories = createSupportDirectoriesIfNeeded() else { return } - - defer { - UserDefaults.shared.set( - Date().timeIntervalSince1970, - for: \.lastSyncedHighlightJSThemeCreatedAt - ) - } - - let xcodeUserDefaults = UserDefaults(suiteName: "com.apple.dt.Xcode")! - - if let darkThemeName = xcodeUserDefaults - .value(forKey: "XCFontAndColorCurrentDarkTheme") as? String - { - syncXcodeThemeIfNeeded( - xcodeThemeName: darkThemeName, - light: false, - in: directories.themeDirectory, - forceRefresh: forceRefresh - ) - } - - if let lightThemeName = xcodeUserDefaults - .value(forKey: "XCFontAndColorCurrentTheme") as? String - { - syncXcodeThemeIfNeeded( - xcodeThemeName: lightThemeName, - light: true, - in: directories.themeDirectory, - forceRefresh: forceRefresh - ) - } - } - - func syncXcodeThemeIfNeeded( - xcodeThemeName: String, - light: Bool, - in directoryURL: URL, - forceRefresh: Bool = false - ) { - let targetName = light ? "highlightjs-light" : "highlightjs-dark" - guard let xcodeThemeURL = locateXcodeTheme(named: xcodeThemeName) else { - Logger.service.error("Xcode theme not found: \(xcodeThemeName)") - return - } - let targetThemeURL = directoryURL.appendingPathComponent(targetName) - let lastSyncTimestamp = UserDefaults.shared - .value(for: \.lastSyncedHighlightJSThemeCreatedAt) - - let shouldSync = { - if forceRefresh { return true } - if light, UserDefaults.shared.value(for: \.lightXcodeTheme) == nil { return true } - if !light, UserDefaults.shared.value(for: \.darkXcodeTheme) == nil { return true } - if light, xcodeThemeName != UserDefaults.shared.value(for: \.lightXcodeThemeName) { - return true - } - if !light, xcodeThemeName != UserDefaults.shared.value(for: \.darkXcodeThemeName) { - return true - } - if !FileManager.default.fileExists(atPath: targetThemeURL.path) { return true } - - let xcodeThemeFileUpdated = { - guard let xcodeThemeModifiedDate = try? xcodeThemeURL - .resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate - else { return true } - return xcodeThemeModifiedDate.timeIntervalSince1970 > lastSyncTimestamp - }() - - if xcodeThemeFileUpdated { return true } - - return false - }() - - if shouldSync { - Logger.service.info("Syncing Xcode theme: \(xcodeThemeName)") - do { - let theme = try XcodeTheme(fileURL: xcodeThemeURL) - let highlightrTheme = theme.asHighlightJSTheme() - try highlightrTheme.write(to: targetThemeURL, atomically: true, encoding: .utf8) - - Task { @MainActor in - if light { - UserDefaults.shared.set(xcodeThemeName, for: \.lightXcodeThemeName) - UserDefaults.shared.set(.init(theme), for: \.lightXcodeTheme) - UserDefaults.shared.set( - .init(theme.plainTextColor.storable), - for: \.codeForegroundColorLight - ) - UserDefaults.shared.set( - .init(theme.backgroundColor.storable), - for: \.codeBackgroundColorLight - ) - UserDefaults.shared.set( - .init(theme.plainTextFont.storable), - for: \.codeFontLight - ) - UserDefaults.shared.set( - .init(theme.currentLineColor.storable), - for: \.currentLineBackgroundColorLight - ) - } else { - UserDefaults.shared.set(xcodeThemeName, for: \.darkXcodeThemeName) - UserDefaults.shared.set(.init(theme), for: \.darkXcodeTheme) - UserDefaults.shared.set( - .init(theme.plainTextColor.storable), - for: \.codeForegroundColorDark - ) - UserDefaults.shared.set( - .init(theme.backgroundColor.storable), - for: \.codeBackgroundColorDark - ) - UserDefaults.shared.set( - .init(theme.plainTextFont.storable), - for: \.codeFontDark - ) - UserDefaults.shared.set( - .init(theme.currentLineColor.storable), - for: \.currentLineBackgroundColorDark - ) - } - } - } catch { - Logger.service.error("Failed to sync Xcode theme \"\(xcodeThemeName)\": \(error)") - } - } - } - - func locateXcodeTheme(named name: String) -> URL? { - if let customThemeURL = FileManager.default.urls( - for: .libraryDirectory, - in: .userDomainMask - ).first?.appendingPathComponent("Developer/Xcode/UserData/FontAndColorThemes") - .appendingPathComponent(name), - FileManager.default.fileExists(atPath: customThemeURL.path) - { - return customThemeURL - } - - let xcodeURL: URL? = { - // Use the latest running Xcode - if let running = XcodeInspector.shared.latestActiveXcode?.bundleURL { - return running - } - // Use the main Xcode.app - let proposedXcodeURL = URL(fileURLWithPath: "/Applications/Xcode.app") - if FileManager.default.fileExists(atPath: proposedXcodeURL.path) { - return proposedXcodeURL - } - // Look for an Xcode.app - if let applicationsURL = FileManager.default.urls( - for: .applicationDirectory, - in: .localDomainMask - ).first { - struct InfoPlist: Codable { - var CFBundleIdentifier: String - } - - let appBundleIdentifier = "com.apple.dt.Xcode" - let appDirectories = try? FileManager.default.contentsOfDirectory( - at: applicationsURL, - includingPropertiesForKeys: [], - options: .skipsHiddenFiles - ) - for appDirectoryURL in appDirectories ?? [] { - let infoPlistURL = appDirectoryURL.appendingPathComponent("Contents/Info.plist") - if let data = try? Data(contentsOf: infoPlistURL), - let infoPlist = try? PropertyListDecoder().decode( - InfoPlist.self, - from: data - ), - infoPlist.CFBundleIdentifier == appBundleIdentifier - { - return appDirectoryURL - } - } - } - return nil - }() - - if let url = xcodeURL? - .appendingPathComponent("Contents/SharedFrameworks/DVTUserInterfaceKit.framework") - .appendingPathComponent("Versions/A/Resources/FontAndColorThemes") - .appendingPathComponent(name), - FileManager.default.fileExists(atPath: url.path) - { - return url - } - - return nil - } - - func createSupportDirectoriesIfNeeded() -> (supportDirectory: URL, themeDirectory: URL)? { - guard let supportURL = FileManager.default.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first?.appendingPathComponent( - Bundle.main - .object(forInfoDictionaryKey: "APPLICATION_SUPPORT_FOLDER") as! String - ) else { - Logger.service.error("Could not determine support directory for Xcode theme synching") - return nil - } - - let themeURL = supportURL.appendingPathComponent("Themes") - - do { - if !FileManager.default.fileExists(atPath: supportURL.path) { - try FileManager.default.createDirectory( - at: supportURL, - withIntermediateDirectories: true, - attributes: nil - ) - } - - if !FileManager.default.fileExists(atPath: themeURL.path) { - try FileManager.default.createDirectory( - at: themeURL, - withIntermediateDirectories: true, - attributes: nil - ) - } - } catch { - Logger.service.error("Failed to create support directories for Xcode theme synching: \(error)") - return nil - } - - return (supportURL, themeURL) - } -} - diff --git a/Core/Sources/XcodeThemeController/XcodeThemeParser.swift b/Core/Sources/XcodeThemeController/XcodeThemeParser.swift deleted file mode 100644 index cb4f0fbe..00000000 --- a/Core/Sources/XcodeThemeController/XcodeThemeParser.swift +++ /dev/null @@ -1,358 +0,0 @@ -import Foundation -import Preferences - -public struct XcodeTheme: Codable { - public struct ThemeColor: Codable { - public var red: Double - public var green: Double - public var blue: Double - public var alpha: Double - - public var hexString: String { - let red = Int(self.red * 255) - let green = Int(self.green * 255) - let blue = Int(self.blue * 255) - let alpha = Int(self.alpha * 255) - return String(format: "#%02X%02X%02X%02X", red, green, blue, alpha) - } - - var storable: StorableColor { - .init(red: red, green: green, blue: blue, alpha: alpha) - } - } - - public struct ThemeFont: Codable { - public var name: String - public var size: Double - - var storable: StorableFont { - .init(name: name, size: size) - } - } - - public var plainTextColor: ThemeColor - public var plainTextFont: ThemeFont - public var commentColor: ThemeColor - public var documentationMarkupColor: ThemeColor - public var documentationMarkupKeywordColor: ThemeColor - public var marksColor: ThemeColor - public var stringsColor: ThemeColor - public var charactersColor: ThemeColor - public var numbersColor: ThemeColor - public var regexLiteralsColor: ThemeColor - public var regexLiteralNumbersColor: ThemeColor - public var regexLiteralCaptureNamesColor: ThemeColor - public var regexLiteralCharacterClassNamesColor: ThemeColor - public var regexLiteralOperatorsColor: ThemeColor - public var keywordsColor: ThemeColor - public var preprocessorStatementsColor: ThemeColor - public var urlsColor: ThemeColor - public var attributesColor: ThemeColor - public var typeDeclarationsColor: ThemeColor - public var otherDeclarationsColor: ThemeColor - public var projectClassNamesColor: ThemeColor - public var projectFunctionAndMethodNamesColor: ThemeColor - public var projectConstantsColor: ThemeColor - public var projectTypeNamesColor: ThemeColor - public var projectPropertiesAndGlobalsColor: ThemeColor - public var projectPreprocessorMacrosColor: ThemeColor - public var otherClassNamesColor: ThemeColor - public var otherFunctionAndMethodNamesColor: ThemeColor - public var otherConstantsColor: ThemeColor - public var otherTypeNamesColor: ThemeColor - public var otherPropertiesAndGlobalsColor: ThemeColor - public var otherPreprocessorMacrosColor: ThemeColor - public var headingColor: ThemeColor - public var backgroundColor: ThemeColor - public var selectionColor: ThemeColor - public var cursorColor: ThemeColor - public var currentLineColor: ThemeColor - public var invisibleCharactersColor: ThemeColor - public var debuggerConsolePromptColor: ThemeColor - public var debuggerConsoleOutputColor: ThemeColor - public var debuggerConsoleInputColor: ThemeColor - public var executableConsoleOutputColor: ThemeColor - public var executableConsoleInputColor: ThemeColor - - public func asHighlightJSTheme() -> String { - buildHighlightJSTheme(self) - .replacingOccurrences(of: "\n", with: "") - .replacingOccurrences(of: ": ", with: ":") - .replacingOccurrences(of: "} ", with: "}") - .replacingOccurrences(of: " {", with: "{") - .replacingOccurrences(of: ";}", with: "}") - .replacingOccurrences(of: " ", with: "") - } -} - -public extension XcodeTheme { - /// Color scheme locations: - /// ~/Library/Developer/Xcode/UserData/FontAndColorThemes/ - /// Xcode.app/Contents/SharedFrameworks/DVTUserInterfaceKit.framework/Versions/A/Resources/FontAndColorThemes - init(fileURL: URL) throws { - let parser = XcodeThemeParser() - self = try parser.parse(fileURL: fileURL) - } -} - -struct XcodeThemeParser { - enum Error: Swift.Error { - case fileNotFound - case invalidData - } - - func parse(fileURL: URL) throws -> XcodeTheme { - guard let data = try? Data(contentsOf: fileURL) else { - throw Error.fileNotFound - } - - if fileURL.pathExtension == "xccolortheme" { - return try parseXCColorTheme(data) - } else { - throw Error.invalidData - } - } - - func parseXCColorTheme(_ data: Data) throws -> XcodeTheme { - let plist = try? PropertyListSerialization.propertyList( - from: data, - options: .mutableContainers, - format: nil - ) as? [String: Any] - - guard let theme = plist else { throw Error.invalidData } - - func getRawThemeValue(at path: [String]) -> String? { - guard !path.isEmpty else { return nil } - let keys = path.dropLast(1) - var currentDict = theme - for key in keys { - guard let value = currentDict[key] as? [String: Any] else { - return nil - } - currentDict = value - } - return currentDict[path.last!] as? String - } - - /// The source value is an `r g b a` string, for example: `0.5 0.5 0.2 1` - func convertColor(source: String) -> XcodeTheme.ThemeColor { - let components = source.split(separator: " ") - let red = (components[0] as NSString).doubleValue - let green = (components[1] as NSString).doubleValue - let blue = (components[2] as NSString).doubleValue - let alpha = (components[3] as NSString).doubleValue - return .init(red: red, green: green, blue: blue, alpha: alpha) - } - - func getThemeValue( - at path: [String], - defaultValue: XcodeTheme.ThemeColor = .init(red: 0, green: 0, blue: 0, alpha: 1) - ) -> XcodeTheme.ThemeColor { - if let value = getRawThemeValue(at: path) { - return convertColor(source: value) - } - return defaultValue - } - - /// The source value is an `FontName - size` string, for example: `SFMono-Medium - 12.0` - func convertFont(source: String) -> XcodeTheme.ThemeFont? { - if let separator = source.range(of: " - ") { - let name = String(source.prefix(upTo: separator.lowerBound)) - let size = Double(source.suffix(from: separator.upperBound)) ?? 0.0 - return .init(name: name, size: size) - } - return nil - } - - func getThemeFont( - at path: [String], - defaultValue: XcodeTheme.ThemeFont = .init(name: "SFMono-Medium", size: 12.0) - ) -> XcodeTheme.ThemeFont { - if let value = getRawThemeValue(at: path) { - return convertFont(source: value) ?? defaultValue - } - return defaultValue - } - - let black = XcodeTheme.ThemeColor(red: 0, green: 0, blue: 0, alpha: 1) - let white = XcodeTheme.ThemeColor(red: 1, green: 1, blue: 1, alpha: 1) - - let xcodeTheme = XcodeTheme( - plainTextColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], - defaultValue: black - ), - plainTextFont: getThemeFont( - at: ["DVTSourceTextSyntaxFonts", "xcode.syntax.plain"] - ), - commentColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment"], - defaultValue: black - ), - documentationMarkupColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc"], - defaultValue: black - ), - documentationMarkupKeywordColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc.keyword"], - defaultValue: black - ), - marksColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.mark"], - defaultValue: black - ), - stringsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.string"], - defaultValue: black - ), - charactersColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.character"], - defaultValue: black - ), - numbersColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], - defaultValue: black - ), - regexLiteralsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], - defaultValue: black - ), - regexLiteralNumbersColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], - defaultValue: black - ), - regexLiteralCaptureNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], - defaultValue: black - ), - regexLiteralCharacterClassNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], - defaultValue: black - ), - regexLiteralOperatorsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], - defaultValue: black - ), - keywordsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.keyword"], - defaultValue: black - ), - preprocessorStatementsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.preprocessor"], - defaultValue: black - ), - urlsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.url"], - defaultValue: black - ), - attributesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.attribute"], - defaultValue: black - ), - typeDeclarationsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.type"], - defaultValue: black - ), - otherDeclarationsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.other"], - defaultValue: black - ), - projectClassNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class"], - defaultValue: black - ), - projectFunctionAndMethodNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function"], - defaultValue: black - ), - projectConstantsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant"], - defaultValue: black - ), - projectTypeNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type"], - defaultValue: black - ), - projectPropertiesAndGlobalsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable"], - defaultValue: black - ), - projectPreprocessorMacrosColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro"], - defaultValue: black - ), - otherClassNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class.system"], - defaultValue: black - ), - otherFunctionAndMethodNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function.system"], - defaultValue: black - ), - otherConstantsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant.system"], - defaultValue: black - ), - otherTypeNamesColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type.system"], - defaultValue: black - ), - otherPropertiesAndGlobalsColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable.system"], - defaultValue: black - ), - otherPreprocessorMacrosColor: getThemeValue( - at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro.system"], - defaultValue: black - ), - headingColor: getThemeValue( - at: ["DVTMarkupTextPrimaryHeadingColor"], - defaultValue: black - ), - backgroundColor: getThemeValue( - at: ["DVTSourceTextBackground"], - defaultValue: white - ), - selectionColor: getThemeValue( - at: ["DVTSourceTextSelectionColor"], - defaultValue: black - ), - cursorColor: getThemeValue( - at: ["DVTSourceTextInsertionPointColor"], - defaultValue: black - ), - currentLineColor: getThemeValue( - at: ["DVTSourceTextCurrentLineHighlightColor"], - defaultValue: black - ), - invisibleCharactersColor: getThemeValue( - at: ["DVTSourceTextInvisiblesColor"], - defaultValue: black - ), - debuggerConsolePromptColor: getThemeValue( - at: ["DVTConsoleDebuggerPromptTextColor"], - defaultValue: black - ), - debuggerConsoleOutputColor: getThemeValue( - at: ["DVTConsoleDebuggerOutputTextColor"], - defaultValue: black - ), - debuggerConsoleInputColor: getThemeValue( - at: ["DVTConsoleDebuggerInputTextColor"], - defaultValue: black - ), - executableConsoleOutputColor: getThemeValue( - at: ["DVTConsoleExectuableOutputTextColor"], - defaultValue: black - ), - executableConsoleInputColor: getThemeValue( - at: ["DVTConsoleExectuableInputTextColor"], - defaultValue: black - ) - ) - - return xcodeTheme - } -} - diff --git a/Core/Tests/ChatServiceTests/ChatServiceTests.swift b/Core/Tests/ChatServiceTests/ChatServiceTests.swift deleted file mode 100644 index f8b5ec26..00000000 --- a/Core/Tests/ChatServiceTests/ChatServiceTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -import XCTest - -@testable import ChatService - -final class ReplaceFirstWordTests: XCTestCase { - func test_replace_first_word() { - let cases: [(String, String)] = [ - ("", ""), - ("workspace 001", "workspace 001"), - ("workspace001", "workspace001"), - ("@workspace", "@project"), - ("@workspace001", "@workspace001"), - ("@workspace 001", "@project 001"), - ] - - for (input, expected) in cases { - let result = replaceFirstWord(in: input, from: "@workspace", to: "@project") - XCTAssertEqual(result, expected, "Input: \(input), Expected: \(expected), Result: \(result)") - } - } -} - diff --git a/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift b/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift deleted file mode 100644 index 70469700..00000000 --- a/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift +++ /dev/null @@ -1,256 +0,0 @@ -import Foundation -import XCTest - -@testable import Workspace -@testable import KeyBindingManager - -class TabToAcceptSuggestionTests: XCTestCase { - @WorkspaceActor - func test_should_accept() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (true, nil) - ) - } - - @WorkspaceActor - func test_should_not_accept_without_suggestion() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL, skipSuggestion: true) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, "No suggestion") - ) - } - - @WorkspaceActor - func test_should_not_accept_without_filespace() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, "No filespace") - ) - } - - @WorkspaceActor - func test_should_not_accept_without_editor_focused() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: false - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: CGEvent(keyboardEventSource: nil, virtualKey: 48, keyDown: true)!, - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, "No focused editor") - ) - } - - @WorkspaceActor - func test_should_not_accept_without_active_xcode() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: false, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, "No active Xcode") - ) - } - - @WorkspaceActor - func test_should_not_accept_without_active_document() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: nil, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, "No active document") - ) - } - - @WorkspaceActor - func test_should_not_accept_with_shift() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48, flags: .maskShift), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, nil) - ) - } - - @WorkspaceActor - func test_should_not_accept_with_command() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48, flags: .maskCommand), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, nil) - ) - } - - @WorkspaceActor - func test_should_not_accept_with_control() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48, flags: .maskControl), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, nil) - ) - } - - @WorkspaceActor - func test_should_not_accept_with_help() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(48, flags: .maskHelp), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, nil) - ) - } - - @WorkspaceActor - func test_should_not_accept_without_tab() { - let fileURL = URL(string: "file:///test")! - let workspacePool = FakeWorkspacePool() - workspacePool.setTestFile(fileURL: fileURL) - let xcodeInspector = FakeThreadSafeAccessToXcodeInspector( - activeDocumentURL: fileURL, - hasActiveXcode: true, - hasFocusedEditor: true - ) - assertEqual( - TabToAcceptSuggestion.shouldAcceptSuggestion( - event: createEvent(50), - workspacePool: workspacePool, - xcodeInspector: xcodeInspector - ), (false, nil) - ) - } -} - -private func assertEqual( - _ result: (Bool, String?), - _ expected: (Bool, String?) -) { - if result != expected { - XCTFail("Expected \(expected), got \(result)") - } -} - -private func createEvent(_ keyCode: CGKeyCode, flags: CGEventFlags = []) -> CGEvent { - let event = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true)! - event.flags = flags - return event -} - -private struct FakeThreadSafeAccessToXcodeInspector: ThreadSafeAccessToXcodeInspectorProtocol { - let activeDocumentURL: URL? - let hasActiveXcode: Bool - let hasFocusedEditor: Bool -} - -private class FakeWorkspacePool: WorkspacePool { - private var fileURL: URL? - private var filespace: Filespace? - - @WorkspaceActor - func setTestFile(fileURL: URL, skipSuggestion: Bool = false) { - self.fileURL = fileURL - self.filespace = Filespace(fileURL: fileURL, onSave: {_ in }, onClose: {_ in }) - if skipSuggestion { return } - guard let filespace = self.filespace else { return } - filespace.setSuggestions([.init(id: "id", text: "test", position: .zero, range: .zero)]) - } - - override func fetchFilespaceIfExisted(fileURL: URL) -> Filespace? { - guard fileURL == self.fileURL else { return .none } - return filespace - } -} - diff --git a/Core/Tests/ServiceTests/Environment.swift b/Core/Tests/ServiceTests/Environment.swift deleted file mode 100644 index 191bf6aa..00000000 --- a/Core/Tests/ServiceTests/Environment.swift +++ /dev/null @@ -1,74 +0,0 @@ -import AppKit -import Client -import Foundation -import GitHubCopilotService -import SuggestionBasic -import Workspace -import XCTest -import XPCShared - -@testable import Service - -func completion(text: String, range: CursorRange, uuid: String = "") -> CodeSuggestion { - .init(id: uuid, text: text, position: range.start, range: range) -} - -class MockSuggestionService: GitHubCopilotSuggestionServiceType { - func terminate() async { - fatalError() - } - - func cancelRequest() async { - fatalError() - } - - func notifyOpenTextDocument(fileURL: URL, content: String) async throws { - fatalError() - } - - func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws { - fatalError() - } - - func notifyCloseTextDocument(fileURL: URL) async throws { - fatalError() - } - - func notifySaveTextDocument(fileURL: URL) async throws { - fatalError() - } - - var completions = [CodeSuggestion]() - var shown: String? - var accepted: String? - var rejected: [String] = [] - - init(completions: [CodeSuggestion]) { - self.completions = completions - } - - func getCompletions( - fileURL: URL, - content: String, - originalContent: String, - cursorPosition: SuggestionBasic.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool - ) async throws -> [SuggestionBasic.CodeSuggestion] { - completions - } - - func notifyShown(_ completion: SuggestionBasic.CodeSuggestion) async { - shown = completion.id - } - - func notifyAccepted(_ completion: CodeSuggestion, acceptedLength: Int? = nil) async { - accepted = completion.id - } - - func notifyRejected(_ completions: [CodeSuggestion]) async { - rejected = completions.map(\.id) - } -} - diff --git a/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift b/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift deleted file mode 100644 index c5bd977c..00000000 --- a/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -import SuggestionBasic -import XCTest -@testable import Service -@testable import XPCShared - -class ExtractSelectedCodeTests: XCTestCase { - func test_empty_selection() { - let selection = EditorContent.Selection( - start: CursorPosition(line: 0, character: 0), - end: CursorPosition(line: 0, character: 0) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = selectedCode(in: selection, for: lines) - XCTAssertEqual(result, "") - } - - func test_single_line_selection() { - let selection = EditorContent.Selection( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = selectedCode(in: selection, for: lines) - XCTAssertEqual(result, "foo = ") - } - - func test_single_line_selection_at_line_end() { - let selection = EditorContent.Selection( - start: CursorPosition(line: 0, character: 8), - end: CursorPosition(line: 0, character: 11) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = selectedCode(in: selection, for: lines) - XCTAssertEqual(result, "= 1") - } - - func test_multi_line_selection() { - let selection = EditorContent.Selection( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 1, character: 11) - ) - let lines = ["let foo = 1\n", "let bar = 2\n", "let baz = 3\n"] - let result = selectedCode(in: selection, for: lines) - XCTAssertEqual(result, "foo = 1\nlet bar = 2") - } - - func test_invalid_selection() { - let selection = EditorContent.Selection( - start: CursorPosition(line: 1, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let foo = 1", "let bar = 2"] - let result = selectedCode(in: selection, for: lines) - XCTAssertEqual(result, "") - } -} diff --git a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift deleted file mode 100644 index 941a6c84..00000000 --- a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift +++ /dev/null @@ -1,275 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest -import WorkspaceSuggestionService - -@testable import Service -@testable import Workspace - -class FilespaceSuggestionInvalidationTests: XCTestCase { - @WorkspaceActor - func prepare( - lines: [String] = [ - "let one = 1\n", - "\n", - "let three = 3\n", - ], - cursorPosition: CursorPosition = .init(line: 1, character: 0), - suggestionText: String = "let two = 2", - range: CursorRange = .init(startPair: (1, 0), endPair: (1, 0)) - ) async throws -> (Filespace, FilespaceSuggestionSnapshot) { - let pool = WorkspacePool() - let filespace = Filespace( - fileURL: URL(fileURLWithPath: "file/path/to.swift"), - onSave: { _ in }, - onClose: { _ in } - ) - filespace.suggestions = [ - .init( - id: "", - text: suggestionText, - position: cursorPosition, - range: range - ), - ] - let snapshot = FilespaceSuggestionSnapshot(lines: lines, cursorPosition: cursorPosition) - filespace.suggestionSourceSnapshot = snapshot - return (filespace, snapshot) - } - - func testUnchangedDocument_IsValid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 0) - ) - - XCTAssertTrue(isValid) - XCTAssertNotNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertEqual(snapshot, priorSnapshot) - } - - func testTypingIntoCompletion_IsValid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let \n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 4) - ) - - XCTAssertTrue(isValid) - XCTAssertNotNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertEqual(snapshot, priorSnapshot) - } - - func testTypingIntoMultibyteCharacterCompletion_IsValid() async throws { - let (filespace, priorSnapshot) = try await prepare( - suggestionText: "let t🎆🎆 = 2" - ) - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let t🎆🎆\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 7) - ) - - XCTAssertTrue(isValid) - XCTAssertNotNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertEqual(snapshot, priorSnapshot) - } - - func testTypingNonMatchingText_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "var \n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 4) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testMiddleOfLinePosition_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let \n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 2) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testCompletingBracesAfterCursor_IsValid() async throws { - let (filespace, priorSnapshot) = try await prepare( - suggestionText: "let two = (2, 2)" - ) - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let two = (2)\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 12) - ) - - XCTAssertTrue(isValid) - XCTAssertNotNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertEqual(snapshot, priorSnapshot) - } - - func testTypingFullCompletion_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let two = 2\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 11) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testTypingPastCompletion_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let two = 22\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 12) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testAlteringOtherDocumentParts_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "\n", - ], - cursorPosition: .init(line: 1, character: 0) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testNotPresentingSuggestion_IsInvalid() async throws { - let (filespace, _) = try await prepare() - await filespace.reset() - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 0) - ) - - XCTAssertFalse(isValid) - } - - func testCompletionNotAtStartOfLine_IsInvalid() async throws { - let (filespace, priorSnapshot) = try await prepare( - lines: [ - "let one = 1\n", - "var \n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 4), - suggestionText: "two = 2", - range: .init(startPair: (1, 4), endPair: (1, 4)) - ) - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let \n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 4) - ) - - XCTAssertFalse(isValid) - XCTAssertNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertNotEqual(snapshot, priorSnapshot) - } - - func testCompletionReplacingBracesAfterCursor_IsValid() async throws { - let (filespace, priorSnapshot) = try await prepare( - lines: [ - "let one = 1\n", - "let two = (2, (2))\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 16), - suggestionText: "let two = (2, (2, 2))", - range: .init(startPair: (1, 0), endPair: (1, 18)) - ) - - let isValid = await filespace.validateSuggestions( - lines: [ - "let one = 1\n", - "let two = (2, (2,))\n", - "let three = 3\n", - ], - cursorPosition: .init(line: 1, character: 17) - ) - - XCTAssertTrue(isValid) - XCTAssertNotNil(filespace.presentingSuggestion) - let snapshot = await filespace.suggestionSourceSnapshot - XCTAssertEqual(snapshot, priorSnapshot) - } -} - diff --git a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift deleted file mode 100644 index dfdb4b3e..00000000 --- a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift +++ /dev/null @@ -1,866 +0,0 @@ -import SuggestionBasic -import XCTest - -@testable import SuggestionInjector - -final class AcceptSuggestionTests: XCTestCase { - func test_accept_suggestion_single_line() async throws { - let content = """ - struct Cat { - - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 1), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 0) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 1) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo, - suggestionLineLimit: 1 - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 4)) - XCTAssertEqual( - lines.joined(separator: ""), - [ - "struct Cat {", - " var name: String", - " ", - "}", - "" - ].joined(separator: "\n"), - "There is always a new line at the end of each line! When you join them, it will look like this" - ) - } - - func test_accept_suggestion_no_overlap() async throws { - let content = """ - struct Cat { - - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 1), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 0) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 1) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual( - lines.joined(separator: ""), - """ - struct Cat { - var name: String - var age: String - } - - """, - "There is always a new line at the end of each line! When you join them, it will look like this" - ) - } - - func test_accept_suggestion_start_from_previous_line() async throws { - let content = """ - struct Cat { - } - """ - let text = """ - struct Cat { - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 12), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 12) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 12) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name: String - var age: String - } - - """) - } - - func test_accept_suggestion_overlap() async throws { - let content = """ - struct Cat { - var name - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 12), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 12) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 1, character: 12) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name: String - var age: String - } - - """) - } - - func test_accept_suggestion_overlap_continue_typing() async throws { - let content = """ - struct Cat { - var name: Str - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 12), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 12) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 1, character: 12) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name: String - var age: String - } - - """) - } - - func test_accept_suggestion_overlap_continue_typing_has_suffix_typed() async throws { - let content = """ - print("") - """ - let text = """ - print("Hello World!") - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 6), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 6) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 7) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 0, character: 21)) - XCTAssertEqual(lines.joined(separator: ""), """ - print("Hello World!") - - """) - } - - func test_accept_suggestion_overlap_continue_typing_suggestion_in_the_middle() async throws { - let content = """ - print("He") - """ - let text = """ - print("Hello World! - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 6), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 6) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 7) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 0, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - print("Hello World!") - - """) - } - - func test_accept_suggestion_overlap_continue_typing_has_suffix_typed_suggestion_has_multiple_lines( - ) async throws { - let content = """ - struct Cat {} - """ - let text = """ - struct Cat { - var name: String - var kind: String - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 6), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 6) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 12) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 3, character: 1)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name: String - var kind: String - } - - """) - } - - func test_propose_suggestion_partial_overlap() async throws { - let content = "func quickSort() {}}" - let text = """ - func quickSort() { - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 18), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 20) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 18) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 6, character: 1)) - XCTAssertEqual(lines.joined(separator: ""), """ - func quickSort() { - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - - """) - } - - func test_no_overlap_append_to_the_end() async throws { - let content = "func quickSort() {" - let text = """ - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 18), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 0) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 18) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 6, character: 1)) - XCTAssertEqual(lines.joined(separator: ""), """ - func quickSort() { - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - - """) - } - - func test_replacing_multiple_lines() async throws { - let content = """ - struct Cat { - func speak() { print("meow") } - } - """ - let text = """ - struct Dog { - func speak() { - print("woof") - } - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 7), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 2, character: 1) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 7) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 4, character: 1)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Dog { - func speak() { - print("woof") - } - } - - """) - } - - func test_replacing_multiple_lines_in_the_middle() async throws { - let content = """ - protocol Animal { - func speak() - } - - struct Cat: Animal { - func speak() { print("meow") } - } - - func foo() {} - """ - let text = """ - Dog { - func speak() { - print("woof") - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 5, character: 34), - range: .init( - start: .init(line: 4, character: 7), - end: .init(line: 5, character: 34) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 5, character: 34) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 7, character: 5)) - XCTAssertEqual(lines.joined(separator: ""), """ - protocol Animal { - func speak() - } - - struct Dog { - func speak() { - print("woof") - } - } - - func foo() {} - - """) - } - - func test_replacing_single_line_in_the_middle_should_not_remove_the_next_character( - ) async throws { - let content = """ - apiKeyName: ,, - """ - - let suggestion = CodeSuggestion( - id: "", - text: "apiKeyName: azureOpenAIAPIKeyName", - position: .init(line: 0, character: 12), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 12) - ) - ) - - var lines = content.breakIntoEditorStyleLines() - var extraInfo = SuggestionInjector.ExtraInfo() - var cursor = CursorPosition(line: 5, character: 34) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertEqual(cursor, .init(line: 0, character: 33)) - XCTAssertEqual(lines.joined(separator: ""), """ - apiKeyName: azureOpenAIAPIKeyName,, - - """) - } - - func test_remove_the_first_adjacent_placeholder_in_the_last_line( - ) async throws { - let content = """ - apiKeyName: <#T##value: BinaryInteger##BinaryInteger#> <#Hello#>, - """ - - let suggestion = CodeSuggestion( - id: "", - text: "apiKeyName: azureOpenAIAPIKeyName", - position: .init(line: 0, character: 12), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 12) - ) - ) - - var lines = content.breakIntoEditorStyleLines() - var extraInfo = SuggestionInjector.ExtraInfo() - var cursor = CursorPosition(line: 5, character: 34) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertEqual(cursor, .init(line: 0, character: 33)) - XCTAssertEqual(lines.joined(separator: ""), """ - apiKeyName: azureOpenAIAPIKeyName <#Hello#>, - - """) - } - - func test_accept_suggestion_start_from_previous_line_has_emoji_inside() async throws { - let content = """ - struct 😹😹 { - } - """ - let text = """ - struct 😹😹 { - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 13), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 13) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 13) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct 😹😹 { - var name: String - var age: String - } - - """) - } - - func test_accept_suggestion_overlap_with_emoji_in_the_previous_code() async throws { - let content = """ - struct 😹😹 { - var name - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 13), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 13) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 1, character: 13) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct 😹😹 { - var name: String - var age: String - } - - """) - } - - func test_accept_suggestion_overlap_continue_typing_has_emoji_inside() async throws { - let content = """ - struct 😹😹 { - var name: Str - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 13), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 1, character: 13) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 1, character: 13) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 2, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct 😹😹 { - var name: String - var age: String - } - - """) - } - - func test_replacing_multiple_lines_with_emoji() async throws { - let content = """ - struct 😹😹 { - func speak() { print("meow") } - } - """ - let text = """ - struct 🐶🐶 { - func speak() { - print("woof") - } - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 7), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 2, character: 1) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 7) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 4, character: 1)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct 🐶🐶 { - func speak() { - print("woof") - } - } - - """) - } - - func test_accept_suggestion_overlap_continue_typing_suggestion_with_emoji_in_the_middle() async throws { - let content = """ - print("🐶") - """ - let text = """ - print("🐶llo 🐶rld! - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 6), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 6) - ) - ) - - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakIntoEditorStyleLines() - var cursor = CursorPosition(line: 0, character: 7) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications)) - XCTAssertEqual(cursor, .init(line: 0, character: 19)) - XCTAssertEqual(lines.joined(separator: ""), """ - print("🐶llo 🐶rld!") - - """) - } - - func test_replacing_single_line_in_the_middle_should_not_remove_the_next_character_with_emoji( - ) async throws { - let content = """ - 🐶KeyName: ,, - """ - - let suggestion = CodeSuggestion( - id: "", - text: "🐶KeyName: azure👩‍❤️‍👨AIAPIKeyName", - position: .init(line: 0, character: 11), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 0, character: 11) - ) - ) - - var lines = content.breakIntoEditorStyleLines() - var extraInfo = SuggestionInjector.ExtraInfo() - var cursor = CursorPosition(line: 5, character: 34) - SuggestionInjector().acceptSuggestion( - intoContentWithoutSuggestion: &lines, - cursorPosition: &cursor, - completion: suggestion, - extraInfo: &extraInfo - ) - - XCTAssertEqual(cursor, .init(line: 0, character: 36)) - XCTAssertEqual(lines.joined(separator: ""), """ - 🐶KeyName: azure👩‍❤️‍👨AIAPIKeyName,, - - """) - } -} - -extension String { - func breakIntoEditorStyleLines() -> [String] { - split(separator: "\n", omittingEmptySubsequences: false).map { $0 + "\n" } - } -} - diff --git a/Core/Tests/SuggestionWidgetTests/File.swift b/Core/Tests/SuggestionWidgetTests/File.swift deleted file mode 100644 index fecc4ab4..00000000 --- a/Core/Tests/SuggestionWidgetTests/File.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 5e7a287b..00000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,85 +0,0 @@ -# Development - -## Prerequisites - -Requires Node installed and `npm` available on your system path, e.g. - -```sh -sudo ln -s `which npm` /usr/local/bin -sudo ln -s `which node` /usr/local/bin -``` - -For context, this is used by an Xcode run script as part of the build. Run -scripts use a very limited path to resolve commands. - -## Targets - -### Copilot for Xcode - -Copilot for Xcode is the host app containing both the XPCService and the editor extension. It provides the settings UI. - -### EditorExtension - -As its name suggests, the Xcode source editor extension. Its sole purpose is to forward editor content to the XPCService for processing, and update the editor with the returned content. Due to the sandboxing requirements for editor extensions, it has to communicate with a trusted, non-sandboxed XPCService (CommunicationBridge and ExtensionService) to bypass the limitations. The XPCService service name must be included in the `com.apple.security.temporary-exception.mach-lookup.global-name` entitlements. - -### ExtensionService - -The `ExtensionService` is a program that operates in the background. All features are implemented in this target. - -### CommunicationBridge - -It's responsible for maintaining the communication between the Copilot for Xcode/EditorExtension and ExtensionService. - -### Core and Tool - -Most of the logics are implemented inside the package `Core` and `Tool`. - -- The `Service` contains the implementations of the ExtensionService target. -- The `HostApp` contains the implementations of the Copilot for Xcode target. - -## Building and Archiving the App - -1. Update the xcconfig files, bridgeLaunchAgent.plist, and Tool/Configs/Configurations.swift. -2. Build or archive the Copilot for Xcode target. -3. If Xcode complains that the pro package doesn't exist, please remove the package from the project. - -## Testing Source Editor Extension - -Just run both the `ExtensionService`, `CommunicationBridge` and the `EditorExtension` Target. Read [Testing Your Source Editor Extension](https://developer.apple.com/documentation/xcodekit/testing_your_source_editor_extension) for more details. - -## Local Build - -To build the application locally, follow these steps: - -1. Navigate to the Script directory and run the build scripts: - - ```sh - cd ./Script - sh ./uninstall-app.sh # Remove any previous installation - rm -rf ../build # Clean the build directory - sh ./localbuild-app.sh # Build a fresh copy of the app - ``` - -2. After successful build, the application will be available in the build directory. Copy `GitHub Copilot for Xcode.app` to your Applications folder to test it locally. - -## SwiftUI Previews - -Looks like SwiftUI Previews are not very happy with Objective-C packages when running with app targets. To use previews, please switch schemes to the package product targets. - -## Unit Tests - -To run unit tests, just run test from the `Copilot for Xcode` target. - -For new tests, they should be added to the `TestPlan.xctestplan`. - -## Code Style - -We use SwiftFormat to format the code. - -The source code mostly follows the [Ray Wenderlich Style Guide](https://github.com/raywenderlich/swift-style-guide) very closely with the following exception: - -- Use the Xcode default of 4 spaces for indentation. - -## App Versioning - -The app version and all targets' version in controlled by `Version.xcconfig`. diff --git a/Docs/AppIcon.png b/Docs/AppIcon.png deleted file mode 100644 index 88b20d1d..00000000 Binary files a/Docs/AppIcon.png and /dev/null differ diff --git a/Docs/accessibility-permission-request.png b/Docs/accessibility-permission-request.png deleted file mode 100644 index 302fd0b4..00000000 Binary files a/Docs/accessibility-permission-request.png and /dev/null differ diff --git a/Docs/accessibility-permission.png b/Docs/accessibility-permission.png deleted file mode 100644 index 0db1583a..00000000 Binary files a/Docs/accessibility-permission.png and /dev/null differ diff --git a/Docs/background-item.png b/Docs/background-item.png deleted file mode 100644 index 9eea5b3f..00000000 Binary files a/Docs/background-item.png and /dev/null differ diff --git a/Docs/background-permission-required.png b/Docs/background-permission-required.png deleted file mode 100644 index fb35d34b..00000000 Binary files a/Docs/background-permission-required.png and /dev/null differ diff --git a/Docs/chat_dark.gif b/Docs/chat_dark.gif deleted file mode 100644 index abd5cc20..00000000 Binary files a/Docs/chat_dark.gif and /dev/null differ diff --git a/Docs/connect-comm-bridge-failed.png b/Docs/connect-comm-bridge-failed.png deleted file mode 100644 index 4e8d2587..00000000 Binary files a/Docs/connect-comm-bridge-failed.png and /dev/null differ diff --git a/Docs/copilot-menu_dark.png b/Docs/copilot-menu_dark.png deleted file mode 100644 index 35b36e7b..00000000 Binary files a/Docs/copilot-menu_dark.png and /dev/null differ diff --git a/Docs/demo.gif b/Docs/demo.gif deleted file mode 100644 index a21db968..00000000 Binary files a/Docs/demo.gif and /dev/null differ diff --git a/Docs/device-code.png b/Docs/device-code.png deleted file mode 100644 index a345a732..00000000 Binary files a/Docs/device-code.png and /dev/null differ diff --git a/Docs/dmg-open.png b/Docs/dmg-open.png deleted file mode 100644 index cf50f7da..00000000 Binary files a/Docs/dmg-open.png and /dev/null differ diff --git a/Docs/extension-permission.png b/Docs/extension-permission.png deleted file mode 100644 index 6f613029..00000000 Binary files a/Docs/extension-permission.png and /dev/null differ diff --git a/Docs/macos-download-open-confirm.png b/Docs/macos-download-open-confirm.png deleted file mode 100644 index de58a9a6..00000000 Binary files a/Docs/macos-download-open-confirm.png and /dev/null differ diff --git a/Docs/signin-button.png b/Docs/signin-button.png deleted file mode 100644 index ac566c9b..00000000 Binary files a/Docs/signin-button.png and /dev/null differ diff --git a/Docs/update-message.png b/Docs/update-message.png deleted file mode 100644 index 35035861..00000000 Binary files a/Docs/update-message.png and /dev/null differ diff --git a/Docs/xcode-menu.png b/Docs/xcode-menu.png deleted file mode 100644 index c30e539c..00000000 Binary files a/Docs/xcode-menu.png and /dev/null differ diff --git a/Docs/xcode-menu_dark.png b/Docs/xcode-menu_dark.png deleted file mode 100644 index 28b957b7..00000000 Binary files a/Docs/xcode-menu_dark.png and /dev/null differ diff --git a/EditorExtension/AcceptPromptToCodeCommand.swift b/EditorExtension/AcceptPromptToCodeCommand.swift deleted file mode 100644 index 51bea4a4..00000000 --- a/EditorExtension/AcceptPromptToCodeCommand.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class AcceptPromptToCodeCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Accept Prompt to Code" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - Task { - do { - try await (Task(timeout: 7) { - let service = try getService() - if let content = try await service.getPromptToCodeAcceptedCode( - editorContent: .init(invocation) - ) { - invocation.accept(content) - } - completionHandler(nil) - }.value) - } catch is CancellationError { - completionHandler(nil) - } catch { - completionHandler(error) - } - } - } -} diff --git a/EditorExtension/AcceptSuggestionCommand.swift b/EditorExtension/AcceptSuggestionCommand.swift deleted file mode 100644 index a1ea71f6..00000000 --- a/EditorExtension/AcceptSuggestionCommand.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit -import XPCShared - -class AcceptSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Accept Suggestion" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - Task { - do { - try await (Task(timeout: 7) { - let service = try getService() - if let content = try await service.getSuggestionAcceptedCode( - editorContent: .init(invocation) - ) { - invocation.accept(content) - } - completionHandler(nil) - }.value) - } catch is CancellationError { - completionHandler(nil) - } catch { - completionHandler(error) - } - } - } -} - diff --git a/EditorExtension/CloseIdleTabsCommand.swift b/EditorExtension/CloseIdleTabsCommand.swift deleted file mode 100644 index 0e9537ee..00000000 --- a/EditorExtension/CloseIdleTabsCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class CloseIdleTabsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Close Idle Tabs" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.postNotification(name: "CloseIdleTabsOfXcodeWindow") - } - } -} - diff --git a/EditorExtension/CustomCommand.swift b/EditorExtension/CustomCommand.swift deleted file mode 100644 index 0a43a51d..00000000 --- a/EditorExtension/CustomCommand.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class CustomCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String = "" - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.customCommand( - id: customCommandMap[invocation.commandIdentifier] ?? "", - editorContent: .init(invocation) - ) - } - } -} - diff --git a/EditorExtension/EditorExtension.entitlements b/EditorExtension/EditorExtension.entitlements deleted file mode 100644 index 776babcc..00000000 --- a/EditorExtension/EditorExtension.entitlements +++ /dev/null @@ -1,17 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE) - - com.apple.security.temporary-exception.mach-lookup.global-name - - $(BUNDLE_IDENTIFIER_BASE).CommunicationBridge - $(BUNDLE_IDENTIFIER_BASE).ExtensionService - - - diff --git a/EditorExtension/GetSuggestionsCommand.swift b/EditorExtension/GetSuggestionsCommand.swift deleted file mode 100644 index 6be1c417..00000000 --- a/EditorExtension/GetSuggestionsCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class GetSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Get Suggestions" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.getSuggestedCode(editorContent: .init(invocation)) - } - } -} - diff --git a/EditorExtension/Helpers.swift b/EditorExtension/Helpers.swift deleted file mode 100644 index 8851c279..00000000 --- a/EditorExtension/Helpers.swift +++ /dev/null @@ -1,98 +0,0 @@ -import SuggestionBasic -import Foundation -import XcodeKit -import XPCShared - -extension XCSourceEditorCommandInvocation { - func mutateCompleteBuffer(modifications: [Modification], restoringSelections restore: Bool) { - if restore { - let selectionsRangesToRestore = buffer.selections - .compactMap { $0 as? XCSourceTextRange } - buffer.selections.removeAllObjects() - buffer.lines.apply(modifications) - for range in selectionsRangesToRestore { - buffer.selections.add(range) - } - } else { - buffer.lines.apply(modifications) - } - } - - func accept(_ updatedContent: UpdatedContent) { - if let newSelection = updatedContent.newSelection { - mutateCompleteBuffer( - modifications: updatedContent.modifications, - restoringSelections: false - ) - buffer.selections.removeAllObjects() - buffer.selections.add(XCSourceTextRange( - start: .init(line: newSelection.start.line, column: newSelection.start.character), - end: .init(line: newSelection.end.line, column: newSelection.end.character) - )) - } else { - mutateCompleteBuffer( - modifications: updatedContent.modifications, - restoringSelections: true - ) - } - } -} - -extension EditorContent { - init(_ invocation: XCSourceEditorCommandInvocation) { - let buffer = invocation.buffer - self.init( - content: buffer.completeBuffer, - lines: buffer.lines as? [String] ?? [], - uti: buffer.contentUTI, - cursorPosition: ((buffer.selections.lastObject as? XCSourceTextRange)?.end).map { - CursorPosition(line: $0.line, character: $0.column) - } ?? CursorPosition(line: 0, character: 0), - cursorOffset: -1, - selections: buffer.selections.map { - let sl = ($0 as? XCSourceTextRange)?.start.line ?? 0 - let sc = ($0 as? XCSourceTextRange)?.start.column ?? 0 - let el = ($0 as? XCSourceTextRange)?.end.line ?? 0 - let ec = ($0 as? XCSourceTextRange)?.end.column ?? 0 - - return Selection( - start: CursorPosition( line: sl, character: sc ), - end: CursorPosition( line: el, character: ec ) - ) - }, - tabSize: buffer.tabWidth, - indentSize: buffer.indentationWidth, - usesTabsForIndentation: buffer.usesTabsForIndentation - ) - } -} - -/// https://gist.github.com/swhitty/9be89dfe97dbb55c6ef0f916273bbb97 -extension Task where Failure == Error { - // Start a new Task with a timeout. If the timeout expires before the operation is - // completed then the task is cancelled and an error is thrown. - init( - priority: TaskPriority? = nil, - timeout: TimeInterval, - operation: @escaping @Sendable () async throws -> Success - ) { - self = Task(priority: priority) { - try await withThrowingTaskGroup(of: Success.self) { group -> Success in - group.addTask(operation: operation) - group.addTask { - try await _Concurrency.Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) - throw TimeoutError() - } - guard let success = try await group.next() else { - throw _Concurrency.CancellationError() - } - group.cancelAll() - return success - } - } - } -} - -private struct TimeoutError: LocalizedError { - var errorDescription: String? = "Task timed out before completion" -} diff --git a/EditorExtension/Info.plist b/EditorExtension/Info.plist deleted file mode 100644 index 13a9bdb6..00000000 --- a/EditorExtension/Info.plist +++ /dev/null @@ -1,48 +0,0 @@ - - - - - APPLICATION_SUPPORT_FOLDER - $(APPLICATION_SUPPORT_FOLDER) - BUNDLE_IDENTIFIER_BASE - $(BUNDLE_IDENTIFIER_BASE) - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - $(EXTENSION_BUNDLE_DISPLAY_NAME) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(EXTENSION_BUNDLE_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - HOST_APP_NAME - $(HOST_APP_NAME) - NSExtension - - NSExtensionAttributes - - XCSourceEditorCommandDefinitions - - XCSourceEditorExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).SourceEditorExtension - - NSExtensionPointIdentifier - com.apple.dt.Xcode.extension.source-editor - - NSHumanReadableCopyright - - TEAM_ID_PREFIX - $(TeamIdentifierPrefix) - STANDARD_TELEMETRY_CHANNEL_KEY - $(STANDARD_TELEMETRY_CHANNEL_KEY) - - diff --git a/EditorExtension/NextSuggestionCommand.swift b/EditorExtension/NextSuggestionCommand.swift deleted file mode 100644 index f07f4017..00000000 --- a/EditorExtension/NextSuggestionCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class NextSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Next Suggestion" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.getNextSuggestedCode(editorContent: .init(invocation)) - } - } -} - diff --git a/EditorExtension/OpenChat.swift b/EditorExtension/OpenChat.swift deleted file mode 100644 index 7ee1d945..00000000 --- a/EditorExtension/OpenChat.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class OpenChatCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Open Chat" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - Task { - do { - let service = try getService() - try await service.openChat() - completionHandler(nil) - } catch is CancellationError { - completionHandler(nil) - } catch { - completionHandler(error) - } - } - } -} diff --git a/EditorExtension/OpenSettingsCommand.swift b/EditorExtension/OpenSettingsCommand.swift deleted file mode 100644 index b1262c4b..00000000 --- a/EditorExtension/OpenSettingsCommand.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// OpenSettingsCommand.swift -// EditorExtension -// -// Opens the settings app -// - -import Foundation -import XcodeKit -import HostAppActivator - - - -class OpenSettingsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Open \(hostAppName()) Settings" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - Task { - do { - try launchHostAppSettings() - completionHandler(nil) - } catch { - completionHandler( - GitHubCopilotForXcodeSettingsLaunchError - .openFailed( - errorDescription: error.localizedDescription - ) - ) - } - } - } -} - -func hostAppName() -> String { - return Bundle.main.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String - ?? "GitHub Copilot for Xcode" -} diff --git a/EditorExtension/PrefetchSuggestionsCommand.swift b/EditorExtension/PrefetchSuggestionsCommand.swift deleted file mode 100644 index bc43c40e..00000000 --- a/EditorExtension/PrefetchSuggestionsCommand.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class PrefetchSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Prefetch Suggestions" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - await service.prefetchRealtimeSuggestions(editorContent: .init(invocation)) - } - } -} diff --git a/EditorExtension/PreviousSuggestionCommand.swift b/EditorExtension/PreviousSuggestionCommand.swift deleted file mode 100644 index 61894bab..00000000 --- a/EditorExtension/PreviousSuggestionCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class PreviousSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Previous Suggestion" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.getPreviousSuggestedCode(editorContent: .init(invocation)) - } - } -} - diff --git a/EditorExtension/PromptToCodeCommand.swift b/EditorExtension/PromptToCodeCommand.swift deleted file mode 100644 index 13e4f3be..00000000 --- a/EditorExtension/PromptToCodeCommand.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class PromptToCodeCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Prompt to Code" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.promptToCode(editorContent: .init(invocation)) - } - } -} diff --git a/EditorExtension/RejectSuggestionCommand.swift b/EditorExtension/RejectSuggestionCommand.swift deleted file mode 100644 index d1091237..00000000 --- a/EditorExtension/RejectSuggestionCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import Foundation -import SuggestionBasic -import XcodeKit - -class RejectSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Decline Suggestion" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.getSuggestionRejectedCode(editorContent: .init(invocation)) - } - } -} - diff --git a/EditorExtension/SeparatorCommand.swift b/EditorExtension/SeparatorCommand.swift deleted file mode 100644 index 79e4b138..00000000 --- a/EditorExtension/SeparatorCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class SeparatorCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String = "" - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - } - - func named(_ name: String) -> Self { - self.name = name - return self - } -} diff --git a/EditorExtension/SourceEditorExtension.swift b/EditorExtension/SourceEditorExtension.swift deleted file mode 100644 index a9d252f9..00000000 --- a/EditorExtension/SourceEditorExtension.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Client -import Foundation -import GitHubCopilotService -import Preferences -import XcodeKit - -#if canImport(PreferencesPlus) -import PreferencesPlus -#endif - -class SourceEditorExtension: NSObject, XCSourceEditorExtension { - var builtin: [[XCSourceEditorCommandDefinitionKey: Any]] { - [ - AcceptSuggestionCommand(), - RejectSuggestionCommand(), - GetSuggestionsCommand(), - NextSuggestionCommand(), - PreviousSuggestionCommand(), - SyncTextSettingsCommand(), - ToggleRealtimeSuggestionsCommand(), - ].map(makeCommandDefinition) - } - - var chat: [[XCSourceEditorCommandDefinitionKey: Any]] { - [ - OpenChatCommand() - ].map(makeCommandDefinition) - } - - var additionalBuiltin: [[XCSourceEditorCommandDefinitionKey: Any]] { - [ - OpenSettingsCommand(), - ].map(makeCommandDefinition) - } - - var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] { - var definitions = builtin - - if FeatureFlagNotifierImpl.shared.featureFlags.chat { - definitions += chat - } - - definitions += additionalBuiltin - - return definitions - } - - func extensionDidFinishLaunching() { - #if DEBUG - // In a debug build, we usually want to use the XPC service run from Xcode. - #else - // When the source extension is initialized - // we can call a random command to wake up the XPC service. - Task.detached { - try await Task.sleep(nanoseconds: 1_000_000_000) - let service = try getService() - _ = try await service.getXPCServiceVersion() - } - #endif - } -} - -let identifierPrefix: String = Bundle.main.bundleIdentifier ?? "" - -var customCommandMap = [String: String]() - -protocol CommandType: AnyObject { - var commandClassName: String { get } - var identifier: String { get } - var name: String { get } -} - -extension CommandType where Self: NSObject { - var commandClassName: String { Self.className() } - var identifier: String { commandClassName } -} - -extension CommandType { - func makeCommandDefinition() -> [XCSourceEditorCommandDefinitionKey: Any] { - [.classNameKey: commandClassName, - .identifierKey: identifierPrefix + identifier, - .nameKey: name] - } -} - -func makeCommandDefinition(_ commandType: CommandType) - -> [XCSourceEditorCommandDefinitionKey: Any] -{ - commandType.makeCommandDefinition() -} diff --git a/EditorExtension/SyncTextSettingsCommand.swift b/EditorExtension/SyncTextSettingsCommand.swift deleted file mode 100644 index f1c54561..00000000 --- a/EditorExtension/SyncTextSettingsCommand.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class SyncTextSettingsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Sync Text Settings" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - completionHandler(nil) - Task { - let service = try getService() - _ = try await service.getRealtimeSuggestedCode(editorContent: .init(invocation)) - } - } -} diff --git a/EditorExtension/ToggleRealtimeSuggestionsCommand.swift b/EditorExtension/ToggleRealtimeSuggestionsCommand.swift deleted file mode 100644 index 690143da..00000000 --- a/EditorExtension/ToggleRealtimeSuggestionsCommand.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Client -import SuggestionBasic -import Foundation -import XcodeKit - -class ToggleRealtimeSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType { - var name: String { "Enable/Disable Completions" } - - func perform( - with invocation: XCSourceEditorCommandInvocation, - completionHandler: @escaping (Error?) -> Void - ) { - Task { - do { - let service = try getService() - try await service.toggleRealtimeSuggestion() - completionHandler(nil) - } catch is CancellationError { - completionHandler(nil) - } catch { - completionHandler(error) - } - } - } -} diff --git a/ExtensionPoint.appextensionpoint b/ExtensionPoint.appextensionpoint deleted file mode 100644 index 31f4275c..00000000 --- a/ExtensionPoint.appextensionpoint +++ /dev/null @@ -1,11 +0,0 @@ - - - - - com.github.CopilotForXcode.ExtensionService.Extension - - EXPresentsUserInterface - - - - diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift deleted file mode 100644 index 4dfc0da1..00000000 --- a/ExtensionService/AppDelegate+Menu.swift +++ /dev/null @@ -1,391 +0,0 @@ -import AppKit -import Foundation -import Preferences -import Status -import SuggestionBasic -import XcodeInspector -import Logger -import StatusBarItemView -import GitHubCopilotViewModel - -extension AppDelegate { - fileprivate var statusBarMenuIdentifier: NSUserInterfaceItemIdentifier { - .init("statusBarMenu") - } - - fileprivate var xcodeInspectorDebugMenuIdentifier: NSUserInterfaceItemIdentifier { - .init("xcodeInspectorDebugMenu") - } - - fileprivate var sourceEditorDebugMenu: NSUserInterfaceItemIdentifier { - .init("sourceEditorDebugMenu") - } - - @MainActor - @objc func buildStatusBarMenu() { - let statusBar = NSStatusBar.system - statusBarItem = statusBar.statusItem( - withLength: NSStatusItem.squareLength - ) - statusBarItem.button?.image = NSImage(named: "MenuBarIcon") - - let statusBarMenu = NSMenu(title: "Status Bar Menu") - statusBarMenu.identifier = statusBarMenuIdentifier - statusBarItem.menu = statusBarMenu - - let checkForUpdate = NSMenuItem( - title: "Check for Updates", - action: #selector(checkForUpdate), - keyEquivalent: "" - ) - - openCopilotForXcodeItem = NSMenuItem( - title: "Settings", - action: #selector(openCopilotForXcodeSettings), - keyEquivalent: "" - ) - - let xcodeInspectorDebug = NSMenuItem( - title: "Xcode Inspector Debug", - action: nil, - keyEquivalent: "" - ) - - let xcodeInspectorDebugMenu = NSMenu(title: "Xcode Inspector Debug") - xcodeInspectorDebugMenu.identifier = xcodeInspectorDebugMenuIdentifier - xcodeInspectorDebug.submenu = xcodeInspectorDebugMenu - xcodeInspectorDebug.isHidden = false - - axStatusItem = NSMenuItem( - title: "", - action: #selector(openAXStatusLink), - keyEquivalent: "" - ) - axStatusItem.isHidden = true - - extensionStatusItem = NSMenuItem( - title: "", - action: #selector(openExtensionStatusLink), - keyEquivalent: "" - ) - extensionStatusItem.isHidden = true - - let quitItem = NSMenuItem( - title: "Quit", - action: #selector(quit), - keyEquivalent: "" - ) - quitItem.target = self - - toggleCompletions = NSMenuItem( - title: "Enable/Disable Completions", - action: #selector(toggleCompletionsEnabled), - keyEquivalent: "" - ) - - toggleIgnoreLanguage = NSMenuItem( - title: "No Active Document", - action: nil, - keyEquivalent: "" - ) - - // Auth menu item with custom view - accountItem = NSMenuItem() - accountItem.view = AccountItemView( - target: self, - action: #selector(signIntoGitHub) - ) - - authStatusItem = NSMenuItem( - title: "", - action: nil, - keyEquivalent: "" - ) - authStatusItem.isHidden = true - - quotaItem = NSMenuItem() - quotaItem.view = QuotaView( - chat: .init( - percentRemaining: 0, - unlimited: false, - overagePermitted: false - ), - completions: .init( - percentRemaining: 0, - unlimited: false, - overagePermitted: false - ), - premiumInteractions: .init( - percentRemaining: 0, - unlimited: false, - overagePermitted: false - ), - resetDate: "", - copilotPlan: "" - ) - quotaItem.isHidden = true - - let openDocs = NSMenuItem( - title: "View Documentation", - action: #selector(openCopilotDocs), - keyEquivalent: "" - ) - - let openForum = NSMenuItem( - title: "Feedback Forum", - action: #selector(openCopilotForum), - keyEquivalent: "" - ) - - openChat = NSMenuItem( - title: "Open Chat", - action: #selector(openGlobalChat), - keyEquivalent: "" - ) - - signOutItem = NSMenuItem( - title: "Sign Out", - action: #selector(signOutGitHub), - keyEquivalent: "" - ) - - statusBarMenu.addItem(accountItem) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(authStatusItem) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(quotaItem) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(axStatusItem) - statusBarMenu.addItem(extensionStatusItem) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(checkForUpdate) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(openChat) - statusBarMenu.addItem(toggleCompletions) - statusBarMenu.addItem(toggleIgnoreLanguage) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(openCopilotForXcodeItem) - statusBarMenu.addItem(openDocs) - statusBarMenu.addItem(openForum) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(signOutItem) - statusBarMenu.addItem(.separator()) - statusBarMenu.addItem(xcodeInspectorDebug) - statusBarMenu.addItem(quitItem) - - statusBarMenu.delegate = self - xcodeInspectorDebugMenu.delegate = self - } -} - -extension AppDelegate: NSMenuDelegate { - func menuWillOpen(_ menu: NSMenu) { - switch menu.identifier { - case statusBarMenuIdentifier: - if let xcodeInspectorDebug = menu.items.first(where: { item in - item.submenu?.identifier == xcodeInspectorDebugMenuIdentifier - }) { - xcodeInspectorDebug.isHidden = !UserDefaults.shared - .value(for: \.enableXcodeInspectorDebugMenu) - } - - if toggleCompletions != nil { - toggleCompletions.title = "\(UserDefaults.shared.value(for: \.realtimeSuggestionToggle) ? "Disable" : "Enable") Completions" - } - - if toggleIgnoreLanguage != nil { - if let lang = DisabledLanguageList.shared.activeDocumentLanguage { - toggleIgnoreLanguage.title = "\(DisabledLanguageList.shared.isEnabled(lang) ? "Disable" : "Enable") Completions for \(lang.rawValue)" - toggleIgnoreLanguage.action = #selector( - toggleIgnoreLanguageEnabled - ) - } else { - toggleIgnoreLanguage.title = "No Active Document" - toggleIgnoreLanguage.action = nil - } - } - - Task { - await forceAuthStatusCheck() - updateStatusBarItem() - } - - case xcodeInspectorDebugMenuIdentifier: - let inspector = XcodeInspector.shared - menu.items.removeAll() - menu.items - .append(.text("Active Project: \(inspector.activeProjectRootURL?.path ?? "N/A")")) - menu.items - .append(.text("Active Workspace: \(inspector.activeWorkspaceURL?.path ?? "N/A")")) - menu.items - .append(.text("Active Document: \(inspector.activeDocumentURL?.path ?? "N/A")")) - - if let focusedWindow = inspector.focusedWindow { - menu.items.append(.text( - "Active Window: \(focusedWindow.uiElement.identifier)" - )) - } else { - menu.items.append(.text("Active Window: N/A")) - } - - if let focusedElement = inspector.focusedElement { - menu.items.append(.text( - "Focused Element: \(focusedElement.description)" - )) - } else { - menu.items.append(.text("Focused Element: N/A")) - } - - if let sourceEditor = inspector.focusedEditor { - let label = sourceEditor.element.description - menu.items - .append(.text("Active Source Editor: \(label.isEmpty ? "Unknown" : label)")) - } else { - menu.items.append(.text("Active Source Editor: N/A")) - } - - menu.items.append(.separator()) - - for xcode in inspector.xcodes { - let item = NSMenuItem( - title: "Xcode \(xcode.processIdentifier)", - action: nil, - keyEquivalent: "" - ) - menu.addItem(item) - let xcodeMenu = NSMenu() - item.submenu = xcodeMenu - xcodeMenu.items.append(.text("Is Active: \(xcode.isActive)")) - xcodeMenu.items - .append(.text("Active Project: \(xcode.projectRootURL?.path ?? "N/A")")) - xcodeMenu.items - .append(.text("Active Workspace: \(xcode.workspaceURL?.path ?? "N/A")")) - xcodeMenu.items - .append(.text("Active Document: \(xcode.documentURL?.path ?? "N/A")")) - - for (key, workspace) in xcode.realtimeWorkspaces { - let workspaceItem = NSMenuItem( - title: "Workspace \(key)", - action: nil, - keyEquivalent: "" - ) - xcodeMenu.items.append(workspaceItem) - let workspaceMenu = NSMenu() - workspaceItem.submenu = workspaceMenu - let tabsItem = NSMenuItem( - title: "Tabs", - action: nil, - keyEquivalent: "" - ) - workspaceMenu.addItem(tabsItem) - let tabsMenu = NSMenu() - tabsItem.submenu = tabsMenu - for tab in workspace.tabs { - tabsMenu.addItem(.text(tab)) - } - } - } - - menu.items.append(.separator()) - - menu.items.append(NSMenuItem( - title: "Restart Xcode Inspector", - action: #selector(restartXcodeInspector), - keyEquivalent: "" - )) - - default: - break - } - } -} - -import XPCShared - -private extension AppDelegate { - @objc func restartXcodeInspector() { - Task { - await XcodeInspector.shared.restart(cleanUp: true) - } - } - - @objc func toggleCompletionsEnabled() { - Task { - let initialSetting = UserDefaults.shared.value(for: \.realtimeSuggestionToggle) - do { - let service = getXPCExtensionService() - try await service.toggleRealtimeSuggestion() - } catch { - Logger.service.error("Failed to toggle completions enabled via XPC: \(error)") - UserDefaults.shared.set(!initialSetting, for: \.realtimeSuggestionToggle) - } - } - } - - @objc func toggleIgnoreLanguageEnabled() { - guard let lang = DisabledLanguageList.shared.activeDocumentLanguage else { return } - - if DisabledLanguageList.shared.isEnabled(lang) { - DisabledLanguageList.shared.disable(lang) - } else { - DisabledLanguageList.shared.enable(lang) - } - } - - @objc func openCopilotDocs() { - if let urlString = Bundle.main.object(forInfoDictionaryKey: "COPILOT_DOCS_URL") as? String { - if let url = URL(string: urlString) { - NSWorkspace.shared.open(url) - } - } - } - - @objc func openCopilotForum() { - if let urlString = Bundle.main.object(forInfoDictionaryKey: "COPILOT_FORUM_URL") as? String { - if let url = URL(string: urlString) { - NSWorkspace.shared.open(url) - } - } - } - - @objc func openAXStatusLink() { - Task { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { - NSWorkspace.shared.open(url) - } - } - } - - @objc func openExtensionStatusLink() { - Task { - let status = await Status.shared.getExtensionStatus() - if status == .notGranted { - if let url = URL(string: "x-apple.systempreferences:com.apple.ExtensionsPreferences?extensionPointIdentifier=com.apple.dt.Xcode.extension.source-editor") { - NSWorkspace.shared.open(url) - } - } else { - NSWorkspace.restartXcode() - } - } - } - - @objc func openUpSellLink() { - Task { - if let url = URL(string: "https://aka.ms/github-copilot-settings") { - NSWorkspace.shared.open(url) - } - } - } -} - -private extension NSMenuItem { - static func text(_ text: String) -> NSMenuItem { - let item = NSMenuItem( - title: text, - action: nil, - keyEquivalent: "" - ) - item.isEnabled = false - return item - } -} diff --git a/ExtensionService/AppDelegate.swift b/ExtensionService/AppDelegate.swift deleted file mode 100644 index 7f89e6cf..00000000 --- a/ExtensionService/AppDelegate.swift +++ /dev/null @@ -1,541 +0,0 @@ -import Combine -import FileChangeChecker -import GitHubCopilotService -import LaunchAgentManager -import Logger -import Preferences -import Service -import ServiceManagement -import Status -import SwiftUI -import UpdateChecker -import UserDefaultsObserver -import UserNotifications -import XcodeInspector -import XPCShared -import GitHubCopilotViewModel -import StatusBarItemView -import HostAppActivator - -let bundleIdentifierBase = Bundle.main - .object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String -let serviceIdentifier = bundleIdentifierBase + ".ExtensionService" - -class ExtensionUpdateCheckerDelegate: UpdateCheckerDelegate { - func prepareForRelaunch(finish: @escaping () -> Void) { - Task { - await Service.shared.prepareForExit() - finish() - } - } -} - -@main -class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { - let service = Service.shared - var statusBarItem: NSStatusItem! - var axStatusItem: NSMenuItem! - var extensionStatusItem: NSMenuItem! - var openCopilotForXcodeItem: NSMenuItem! - var accountItem: NSMenuItem! - var authStatusItem: NSMenuItem! - var quotaItem: NSMenuItem! - var toggleCompletions: NSMenuItem! - var toggleIgnoreLanguage: NSMenuItem! - var openChat: NSMenuItem! - var signOutItem: NSMenuItem! - var xpcController: XPCController? - let updateChecker = - UpdateChecker( - hostBundle: Bundle(url: HostAppURL!), - checkerDelegate: ExtensionUpdateCheckerDelegate() - ) - var xpcExtensionService: XPCExtensionService? - private var cancellables = Set() - private var progressView: NSProgressIndicator? - - func applicationDidFinishLaunching(_: Notification) { - if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return } - _ = XcodeInspector.shared - service.start() - AXIsProcessTrustedWithOptions([ - kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, - ] as CFDictionary) - setupQuitOnUpdate() - setupQuitOnUserTerminated() - xpcController = .init() - Logger.service.info("XPC Service started.") - NSApp.setActivationPolicy(.accessory) - buildStatusBarMenu() - watchServiceStatus() - watchAXStatus() - watchAuthStatus() - setInitialStatusBarStatus() - UserDefaults.shared.set(false, for: \.clsWarningDismissedUntilRelaunch) - } - - @objc func quit() { - if let hostApp = getRunningHostApp() { - hostApp.terminate() - } - - // Start shutdown process in a task - Task { @MainActor in - await service.prepareForExit() - await xpcController?.quit() - NSApp.terminate(self) - } - } - - @objc func openCopilotForXcodeSettings() { - try? launchHostAppSettings() - } - - @objc func signIntoGitHub() { - Task { @MainActor in - let viewModel = GitHubCopilotViewModel.shared - // Don't trigger the shared viewModel's alert - do { - guard let signInResponse = try await viewModel.preSignIn() else { - return - } - - NSApp.activate(ignoringOtherApps: true) - let alert = NSAlert() - alert.messageText = signInResponse.userCode - alert.informativeText = """ - Please enter the above code in the GitHub website to authorize your \ - GitHub account with Copilot for Xcode. - \(signInResponse.verificationURL.absoluteString) - """ - alert.addButton(withTitle: "Copy Code and Open") - alert.addButton(withTitle: "Cancel") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - viewModel.signInResponse = signInResponse - viewModel.copyAndOpen() - } - } catch { - Logger.service.error("GitHub copilot view model Sign in fails: \(error)") - } - } - } - - @objc func signOutGitHub() { - Task { @MainActor in - let viewModel = GitHubCopilotViewModel.shared - viewModel.signOut() - } - } - - @objc func openGlobalChat() { - Task { @MainActor in - let serviceGUI = Service.shared.guiController - serviceGUI.openGlobalChat() - } - } - - func setupQuitOnUpdate() { - Task { - guard let url = Bundle.main.executableURL else { return } - let checker = await FileChangeChecker(fileURL: url) - - // If Xcode or Copilot for Xcode is made active, check if the executable of this program - // is changed. If changed, quit this program. - - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didActivateApplicationNotification) - for await notification in sequence { - try Task.checkCancellation() - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, - app.isUserOfService - else { continue } - guard await checker.checkIfChanged() else { - Logger.service.info("Extension Service is not updated, no need to quit.") - continue - } - Logger.service.info("Extension Service will quit.") - #if DEBUG - #else - quit() - #endif - } - } - } - - func setupQuitOnUserTerminated() { - Task { - // Whenever Xcode or the host application quits, check if any of the two is running. - // If none, quit the XPC service. - - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didTerminateApplicationNotification) - for await notification in sequence { - try Task.checkCancellation() - guard UserDefaults.shared.value(for: \.quitXPCServiceOnXcodeAndAppQuit) - else { continue } - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, - app.isUserOfService - else { continue } - - // Check if Xcode is running - let isXcodeRunning = NSWorkspace.shared.runningApplications.contains { - $0.bundleIdentifier == "com.apple.dt.Xcode" - } - - if !isXcodeRunning { - Logger.client.info("No Xcode instances running, preparing to quit") - quit() - } - } - } - } - - func requestAccessoryAPIPermission() { - AXIsProcessTrustedWithOptions([ - kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true, - ] as NSDictionary) - } - - @objc func checkForUpdate() { - guard let updateChecker = updateChecker else { - Logger.service.error("Unable to check for updates: updateChecker is nil.") - return - } - updateChecker.checkForUpdates() - } - - func getXPCExtensionService() -> XPCExtensionService { - if let service = xpcExtensionService { return service } - let service = XPCExtensionService(logger: .service) - xpcExtensionService = service - return service - } - - func watchServiceStatus() { - let notifications = NotificationCenter.default.notifications(named: .serviceStatusDidChange) - Task { [weak self] in - for await _ in notifications { - guard let self else { return } - self.updateStatusBarItem() - } - } - } - - func watchAXStatus() { - let osNotifications = DistributedNotificationCenter.default().notifications(named: NSNotification.Name("com.apple.accessibility.api")) - Task { [weak self] in - for await _ in osNotifications { - guard let self else { return } - self.updateStatusBarItem() - } - } - } - - func watchAuthStatus() { - let notifications = DistributedNotificationCenter.default().notifications(named: .authStatusDidChange) - Task { [weak self] in - for await _ in notifications { - guard self != nil else { return } - do { - let service = try await GitHubCopilotViewModel.shared.getGitHubCopilotAuthService() - let accountStatus = try await service.checkStatus() - if accountStatus == .notSignedIn { - try await GitHubCopilotService.signOutAll() - } - } catch { - Logger.service.error("Failed to watch auth status: \(error)") - } - } - } - } - - func setInitialStatusBarStatus() { - Task { - let authStatus = await Status.shared.getAuthStatus() - if authStatus.status == .unknown { - // temporarily kick off a language server instance to prime the initial auth status - await forceAuthStatusCheck() - } - updateStatusBarItem() - } - } - - func forceAuthStatusCheck() async { - do { - let service = try await GitHubCopilotViewModel.shared.getGitHubCopilotAuthService() - let accountStatus = try await service.checkStatus() - if accountStatus == .ok || accountStatus == .maybeOk { - let quota = try await service.checkQuota() - Logger.service.info("User quota checked successfully: \(quota)") - } - } catch { - Logger.service.error("Failed to read auth status: \(error)") - } - } - - private func configureNotLoggedIn() { - self.accountItem.view = AccountItemView( - target: self, - action: #selector(signIntoGitHub) - ) - self.authStatusItem.isHidden = true - self.quotaItem.isHidden = true - self.toggleCompletions.isHidden = true - self.toggleIgnoreLanguage.isHidden = true - self.signOutItem.isHidden = true - } - - private func configureLoggedIn(status: StatusResponse) { - self.accountItem.view = AccountItemView( - target: self, - action: nil, - userName: status.userName ?? "" - ) - if !status.clsMessage.isEmpty { - let CLSMessageSummary = getCLSMessageSummary(status.clsMessage) - // If the quota is nil, keep the original auth status item - // Else only log the CLS error other than quota limit reached error - if CLSMessageSummary.summary == CLSMessageType.other.summary || status.quotaInfo == nil { - self.authStatusItem.isHidden = false - self.authStatusItem.title = CLSMessageSummary.summary - - let submenu = NSMenu() - let attributedCLSErrorItem = NSMenuItem() - attributedCLSErrorItem.view = ErrorMessageView( - errorMessage: CLSMessageSummary.detail - ) - submenu.addItem(attributedCLSErrorItem) - submenu.addItem(.separator()) - submenu.addItem( - NSMenuItem( - title: "View Details on GitHub", - action: #selector(openGitHubDetailsLink), - keyEquivalent: "" - ) - ) - - self.authStatusItem.submenu = submenu - self.authStatusItem.isEnabled = true - } - } else { - self.authStatusItem.isHidden = true - } - - if let quotaInfo = status.quotaInfo, !quotaInfo.resetDate.isEmpty { - self.quotaItem.isHidden = false - self.quotaItem.view = QuotaView( - chat: .init( - percentRemaining: quotaInfo.chat.percentRemaining, - unlimited: quotaInfo.chat.unlimited, - overagePermitted: quotaInfo.chat.overagePermitted - ), - completions: .init( - percentRemaining: quotaInfo.completions.percentRemaining, - unlimited: quotaInfo.completions.unlimited, - overagePermitted: quotaInfo.completions.overagePermitted - ), - premiumInteractions: .init( - percentRemaining: quotaInfo.premiumInteractions.percentRemaining, - unlimited: quotaInfo.premiumInteractions.unlimited, - overagePermitted: quotaInfo.premiumInteractions.overagePermitted - ), - resetDate: quotaInfo.resetDate, - copilotPlan: quotaInfo.copilotPlan - ) - } else { - self.quotaItem.isHidden = true - } - - self.toggleCompletions.isHidden = false - self.toggleIgnoreLanguage.isHidden = false - self.signOutItem.isHidden = false - } - - private func configureNotAuthorized(status: StatusResponse) { - self.accountItem.view = AccountItemView( - target: self, - action: nil, - userName: status.userName ?? "" - ) - self.authStatusItem.isHidden = false - self.authStatusItem.title = "No Subscription" - - let submenu = NSMenu() - let attributedNotAuthorizedItem = NSMenuItem() - attributedNotAuthorizedItem.view = ErrorMessageView( - errorMessage: "GitHub Copilot features are disabled. Check your subscription to enable them." - ) - attributedNotAuthorizedItem.isEnabled = true - submenu.addItem(attributedNotAuthorizedItem) - - self.authStatusItem.submenu = submenu - self.authStatusItem.isEnabled = true - - self.quotaItem.isHidden = true - self.toggleCompletions.isHidden = true - self.toggleIgnoreLanguage.isHidden = true - self.signOutItem.isHidden = false - } - - private func configureUnknown() { - self.accountItem.view = AccountItemView( - target: self, - action: nil, - userName: "Unknown User" - ) - self.authStatusItem.isHidden = true - self.quotaItem.isHidden = true - self.toggleCompletions.isHidden = false - self.toggleIgnoreLanguage.isHidden = false - self.signOutItem.isHidden = false - } - - func updateStatusBarItem() { - Task { @MainActor in - let status = await Status.shared.getStatus() - /// Update status bar icon - self.statusBarItem.button?.image = status.icon.nsImage - - /// Update auth status related status bar items - switch status.authStatus { - case .notLoggedIn: configureNotLoggedIn() - case .loggedIn: configureLoggedIn(status: status) - case .notAuthorized: configureNotAuthorized(status: status) - case .unknown: configureUnknown() - } - - /// Update accessibility permission status bar item - let exclamationmarkImage = NSImage( - systemSymbolName: "exclamationmark.circle.fill", - accessibilityDescription: "Permission not granted" - ) - exclamationmarkImage?.isTemplate = false - exclamationmarkImage?.withSymbolConfiguration(.init(paletteColors: [.red])) - - if let message = status.message { - self.axStatusItem.title = message - if let image = exclamationmarkImage { - self.axStatusItem.image = image - } - self.axStatusItem.isHidden = false - self.axStatusItem.isEnabled = status.url != nil - } else { - self.axStatusItem.isHidden = true - } - - /// Update settings status bar item - if status.extensionStatus == .disabled || status.extensionStatus == .notGranted { - if let image = exclamationmarkImage{ - if #available(macOS 15.0, *){ - self.extensionStatusItem.image = image - self.extensionStatusItem.title = status.extensionStatus == .notGranted ? "Enable extension for full-featured completion" : "Quit and restart Xcode to enable extension" - self.extensionStatusItem.isHidden = false - self.extensionStatusItem.isEnabled = status.extensionStatus == .notGranted - } else { - self.extensionStatusItem.isHidden = true - self.openCopilotForXcodeItem.image = image - } - } - } else { - self.openCopilotForXcodeItem.image = nil - self.extensionStatusItem.isHidden = true - } - self.markAsProcessing(status.inProgress) - } - } - - func markAsProcessing(_ isProcessing: Bool) { - if !isProcessing { - // No longer in progress - progressView?.removeFromSuperview() - progressView = nil - return - } - if progressView != nil { - // Already in progress - return - } - let progress = NSProgressIndicator() - progress.style = .spinning - progress.sizeToFit() - progress.frame = statusBarItem.button?.bounds ?? .zero - progress.isIndeterminate = true - progress.startAnimation(nil) - statusBarItem.button?.addSubview(progress) - statusBarItem.button?.image = nil - progressView = progress - } - - @objc func openGitHubDetailsLink() { - Task { - if let url = URL(string: "https://github.com/copilot") { - NSWorkspace.shared.open(url) - } - } - } -} - -extension NSRunningApplication { - var isUserOfService: Bool { - [ - "com.apple.dt.Xcode", - bundleIdentifierBase, - ].contains(bundleIdentifier) - } -} - -enum CLSMessageType { - case chatLimitReached - case completionLimitReached - case other - - var summary: String { - switch self { - case .chatLimitReached: - return "Monthly Chat Limit Reached" - case .completionLimitReached: - return "Monthly Completion Limit Reached" - case .other: - return "CLS Error" - } - } -} - -struct CLSMessage { - let summary: String - let detail: String -} - -func extractDateFromCLSMessage(_ message: String) -> String? { - let pattern = #"until (\d{1,2}/\d{1,2}/\d{4}, \d{1,2}:\d{2}:\d{2} [AP]M)"# - if let range = message.range(of: pattern, options: .regularExpression) { - return String(message[range].dropFirst(6)) - } - return nil -} - -func getCLSMessageSummary(_ message: String) -> CLSMessage { - let messageType: CLSMessageType - - if message.contains("You've reached your monthly chat messages limit") || - message.contains("You've reached your monthly chat messages quota") { - messageType = .chatLimitReached - } else if message.contains("Completions limit reached") { - messageType = .completionLimitReached - } else { - messageType = .other - } - - let detail: String - if let date = extractDateFromCLSMessage(message) { - detail = "Visit GitHub to check your usage and upgrade to Copilot Pro or wait until \(date) for your limit to reset." - } else { - detail = message - } - - return CLSMessage(summary: messageType.summary, detail: detail) -} diff --git a/ExtensionService/Assets.xcassets/AccentColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/ExtensionService/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/Contents.json b/ExtensionService/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 0a30d46d..00000000 --- a/ExtensionService/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "filename" : "CopilotforXcode-Icon@16w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "CopilotforXcode-Icon@16w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "CopilotforXcode-Icon@32w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "CopilotforXcode-Icon@32w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "CopilotforXcode-Icon@128w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "CopilotforXcode-Icon@128w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "CopilotforXcode-Icon@256w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "CopilotforXcode-Icon@256w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "CopilotforXcode-Icon@512w_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "CopilotforXcode-Icon@512w_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png deleted file mode 100644 index 3ee52427..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_1x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png deleted file mode 100644 index 88b20d1d..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@128w_2x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png deleted file mode 100644 index 2bb554dc..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_1x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png deleted file mode 100644 index ce02bac7..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@16w_2x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png deleted file mode 100644 index 7674f663..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_1x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png deleted file mode 100644 index fc705969..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@256w_2x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png deleted file mode 100644 index ce02bac7..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_1x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png deleted file mode 100644 index 4d52c81b..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@32w_2x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png deleted file mode 100644 index fc705969..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_1x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png b/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png deleted file mode 100644 index 54da6e3f..00000000 Binary files a/ExtensionService/Assets.xcassets/AppIcon.appiconset/CopilotforXcode-Icon@512w_2x.png and /dev/null differ diff --git a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/Contents.json deleted file mode 100644 index c48d2889..00000000 --- a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "light1x.svg", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "dark1x.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/dark1x.svg b/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/dark1x.svg deleted file mode 100644 index b0e60fbf..00000000 --- a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/dark1x.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/light1x.svg b/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/light1x.svg deleted file mode 100644 index 1f52da33..00000000 --- a/ExtensionService/Assets.xcassets/CodeBlockInsertIcon.imageset/light1x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ExtensionService/Assets.xcassets/Contents.json b/ExtensionService/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/ExtensionService/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/CopilotLogo.imageset/Contents.json b/ExtensionService/Assets.xcassets/CopilotLogo.imageset/Contents.json deleted file mode 100644 index 2e35661e..00000000 --- a/ExtensionService/Assets.xcassets/CopilotLogo.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "copilot.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/CopilotLogo.imageset/copilot.svg b/ExtensionService/Assets.xcassets/CopilotLogo.imageset/copilot.svg deleted file mode 100644 index 8284dce7..00000000 --- a/ExtensionService/Assets.xcassets/CopilotLogo.imageset/copilot.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/DiffEditor.imageset/Contents.json b/ExtensionService/Assets.xcassets/DiffEditor.imageset/Contents.json deleted file mode 100644 index b0971b3c..00000000 --- a/ExtensionService/Assets.xcassets/DiffEditor.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Editor.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/DiffEditor.imageset/Editor.svg b/ExtensionService/Assets.xcassets/DiffEditor.imageset/Editor.svg deleted file mode 100644 index ad643fcf..00000000 --- a/ExtensionService/Assets.xcassets/DiffEditor.imageset/Editor.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/Discard.imageset/Contents.json b/ExtensionService/Assets.xcassets/Discard.imageset/Contents.json deleted file mode 100644 index 0a27c3ef..00000000 --- a/ExtensionService/Assets.xcassets/Discard.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "discard.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/Discard.imageset/discard.svg b/ExtensionService/Assets.xcassets/Discard.imageset/discard.svg deleted file mode 100644 index a22942fe..00000000 --- a/ExtensionService/Assets.xcassets/Discard.imageset/discard.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ExtensionService/Assets.xcassets/Eye.imageset/Contents.json b/ExtensionService/Assets.xcassets/Eye.imageset/Contents.json deleted file mode 100644 index 107bc195..00000000 --- a/ExtensionService/Assets.xcassets/Eye.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "eye.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/Eye.imageset/eye.svg b/ExtensionService/Assets.xcassets/Eye.imageset/eye.svg deleted file mode 100644 index 4b83cd92..00000000 --- a/ExtensionService/Assets.xcassets/Eye.imageset/eye.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/EyeClosed.imageset/Contents.json b/ExtensionService/Assets.xcassets/EyeClosed.imageset/Contents.json deleted file mode 100644 index e874ab47..00000000 --- a/ExtensionService/Assets.xcassets/EyeClosed.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "eye-closed.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/EyeClosed.imageset/eye-closed.svg b/ExtensionService/Assets.xcassets/EyeClosed.imageset/eye-closed.svg deleted file mode 100644 index 76407a31..00000000 --- a/ExtensionService/Assets.xcassets/EyeClosed.imageset/eye-closed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/Icons/Contents.json b/ExtensionService/Assets.xcassets/Icons/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/ExtensionService/Assets.xcassets/Icons/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/Contents.json b/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/Contents.json deleted file mode 100644 index d5d75895..00000000 --- a/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "chevron-down.svg", - "idiom" : "mac" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "localizable" : true, - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/chevron-down.svg b/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/chevron-down.svg deleted file mode 100644 index 1547b27d..00000000 --- a/ExtensionService/Assets.xcassets/Icons/chevron.down.imageset/chevron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/ItemSelectedColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/ItemSelectedColor.colorset/Contents.json deleted file mode 100644 index 955c4738..00000000 --- a/ExtensionService/Assets.xcassets/ItemSelectedColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "248", - "green" : "154", - "red" : "98" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "194", - "green" : "108", - "red" : "55" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Contents.json deleted file mode 100644 index 4ebbfc18..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "Status=error, Mode=dark.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Status=error, Mode=dark.svg b/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Status=error, Mode=dark.svg deleted file mode 100644 index d3263f54..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarErrorIcon.imageset/Status=error, Mode=dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Contents.json deleted file mode 100644 index 4ab2faba..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "Status=active, Mode=dark.svg", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Status=active, Mode=white.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=dark.svg b/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=dark.svg deleted file mode 100644 index 7e472bde..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=dark.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=white.svg b/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=white.svg deleted file mode 100644 index 22dd8c1a..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarIcon.imageset/Status=active, Mode=white.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Contents.json deleted file mode 100644 index 4829284b..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "Status=inactive, Mode=dark.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Status=inactive, Mode=dark.svg b/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Status=inactive, Mode=dark.svg deleted file mode 100644 index 58b44f03..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarInactiveIcon.imageset/Status=inactive, Mode=dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Contents.json deleted file mode 100644 index c9b66241..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "Status=warning, Mode=dark.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Status=warning, Mode=dark.svg b/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Status=warning, Mode=dark.svg deleted file mode 100644 index 6f037e5d..00000000 --- a/ExtensionService/Assets.xcassets/MenuBarWarningIcon.imageset/Status=warning, Mode=dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/Sparkle.imageset/Contents.json b/ExtensionService/Assets.xcassets/Sparkle.imageset/Contents.json deleted file mode 100644 index db53bbf8..00000000 --- a/ExtensionService/Assets.xcassets/Sparkle.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "filename" : "sparkle.svg", - "idiom" : "mac" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "sparkle_dark.svg", - "idiom" : "mac" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle.svg b/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle.svg deleted file mode 100644 index 442e6cc3..00000000 --- a/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle_dark.svg b/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle_dark.svg deleted file mode 100644 index 2102024b..00000000 --- a/ExtensionService/Assets.xcassets/Sparkle.imageset/sparkle_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/Terminal.imageset/Contents.json b/ExtensionService/Assets.xcassets/Terminal.imageset/Contents.json deleted file mode 100644 index 0f6b450f..00000000 --- a/ExtensionService/Assets.xcassets/Terminal.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "terminal.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "template" - } -} diff --git a/ExtensionService/Assets.xcassets/Terminal.imageset/terminal.svg b/ExtensionService/Assets.xcassets/Terminal.imageset/terminal.svg deleted file mode 100644 index d5c43adc..00000000 --- a/ExtensionService/Assets.xcassets/Terminal.imageset/terminal.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ExtensionService/Assets.xcassets/ToastActionButtonColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/ToastActionButtonColor.colorset/Contents.json deleted file mode 100644 index 41903f4d..00000000 --- a/ExtensionService/Assets.xcassets/ToastActionButtonColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.080", - "blue" : "0x00", - "green" : "0x00", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.800", - "blue" : "0x3C", - "green" : "0x3C", - "red" : "0x3C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/ToastBackgroundColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/ToastBackgroundColor.colorset/Contents.json deleted file mode 100644 index ee9f736a..00000000 --- a/ExtensionService/Assets.xcassets/ToastBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF7", - "green" : "0xF7", - "red" : "0xF7" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x23", - "green" : "0x23", - "red" : "0x23" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/ToastDismissButtonColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/ToastDismissButtonColor.colorset/Contents.json deleted file mode 100644 index ab8dfaf8..00000000 --- a/ExtensionService/Assets.xcassets/ToastDismissButtonColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.500", - "green" : "0.500", - "red" : "0.500" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.800", - "green" : "0.800", - "red" : "0.800" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/ToastStrokeColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/ToastStrokeColor.colorset/Contents.json deleted file mode 100644 index 2a52454e..00000000 --- a/ExtensionService/Assets.xcassets/ToastStrokeColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.550", - "blue" : "0xC0", - "green" : "0xC0", - "red" : "0xC0" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2B", - "green" : "0x2B", - "red" : "0x2B" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/WorkingSetHeaderKeepButtonColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/WorkingSetHeaderKeepButtonColor.colorset/Contents.json deleted file mode 100644 index bce38459..00000000 --- a/ExtensionService/Assets.xcassets/WorkingSetHeaderKeepButtonColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "1.000", - "blue" : "212", - "green" : "120", - "red" : "0" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "1.000", - "blue" : "212", - "green" : "120", - "red" : "0" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/WorkingSetHeaderUndoButtonColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/WorkingSetHeaderUndoButtonColor.colorset/Contents.json deleted file mode 100644 index 0bdd57d7..00000000 --- a/ExtensionService/Assets.xcassets/WorkingSetHeaderUndoButtonColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "1.000", - "blue" : "204", - "green" : "204", - "red" : "204" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "1.000", - "blue" : "49", - "green" : "49", - "red" : "49" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/WorkingSetItemColor.colorset/Contents.json b/ExtensionService/Assets.xcassets/WorkingSetItemColor.colorset/Contents.json deleted file mode 100644 index 4de580b8..00000000 --- a/ExtensionService/Assets.xcassets/WorkingSetItemColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "0.850", - "blue" : "0", - "green" : "0", - "red" : "0" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "extended-srgb", - "components" : { - "alpha" : "0.850", - "blue" : "255", - "green" : "255", - "red" : "255" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Contents.json b/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Contents.json deleted file mode 100644 index c4b93a1c..00000000 --- a/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Xcode_16x16.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Xcode_16x16.svg b/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Xcode_16x16.svg deleted file mode 100644 index 0e118ea5..00000000 --- a/ExtensionService/Assets.xcassets/XcodeIcon.imageset/Xcode_16x16.svg +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ExtensionService/Assets.xcassets/codeReview.imageset/Contents.json b/ExtensionService/Assets.xcassets/codeReview.imageset/Contents.json deleted file mode 100644 index ddb0a503..00000000 --- a/ExtensionService/Assets.xcassets/codeReview.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "codeReview.svg", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "codeReview 1.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview 1.svg b/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview 1.svg deleted file mode 100644 index 44ce60ee..00000000 --- a/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview 1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview.svg b/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview.svg deleted file mode 100644 index 6084e72c..00000000 --- a/ExtensionService/Assets.xcassets/codeReview.imageset/codeReview.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/ExtensionService/Assets.xcassets/editor.focusedStackFrameHighlightBackground.colorset/Contents.json b/ExtensionService/Assets.xcassets/editor.focusedStackFrameHighlightBackground.colorset/Contents.json deleted file mode 100644 index e475d8e3..00000000 --- a/ExtensionService/Assets.xcassets/editor.focusedStackFrameHighlightBackground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "202", - "green" : "223", - "red" : "203" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "57", - "green" : "77", - "red" : "57" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/editorOverviewRuler.inlineChatRemoved.colorset/Contents.json b/ExtensionService/Assets.xcassets/editorOverviewRuler.inlineChatRemoved.colorset/Contents.json deleted file mode 100644 index abd021c3..00000000 --- a/ExtensionService/Assets.xcassets/editorOverviewRuler.inlineChatRemoved.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "211", - "green" : "214", - "red" : "242" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "25", - "green" : "25", - "red" : "55" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/gitDecoration.addedResourceForeground.colorset/Contents.json b/ExtensionService/Assets.xcassets/gitDecoration.addedResourceForeground.colorset/Contents.json deleted file mode 100644 index a19edf2b..00000000 --- a/ExtensionService/Assets.xcassets/gitDecoration.addedResourceForeground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "52", - "green" : "138", - "red" : "56" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "52", - "green" : "138", - "red" : "56" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/Assets.xcassets/gitDecoration.deletedResourceForeground.colorset/Contents.json b/ExtensionService/Assets.xcassets/gitDecoration.deletedResourceForeground.colorset/Contents.json deleted file mode 100644 index f8b5d709..00000000 --- a/ExtensionService/Assets.xcassets/gitDecoration.deletedResourceForeground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "57", - "green" : "78", - "red" : "199" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "57", - "green" : "78", - "red" : "199" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ExtensionService/ExtensionService.entitlements b/ExtensionService/ExtensionService.entitlements deleted file mode 100644 index 3c568976..00000000 --- a/ExtensionService/ExtensionService.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.application-groups - - $(TeamIdentifierPrefix)group.$(BUNDLE_IDENTIFIER_BASE) - - com.apple.security.cs.disable-library-validation - - - diff --git a/ExtensionService/Info.plist b/ExtensionService/Info.plist deleted file mode 100644 index 19f114ff..00000000 --- a/ExtensionService/Info.plist +++ /dev/null @@ -1,33 +0,0 @@ - - - - - APPLICATION_SUPPORT_FOLDER - $(APPLICATION_SUPPORT_FOLDER) - APP_ID_PREFIX - $(AppIdentifierPrefix) - BUNDLE_IDENTIFIER_BASE - $(BUNDLE_IDENTIFIER_BASE) - EXTENSION_BUNDLE_NAME - $(EXTENSION_BUNDLE_NAME) - HOST_APP_NAME - $(HOST_APP_NAME) - LANGUAGE_SERVER_PATH - $(LANGUAGE_SERVER_PATH) - NODE_PATH - $(NODE_PATH) - TEAM_ID_PREFIX - $(TeamIdentifierPrefix) - XPCService - - ServiceType - Application - - COPILOT_DOCS_URL - $(COPILOT_DOCS_URL) - COPILOT_FORUM_URL - $(COPILOT_FORUM_URL) - STANDARD_TELEMETRY_CHANNEL_KEY - $(STANDARD_TELEMETRY_CHANNEL_KEY) - - diff --git a/ExtensionService/Main.storyboard b/ExtensionService/Main.storyboard deleted file mode 100644 index 5fc73e7e..00000000 --- a/ExtensionService/Main.storyboard +++ /dev/null @@ -1,684 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ExtensionService/ServiceDelegate.swift b/ExtensionService/ServiceDelegate.swift deleted file mode 100644 index 6280582f..00000000 --- a/ExtensionService/ServiceDelegate.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import Service -import XPCShared - -class ServiceDelegate: NSObject, NSXPCListenerDelegate { - func listener( - _: NSXPCListener, - shouldAcceptNewConnection newConnection: NSXPCConnection - ) -> Bool { - newConnection.exportedInterface = NSXPCInterface( - with: XPCServiceProtocol.self - ) - - let exportedObject = XPCService() - newConnection.exportedObject = exportedObject - newConnection.resume() - return true - } -} - diff --git a/ExtensionService/XPCController.swift b/ExtensionService/XPCController.swift deleted file mode 100644 index 02656f85..00000000 --- a/ExtensionService/XPCController.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import Logger -import XPCShared - -final class XPCController: XPCServiceDelegate { - let bridge: XPCCommunicationBridge - let xpcListener: NSXPCListener - let xpcServiceDelegate: ServiceDelegate - - var pingTask: Task? - - init() { - let bridge = XPCCommunicationBridge(logger: .client) - let listener = NSXPCListener.anonymous() - let delegate = ServiceDelegate() - listener.delegate = delegate - listener.resume() - xpcListener = listener - xpcServiceDelegate = delegate - self.bridge = bridge - - Task { - bridge.setDelegate(self) - createPingTask() - } - } - - func quit() async { - bridge.setDelegate(nil) - pingTask?.cancel() - try? await bridge.quit() - } - - deinit { - xpcListener.invalidate() - pingTask?.cancel() - } - - func createPingTask() { - pingTask?.cancel() - pingTask = Task { [weak self] in - var consecutiveFailures = 0 - var backoffDelay = 1_000_000_000 // Start with 1 second - - while !Task.isCancelled { - guard let self else { return } - do { - try await self.bridge.updateServiceEndpoint(self.xpcListener.endpoint) - // Reset on success - consecutiveFailures = 0 - backoffDelay = 1_000_000_000 - try await Task.sleep(nanoseconds: 60_000_000_000) // 60 seconds between successful pings - } catch { - consecutiveFailures += 1 - // Log only on 1st, 5th (31 sec), 10th failures, etc. to avoid flooding - let shouldLog = consecutiveFailures == 1 || consecutiveFailures % 5 == 0 - - #if DEBUG - // No log, but you should run CommunicationBridge, too. - #else - if consecutiveFailures == 5 { - if #available(macOS 13.0, *) { - showBackgroundPermissionAlert() - } - } - if shouldLog { - Logger.service.error("Failed to connect to bridge (\(consecutiveFailures) consecutive failures): \(error.localizedDescription)") - } - #endif - - // Exponential backoff with a cap - backoffDelay = min(backoffDelay * 2, 120_000_000_000) // Cap at 120 seconds - try await Task.sleep(nanoseconds: UInt64(backoffDelay)) - } - } - } - } - - func connectionDidInvalidate() async { - // ignore - } - - func connectionDidInterrupt() async { - createPingTask() // restart the ping task so that it can bring the bridge back immediately. - } -} - diff --git a/Helper/ReloadLaunchAgent.swift b/Helper/ReloadLaunchAgent.swift deleted file mode 100644 index 99c934b0..00000000 --- a/Helper/ReloadLaunchAgent.swift +++ /dev/null @@ -1,53 +0,0 @@ -import ArgumentParser -import Foundation - -struct ReloadLaunchAgent: ParsableCommand { - static var configuration = CommandConfiguration( - abstract: "Reload the launch agent" - ) - - @Option(name: .long, help: "The service identifier of the service.") - var serviceIdentifier: String - - var launchAgentDirURL: URL { - FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/LaunchAgents") - } - - var launchAgentPath: String { - launchAgentDirURL.appendingPathComponent("\(serviceIdentifier).plist").path - } - - func run() throws { - try? launchctl("unload", launchAgentPath) - try launchctl("load", launchAgentPath) - } -} - -private func launchctl(_ args: String...) throws { - return try process("/bin/launchctl", args) -} - -private func process(_ launchPath: String, _ args: [String]) throws { - let task = Process() - task.launchPath = launchPath - task.arguments = args - task.environment = [ - "PATH": "/usr/bin", - ] - let outpipe = Pipe() - task.standardOutput = outpipe - try task.run() - task.waitUntilExit() - - struct E: Error, LocalizedError { - var errorDescription: String? - } - - if task.terminationStatus == 0 { - return - } - throw E( - errorDescription: "Failed to restart. Please make sure the launch agent is already loaded." - ) -} diff --git a/Helper/main.swift b/Helper/main.swift deleted file mode 100644 index ef9f2625..00000000 --- a/Helper/main.swift +++ /dev/null @@ -1,14 +0,0 @@ -import ArgumentParser -import Foundation - -struct Helper: ParsableCommand { - static var configuration = CommandConfiguration( - commandName: "helper", - abstract: "Helper CLI for Copilot for Xcode", - subcommands: [ - ReloadLaunchAgent.self, - ] - ) -} - -Helper.main() diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 163ff113..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 GitHub - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/PackageAssets/DSStore.template b/PackageAssets/DSStore.template deleted file mode 100644 index 18678db8..00000000 Binary files a/PackageAssets/DSStore.template and /dev/null differ diff --git a/PackageAssets/background.png b/PackageAssets/background.png deleted file mode 100644 index 84feaf0f..00000000 Binary files a/PackageAssets/background.png and /dev/null differ diff --git a/README.md b/README.md deleted file mode 100644 index d9c550d1..00000000 --- a/README.md +++ /dev/null @@ -1,156 +0,0 @@ -# GitHub Copilot for Xcode - -[GitHub Copilot](https://github.com/features/copilot) is an AI pair programmer -tool that helps you write code faster and smarter. Copilot for Xcode is an Xcode extension that provides inline coding suggestions as you type and a chat assistant to answer your coding questions. - -## Chat - -GitHub Copilot Chat provides suggestions to your specific coding tasks via chat. -Chat of GitHub Copilot for Xcode - -## Agent Mode - -GitHub Copilot Agent Mode provides AI-powered assistance that can understand and modify your codebase directly. With Agent Mode, you can: -- Get intelligent code edits applied directly to your files -- Run terminal commands and view their output without leaving the interface -- Search through your codebase to find relevant files and code snippets -- Create new files and directories as needed for your project -- Get assistance with enhanced context awareness across multiple files and folders -- Run Model Context Protocol (MCP) tools you configured to extend the capabilities - -Agent Mode integrates with Xcode's environment, creating a seamless development experience where Copilot can help implement features, fix bugs, and refactor code with comprehensive understanding of your project. - -## Code Completion - -You can receive auto-complete type suggestions from GitHub Copilot either by starting to write the code you want to use, or by writing a natural language comment describing what you want the code to do. -Code Completion of GitHub Copilot for Xcode - -## Requirements - -- macOS 12+ -- Xcode 8+ -- A GitHub Copilot subscription. To learn more, visit [https://github.com/features/copilot](https://github.com/features/copilot). - -## Getting Started - -1. Install via [Homebrew](https://brew.sh/): - - ```sh - brew install --cask github-copilot-for-xcode - ``` - - Or download the `dmg` from - [the latest release](https://github.com/github/CopilotForXcode/releases/latest/download/GitHubCopilotForXcode.dmg). - Drag `GitHub Copilot for Xcode` into the `Applications` folder: - -

- Screenshot of opened dmg -

- - Updates can be downloaded and installed by the app. - -1. Open the `GitHub Copilot for Xcode` application (from the `Applications` folder). Accept the security warning. -

- Screenshot of MacOS download permission request -

- - -1. A background item will be added to enable the GitHub Copilot for Xcode extension app to connect to the host app. This permission is usually automatically added when first launching the app. -

- Screenshot of background item -

- -1. Three permissions are required for GitHub Copilot for Xcode to function properly: `Background`, `Accessibility`, and `Xcode Source Editor Extension`. For more details on why these permissions are required see [TROUBLESHOOTING.md](./TROUBLESHOOTING.md). - - The first time the application is run the `Accessibility` permission should be requested: - -

- Screenshot of accessibility permission request -

- - The `Xcode Source Editor Extension` permission needs to be enabled manually. Click - `Extension Permission` from the `GitHub Copilot for Xcode` application settings to open the - System Preferences to the `Extensions` panel. Select `Xcode Source Editor` - and enable `GitHub Copilot`: - -

- Screenshot of extension permission -

- -1. After granting the extension permission, open Xcode. Verify that the - `Github Copilot` menu is available and enabled under the Xcode `Editor` - menu. -
-

- Screenshot of Xcode Editor GitHub Copilot menu item -

- - Keyboard shortcuts can be set for all menu items in the `Key Bindings` - section of Xcode preferences. - -1. To sign into GitHub Copilot, click the `Sign in` button in the settings application. This will open a browser window and copy a code to the clipboard. Paste the code into the GitHub login page and authorize the application. -

- Screenshot of sign-in popup -

- -1. To install updates, click `Check for Updates` from the menu item or in the - settings application. - - After installing a new version, Xcode must be restarted to use the new - version correctly. - - New versions can also be installed from `dmg` files downloaded from the - releases page. When installing a new version via `dmg`, the application must - be run manually the first time to accept the downloaded from the internet - warning. - -1. To avoid confusion, we recommend disabling `Predictive code completion` under - `Xcode` > `Preferences` > `Text Editing` > `Editing`. - -1. Press `tab` to accept the first line of a suggestion, hold `option` to view - the full suggestion, and press `option` + `tab` to accept the full suggestion. - -## How to use Chat - - Open Copilot Chat in GitHub Copilot. - - Open via the Xcode menu `Xcode -> Editor -> GitHub Copilot -> Open Chat`. -

- Screenshot of Xcode Editor GitHub Copilot menu item -

- - - Open via GitHub Copilot app menu `Open Chat`. - -

- Screenshot of GitHub Copilot menu item -

- -## How to use Code Completion - - Press `tab` to accept the first line of a suggestion, hold `option` to view - the full suggestion, and press `option` + `tab` to accept the full suggestion. - -## License - -This project is licensed under the terms of the MIT open source license. Please -refer to [LICENSE.txt](./LICENSE.txt) for the full terms. - -## Privacy - -We follow responsible practices in accordance with our -[Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement). - -To get the latest security fixes, please use the latest version of the GitHub -Copilot for Xcode. - -## Support - -We’d love to get your help in making GitHub Copilot better! If you have -feedback or encounter any problems, please reach out on our [Feedback -forum](https://github.com/orgs/community/discussions/categories/copilot). - -## Acknowledgements - -Thank you to @intitni for creating the original project that this is based on. - -Attributions can be found under About when running the app or in -[Credits.rtf](./Copilot%20for%20Xcode/Credits.rtf). \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md deleted file mode 100644 index 00538da1..00000000 --- a/ReleaseNotes.md +++ /dev/null @@ -1,19 +0,0 @@ -### GitHub Copilot for Xcode 0.41.0 - -**🚀 Highlights** - -* Code review feature. -* Chat: Support for new model `GPT-5`. -* Agent mode: Added support for new tool to read web URL contents. -* Support disabling MCP when it's disabled by policy. -* Support for opening MCP logs directly from the MCP settings page. -* OAuth support for remote GitHub MCP server. - -**💪 Improvements** - -* Performance: Improved instant-apply speed for edit_file tool. - -**🛠️ Bug Fixes** - -* Chat Agent repeatedly reverts its own changes when editing the same file. -* Performance: Avoid chat panel being stuck when sending a large text for chat. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 4279c87f..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,31 +0,0 @@ -Thanks for helping make GitHub safe for everyone. - -# Security - -GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). - -Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. - -## Reporting Security Issues - -If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. - -**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** - -Instead, please send an email to opensource-security[@]github.com. - -Please include as much of the information listed below as you can to help us better understand and resolve the issue: - - * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -## Policy - -See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) \ No newline at end of file diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 33762051..00000000 --- a/SUPPORT.md +++ /dev/null @@ -1,20 +0,0 @@ -# Support - -## How to get help - -We’d love to get your help in making GitHub Copilot better! If you have -feedback or encounter any problems, please reach out on our [Feedback -forum](https://github.com/orgs/community/discussions/categories/copilot). - -GitHub Copilot for Xcode is under active development and maintained by GitHub -staff. We will do our best to respond to support, feature requests, and -community questions in a timely manner. - -## GitHub Support Policy - -GitHub Copilot for Xcode is considered a Beta Preview under the [GitHub Terms of -Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#j-beta-previews). - -Once GitHub Copilot for Xcode is generally available, it will be subject to the -[GitHub Additional Product -Terms](https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features). diff --git a/SandboxedClientTester/Assets.xcassets/AccentColor.colorset/Contents.json b/SandboxedClientTester/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/SandboxedClientTester/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SandboxedClientTester/Assets.xcassets/AppIcon.appiconset/Contents.json b/SandboxedClientTester/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 3f00db43..00000000 --- a/SandboxedClientTester/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "images" : [ - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SandboxedClientTester/Assets.xcassets/Contents.json b/SandboxedClientTester/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/SandboxedClientTester/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SandboxedClientTester/ContentView.swift b/SandboxedClientTester/ContentView.swift deleted file mode 100644 index b56ddd05..00000000 --- a/SandboxedClientTester/ContentView.swift +++ /dev/null @@ -1,29 +0,0 @@ -import SwiftUI -import Client - -struct ContentView: View { - @State var text: String = "Hello, world!" - var body: some View { - VStack { - Button(action: { - Task { - do { - let service = try getService() - let version = try await service.getXPCServiceVersion() - text = "Version: \(version.version) Build: \(version.build)" - } catch { - text = error.localizedDescription - } - } - }) { - Text("Test") - } - Text(text) - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/SandboxedClientTester/Info.plist b/SandboxedClientTester/Info.plist deleted file mode 100644 index cb7f95c4..00000000 --- a/SandboxedClientTester/Info.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - BUNDLE_IDENTIFIER_BASE - $(BUNDLE_IDENTIFIER_BASE) - - diff --git a/SandboxedClientTester/Preview Content/Preview Assets.xcassets/Contents.json b/SandboxedClientTester/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/SandboxedClientTester/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/SandboxedClientTester/SandboxedClientTester.entitlements b/SandboxedClientTester/SandboxedClientTester.entitlements deleted file mode 100644 index 9e6f3194..00000000 --- a/SandboxedClientTester/SandboxedClientTester.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - com.apple.security.temporary-exception.mach-lookup.global-name - - $(BUNDLE_IDENTIFIER_BASE).CommunicationBridge - - - diff --git a/SandboxedClientTester/SandboxedClientTesterApp.swift b/SandboxedClientTester/SandboxedClientTesterApp.swift deleted file mode 100644 index ef03ae51..00000000 --- a/SandboxedClientTester/SandboxedClientTesterApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct SandboxedClientTesterApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Script/MakeDSStore.py b/Script/MakeDSStore.py deleted file mode 100644 index a257185a..00000000 --- a/Script/MakeDSStore.py +++ /dev/null @@ -1,56 +0,0 @@ -# Run MakeDSStore.sh rather than use this script directly. -import struct -from ds_store import DSStore -from mac_alias import Alias - -# See https://github.com/gitpan/Mac-Finder-DSStore/blob/master/DSStoreFormat.pod - -with DSStore.open('/Volumes/GitHub Copilot for Xcode/DSStore.template', 'w+') as ds: - # finder window coordinates (top, left, bottom, right) - # icnv indicates icon view, followed by four unknown bytes - fwi0 = struct.pack('>H', 100) + \ - struct.pack('>H', 200) + \ - struct.pack('>H', 400) + \ - struct.pack('>H', 600) + \ - bytes('icnv', 'ascii') + bytearray([0] * 4) - ds['.']['fwi0'] = ('blob', fwi0) - - # location of the app icon - ds['GitHub Copilot for Xcode.app']['Iloc'] = (100, 150) - # location of the Applications folder - ds['Applications']['Iloc'] = (300, 150) - - # hidden files outside the window - ds['.DS_Store']['Iloc'] = (650, 175) - ds['.background']['Iloc'] = (700, 175) - - # a plist with settings for the icon view - icvp = { - 'viewOptionsVersion': 1, - 'gridOffsetX': 0, - 'gridOffsetY': 0, - 'gridSpacing': 100, - 'iconSize': 128, - 'textSize': 12, - 'showIconPreview': True, - 'showItemInfo': False, - 'labelOnBottom': True, - 'scrollPositionX': 0, - 'scrollPositionY': 0, - 'arrangeBy': 'none', - 'backgroundColorRed': 1.0, - 'backgroundColorGreen': 1.0, - 'backgroundColorBlue': 1.0, - 'backgroundType': 2, - 'backgroundImageAlias': Alias.for_file('/Volumes/GitHub Copilot for Xcode/.background/background.png').to_bytes(), - } - ds['.']['icvp'] = icvp - - # window sidebar width - ds['.']['fwsw'] = ('long', 0) - # window height - ds['.']['fwvh'] = ('shor', 300) - # unknown meaning - ds['.']['ICVO'] = ('bool', True) - # text size - ds['.']['icvt'] = ('shor', 12) diff --git a/Script/MakeDSStore.sh b/Script/MakeDSStore.sh deleted file mode 100755 index 7e42e44c..00000000 --- a/Script/MakeDSStore.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Ensure we're in the root of the repo -cd "$(dirname "$0")/.." - -# Must have python3 installed -if ! command -v python3 &> /dev/null -then - echo "python3 could not be found. Install phyton3 and try again." - exit 1 -fi - -# We need a volume with the background image in order to create the correct alias for it -mkdir -p build/image/.background -cp PackageAssets/background.png build/image/.background -hdiutil create -volname "GitHub Copilot for Xcode" -srcfolder build/image -format UDRW build/GitHubCopilotforXcode.dmg -hdiutil attach -readwrite build/GitHubCopilotforXcode.dmg - - -# Create a python virtual environment -mkdir -p build/venv -python3 -m venv build/venv - -# Install ds-store -./build/venv/bin/pip install ds-store mac-alias==2.2.0 ds-store==1.3.0 -./build/venv/bin/python Script/MakeDSStore.py - -# Run it -./build/venv/bin/python Script/MakeDSStore.py - -# Save the created .DS_Store file -cp '/Volumes/GitHub Copilot for Xcode/DSStore.template' PackageAssets/DSStore.template - -# Clean up -hdiutil detach '/Volumes/GitHub Copilot for Xcode' -rm -rf build/GitHubCopilotforXcode.dmg -rm -rf build/image -rm -rf build/venv diff --git a/Script/export-options-local.plist b/Script/export-options-local.plist deleted file mode 100644 index 9c4fb9f7..00000000 --- a/Script/export-options-local.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - method - debugging - signingStyle - automatic - - \ No newline at end of file diff --git a/Script/localbuild-app.sh b/Script/localbuild-app.sh deleted file mode 100644 index 177c20fe..00000000 --- a/Script/localbuild-app.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Determine paths relative to script location -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "${SCRIPT_DIR}/.." && pwd )" -PROJECT_NAME=$(basename "${PROJECT_ROOT}") - -# Define build directory -BUILD_DIR="${PROJECT_ROOT}/build" -mkdir -p "${BUILD_DIR}" - -# Set variables -APP_NAME="CopiloForXcode" -SCHEME_NAME="Copilot for Xcode" -CONFIGURATION="Release" -ARCHIVE_PATH="${BUILD_DIR}/Archives/${APP_NAME}.xcarchive" -XCWORKSPACE_PATH="${PROJECT_ROOT}/Copilot for Xcode.xcworkspace" -EXPORT_PATH="${BUILD_DIR}/Export" -EXPORT_OPTIONS_PLIST="${PROJECT_ROOT}/Script/export-options-local.plist" - -# Clean and build archive -xcodebuild \ - -scheme "${SCHEME_NAME}" \ - -quiet \ - -archivePath "${ARCHIVE_PATH}" \ - -configuration "${CONFIGURATION}" \ - -skipMacroValidation \ - -showBuildTimingSummary \ - -disableAutomaticPackageResolution \ - -workspace "${XCWORKSPACE_PATH}" -verbose -arch arm64 \ - archive \ - APP_VERSION='0.0.0' - -# Export archive to .app -xcodebuild -exportArchive \ - -archivePath "${ARCHIVE_PATH}" \ - -exportOptionsPlist "${EXPORT_OPTIONS_PLIST}" \ - -exportPath "${EXPORT_PATH}" - -echo "App packaged successfully at ${EXPORT_PATH}/${APP_NAME}.app" - -open "${EXPORT_PATH}" \ No newline at end of file diff --git a/Script/uninstall-app.sh b/Script/uninstall-app.sh deleted file mode 100755 index 3cf092d6..00000000 --- a/Script/uninstall-app.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Uninstall the application and remove the settings and permissions -# -# Usage: ./uninstall-app.sh - -# Remove the settings and permissions (should happen before removing the app) -tccutil reset All com.github.CopilotForXcode -tccutil reset All com.github.CopilotForXcode.ExtensionService - -# Remove dev versions as well -tccutil reset All dev.com.github.CopilotForXcode -tccutil reset All dev.com.github.CopilotForXcode.ExtensionService - -# Remove launch agent -launchctl remove com.github.CopilotForXcode.CommunicationBridge -launchctl remove dev.com.github.CopilotForXcode.CommunicationBridge - -# Remove app -rm -rf /Applications/Copilot\ for\ Xcode.app -rm -rf /Applications/GitHub\ Copilot\ for\ Xcode.app - -# Remove user preferences -rm -f ~/Library/Preferences/com.github.CopilotForXcode.plist -rm -f ~/Library/Preferences/com.github.CopilotForXcode.ExtensionService.plist -rm -f ~/Library/Preferences/dev.com.github.CopilotForXcode.plist -rm -f ~/Library/Preferences/dev.com.github.CopilotForXcode.ExtensionService.plist - -defaults delete com.github.CopilotForXcode -defaults delete dev.com.github.CopilotForXcode -defaults delete VEKTX9H2N7.group.com.github.CopilotForXcode.prefs -defaults delete VEKTX9H2N7.group.dev.com.github.CopilotForXcode.prefs - -echo 'Finished uninstalling Copilot for Xcode' - diff --git a/Server/package-lock.json b/Server/package-lock.json deleted file mode 100644 index bc7fe532..00000000 --- a/Server/package-lock.json +++ /dev/null @@ -1,2105 +0,0 @@ -{ - "name": "@github/copilot-xcode", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@github/copilot-xcode", - "version": "0.0.1", - "dependencies": { - "@github/copilot-language-server": "^1.355.0", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "monaco-editor": "0.52.2" - }, - "devDependencies": { - "@types/node": "^22.15.17", - "copy-webpack-plugin": "^13.0.0", - "css-loader": "^7.1.2", - "style-loader": "^4.0.0", - "terser-webpack-plugin": "^5.3.14", - "ts-loader": "^9.5.2", - "typescript": "^5.8.3", - "webpack": "^5.99.9", - "webpack-cli": "^6.0.1" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17.0" - } - }, - "node_modules/@github/copilot-language-server": { - "version": "1.355.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.355.0.tgz", - "integrity": "sha512-Utuljxab2sosUPIilHdLDwBkr+A1xKju+KHG+iLoxDJNA8FGWtoalZv9L3QhakmvC9meQtvMciAYcdeeKPbcaQ==", - "license": "https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features", - "dependencies": { - "vscode-languageserver-protocol": "^3.17.5" - }, - "bin": { - "copilot-language-server": "dist/language-server.js" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", - "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", - "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", - "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "webpack": "^5.82.0", - "webpack-cli": "6.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } - }, - "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001715", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", - "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-webpack-plugin": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", - "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.142", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.142.tgz", - "integrity": "sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/monaco-editor": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", - "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/style-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", - "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.27.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" - }, - "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.99.9", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", - "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", - "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.6.1", - "@webpack-cli/configtest": "^3.0.1", - "@webpack-cli/info": "^3.0.1", - "@webpack-cli/serve": "^3.0.1", - "colorette": "^2.0.14", - "commander": "^12.1.0", - "cross-spawn": "^7.0.3", - "envinfo": "^7.14.0", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^6.0.1" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.82.0" - }, - "peerDependenciesMeta": { - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/Server/package.json b/Server/package.json deleted file mode 100644 index d5ccd3f8..00000000 --- a/Server/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@github/copilot-xcode", - "version": "0.0.1", - "description": "Package for downloading @github/copilot-language-server", - "private": true, - "scripts": { - "build": "webpack" - }, - "dependencies": { - "@github/copilot-language-server": "^1.355.0", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "monaco-editor": "0.52.2" - }, - "devDependencies": { - "@types/node": "^22.15.17", - "copy-webpack-plugin": "^13.0.0", - "css-loader": "^7.1.2", - "style-loader": "^4.0.0", - "terser-webpack-plugin": "^5.3.14", - "ts-loader": "^9.5.2", - "typescript": "^5.8.3", - "webpack": "^5.99.9", - "webpack-cli": "^6.0.1" - } -} diff --git a/Server/src/diffView/css/style.css b/Server/src/diffView/css/style.css deleted file mode 100644 index 2e430145..00000000 --- a/Server/src/diffView/css/style.css +++ /dev/null @@ -1,192 +0,0 @@ -/* Diff Viewer Styles */ -:root { - /* Light theme variables */ - --bg-color: #ffffff; - --text-color: #333333; - --border-color: #dddddd; - --button-bg: #007acc; - --button-text: white; - --secondary-button-bg: #f0f0f0; - --secondary-button-text: #333333; - --secondary-button-border: #dddddd; - --secondary-button-hover: #e0e0e0; - --additions-foreground-color: #2EA043; - --deletions-foreground-color: #F85149; -} - -@media (prefers-color-scheme: dark) { - :root { - /* Dark theme variables */ - --bg-color: #1e1e1e; - --text-color: #cccccc; - --border-color: #444444; - --button-bg: #0e639c; - --button-text: white; - --secondary-button-bg: #6E6D70; - --secondary-button-text: #DFDEDF; - --secondary-button-border: #555555; - --secondary-button-hover: #505050; - --additions-foreground-color: #2EA043; - --deletions-foreground-color: #F85149; - } -} - -html, body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - overflow: hidden; - background-color: var(--bg-color); - color: var(--text-color); -} - -#container { - width: calc(100% - 40px); /* 20px padding on each side */ - height: calc(100vh - 84px); /* 40px header + 4px top padding + 40px bottom padding */ - border: 1px solid var(--border-color); - margin: 0 20px 40px 20px; - padding: 0; - margin-top: 44px; /* 40px header + 4px top padding */ - box-sizing: border-box; -} - -.loading { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-family: -apple-system, BlinkMacSystemFont, sans-serif; - color: var(--text-color); -} - -.header { - position: absolute; - top: 4px; - left: 10px; - right: 10px; - height: 40px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 10px; - background-color: var(--bg-color); - box-sizing: border-box; -} - -.action-button { - margin-left: 2px; - padding: 4px 14px; - background-color: var(--button-bg); - color: var(--button-text); - border: none; - border-radius: 4px; - cursor: pointer; - font-family: -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 14px; - font-weight: 500; -} - -.action-button:hover { - background-color: #0062a3; -} - -.action-button.secondary { - background-color: var(--secondary-button-bg); - color: var(--secondary-button-text); - border: 1px solid var(--secondary-button-border); -} - -.action-button.secondary:hover { - background-color: var(--secondary-button-hover); -} - -.hidden { - display: none; -} - -.header-left { - display: flex; - align-items: center; - overflow: hidden; - gap: 4px; -} - -/* file path */ -.file-path { - font-family: -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 14px; - font-weight: 600; - color: var(--text-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Diff stats */ -.diff-stats { - font-family: -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 12px; - font-weight: 500; - display: flex; - gap: 4px; -} - -.additions-count { - color: var(--additions-foreground-color); - font-weight: 600; -} - -.deletions-count { - color: var(--deletions-foreground-color); - font-weight: 600; -} - -/* Style for gutter indicators using data attributes */ -.monaco-editor .codicon.codicon-diff-insert:before { - content: "+" !important; - font-family: inherit !important; - font-size: inherit !important; - font-weight: bold; - color: var(--additions-foreground-color) !important; - padding: 0 2px; -} - -.monaco-editor .codicon.codicon-diff-remove:before { - content: "-" !important; - font-family: inherit !important; - font-size: inherit !important; - font-weight: bold; - color: var(--deletions-foreground-color) !important; - padding: 0 2px; -} - -/* Force show for Monaco Editor 0.52.2 */ -.monaco-editor .diff-side-insert .margin-view-zone .codicon, -.monaco-editor .diff-side-delete .margin-view-zone .codicon { - display: inline-block !important; - visibility: visible !important; - opacity: 1 !important; -} - -/* Hide the diff overview bar completely */ -.monaco-diff-editor .diffOverview { - display: none !important; -} - -/* Hide all lightbulb icons (Copy Changed Line buttons) */ -.monaco-editor .codicon-lightbulb, -.monaco-editor .codicon-lightbulb-autofix, -.monaco-editor .lightbulb-glyph { - display: none !important; - visibility: hidden !important; - pointer-events: none !important; -} - -/* Unfold icon */ -.monaco-editor .codicon.codicon-unfold:before { - content:"···" !important; - font-family: inherit !important; - font-size: inherit !important; - font-weight: bold; -} \ No newline at end of file diff --git a/Server/src/diffView/diffView.html b/Server/src/diffView/diffView.html deleted file mode 100644 index de32b013..00000000 --- a/Server/src/diffView/diffView.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Diff Viewer - - - -
Loading diff viewer...
- -
-
-
-
- +0 - -0 -
-
- -
- - -
-
- -
- - - - diff --git a/Server/src/diffView/index.ts b/Server/src/diffView/index.ts deleted file mode 100644 index 05eb6fdf..00000000 --- a/Server/src/diffView/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -// index.ts - Main entry point for the Monaco Editor diff view -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { initDiffEditor } from './js/monaco-diff-editor'; -import { setupUI } from './js/ui-controller'; -import DiffViewer from './js/api'; - -// Initialize everything when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - // Hide loading indicator as Monaco is directly imported - const loadingElement = document.getElementById('loading'); - if (loadingElement) { - loadingElement.style.display = 'none'; - } - - // Set up UI elements and event handlers - setupUI(); - - // Make sure the editor follows the system theme - DiffViewer.followSystemTheme(); - - // Handle window resize events - window.addEventListener('resize', () => { - DiffViewer.handleResize(); - }); -}); - -// Define DiffViewer on the window object -declare global { - interface Window { - DiffViewer: typeof DiffViewer; - } -} - -// Expose the MonacoDiffViewer API to the global scope -window.DiffViewer = DiffViewer; - -// Export the MonacoDiffViewer for webpack -export default DiffViewer; diff --git a/Server/src/diffView/js/api.ts b/Server/src/diffView/js/api.ts deleted file mode 100644 index 2774e0c9..00000000 --- a/Server/src/diffView/js/api.ts +++ /dev/null @@ -1,121 +0,0 @@ -// api.ts - Public API for external use -import { initDiffEditor, updateDiffContent, getEditor, setEditorTheme, updateDiffStats } from './monaco-diff-editor'; -import { updateFileMetadata } from './ui-controller'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; - -/** - * Interface for the DiffViewer API - */ -interface DiffViewerAPI { - init: ( - originalContent: string, - modifiedContent: string, - path: string | null, - status: string | null, - options?: monaco.editor.IDiffEditorConstructionOptions - ) => void; - update: ( - originalContent: string, - modifiedContent: string, - path: string | null, - status: string | null - ) => void; - handleResize: () => void; - setTheme: (theme: 'light' | 'dark') => void; - followSystemTheme: () => void; -} - -/** - * The public API that will be exposed to the global scope - */ -const DiffViewer: DiffViewerAPI = { - /** - * Initialize the diff editor with content - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - * @param {string} path - File path - * @param {string} status - File edit status - * @param {Object} options - Optional configuration for the diff editor - */ - init: function( - originalContent: string, - modifiedContent: string, - path: string | null, - status: string | null, - options?: monaco.editor.IDiffEditorConstructionOptions - ): void { - // Initialize editor - initDiffEditor(originalContent, modifiedContent, options || {}); - - // Update file metadata and UI - updateFileMetadata(path, status); - }, - - /** - * Update the diff editor with new content - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - * @param {string} path - File path - * @param {string} status - File edit status - */ - update: function( - originalContent: string, - modifiedContent: string, - path: string | null, - status: string | null - ): void { - // Update editor content - updateDiffContent(originalContent, modifiedContent); - - // Update file metadata and UI - updateFileMetadata(path, status); - - // Update diff stats - updateDiffStats(); - }, - - /** - * Handle resize events - */ - handleResize: function(): void { - const editor = getEditor(); - if (editor) { - const container = document.getElementById('container'); - if (container) { - const headerHeight = 40; - const topPadding = 4; - const bottomPadding = 40; - - const availableHeight = window.innerHeight - headerHeight - topPadding - bottomPadding; - container.style.height = `${availableHeight}px`; - } - - editor.layout(); - } - }, - - /** - * Set the theme for the editor - */ - setTheme: function(theme: 'light' | 'dark'): void { - setEditorTheme(theme); - }, - - /** - * Follow the system theme - */ - followSystemTheme: function(): void { - // Set initial theme based on system preference - const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - setEditorTheme(isDarkMode ? 'dark' : 'light'); - - // Add listener for theme changes - if (window.matchMedia) { - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { - setEditorTheme(event.matches ? 'dark' : 'light'); - }); - } - } -}; - -export default DiffViewer; diff --git a/Server/src/diffView/js/monaco-diff-editor.ts b/Server/src/diffView/js/monaco-diff-editor.ts deleted file mode 100644 index 0a87ac4c..00000000 --- a/Server/src/diffView/js/monaco-diff-editor.ts +++ /dev/null @@ -1,346 +0,0 @@ -// monaco-diff-editor.ts - Monaco Editor diff view core functionality -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; - -// Editor state -let diffEditor: monaco.editor.IStandaloneDiffEditor | null = null; -let originalModel: monaco.editor.ITextModel | null = null; -let modifiedModel: monaco.editor.ITextModel | null = null; -let resizeObserver: ResizeObserver | null = null; -const DEFAULT_EDITOR_OPTIONS: monaco.editor.IDiffEditorConstructionOptions = { - renderSideBySide: false, - readOnly: true, - // Enable automatic layout adjustments - automaticLayout: true, - glyphMargin: false, - // Collapse unchanged regions - folding: true, - hideUnchangedRegions: { - enabled: true, - revealLineCount: 20, - minimumLineCount: 2, - contextLineCount: 2 - - }, - // Disable overview ruler and related features - renderOverviewRuler: false, - overviewRulerBorder: false, - overviewRulerLanes: 0, - scrollBeyondLastLine: false, - scrollbar: { - vertical: 'auto', - horizontal: 'auto', - useShadows: false, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false, - }, - lineHeight: 24, -} - -/** - * Initialize the Monaco diff editor - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - * @param {Object} options - Optional configuration for the diff editor - * @returns {Object} The diff editor instance - */ -function initDiffEditor( - originalContent: string, - modifiedContent: string, - options: monaco.editor.IDiffEditorConstructionOptions = {} -): monaco.editor.IStandaloneDiffEditor | null { - try { - // Default options - const editorOptions: monaco.editor.IDiffEditorConstructionOptions = { - ...DEFAULT_EDITOR_OPTIONS, - lineNumbersMinChars: calculateLineNumbersMinChars(originalContent, modifiedContent), - ...options - }; - - // Create the diff editor if it doesn't exist yet - if (!diffEditor) { - const container = document.getElementById("container"); - if (!container) { - throw new Error("Container element not found"); - } - - // Set initial container size to viewport height - // const headerHeight = 40; - // container.style.height = `${window.innerHeight - headerHeight}px`; - // Set initial container size to viewport height with precise calculations - const visibleHeight = window.innerHeight; - const headerHeight = 40; - const topPadding = 4; - const bottomPadding = 40; - const availableHeight = visibleHeight - headerHeight - topPadding - bottomPadding; - container.style.height = `${Math.floor(availableHeight)}px`; - container.style.overflow = "hidden"; // Ensure container doesn't have scrollbars - - diffEditor = monaco.editor.createDiffEditor( - container, - editorOptions - ); - - // Add resize handling - setupResizeHandling(); - - // Initialize theme - initializeTheme(); - } else { - // Apply any new options - diffEditor.updateOptions(editorOptions); - } - - // Create and set models - updateModels(originalContent, modifiedContent); - - return diffEditor; - } catch (error) { - console.error("Error initializing diff editor:", error); - return null; - } -} - -/** - * Setup proper resize handling for the editor - */ -function setupResizeHandling(): void { - window.addEventListener('resize', () => { - if (diffEditor) { - diffEditor.layout(); - } - }); - - if (window.ResizeObserver && !resizeObserver) { - const container = document.getElementById('container'); - - if (container) { - resizeObserver = new ResizeObserver(() => { - if (diffEditor) { - diffEditor.layout() - } - }); - resizeObserver.observe(container); - } - } -} - -/** - * Create or update the models for the diff editor - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - */ -function updateModels(originalContent: string, modifiedContent: string): void { - try { - // Clean up existing models if they exist - if (originalModel) { - originalModel.dispose(); - } - if (modifiedModel) { - modifiedModel.dispose(); - } - - // Create new models with the content - originalModel = monaco.editor.createModel(originalContent || "", "plaintext"); - modifiedModel = monaco.editor.createModel(modifiedContent || "", "plaintext"); - - // Set the models to show the diff - if (diffEditor) { - diffEditor.setModel({ - original: originalModel, - modified: modifiedModel, - }); - - // Add timeout to give Monaco time to calculate diffs - setTimeout(() => { - updateDiffStats(); - adjustContainerHeight(); - }, 100); // 100ms delay allows diff calculation to complete - } - } catch (error) { - console.error("Error updating models:", error); - } -} - -/** - * Update the diff view with new content - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - */ -function updateDiffContent(originalContent: string, modifiedContent: string): void { - // If editor exists, update it - if (diffEditor && diffEditor.getModel()) { - const model = diffEditor.getModel(); - - // Update model values - if (model) { - model.original.setValue(originalContent || ""); - model.modified.setValue(modifiedContent || ""); - } - } else { - // Initialize if not already done - initDiffEditor(originalContent, modifiedContent); - } -} - -/** - * Get the current diff editor instance - * @returns {Object|null} The diff editor instance or null - */ -function getEditor(): monaco.editor.IStandaloneDiffEditor | null { - return diffEditor; -} - -/** - * Calculate the number of line differences - * @returns {Object} The number of additions and deletions - */ -function calculateLineDifferences(): { additions: number, deletions: number } { - if (!diffEditor || !diffEditor.getModel()) { - return { additions: 0, deletions: 0 }; - } - - let additions = 0; - let deletions = 0; - const lineChanges = diffEditor.getLineChanges(); - console.log(">>> Line Changes:", lineChanges); - if (lineChanges) { - for (const change of lineChanges) { - console.log(change); - if (change.originalEndLineNumber >= change.originalStartLineNumber) { - deletions += change.originalEndLineNumber - change.originalStartLineNumber + 1; - } - if (change.modifiedEndLineNumber >= change.modifiedStartLineNumber) { - additions += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1; - } - } - } - - return { additions, deletions }; -} - -/** - * Update the diff statistics displayed in the UI - */ -function updateDiffStats(): void { - const { additions, deletions } = calculateLineDifferences(); - - const additionsElement = document.getElementById('additions-count'); - const deletionsElement = document.getElementById('deletions-count'); - - if (additionsElement) { - additionsElement.textContent = `+${additions}`; - } - - if (deletionsElement) { - deletionsElement.textContent = `-${deletions}`; - } -} - -/** - * Dynamically adjust container height based on content - */ -function adjustContainerHeight(): void { - const container = document.getElementById('container'); - if (!container || !diffEditor) return; - - // Always use the full viewport height - const visibleHeight = window.innerHeight; - const headerHeight = 40; // Height of the header - const topPadding = 4; // Top padding - const bottomPadding = 40; // Bottom padding - const availableHeight = visibleHeight - headerHeight - topPadding - bottomPadding; - - container.style.height = `${Math.floor(availableHeight)}px`; - - diffEditor.layout(); -} - -/** - * Set the editor theme - * @param {string} theme - The theme to set ('light' or 'dark') - */ -function setEditorTheme(theme: 'light' | 'dark'): void { - if (!diffEditor) return; - - monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs'); -} - -/** - * Detect the system theme preference - * @returns {string} The detected theme ('light' or 'dark') - */ -function detectSystemTheme(): 'light' | 'dark' { - return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -} - -/** - * Initialize the theme based on system preference - * and set up a listener for changes - */ -function initializeTheme(): void { - const theme = detectSystemTheme(); - setEditorTheme(theme); - - // Listen for changes in system theme preference - if (window.matchMedia) { - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { - setEditorTheme(event.matches ? 'dark' : 'light'); - }); - } -} - -/** - * Calculate the optimal number of characters for line numbers - * @param {string} originalContent - Content for the original side - * @param {string} modifiedContent - Content for the modified side - * @returns {number} The minimum number of characters needed for line numbers - */ -function calculateLineNumbersMinChars(originalContent: string, modifiedContent: string): number { - // Count the number of lines in both contents - const originalLineCount = originalContent ? originalContent.split('\n').length : 0; - const modifiedLineCount = modifiedContent ? modifiedContent.split('\n').length : 0; - - // Get the maximum line count - const maxLineCount = Math.max(originalLineCount, modifiedLineCount); - - // Calculate the number of digits in the max line count - // Use Math.log10 and Math.ceil to get the number of digits - // Add 1 to ensure some extra padding - const digits = maxLineCount > 0 ? Math.floor(Math.log10(maxLineCount) + 1) + 1 : 2; - - // Return a minimum of 2 characters, maximum of 5 - return Math.min(Math.max(digits, 2), 5); -} - -/** - * Dispose of the editor and models to clean up resources - */ -function dispose(): void { - if (resizeObserver) { - resizeObserver.disconnect(); - resizeObserver = null; - } - - if (originalModel) { - originalModel.dispose(); - originalModel = null; - } - if (modifiedModel) { - modifiedModel.dispose(); - modifiedModel = null; - } - if (diffEditor) { - diffEditor.dispose(); - diffEditor = null; - } -} - -export { - initDiffEditor, - updateDiffContent, - getEditor, - dispose, - setEditorTheme, - updateDiffStats -}; diff --git a/Server/src/diffView/js/ui-controller.ts b/Server/src/diffView/js/ui-controller.ts deleted file mode 100644 index 6e8579ea..00000000 --- a/Server/src/diffView/js/ui-controller.ts +++ /dev/null @@ -1,162 +0,0 @@ -// ui-controller.ts - UI event handlers and state management -import { DiffViewMessageHandler } from '../../shared/webkit'; -/** - * UI state and file metadata - */ -let filePath: string | null = null; -let fileEditStatus: string | null = null; - -/** - * Interface for messages sent to Swift handlers - */ -interface SwiftMessage { - event: string; - data: { - filePath: string | null; - [key: string]: any; - }; -} - -/** - * Initialize and set up UI elements and their event handlers - * @param {string} initialPath - The initial file path - * @param {string} initialStatus - The initial file edit status - */ -function setupUI(initialPath: string | null = null, initialStatus: string | null = null): void { - filePath = initialPath; - fileEditStatus = initialStatus; - - if (filePath) { - showFilePath(filePath); - } - - const keepButton = document.getElementById('keep-button'); - const undoButton = document.getElementById('undo-button'); - const choiceButtons = document.getElementById('choice-buttons'); - - if (!keepButton || !undoButton || !choiceButtons) { - console.error("Could not find UI elements"); - return; - } - - // Set initial UI state - updateUIStatus(initialStatus); - - // Setup event listeners - keepButton.addEventListener('click', handleKeepButtonClick); - undoButton.addEventListener('click', handleUndoButtonClick); -} - -/** - * Update the UI based on file edit status - * @param {string} status - The current file edit status - */ -function updateUIStatus(status: string | null): void { - fileEditStatus = status; - const choiceButtons = document.getElementById('choice-buttons'); - - if (!choiceButtons) return; - - // Hide buttons if file has been modified - if (status && status !== "none") { - choiceButtons.classList.add('hidden'); - } else { - choiceButtons.classList.remove('hidden'); - } -} - -/** - * Update the file metadata - * @param {string} path - The file path - * @param {string} status - The file edit status - */ -function updateFileMetadata(path: string | null, status: string | null): void { - filePath = path; - updateUIStatus(status); - if (filePath) { - showFilePath(filePath) - } -} - -/** - * Handle the "Keep" button click - */ -function handleKeepButtonClick(): void { - // Send message to Swift handler - if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.swiftHandler) { - const message: SwiftMessage = { - event: 'keepButtonClicked', - data: { - filePath: filePath - } - }; - window.webkit.messageHandlers.swiftHandler.postMessage(message); - } else { - console.log('Keep button clicked, but no message handler found'); - } - - // Hide the choice buttons - const choiceButtons = document.getElementById('choice-buttons'); - if (choiceButtons) { - choiceButtons.classList.add('hidden'); - } -} - -/** - * Handle the "Undo" button click - */ -function handleUndoButtonClick(): void { - // Send message to Swift handler - if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.swiftHandler) { - const message: SwiftMessage = { - event: 'undoButtonClicked', - data: { - filePath: filePath - } - }; - window.webkit.messageHandlers.swiftHandler.postMessage(message); - } else { - console.log('Undo button clicked, but no message handler found'); - } - - // Hide the choice buttons - const choiceButtons = document.getElementById('choice-buttons'); - if (choiceButtons) { - choiceButtons.classList.add('hidden'); - } -} - -/** - * Get the current file path - * @returns {string} The current file path - */ -function getFilePath(): string | null { - return filePath; -} - -/** - * Show the current file path - */ -function showFilePath(path: string): void { - const filePathElement = document.getElementById('file-path'); - const fileName = path.split('/').pop() ?? ''; - if (filePathElement) { - filePathElement.textContent = fileName - } -} - -/** - * Get the current file edit status - * @returns {string} The current file edit status - */ -function getFileEditStatus(): string | null { - return fileEditStatus; -} - -export { - setupUI, - updateUIStatus, - updateFileMetadata, - getFilePath, - getFileEditStatus -}; \ No newline at end of file diff --git a/Server/src/shared/webkit.ts b/Server/src/shared/webkit.ts deleted file mode 100644 index 3b6948fe..00000000 --- a/Server/src/shared/webkit.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Type definitions for WebKit message handlers used in WebView communication - */ - -/** - * Base WebKit message handler interface - */ -export interface WebkitMessageHandler { - postMessage(message: any): void; -} - -/** - * Terminal-specific message handler - */ -export interface TerminalMessageHandler extends WebkitMessageHandler { - postMessage(message: string): void; -} - -/** - * DiffView-specific message handler - */ -export interface DiffViewMessageHandler extends WebkitMessageHandler { - postMessage(message: object): void; -} - -/** - * WebKit message handlers container interface - */ -export interface WebkitMessageHandlers { - terminalInput: TerminalMessageHandler; - swiftHandler: DiffViewMessageHandler; - [key: string]: WebkitMessageHandler | undefined; -} - -/** - * Main WebKit interface exposed by WebViews - */ -export interface WebkitHandler { - messageHandlers: WebkitMessageHandlers; -} - -/** - * Add webkit to the global Window interface - */ -declare global { - interface Window { - webkit: WebkitHandler; - } -} \ No newline at end of file diff --git a/Server/src/terminal/index.ts b/Server/src/terminal/index.ts deleted file mode 100644 index e97ee33c..00000000 --- a/Server/src/terminal/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import '@xterm/xterm/css/xterm.css'; -import { Terminal } from '@xterm/xterm'; -import { TerminalAddon } from './terminalAddon'; - -declare global { - interface Window { - initializeTerminal: () => Terminal; - writeToTerminal: (text: string) => void; - clearTerminal: () => void; - } -} - -window.initializeTerminal = function (): Terminal { - const term = new Terminal({ - cursorBlink: true, - theme: { - background: '#1e1e1e', - foreground: '#cccccc', - cursor: '#ffffff', - selectionBackground: 'rgba(128, 128, 128, 0.4)' - }, - fontFamily: 'Menlo, Monaco, "Courier New", monospace', - fontSize: 13 - }); - - const terminalAddon = new TerminalAddon(); - term.loadAddon(terminalAddon); - - const terminalElement = document.getElementById('terminal'); - if (!terminalElement) { - throw new Error('Terminal element not found'); - } - term.open(terminalElement); - terminalAddon.fit(); - - // Handle window resize - window.addEventListener('resize', () => { - terminalAddon.fit(); - }); - - // Expose terminal API methods - window.writeToTerminal = function (text: string): void { - term.write(text); - terminalAddon.processTerminalOutput(text); - }; - - window.clearTerminal = function (): void { - term.clear(); - }; - - return term; -} diff --git a/Server/src/terminal/terminal.html b/Server/src/terminal/terminal.html deleted file mode 100644 index a35ac6fb..00000000 --- a/Server/src/terminal/terminal.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - -
- - - - diff --git a/Server/src/terminal/terminalAddon.ts b/Server/src/terminal/terminalAddon.ts deleted file mode 100644 index bf78dfe5..00000000 --- a/Server/src/terminal/terminalAddon.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { FitAddon } from '@xterm/addon-fit'; -import { Terminal, ITerminalAddon } from '@xterm/xterm'; -import { TerminalMessageHandler } from '../shared/webkit'; - -interface TermSize { - cols: number; - rows: number; -} - -interface TerminalPosition { - row: number; - col: number; -} - -// https://xtermjs.org/docs/api/vtfeatures/ -// https://en.wikipedia.org/wiki/ANSI_escape_code -const VT = { - ESC: '\x1b', - CSI: '\x1b[', - UP_ARROW: '\x1b[A', - DOWN_ARROW: '\x1b[B', - RIGHT_ARROW: '\x1b[C', - LEFT_ARROW: '\x1b[D', - HOME_KEY: ['\x1b[H', '\x1bOH'], - END_KEY: ['\x1b[F', '\x1bOF'], - DELETE_REST_OF_LINE: '\x1b[K', - CursorUp: (n = 1) => `\x1b[${n}A`, - CursorDown: (n = 1) => `\x1b[${n}B`, - CursorForward: (n = 1) => `\x1b[${n}C`, - CursorBack: (n = 1) => `\x1b[${n}D` -}; - -/** - * Key code constants - */ -const KeyCodes = { - CONTROL_C: 3, - CONTROL_D: 4, - ENTER: 13, - BACKSPACE: 8, - DELETE: 127 -}; - -export class TerminalAddon implements ITerminalAddon { - private term: Terminal | null; - private fitAddon: FitAddon; - private inputBuffer: string; - private cursor: number; - private promptInLastLine: string; - private termSize: TermSize; - - constructor() { - this.term = null; - this.fitAddon = new FitAddon(); - this.inputBuffer = ''; - this.cursor = 0; - this.promptInLastLine = ''; - this.termSize = { - cols: 0, - rows: 0, - }; - } - - dispose(): void { - this.fitAddon.dispose(); - } - - activate(terminal: Terminal): void { - this.term = terminal; - this.termSize = { - cols: terminal.cols, - rows: terminal.rows, - }; - this.fitAddon.activate(terminal); - this.term.onData(this.handleData.bind(this)); - this.term.onResize(this.handleResize.bind(this)); - } - - fit(): void { - this.fitAddon.fit(); - } - - private handleData(data: string): void { - // If the input is a longer string (e.g., from paste), and it contains newlines - if (data.length > 1 && !data.startsWith(VT.ESC)) { - const lines = data.split(/(\r\n|\n|\r)/g); - - let lineIndex = 0; - const processLine = () => { - if (lineIndex >= lines.length) return; - - const line = lines[lineIndex]; - if (line === '\n' || line === '\r' || line === '\r\n') { - if (this.cursor > 0) { - this.clearInputLine(); - this.cursor = 0; - this.renderInputLine(this.inputBuffer); - } - window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + '\n'); - this.inputBuffer = ''; - this.cursor = 0; - lineIndex++; - setTimeout(processLine, 100); - return; - } - - this.handleSingleLine(line); - lineIndex++; - processLine(); - }; - - processLine(); - return; - } - - // Handle escape sequences for special keys - if (data.startsWith(VT.ESC)) { - this.handleEscSequences(data); - return; - } - - this.handleSingleLine(data); - } - - private handleSingleLine(data: string): void { - if (data.length === 0) return; - - const char = data.charCodeAt(0); - // Handle control characters - if (char < 32 || char === 127) { - // Handle Enter key (carriage return) - if (char === KeyCodes.ENTER) { - if (this.cursor > 0) { - this.clearInputLine(); - this.cursor = 0; - this.renderInputLine(this.inputBuffer); - } - window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + '\n'); - this.inputBuffer = ''; - this.cursor = 0; - } - else if (char === KeyCodes.CONTROL_C || char === KeyCodes.CONTROL_D) { - if (this.cursor > 0) { - this.clearInputLine(); - this.cursor = 0; - this.renderInputLine(this.inputBuffer); - } - window.webkit.messageHandlers.terminalInput.postMessage(this.inputBuffer + data); - this.inputBuffer = ''; - this.cursor = 0; - } - // Handle backspace or delete - else if (char === KeyCodes.BACKSPACE || char === KeyCodes.DELETE) { - if (this.cursor > 0) { - this.clearInputLine(); - - // Delete character at cursor position - 1 - const beforeCursor = this.inputBuffer.substring(0, this.cursor - 1); - const afterCursor = this.inputBuffer.substring(this.cursor); - const newInput = beforeCursor + afterCursor; - this.cursor--; - this.renderInputLine(newInput); - } - } - return; - } - - this.clearInputLine(); - - // Insert character at cursor position - const beforeCursor = this.inputBuffer.substring(0, this.cursor); - const afterCursor = this.inputBuffer.substring(this.cursor); - const newInput = beforeCursor + data + afterCursor; - this.cursor += data.length; - this.renderInputLine(newInput); - } - - private handleResize(data: { cols: number; rows: number }): void { - this.clearInputLine(); - this.termSize = { - cols: data.cols, - rows: data.rows, - }; - this.renderInputLine(this.inputBuffer); - } - - private clearInputLine(): void { - if (!this.term) return; - // Move to beginning of the current line - this.term.write('\r'); - const cursorPosition = this.calcCursorPosition(); - const inputEndPosition = this.calcLineWrapPosition(this.promptInLastLine.length + this.inputBuffer.length); - // If cursor is not at the end of input, move to the end - if (cursorPosition.row < inputEndPosition.row) { - this.term.write(VT.CursorDown(inputEndPosition.row - cursorPosition.row)); - } else if (cursorPosition.row > inputEndPosition.row) { - this.term.write(VT.CursorUp(cursorPosition.row - inputEndPosition.row)); - } - - // Clear from the last line upwards - this.term.write('\r' + VT.DELETE_REST_OF_LINE); - for (let i = inputEndPosition.row - 1; i >= 0; i--) { - this.term.write(VT.CursorUp(1)); - this.term.write('\r' + VT.DELETE_REST_OF_LINE); - } - }; - - // Function to render the input line considering line wrapping - private renderInputLine(newInput: string): void { - if (!this.term) return; - this.inputBuffer = newInput; - // Write prompt and input - this.term.write(this.promptInLastLine + this.inputBuffer); - const cursorPosition = this.calcCursorPosition(); - const inputEndPosition = this.calcLineWrapPosition(this.promptInLastLine.length + this.inputBuffer.length); - // If the last input char is at the end of the terminal width, - // need to print an extra empty line to display the cursor. - if (inputEndPosition.col == 0) { - this.term.write(' '); - this.term.write(VT.CursorBack(1)); - this.term.write(VT.DELETE_REST_OF_LINE); - } - - if (this.inputBuffer.length === this.cursor) { - return; - } - - // Move the cursor from the input end to the expected cursor row - if (cursorPosition.row < inputEndPosition.row) { - this.term.write(VT.CursorUp(inputEndPosition.row - cursorPosition.row)); - } - this.term.write('\r'); - if (cursorPosition.col > 0) { - this.term.write(VT.CursorForward(cursorPosition.col)); - } - }; - - private calcCursorPosition(): TerminalPosition { - return this.calcLineWrapPosition(this.promptInLastLine.length + this.cursor); - } - - private calcLineWrapPosition(textLength: number): TerminalPosition { - if (!this.term) { - return { row: 0, col: 0 }; - } - const row = Math.floor(textLength / this.termSize.cols); - const col = textLength % this.termSize.cols; - - return { row, col }; - } - - /** - * Handle ESC sequences - */ - private handleEscSequences(data: string): void { - if (!this.term) return; - switch (data) { - case VT.UP_ARROW: - // TODO: Could implement command history here - break; - - case VT.DOWN_ARROW: - // TODO: Could implement command history here - break; - - case VT.RIGHT_ARROW: - if (this.cursor < this.inputBuffer.length) { - this.clearInputLine(); - this.cursor++; - this.renderInputLine(this.inputBuffer); - } - break; - - case VT.LEFT_ARROW: - if (this.cursor > 0) { - this.clearInputLine(); - this.cursor--; - this.renderInputLine(this.inputBuffer); - } - break; - } - - // Handle Home key variations - if (VT.HOME_KEY.includes(data)) { - this.clearInputLine(); - this.cursor = 0; - this.renderInputLine(this.inputBuffer); - } - - // Handle End key variations - if (VT.END_KEY.includes(data)) { - this.clearInputLine(); - this.cursor = this.inputBuffer.length; - this.renderInputLine(this.inputBuffer); - } - }; - - /** - * Remove OSC escape sequences from text - */ - private removeOscSequences(text: string): string { - // Remove basic OSC sequences - let filteredText = text.replace(/\u001b\]\d+;[^\u0007\u001b]*[\u0007\u001b\\]/g, ''); - - // More comprehensive approach for nested sequences - return filteredText.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, ''); - }; - - /** - * Process terminal output and update prompt tracking - */ - processTerminalOutput(text: string): void { - if (typeof text !== 'string') return; - - const lastNewline = text.lastIndexOf('\n'); - const lastCarriageReturn = text.lastIndexOf('\r'); - const lastControlChar = Math.max(lastNewline, lastCarriageReturn); - let newPromptText = lastControlChar !== -1 ? text.substring(lastControlChar + 1) : text; - - // Filter out OSC sequences - newPromptText = this.removeOscSequences(newPromptText); - - this.promptInLastLine = lastControlChar !== -1 ? - newPromptText : this.promptInLastLine + newPromptText; - }; -} diff --git a/Server/tsconfig.json b/Server/tsconfig.json deleted file mode 100644 index 71eb52f9..00000000 --- a/Server/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist", - "sourceMap": true, - "allowJs": true, - "checkJs": false - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file diff --git a/Server/webpack.config.js b/Server/webpack.config.js deleted file mode 100644 index 2ace244b..00000000 --- a/Server/webpack.config.js +++ /dev/null @@ -1,77 +0,0 @@ -const path = require('path'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const webpack = require('webpack'); -const TerserPlugin = require('terser-webpack-plugin'); - -/* - * The folder structure of `dist` would be: - * dist/ - * ├── terminal/ - * │ ├── terminal.js - * │ └── terminal.html - * └── diffView/ - * ├── diffView.js - * ├── diffView.html - * └── css/ - * └── style.css -*/ -module.exports = { - mode: 'production', - entry: { - // Add more entry points here - terminal: './src/terminal/index.ts', - diffView: './src/diffView/index.ts' - }, - resolve: { - extensions: ['.ts', '.js'] - }, - output: { - filename: '[name]/[name].js', - path: path.resolve(__dirname, 'dist'), - }, - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/ - }, - { - test: /\.css$/, - use: ['style-loader', 'css-loader'] - } - ] - }, - plugins: [ - new CopyWebpackPlugin({ - patterns: [ - /// MARK: - Terminal component files - { - from: 'src/terminal/terminal.html', - to: 'terminal/terminal.html' - }, - - /// MARK: - DiffView component files - { - from: 'src/diffView/diffView.html', - to: 'diffView/diffView.html' - }, - { - from: 'src/diffView/css', - to: 'diffView/css' - } - ] - }), - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - }) - ], - optimization: { - minimizer: [ - new TerserPlugin({ - // Prevent extracting license comments to a separate file - extractComments: false - }) - ] - } -}; diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index 4c179941..00000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,94 +0,0 @@ -# Troubleshooting for Copilot for Xcode - -If you are having trouble with Copilot for Xcode follow these steps to resolve -common issues: - -1. Check for updates and restart Xcode. Ensure that Copilot for Xcode has the - [latest release](https://github.com/github/CopilotForXcode/releases/latest) - by clicking `Check for Updates` in the settings or under the status menu. After - updating, restart Xcode. - -2. Ensure that all required permissions are granted. GitHub Copilot for Xcode app requires these permissions to function properly: - - [Extension Permission](#extension-permission) - Allows GitHub Copilot to integrate with Xcode - - [Accessibility Permission](#accessibility-permission) - Enables real-time code suggestions - - [Background Permission](#background-permission) - Allows extension to connect with host app - - Please note that GitHub Copilot for Xcode may not work properly if any necessary permissions are missing. - -3. Need more help? If these steps don't resolve the issue, please [open an - issue](https://github.com/github/CopilotForXcode/issues/new/choose). Make - sure to [include logs](#logs) and any other relevant information. - -## Extension Permission - -GitHub Copilot for Xcode is an Xcode Source Editor extension and requires the -extension to be enabled. In the Copilot for Xcode settings, clicking `Extension -Permission` will open the System Settings to the Extensions page where `GitHub -Copilot` can be enabled under `Xcode Source Editor`. - -Or you can navigate to the permission manually depending on your OS version: - -| macOS | Location | -| :--- | :--- | -| 15 | System Settings > General > Login Items > Extensions > Xcode Source Editor | -| 13 & 14 | System Settings > Privacy & Security > Extensions > Xcode Source Editor | -| 12 | System Preferences > Extensions | - -## Accessibility Permission - -GitHub Copilot for Xcode requires the accessibility permission to receive -real-time updates from the active Xcode editor. [The XcodeKit -API](https://developer.apple.com/documentation/xcodekit) -enabled by the Xcode Source Editor extension permission only provides -information when manually triggered by the user. In order to generate -suggestions as you type, the accessibility permission is used to read the -Xcode editor content in real-time. - -The accessibility permission is also used to accept suggestions when `tab` is -pressed. - -The accessibility permission is __not__ used to read or write to any -applications besides Xcode. There are no granular options for the permission, -but you can audit the usage in this repository: search for `CGEvent` and `AX`*. - -Enable in System Settings under `Privacy & Security` > `Accessibility` > -`GitHub Copilot for Xcode Extension` and turn on the toggle. - -## Background Permission - -GitHub Copilot for Xcode requires background permission to connect with the host app. This permission ensures proper communication between the components of GitHub Copilot for Xcode, which is essential for its functionality in Xcode. - - -

- Background Permission -

- -This permission is typically granted automatically when you first launch GitHub Copilot for Xcode. However, if you encounter connection issues, alerts, or errors as follows: - -

- Alert of Background Permission Required - Error connecting to the communication bridge -

- -Please ensure that this permission is enabled. You can manually navigate to the background permission setting based on your macOS version: - -| macOS | Location | -| :--- | :--- | -| 15 | System Settings > General > Login Items & Extensions > Allow in the Background | -| 13 & 14 | System Settings > General > Login Items > Allow in the Background | - -Ensure that "GitHub Copilot for Xcode" is enabled in the list of allowed background items. Without this permission, the extension may not be able to properly communicate with the host app, which can result in inconsistent behavior or reduced functionality. - - -## Logs - -Logs can be found in `~/Library/Logs/GitHubCopilot/` the most recent log file -is: - -``` -~/Library/Logs/GitHubCopilot/github-copilot-for-xcode.log -``` - -To enable verbose logging, open the GitHub Copilot for Xcode settings and enable -`Verbose Logging` in the `Advanced` tab. After enabling verbose logging, restart -Copilot for Xcode for the change to take effect. diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan deleted file mode 100644 index a46ddf32..00000000 --- a/TestPlan.xctestplan +++ /dev/null @@ -1,118 +0,0 @@ -{ - "configurations" : [ - { - "id" : "586480F5-DC84-425D-814F-7A5F569A1974", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "environmentVariableEntries" : [ - { - "key" : "IS_UNIT_TEST", - "value" : "YES" - }, - { - "key" : "SUEnableAutomaticChecks", - "value" : "NO" - } - ], - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "ServiceTests", - "name" : "ServiceTests" - } - }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "SuggestionInjectorTests", - "name" : "SuggestionInjectorTests" - } - }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "SuggestionWidgetTests", - "name" : "SuggestionWidgetTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "SuggestionBasicTests", - "name" : "SuggestionBasicTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "SharedUIComponentsTests", - "name" : "SharedUIComponentsTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "GitHubCopilotServiceTests", - "name" : "GitHubCopilotServiceTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "XcodeInspectorTests", - "name" : "XcodeInspectorTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "SuggestionProviderTests", - "name" : "SuggestionProviderTests" - } - }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "KeyBindingManagerTests", - "name" : "KeyBindingManagerTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "WorkspaceSuggestionServiceTests", - "name" : "WorkspaceSuggestionServiceTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "WorkspaceTests", - "name" : "WorkspaceTests" - } - }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "ChatServiceTests", - "name" : "ChatServiceTests" - } - }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "SystemUtilsTests", - "name" : "SystemUtilsTests" - } - } - ], - "version" : 1 -} diff --git a/Tool/Package.swift b/Tool/Package.swift deleted file mode 100644 index c0a2785f..00000000 --- a/Tool/Package.swift +++ /dev/null @@ -1,378 +0,0 @@ -// swift-tools-version: 5.7 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Tool", - platforms: [.macOS(.v12)], - products: [ - .library(name: "XPCShared", targets: ["XPCShared"]), - .library(name: "Terminal", targets: ["Terminal"]), - .library(name: "Preferences", targets: ["Preferences", "Configs"]), - .library(name: "Logger", targets: ["Logger"]), - .library(name: "SystemUtils", targets: ["SystemUtils"]), - .library(name: "ChatAPIService", targets: ["ChatAPIService"]), - .library(name: "ChatTab", targets: ["ChatTab"]), - .library(name: "FileSystem", targets: ["FileSystem"]), - .library(name: "SuggestionBasic", targets: ["SuggestionBasic"]), - .library(name: "Toast", targets: ["Toast"]), - .library(name: "SharedUIComponents", targets: ["SharedUIComponents"]), - .library(name: "Status", targets: ["Status"]), - .library(name: "Persist", targets: ["Persist"]), - .library(name: "UserDefaultsObserver", targets: ["UserDefaultsObserver"]), - .library(name: "Workspace", targets: ["Workspace", "WorkspaceSuggestionService"]), - .library(name: "WebContentExtractor", targets: ["WebContentExtractor"]), - .library( - name: "SuggestionProvider", - targets: ["SuggestionProvider"] - ), - .library( - name: "ConversationServiceProvider", - targets: ["ConversationServiceProvider"] - ), - .library( - name: "TelemetryServiceProvider", - targets: ["TelemetryServiceProvider"] - ), - .library( - name: "TelemetryService", - targets: ["TelemetryService"] - ), - .library( - name: "GitHubCopilotService", - targets: ["GitHubCopilotService"] - ), - .library( - name: "BuiltinExtension", - targets: ["BuiltinExtension"] - ), - .library( - name: "AppMonitoring", - targets: [ - "XcodeInspector", - "ActiveApplicationMonitor", - "AXExtension", - "AXNotificationStream", - "AppActivator", - ] - ), - .library(name: "DebounceFunction", targets: ["DebounceFunction"]), - .library(name: "AsyncPassthroughSubject", targets: ["AsyncPassthroughSubject"]), - .library(name: "CustomAsyncAlgorithms", targets: ["CustomAsyncAlgorithms"]), - .library(name: "AXHelper", targets: ["AXHelper"]), - .library(name: "Cache", targets: ["Cache"]), - .library(name: "StatusBarItemView", targets: ["StatusBarItemView"]), - .library(name: "HostAppActivator", targets: ["HostAppActivator"]), - .library(name: "AppKitExtension", targets: ["AppKitExtension"]), - .library(name: "GitHelper", targets: ["GitHelper"]) - ], - dependencies: [ - // TODO: Update LanguageClient some day. - .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.8.2"), - .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.13.3"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), - .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.9.0"), - .package(url: "https://github.com/devm33/Highlightr", branch: "master"), - .package( - url: "https://github.com/pointfreeco/swift-composable-architecture", - from: "1.10.4" - ), - .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), - // TODO: remove CopilotForXcodeKit dependency once extension provider logic is removed. - .package(url: "https://github.com/devm33/CopilotForXcodeKit", branch: "main"), - .package(url: "https://github.com/stephencelis/SQLite.swift", from: "0.15.3"), - .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.9.6") - ], - targets: [ - // MARK: - Helpers - - .target(name: "XPCShared", dependencies: ["SuggestionBasic", "Logger", "Status", "HostAppActivator", "GitHubCopilotService"]), - - .target(name: "Configs"), - - .target(name: "Preferences", dependencies: ["Configs"]), - - .target(name: "Terminal", dependencies: ["Logger", "SystemUtils"]), - - .target(name: "WebContentExtractor", dependencies: ["Logger", "SwiftSoup", "Preferences"]), - - .target(name: "Logger"), - - .target(name: "FileSystem"), - - .target( - name: "CustomAsyncAlgorithms", - dependencies: [ - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - ] - ), - - .target( - name: "Toast", - dependencies: [ - "AppKitExtension", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture") - ] - ), - - .target(name: "DebounceFunction"), - - .target( - name: "AppActivator", - dependencies: [ - "XcodeInspector", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), - - .target(name: "ActiveApplicationMonitor"), - - .target( - name: "HostAppActivator", - dependencies: [ - "Logger", - ] - ), - - .target( - name: "SuggestionBasic", - dependencies: [ - "LanguageClient", - .product(name: "Parsing", package: "swift-parsing"), - .product(name: "CodableWrappers", package: "CodableWrappers"), - ] - ), - - .testTarget( - name: "SuggestionBasicTests", - dependencies: ["SuggestionBasic"] - ), - - .target(name: "AXExtension"), - - .target( - name: "AXNotificationStream", - dependencies: [ - "Preferences", - "Logger", - "Status", - ] - ), - - .target( - name: "XcodeInspector", - dependencies: [ - "AXExtension", - "SuggestionBasic", - "AXNotificationStream", - "Logger", - "Toast", - "Preferences", - "AsyncPassthroughSubject", - "Status", - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - ] - ), - - .testTarget(name: "XcodeInspectorTests", dependencies: ["XcodeInspector"]), - - .target(name: "UserDefaultsObserver"), - - .target(name: "AsyncPassthroughSubject"), - - .target( - name: "BuiltinExtension", - dependencies: [ - "SuggestionBasic", - "SuggestionProvider", - "Workspace", - .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), - ] - ), - - .target( - name: "SharedUIComponents", - dependencies: [ - "Highlightr", - "Preferences", - "SuggestionBasic", - "DebounceFunction", - "ConversationServiceProvider", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), - .testTarget(name: "SharedUIComponentsTests", dependencies: ["SharedUIComponents"]), - - .target( - name: "Workspace", - dependencies: [ - "UserDefaultsObserver", - "SuggestionBasic", - "Logger", - "Preferences", - "XcodeInspector", - "ConversationServiceProvider" - ] - ), - .testTarget(name: "WorkspaceTests", dependencies: ["Workspace"]), - - .target( - name: "WorkspaceSuggestionService", - dependencies: [ - "Workspace", - "SuggestionProvider", - "XPCShared", - "BuiltinExtension", - "GitHubCopilotService", - ] - ), - - .target( - name: "AXHelper", - dependencies: [ - "XPCShared", - "XcodeInspector" - ] - ), - - .target(name: "StatusBarItemView", dependencies: ["Cache"]), - - .target( - name: "Cache" - ), - - .testTarget( - name: "WorkspaceSuggestionServiceTests", - dependencies: [ - "ConversationServiceProvider", - "WorkspaceSuggestionService" - ] - ), - - // MARK: - Services - - .target( - name: "Status", - dependencies: ["Cache"] - ), - - .target( - name: "Persist", - dependencies: [ - "Logger", - "Status", - .product(name: "SQLite", package: "SQLite.Swift") - ] - ), - - .target(name: "SuggestionProvider", dependencies: [ - "SuggestionBasic", - "UserDefaultsObserver", - "Preferences", - "Logger", - .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), - ]), - .testTarget(name: "SuggestionProviderTests", dependencies: ["SuggestionProvider"]), - - .target(name: "ConversationServiceProvider", dependencies: [ - "GitHelper", - .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), - ]), - - .target(name: "TelemetryServiceProvider", dependencies: [ - .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), - ]), - - .target( - name: "TelemetryService", - dependencies: [ - "TelemetryServiceProvider", - "GitHubCopilotService", - "BuiltinExtension", - "SystemUtils", - "UserDefaultsObserver", - "Preferences" - ]), - - - // MARK: - GitHub Copilot - - .target( - name: "GitHubCopilotService", - dependencies: [ - "LanguageClient", - "SuggestionBasic", - "Logger", - "Preferences", - "Terminal", - "BuiltinExtension", - "ConversationServiceProvider", - "TelemetryServiceProvider", - "Status", - "SystemUtils", - "Workspace", - "Persist", - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), - .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), - ] - ), - .testTarget( - name: "GitHubCopilotServiceTests", - dependencies: ["GitHubCopilotService", - "ConversationServiceProvider"] - ), - - // MARK: - ChatAPI - - .target( - name: "ChatAPIService", - dependencies: [ - "Logger", - "Preferences", - "GitHubCopilotService", - .product(name: "JSONRPC", package: "JSONRPC"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - .product( - name: "ComposableArchitecture", - package: "swift-composable-architecture" - ), - ] - ), - - // MARK: - UI - - .target( - name: "ChatTab", - dependencies: [.product( - name: "ComposableArchitecture", - package: "swift-composable-architecture" - )] - ), - - // MARK: - SystemUtils - - .target( - name: "SystemUtils", - dependencies: ["Logger"] - ), - .testTarget(name: "SystemUtilsTests", dependencies: ["SystemUtils"]), - - // MARK: - AppKitExtension - - .target(name: "AppKitExtension"), - - // MARK: - GitHelper - .target( - name: "GitHelper", - dependencies: [ - "Terminal", - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol") - ] - ), - .testTarget(name: "GitHelperTests", dependencies: ["GitHelper"]) - ] -) - diff --git a/Tool/Sources/AXExtension/AXUIElement.swift b/Tool/Sources/AXExtension/AXUIElement.swift deleted file mode 100644 index b7366398..00000000 --- a/Tool/Sources/AXExtension/AXUIElement.swift +++ /dev/null @@ -1,392 +0,0 @@ -import AppKit -import Foundation - -// MARK: - State - -public extension AXUIElement { - /// Set global timeout in seconds. - static func setGlobalMessagingTimeout(_ timeout: Float) { - AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), timeout) - } - - /// Set timeout in seconds for this element. - func setMessagingTimeout(_ timeout: Float) { - AXUIElementSetMessagingTimeout(self, timeout) - } - - var identifier: String { - (try? copyValue(key: kAXIdentifierAttribute)) ?? "" - } - - var value: String { - (try? copyValue(key: kAXValueAttribute)) ?? "" - } - - var intValue: Int? { - (try? copyValue(key: kAXValueAttribute)) - } - - var title: String { - (try? copyValue(key: kAXTitleAttribute)) ?? "" - } - - var role: String { - (try? copyValue(key: kAXRoleAttribute)) ?? "" - } - - var doubleValue: Double { - (try? copyValue(key: kAXValueAttribute)) ?? 0.0 - } - - var document: String? { - try? copyValue(key: kAXDocumentAttribute) - } - - /// Label in Accessibility Inspector. - var description: String { - (try? copyValue(key: kAXDescriptionAttribute)) ?? "" - } - - /// Type in Accessibility Inspector. - var roleDescription: String { - (try? copyValue(key: kAXRoleDescriptionAttribute)) ?? "" - } - - var label: String { - (try? copyValue(key: kAXLabelValueAttribute)) ?? "" - } - - var isSourceEditor: Bool { - description == "Source Editor" - } - - var isEditorArea: Bool { - description == "editor area" - } - - var isXcodeWorkspaceWindow: Bool { - description == "Xcode.WorkspaceWindow" || identifier == "Xcode.WorkspaceWindow" - } - - var selectedTextRange: ClosedRange? { - guard let value: AXValue = try? copyValue(key: kAXSelectedTextRangeAttribute) - else { return nil } - var range: CFRange = .init(location: 0, length: 0) - if AXValueGetValue(value, .cfRange, &range) { - return range.location...(range.location + range.length) - } - return nil - } - - var isFocused: Bool { - (try? copyValue(key: kAXFocusedAttribute)) ?? false - } - - var isEnabled: Bool { - (try? copyValue(key: kAXEnabledAttribute)) ?? false - } - - var isHidden: Bool { - (try? copyValue(key: kAXHiddenAttribute)) ?? false - } -} - -// MARK: - Rect - -public extension AXUIElement { - var position: CGPoint? { - guard let value: AXValue = try? copyValue(key: kAXPositionAttribute) - else { return nil } - var point: CGPoint = .zero - if AXValueGetValue(value, .cgPoint, &point) { - return point - } - return nil - } - - var size: CGSize? { - guard let value: AXValue = try? copyValue(key: kAXSizeAttribute) - else { return nil } - var size: CGSize = .zero - if AXValueGetValue(value, .cgSize, &size) { - return size - } - return nil - } - - var rect: CGRect? { - guard let position, let size else { return nil } - return .init(origin: position, size: size) - } -} - -// MARK: - Relationship - -public extension AXUIElement { - var focusedElement: AXUIElement? { - try? copyValue(key: kAXFocusedUIElementAttribute) - } - - var sharedFocusElements: [AXUIElement] { - (try? copyValue(key: kAXChildrenAttribute)) ?? [] - } - - var window: AXUIElement? { - try? copyValue(key: kAXWindowAttribute) - } - - var windows: [AXUIElement] { - (try? copyValue(key: kAXWindowsAttribute)) ?? [] - } - - var isFullScreen: Bool { - (try? copyValue(key: "AXFullScreen")) ?? false - } - - var focusedWindow: AXUIElement? { - try? copyValue(key: kAXFocusedWindowAttribute) - } - - var topLevelElement: AXUIElement? { - try? copyValue(key: kAXTopLevelUIElementAttribute) - } - - var rows: [AXUIElement] { - (try? copyValue(key: kAXRowsAttribute)) ?? [] - } - - var parent: AXUIElement? { - try? copyValue(key: kAXParentAttribute) - } - - var children: [AXUIElement] { - (try? copyValue(key: kAXChildrenAttribute)) ?? [] - } - - var menuBar: AXUIElement? { - try? copyValue(key: kAXMenuBarAttribute) - } - - var visibleChildren: [AXUIElement] { - (try? copyValue(key: kAXVisibleChildrenAttribute)) ?? [] - } - - func child( - identifier: String? = nil, - title: String? = nil, - role: String? = nil - ) -> AXUIElement? { - for child in children { - let match = { - if let identifier, child.identifier != identifier { return false } - if let title, child.title != title { return false } - if let role, child.role != role { return false } - return true - }() - if match { return child } - } - for child in children { - if let target = child.child( - identifier: identifier, - title: title, - role: role - ) { return target } - } - return nil - } - - /// Get children that match the requirement - /// - /// - important: If the element has a lot of descendant nodes, it will heavily affect the - /// **performance of Xcode**. Please make use ``AXUIElement\traverse(_:)`` instead. - @available( - *, - deprecated, - renamed: "traverse(_:)", - message: "Please make use ``AXUIElement\traverse(_:)`` instead." - ) - func children(where match: (AXUIElement) -> Bool) -> [AXUIElement] { - var all = [AXUIElement]() - for child in children { - if match(child) { all.append(child) } - } - for child in children { - all.append(contentsOf: child.children(where: match)) - } - return all - } - - func firstParent(where match: (AXUIElement) -> Bool) -> AXUIElement? { - guard let parent = parent else { return nil } - if match(parent) { return parent } - return parent.firstParent(where: match) - } - - func firstChild(where match: (AXUIElement) -> Bool) -> AXUIElement? { - for child in children { - if match(child) { return child } - } - for child in children { - if let target = child.firstChild(where: match) { - return target - } - } - return nil - } - - func visibleChild(identifier: String) -> AXUIElement? { - for child in visibleChildren { - if child.identifier == identifier { return child } - if let target = child.visibleChild(identifier: identifier) { return target } - } - return nil - } - - var verticalScrollBar: AXUIElement? { - try? copyValue(key: kAXVerticalScrollBarAttribute) - } - - func retrieveSourceEditor() -> AXUIElement? { - if self.isSourceEditor { return self } - - if self.isXcodeWorkspaceWindow { - return self.firstChild(where: \.isSourceEditor) - } - - guard let xcodeWorkspaceWindowElement = self.firstParent(where: \.isXcodeWorkspaceWindow) - else { return nil } - - return xcodeWorkspaceWindowElement.firstChild(where: \.isSourceEditor) - } -} - -public extension AXUIElement { - enum SearchNextStep { - case skipDescendants - case skipSiblings - case skipDescendantsAndSiblings - case continueSearching - case stopSearching - } - /// Traversing the element tree. - /// - /// - important: Traversing the element tree is resource consuming and will affect the - /// **performance of Xcode**. Please make sure to skip as much as possible. - /// - /// - todo: Make it not recursive. - func traverse(_ handle: (_ element: AXUIElement, _ level: Int) -> SearchNextStep) { - func _traverse( - element: AXUIElement, - level: Int, - handle: (AXUIElement, Int) -> SearchNextStep - ) -> SearchNextStep { - let nextStep = handle(element, level) - switch nextStep { - case .stopSearching: return .stopSearching - case .skipDescendants: return .continueSearching - case .skipDescendantsAndSiblings: return .skipSiblings - case .continueSearching, .skipSiblings: - for child in element.children { - switch _traverse(element: child, level: level + 1, handle: handle) { - case .skipSiblings, .skipDescendantsAndSiblings: - break - case .stopSearching: - return .stopSearching - case .continueSearching, .skipDescendants: - continue - } - } - return nextStep - } - } - _ = _traverse(element: self, level: 0, handle: handle) - } -} - -// MARK: - Helper - -public extension AXUIElement { - func copyValue(key: String, ofType _: T.Type = T.self) throws -> T { - var value: AnyObject? - let error = AXUIElementCopyAttributeValue(self, key as CFString, &value) - if error == .success, let value = value as? T { - return value - } - throw error - } - - func copyParameterizedValue( - key: String, - parameters: AnyObject, - ofType _: T.Type = T.self - ) throws -> T { - var value: AnyObject? - let error = AXUIElementCopyParameterizedAttributeValue( - self, - key as CFString, - parameters as CFTypeRef, - &value - ) - if error == .success, let value = value as? T { - return value - } - throw error - } -} - -// MARK: - Xcode Specific -public extension AXUIElement { - func findSourceEditorElement(shouldRetry: Bool = true) -> AXUIElement? { - - // 1. Check if the current element is a source editor - if isSourceEditor { - return self - } - - // 2. Search for child that is a source editor - if let sourceEditorChild = firstChild(where: \.isSourceEditor) { - return sourceEditorChild - } - - // 3. Search for parent that is a source editor (XcodeInspector's approach) - if let sourceEditorParent = firstParent(where: \.isSourceEditor) { - return sourceEditorParent - } - - // 4. Search for parent that is an editor area - if let editorAreaParent = firstParent(where: \.isEditorArea) { - // 3.1 Search for child that is a source editor - if let sourceEditorChild = editorAreaParent.firstChild(where: \.isSourceEditor) { - return sourceEditorChild - } - } - - // 5. Search for the workspace window - if let xcodeWorkspaceWindowParent = firstParent(where: \.isXcodeWorkspaceWindow) { - // 4.1 Search for child that is an editor area - if let editorAreaChild = xcodeWorkspaceWindowParent.firstChild(where: \.isEditorArea) { - // 4.2 Search for child that is a source editor - if let sourceEditorChild = editorAreaChild.firstChild(where: \.isSourceEditor) { - return sourceEditorChild - } - } - } - - // 6. retry - if shouldRetry { - Thread.sleep(forTimeInterval: 0.5) - return findSourceEditorElement(shouldRetry: false) - } - - - return nil - - } -} - -#if hasFeature(RetroactiveAttribute) -extension AXError: @retroactive Error {} -#else -extension AXError: Error {} -#endif - diff --git a/Tool/Sources/AXHelper/AXHelper.swift b/Tool/Sources/AXHelper/AXHelper.swift deleted file mode 100644 index 5af9a206..00000000 --- a/Tool/Sources/AXHelper/AXHelper.swift +++ /dev/null @@ -1,98 +0,0 @@ -import XPCShared -import XcodeInspector -import AppKit - -public struct AXHelper { - public init() {} - - /// When Xcode commands are not available, we can fallback to directly - /// set the value of the editor with Accessibility API. - public func injectUpdatedCodeWithAccessibilityAPI( - _ result: UpdatedContent, - focusElement: AXUIElement, - onSuccess: (() -> Void)? = nil, - onError: (() -> Void)? = nil - ) throws { - let oldPosition = focusElement.selectedTextRange - let oldScrollPosition = focusElement.parent?.verticalScrollBar?.doubleValue - - let error = AXUIElementSetAttributeValue( - focusElement, - kAXValueAttribute as CFString, - result.content as CFTypeRef - ) - - if error != AXError.success { - if let onError = onError { - onError() - } - } - - // recover selection range - if let selection = result.newSelection { - var range = SourceEditor.convertCursorRangeToRange(selection, in: result.content) - if let value = AXValueCreate(.cfRange, &range) { - AXUIElementSetAttributeValue( - focusElement, - kAXSelectedTextRangeAttribute as CFString, - value - ) - } - } else if let oldPosition { - var range = CFRange( - location: oldPosition.lowerBound, - length: 0 - ) - if let value = AXValueCreate(.cfRange, &range) { - AXUIElementSetAttributeValue( - focusElement, - kAXSelectedTextRangeAttribute as CFString, - value - ) - } - } - - // recover scroll position - if let oldScrollPosition, - let scrollBar = focusElement.parent?.verticalScrollBar - { - Self.setScrollBarValue(scrollBar, value: oldScrollPosition) - } - - if let onSuccess = onSuccess { - onSuccess() - } - } - - /// Helper method to set scroll bar value using Accessibility API - private static func setScrollBarValue(_ scrollBar: AXUIElement, value: Double) { - AXUIElementSetAttributeValue( - scrollBar, - kAXValueAttribute as CFString, - value as CFTypeRef - ) - } - - private static func getScrollPositionForLine(_ lineNumber: Int, content: String) -> Double? { - let lines = content.components(separatedBy: .newlines) - let linesCount = lines.count - - guard lineNumber > 0 && lineNumber <= linesCount - else { return nil } - - // Calculate relative position (0.0 to 1.0) - let relativePosition = Double(lineNumber - 1) / Double(linesCount - 1) - - // Ensure valid range - return (0.0 <= relativePosition && relativePosition <= 1.0) ? relativePosition : nil - } - - public static func scrollSourceEditorToLine(_ lineNumber: Int, content: String, focusedElement: AXUIElement) { - guard focusedElement.isSourceEditor, - let scrollBar = focusedElement.parent?.verticalScrollBar, - let linePosition = Self.getScrollPositionForLine(lineNumber, content: content) - else { return } - - Self.setScrollBarValue(scrollBar, value: linePosition) - } -} diff --git a/Tool/Sources/AXNotificationStream/AXNotificationStream.swift b/Tool/Sources/AXNotificationStream/AXNotificationStream.swift deleted file mode 100644 index f4b3f194..00000000 --- a/Tool/Sources/AXNotificationStream/AXNotificationStream.swift +++ /dev/null @@ -1,171 +0,0 @@ -import AppKit -import ApplicationServices -import Foundation -import Logger -import Preferences -import Status - -public final class AXNotificationStream: AsyncSequence { - public typealias Stream = AsyncStream - public typealias Continuation = Stream.Continuation - public typealias AsyncIterator = Stream.AsyncIterator - public typealias Element = (name: String, element: AXUIElement, info: CFDictionary) - - private var continuation: Continuation - private let stream: Stream - - private let file: StaticString - private let line: UInt - private let function: StaticString - - public func makeAsyncIterator() -> Stream.AsyncIterator { - stream.makeAsyncIterator() - } - - deinit { - continuation.finish() - } - - public convenience init( - app: NSRunningApplication, - element: AXUIElement? = nil, - notificationNames: String..., - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function - ) { - self.init( - app: app, - element: element, - notificationNames: notificationNames, - file: file, - line: line, - function: function - ) - } - - public init( - app: NSRunningApplication, - element: AXUIElement? = nil, - notificationNames: [String], - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function - ) { - self.file = file - self.line = line - self.function = function - - let mode: CFRunLoopMode = UserDefaults.shared - .value(for: \.observeToAXNotificationWithDefaultMode) ? .defaultMode : .commonModes - - let runLoop: CFRunLoop = CFRunLoopGetMain() - - var cont: Continuation! - stream = Stream { continuation in - cont = continuation - } - continuation = cont - var observer: AXObserver? - - func callback( - observer: AXObserver, - element: AXUIElement, - notificationName: CFString, - userInfo: CFDictionary, - pointer: UnsafeMutableRawPointer? - ) { - guard let pointer = pointer?.assumingMemoryBound(to: Continuation.self) - else { return } - pointer.pointee.yield((notificationName as String, element, userInfo)) - } - - _ = AXObserverCreateWithInfoCallback( - app.processIdentifier, - callback, - &observer - ) - guard let observer else { - continuation.finish() - return - } - - let observingElement = element ?? AXUIElementCreateApplication(app.processIdentifier) - continuation.onTermination = { @Sendable _ in - for name in notificationNames { - AXObserverRemoveNotification(observer, observingElement, name as CFString) - } - CFRunLoopRemoveSource( - runLoop, - AXObserverGetRunLoopSource(observer), - mode - ) - } - - Task { @MainActor [weak self] in - CFRunLoopAddSource( - runLoop, - AXObserverGetRunLoopSource(observer), - mode - ) - var pendingRegistrationNames = Set(notificationNames) - var retry = 0 - var shouldLogAXDisabledEvent: Bool = true - while !pendingRegistrationNames.isEmpty, retry < 100 { - guard let self else { return } - retry += 1 - for name in notificationNames { - await Task.yield() - let e = withUnsafeMutablePointer(to: &self.continuation) { pointer in - AXObserverAddNotification( - observer, - observingElement, - name as CFString, - pointer - ) - } - switch e { - case .success: - shouldLogAXDisabledEvent = true - pendingRegistrationNames.remove(name) - await Status.shared.updateAXStatus(.granted) - case .actionUnsupported: - Logger.service.info("AXObserver: Action unsupported: \(name)") - pendingRegistrationNames.remove(name) - case .apiDisabled: - if shouldLogAXDisabledEvent { // Avoid keeping log AX disabled too many times - shouldLogAXDisabledEvent = false - Logger.service - .error("AXObserver: Accessibility API disabled, will try again later") - } - retry -= 1 - await Status.shared.updateAXStatus(.notGranted) - case .invalidUIElement: - Logger.service - .info("AXObserver: Invalid UI element, notification name \(name)") - pendingRegistrationNames.remove(name) - case .invalidUIElementObserver: - Logger.service.info("AXObserver: Invalid UI element observer") - pendingRegistrationNames.remove(name) - case .cannotComplete: - Logger.service - .info("AXObserver: Failed to observe \(name), will try again later") - case .notificationUnsupported: - Logger.service.info("AXObserver: Notification unsupported: \(name)") - pendingRegistrationNames.remove(name) - case .notificationAlreadyRegistered: - Logger.service.info("AXObserver: Notification already registered: \(name)") - pendingRegistrationNames.remove(name) - default: - Logger.service - .info( - "AXObserver: Unrecognized error \(e) when registering \(name), will try again later" - ) - } - } - try await Task.sleep(nanoseconds: 1_500_000_000) - } - } - } -} - diff --git a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift b/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift deleted file mode 100644 index 12c309ed..00000000 --- a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift +++ /dev/null @@ -1,115 +0,0 @@ -import AppKit - -public struct RunningApplicationInfo: Sendable { - public let isXcode: Bool - public let isActive: Bool - public let isHidden: Bool - public let localizedName: String? - public let bundleIdentifier: String? - public let bundleURL: URL? - public let executableURL: URL? - public let processIdentifier: pid_t - public let launchDate: Date? - public let executableArchitecture: Int - - init(_ application: NSRunningApplication) { - isXcode = application.isXcode - isActive = application.isActive - isHidden = application.isHidden - localizedName = application.localizedName - bundleIdentifier = application.bundleIdentifier - bundleURL = application.bundleURL - executableURL = application.executableURL - processIdentifier = application.processIdentifier - launchDate = application.launchDate - executableArchitecture = application.executableArchitecture - } -} - -public extension NSRunningApplication { - var info: RunningApplicationInfo { RunningApplicationInfo(self) } -} - -public final class ActiveApplicationMonitor { - public static let shared = ActiveApplicationMonitor() - public private(set) var latestXcode: NSRunningApplication? = NSWorkspace.shared - .runningApplications - .first(where: \.isXcode) - public private(set) var previousApp: NSRunningApplication? - public private(set) var activeApplication = NSWorkspace.shared.runningApplications - .first(where: \.isActive) - { - didSet { - if activeApplication?.isXcode ?? false { - latestXcode = activeApplication - } - previousApp = oldValue - } - } - - private var infoContinuations: [UUID: AsyncStream.Continuation] = [:] - - private init() { - activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive) - - Task { - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didActivateApplicationNotification) - for await notification in sequence { - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication - else { continue } - activeApplication = app - notifyContinuations() - } - } - } - - deinit { - for continuation in infoContinuations { - continuation.value.finish() - } - } - - public var activeXcode: NSRunningApplication? { - if activeApplication?.isXcode ?? false { - return activeApplication - } - return nil - } - - public func createInfoStream() -> AsyncStream { - .init { continuation in - let id = UUID() - Task { @MainActor in - continuation.onTermination = { _ in - self.removeInfoContinuation(id: id) - } - addInfoContinuation(continuation, id: id) - continuation.yield(activeApplication?.info) - } - } - } - - func addInfoContinuation( - _ continuation: AsyncStream.Continuation, - id: UUID - ) { - infoContinuations[id] = continuation - } - - func removeInfoContinuation(id: UUID) { - infoContinuations[id] = nil - } - - private func notifyContinuations() { - for continuation in infoContinuations { - continuation.value.yield(activeApplication?.info) - } - } -} - -public extension NSRunningApplication { - var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" } -} - diff --git a/Tool/Sources/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift deleted file mode 100644 index b50f3bf4..00000000 --- a/Tool/Sources/AppActivator/AppActivator.swift +++ /dev/null @@ -1,107 +0,0 @@ -import AppKit -import Dependencies -import XcodeInspector - -public extension NSWorkspace { - static func activateThisApp(delay: TimeInterval = 0.3) { - Task { @MainActor in - try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - - // NSApp.activate may fail. And since macOS 14, it looks like the app needs other - // apps to call `yieldActivationToApplication` to activate itself? - - let activated = NSRunningApplication.current - .activate(options: [.activateIgnoringOtherApps]) - - if activated { return } - - // Fallback solution - - let appleScript = """ - tell application "System Events" - set frontmost of the first process whose unix id is \ - \(ProcessInfo.processInfo.processIdentifier) to true - end tell - """ - try await runAppleScript(appleScript) - } - } - - static func activatePreviousActiveApp(delay: TimeInterval = 0.2) { - Task { @MainActor in - guard let app = await XcodeInspector.shared.safe.previousActiveApplication - else { return } - try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - _ = app.activate() - } - } - - static func activatePreviousActiveXcode(delay: TimeInterval = 0.2) { - Task { @MainActor in - guard let app = await XcodeInspector.shared.safe.latestActiveXcode else { return } - try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - _ = app.activate() - } - } -} - -struct ActivateThisAppDependencyKey: DependencyKey { - static var liveValue: () -> Void = { NSWorkspace.activateThisApp() } -} - -struct ActivatePreviousActiveAppDependencyKey: DependencyKey { - static var liveValue: () -> Void = { NSWorkspace.activatePreviousActiveApp() } -} - -struct ActivatePreviousActiveXcodeDependencyKey: DependencyKey { - static var liveValue: () -> Void = { NSWorkspace.activatePreviousActiveXcode() } -} - -public extension DependencyValues { - var activateThisApp: () -> Void { - get { self[ActivateThisAppDependencyKey.self] } - set { self[ActivateThisAppDependencyKey.self] = newValue } - } - - var activatePreviousActiveApp: () -> Void { - get { self[ActivatePreviousActiveAppDependencyKey.self] } - set { self[ActivatePreviousActiveAppDependencyKey.self] = newValue } - } - - var activatePreviousActiveXcode: () -> Void { - get { self[ActivatePreviousActiveXcodeDependencyKey.self] } - set { self[ActivatePreviousActiveXcodeDependencyKey.self] = newValue } - } -} - -@discardableResult -func runAppleScript(_ appleScript: String) async throws -> String { - let task = Process() - task.launchPath = "/usr/bin/osascript" - task.arguments = ["-e", appleScript] - let outpipe = Pipe() - task.standardOutput = outpipe - task.standardError = Pipe() - - return try await withUnsafeThrowingContinuation { continuation in - do { - task.terminationHandler = { _ in - do { - if let data = try outpipe.fileHandleForReading.readToEnd(), - let content = String(data: data, encoding: .utf8) - { - continuation.resume(returning: content) - return - } - continuation.resume(returning: "") - } catch { - continuation.resume(throwing: error) - } - } - try task.run() - } catch { - continuation.resume(throwing: error) - } - } -} - diff --git a/Tool/Sources/AppKitExtension/NSWorkspace+Extension.swift b/Tool/Sources/AppKitExtension/NSWorkspace+Extension.swift deleted file mode 100644 index 46d1aa98..00000000 --- a/Tool/Sources/AppKitExtension/NSWorkspace+Extension.swift +++ /dev/null @@ -1,51 +0,0 @@ -import AppKit -import Logger - -extension NSWorkspace { - public static func getXcodeBundleURL() -> URL? { - var xcodeBundleURL: URL? - - // Get currently running Xcode application URL - if let xcodeApp = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == "com.apple.dt.Xcode" }) { - xcodeBundleURL = xcodeApp.bundleURL - } - - // Fallback to standard path if we couldn't get the running instance - if xcodeBundleURL == nil { - let standardPath = "/Applications/Xcode.app" - if FileManager.default.fileExists(atPath: standardPath) { - xcodeBundleURL = URL(fileURLWithPath: standardPath) - } - } - - return xcodeBundleURL - } - - public static func openFileInXcode( - fileURL: URL, - completion: ((NSRunningApplication?, Error?) -> Void)? = nil - ) { - guard let xcodeBundleURL = Self.getXcodeBundleURL() else { - if let completion = completion { - completion(nil, NSError(domain: "The Xcode app is not found.", code: 0)) - } - return - } - - let configuration = NSWorkspace.OpenConfiguration() - configuration.activates = true - configuration.promptsUserIfNeeded = false - - Self.shared.open( - [fileURL], - withApplicationAt: xcodeBundleURL, - configuration: configuration - ) { app, error in - if let completion = completion { - completion(app, error) - } else if let error = error { - Logger.client.error("Failed to open file \(String(describing: error))") - } - } - } -} diff --git a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift deleted file mode 100644 index 94d033d7..00000000 --- a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift +++ /dev/null @@ -1,54 +0,0 @@ -import AppKit -import Foundation - -public actor AsyncPassthroughSubject { - var tasks: [AsyncStream.Continuation] = [] - - deinit { - tasks.forEach { $0.finish() } - } - - public init() {} - - public func notifications() -> AsyncStream { - AsyncStream { [weak self] continuation in - let task = Task { [weak self] in - await self?.storeContinuation(continuation) - } - - continuation.onTermination = { termination in - task.cancel() - } - } - } - - nonisolated - public func send(_ element: Element) { - Task { await _send(element) } - } - - func _send(_ element: Element) { - let tasks = tasks - for task in tasks { - task.yield(element) - } - } - - func storeContinuation(_ continuation: AsyncStream.Continuation) { - tasks.append(continuation) - } - - nonisolated - public func finish() { - Task { await _finish() } - } - - func _finish() { - let tasks = self.tasks - self.tasks = [] - for task in tasks { - task.finish() - } - } -} - diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift deleted file mode 100644 index 525b5c54..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift +++ /dev/null @@ -1,25 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import Preferences -import ConversationServiceProvider -import TelemetryServiceProvider - -public typealias CopilotForXcodeCapability = CopilotForXcodeExtensionCapability & CopilotForXcodeChatCapability & CopilotForXcodeTelemetryCapability - -public protocol CopilotForXcodeChatCapability { - var conversationService: ConversationServiceType? { get } -} - -public protocol CopilotForXcodeTelemetryCapability { - var telemetryService: TelemetryServiceType? { get } -} - -public protocol BuiltinExtension: CopilotForXcodeCapability { - /// An id that let the extension manager determine whether the extension is in use. - var suggestionServiceId: BuiltInSuggestionFeatureProvider { get } - - /// It's usually called when the app is about to quit, - /// you should clean up all the resources here. - func terminate() -} - diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionConversationServiceProvider.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionConversationServiceProvider.swift deleted file mode 100644 index 0b62e141..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionConversationServiceProvider.swift +++ /dev/null @@ -1,187 +0,0 @@ -import ConversationServiceProvider -import CopilotForXcodeKit -import Foundation -import Logger -import XcodeInspector -import Workspace - -public final class BuiltinExtensionConversationServiceProvider< - T: BuiltinExtension ->: ConversationServiceProvider { - public func notifyChangeTextDocument(fileURL: URL, content: String, version: Int, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - - try? await conversationService.notifyChangeTextDocument(fileURL: fileURL, content: content, version: version, workspace: workspaceInfo) - } - - - private let extensionManager: BuiltinExtensionManager - - public init( - extension: T.Type, - extensionManager: BuiltinExtensionManager = .shared - ) { - self.extensionManager = extensionManager - } - - var conversationService: ConversationServiceType? { - extensionManager.extensions.first { $0 is T }?.conversationService - } - - private func activeWorkspace(_ workspaceURL: URL? = nil) async -> WorkspaceInfo? { - if let workspaceURL = workspaceURL { - if let workspaceBinding = WorkspaceFile.getWorkspaceInfo(workspaceURL: workspaceURL) { - return workspaceBinding - } - } - - guard let workspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL, - let projectURL = await XcodeInspector.shared.safe.realtimeActiveProjectURL - else { return nil } - - return WorkspaceInfo(workspaceURL: workspaceURL, projectURL: projectURL) - } - - struct BuiltinExtensionChatServiceNotFoundError: Error, LocalizedError { - var errorDescription: String? { - "Builtin chat service not found." - } - } - - public func createConversation(_ request: ConversationRequest, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - - try await conversationService.createConversation(request, workspace: workspaceInfo) - } - - public func createTurn(with conversationId: String, request: ConversationRequest, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - - try await conversationService - .createTurn( - with: conversationId, - request: request, - workspace: workspaceInfo - ) - } - - public func stopReceivingMessage(_ workDoneToken: String, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - - try await conversationService.cancelProgress(workDoneToken, workspace: workspaceInfo) - } - - public func rateConversation(turnId: String, rating: ConversationRating, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - try? await conversationService.rateConversation(turnId: turnId, rating: rating, workspace: workspaceInfo) - } - - public func copyCode(_ request: CopyCodeRequest, workspaceURL: URL?) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - guard let workspaceInfo = await activeWorkspace(workspaceURL) else { - Logger.service.error("Could not get active workspace info") - return - } - try? await conversationService.copyCode(request: request, workspace: workspaceInfo) - } - - public func templates() async throws -> [ChatTemplate]? { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return nil - } - guard let workspaceInfo = await activeWorkspace() else { - Logger.service.error("Could not get active workspace info") - return nil - } - - return (try? await conversationService.templates(workspace: workspaceInfo)) - } - - public func models() async throws -> [CopilotModel]? { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return nil - } - guard let workspaceInfo = await activeWorkspace() else { - Logger.service.error("Could not get active workspace info") - return nil - } - - return (try? await conversationService.models(workspace: workspaceInfo)) - } - - public func notifyDidChangeWatchedFiles(_ event: DidChangeWatchedFilesEvent, workspace: WorkspaceInfo) async throws { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return - } - - try? await conversationService.notifyDidChangeWatchedFiles(event, workspace: workspace) - } - - public func agents() async throws -> [ChatAgent]? { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return nil - } - guard let workspaceInfo = await activeWorkspace() else { - Logger.service.error("Could not get active workspace info") - return nil - } - - return (try? await conversationService.agents(workspace: workspaceInfo)) - } - - public func reviewChanges(_ params: ReviewChangesParams) async throws -> CodeReviewResult? { - guard let conversationService else { - Logger.service.error("Builtin chat service not found.") - return nil - } - guard let workspaceInfo = await activeWorkspace() else { - Logger.service.error("Could not get active workspace info") - return nil - } - - return (try? await conversationService.reviewChanges(workspace: workspaceInfo, params: params)) - } -} diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionManager.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionManager.swift deleted file mode 100644 index 0d65011a..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionManager.swift +++ /dev/null @@ -1,46 +0,0 @@ -import AppKit -import Combine -import Foundation -import XcodeInspector - -public final class BuiltinExtensionManager { - public static let shared: BuiltinExtensionManager = .init() - private(set) var extensions: [any BuiltinExtension] = [] - - private var cancellable: Set = [] - - init() { - XcodeInspector.shared.$activeApplication.sink { [weak self] app in - if let app, app.isXcode || app.isExtensionService { - self?.checkAppConfiguration() - } - }.store(in: &cancellable) - } - - public func setupExtensions(_ extensions: [any BuiltinExtension]) { - self.extensions = extensions - checkAppConfiguration() - } - - public func terminate() { - for ext in extensions { - ext.terminate() - } - } -} - -extension BuiltinExtensionManager { - func checkAppConfiguration() { - let suggestionFeatureProvider = UserDefaults.shared.value(for: \.suggestionFeatureProvider) - for ext in extensions { - let isSuggestionFeatureInUse = suggestionFeatureProvider == - .builtIn(ext.suggestionServiceId) - let isChatFeatureInUse = true - ext.extensionUsageDidChange(.init( - isSuggestionServiceInUse: isSuggestionFeatureInUse, - isChatServiceInUse: isChatFeatureInUse - )) - } - } -} - diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift deleted file mode 100644 index f6234ddf..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift +++ /dev/null @@ -1,177 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import Logger -import Preferences -import SuggestionBasic -import SuggestionProvider - -public final class BuiltinExtensionSuggestionServiceProvider< - T: BuiltinExtension ->: SuggestionServiceProvider { - public var configuration: SuggestionServiceConfiguration { - guard let service else { - return .init( - acceptsRelevantCodeSnippets: true, - mixRelevantCodeSnippetsInSource: true, - acceptsRelevantSnippetsFromOpenedFiles: true - ) - } - - return service.configuration - } - - let extensionManager: BuiltinExtensionManager - - public init( - extension: T.Type, - extensionManager: BuiltinExtensionManager = .shared - ) { - self.extensionManager = extensionManager - } - - var service: CopilotForXcodeKit.SuggestionServiceType? { - extensionManager.extensions.first { $0 is T }?.suggestionService - } - - struct BuiltinExtensionSuggestionServiceNotFoundError: Error, LocalizedError { - var errorDescription: String? { - "Builtin suggestion service not found." - } - } - - public func getSuggestions( - _ request: SuggestionProvider.SuggestionRequest, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async throws -> [SuggestionBasic.CodeSuggestion] { - guard let service else { - Logger.service.error("Builtin suggestion service not found.") - throw BuiltinExtensionSuggestionServiceNotFoundError() - } - return try await service.getSuggestions( - .init( - fileURL: request.fileURL, - relativePath: request.relativePath, - language: .init( - rawValue: languageIdentifierFromFileURL(request.fileURL).rawValue - ) ?? .plaintext, - content: request.content, - originalContent: request.originalContent, - cursorPosition: .init( - line: request.cursorPosition.line, - character: request.cursorPosition.character - ), - tabSize: request.tabSize, - indentSize: request.indentSize, - usesTabsForIndentation: request.usesTabsForIndentation, - relevantCodeSnippets: request.relevantCodeSnippets.map { $0.converted } - ), - workspace: workspaceInfo - ).map { $0.converted } - } - - public func cancelRequest( - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async { - guard let service else { - Logger.service.error("Builtin suggestion service not found.") - return - } - await service.cancelRequest(workspace: workspaceInfo) - } - - public func notifyAccepted( - _ suggestion: SuggestionBasic.CodeSuggestion, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async { - guard let service else { - Logger.service.error("Builtin suggestion service not found.") - return - } - await service.notifyAccepted(suggestion.converted, workspace: workspaceInfo) - } - - public func notifyRejected( - _ suggestions: [SuggestionBasic.CodeSuggestion], - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async { - guard let service else { - Logger.service.error("Builtin suggestion service not found.") - return - } - await service.notifyRejected(suggestions.map(\.converted), workspace: workspaceInfo) - } -} - -extension SuggestionProvider.SuggestionRequest { - var converted: CopilotForXcodeKit.SuggestionRequest { - .init( - fileURL: fileURL, - relativePath: relativePath, - language: .init(rawValue: languageIdentifierFromFileURL(fileURL).rawValue) - ?? .plaintext, - content: content, - originalContent: originalContent, - cursorPosition: .init( - line: cursorPosition.line, - character: cursorPosition.character - ), - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - relevantCodeSnippets: relevantCodeSnippets.map(\.converted) - ) - } -} - -extension SuggestionBasic.CodeSuggestion { - var converted: CopilotForXcodeKit.CodeSuggestion { - .init( - id: id, - text: text, - position: .init( - line: position.line, - character: position.character - ), - range: .init( - start: .init( - line: range.start.line, - character: range.start.character - ), - end: .init( - line: range.end.line, - character: range.end.character - ) - ) - ) - } -} - -extension CopilotForXcodeKit.CodeSuggestion { - var converted: SuggestionBasic.CodeSuggestion { - .init( - id: id, - text: text, - position: .init( - line: position.line, - character: position.character - ), - range: .init( - start: .init( - line: range.start.line, - character: range.start.character - ), - end: .init( - line: range.end.line, - character: range.end.character - ) - ) - ) - } -} - -extension SuggestionProvider.RelevantCodeSnippet { - var converted: CopilotForXcodeKit.RelevantCodeSnippet { - .init(content: content, priority: priority, filePath: filePath) - } -} - diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionTelemetryServiceProvider.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionTelemetryServiceProvider.swift deleted file mode 100644 index df7a3905..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionTelemetryServiceProvider.swift +++ /dev/null @@ -1,59 +0,0 @@ -import TelemetryServiceProvider -import CopilotForXcodeKit -import Foundation -import Logger -import XcodeInspector - -public final class BuiltinExtensionTelemetryServiceProvider< - T: BuiltinExtension ->: TelemetryServiceProvider { - - private let extensionManager: BuiltinExtensionManager - - public init( - extension: T.Type, - extensionManager: BuiltinExtensionManager = .shared - ) { - self.extensionManager = extensionManager - } - - var telemetryService: TelemetryServiceType? { - extensionManager.extensions.first { $0 is T }?.telemetryService - } - - private func activeWorkspace() async -> WorkspaceInfo? { - guard let workspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL, - let projectURL = await XcodeInspector.shared.safe.realtimeActiveProjectURL - else { return nil } - - return WorkspaceInfo(workspaceURL: workspaceURL, projectURL: projectURL) - } - - struct BuiltinExtensionTelemetryServiceNotFoundError: Error, LocalizedError { - var errorDescription: String? { - "Builtin telemetry service not found." - } - } - - struct BuiltinExtensionActiveWorkspaceInfoNotFoundError: Error, LocalizedError { - var errorDescription: String? { - "Builtin active workspace info not found." - } - } - - public func sendError(_ request: TelemetryExceptionRequest) async throws { - guard let telemetryService else { - print("Builtin telemetry service not found.") - throw BuiltinExtensionTelemetryServiceNotFoundError() - } - guard let workspaceInfo = await activeWorkspace() else { - print("Builtin active workspace info not found.") - throw BuiltinExtensionActiveWorkspaceInfoNotFoundError() - } - - try await telemetryService.sendError( - request, - workspace: workspaceInfo - ) - } -} diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift deleted file mode 100644 index a03c34d1..00000000 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionWorkspacePlugin.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation -import Workspace - -public final class BuiltinExtensionWorkspacePlugin: WorkspacePlugin { - let extensionManager: BuiltinExtensionManager - - public init(workspace: Workspace, extensionManager: BuiltinExtensionManager = .shared) { - self.extensionManager = extensionManager - super.init(workspace: workspace) - } - - override public func didOpenFilespace(_ filespace: Filespace) { - notifyOpenFile(filespace: filespace) - } - - override public func didSaveFilespace(_ filespace: Filespace) { - notifySaveFile(filespace: filespace) - } - - override public func didUpdateFilespace(_ filespace: Filespace, content: String) { - notifyUpdateFile(filespace: filespace, content: content) - } - - override public func didCloseFilespace(_ fileURL: URL) { - Task { - for ext in extensionManager.extensions { - ext.workspace( - .init(workspaceURL: workspaceURL, projectURL: projectRootURL), - didCloseDocumentAt: fileURL - ) - } - } - } - - public func notifyOpenFile(filespace: Filespace) { - Task { - guard filespace.isTextReadable else { return } - for ext in extensionManager.extensions { - ext.workspace( - .init(workspaceURL: workspaceURL, projectURL: projectRootURL), - didOpenDocumentAt: filespace.fileURL - ) - } - } - } - - public func notifyUpdateFile(filespace: Filespace, content: String) { - Task { - guard filespace.isTextReadable else { return } - for ext in extensionManager.extensions { - ext.workspace( - .init(workspaceURL: workspaceURL, projectURL: projectRootURL), - didUpdateDocumentAt: filespace.fileURL, - content: content - ) - } - } - } - - public func notifySaveFile(filespace: Filespace) { - Task { - guard filespace.isTextReadable else { return } - for ext in extensionManager.extensions { - ext.workspace( - .init(workspaceURL: workspaceURL, projectURL: projectRootURL), - didSaveDocumentAt: filespace.fileURL - ) - } - } - } -} - diff --git a/Tool/Sources/Cache/AvatarCache.swift b/Tool/Sources/Cache/AvatarCache.swift deleted file mode 100644 index a0a91c47..00000000 --- a/Tool/Sources/Cache/AvatarCache.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import SwiftUI -import AppKit - -public final class AvatarCache { - public static let shared = AvatarCache() - private let cache = NSCache() - - private init () {} - - public func set(forUser username: String) async -> Void { - guard let data = await fetchAvatarData(forUser: username) else { return } - cache.setObject(data as NSData, forKey: username as NSString) - } - - public func get(forUser username: String) -> Data? { - return cache.object(forKey: username as NSString) as Data? - } - - public func remove(forUser username: String) { - cache.removeObject(forKey: username as NSString) - } -} - -extension AvatarCache { - // Directly get the avatar from URL like https://avatars.githubusercontent.com/ - // TODO: when the `agent` feature added, the avatarUrl could be obtained from the response of GitHub LSP - func fetchAvatarData(forUser username: String) async -> Data? { - let avatarUrl = "https://avatars.githubusercontent.com/\(username)" - guard let avatarUrl = URL(string: avatarUrl) else { return nil } - - do { - let (data, _) = try await URLSession.shared.data(from: avatarUrl) - return data - } catch { - return nil - } - } - - public func getAvatarImage(forUser username: String) -> Image? { - guard let data = get(forUser: username), - let nsImage = NSImage(data: data) - else { - return nil - } - - return Image(nsImage: nsImage) - } -} diff --git a/Tool/Sources/Cache/AvatarViewModel.swift b/Tool/Sources/Cache/AvatarViewModel.swift deleted file mode 100644 index 53dcafc8..00000000 --- a/Tool/Sources/Cache/AvatarViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -@MainActor -public class AvatarViewModel: ObservableObject { - @Published private(set) public var avatarImage: Image? - public static let shared = AvatarViewModel() - - public init() { } - - public func loadAvatar(forUser userName: String?) { - guard let userName = userName, !userName.isEmpty - else { - avatarImage = nil - return - } - - // Fetch if not in cache - Task { - await AvatarCache.shared.set(forUser: userName) - self.avatarImage = AvatarCache.shared.getAvatarImage(forUser: userName) - } - } -} diff --git a/Tool/Sources/ChatAPIService/APIs/ChatCompletionsAPIDefinition.swift b/Tool/Sources/ChatAPIService/APIs/ChatCompletionsAPIDefinition.swift deleted file mode 100644 index 165ea645..00000000 --- a/Tool/Sources/ChatAPIService/APIs/ChatCompletionsAPIDefinition.swift +++ /dev/null @@ -1,93 +0,0 @@ -import CodableWrappers -import Foundation -import Preferences - -struct ChatCompletionsRequestBody: Codable, Equatable { - struct Message: Codable, Equatable { - enum Role: String, Codable, Equatable { - case user - case assistant - - var asChatMessageRole: ChatMessage.Role { - switch self { - case .user: - return .user - case .assistant: - return .assistant - } - } - } - - /// The role of the message. - var role: Role - /// The content of the message. - - var content: String - } - - var messages: [Message] - var temperature: Double? - var stream: Bool? - var stop: [String]? - - init( - messages: [Message], - temperature: Double? = nil, - stream: Bool? = nil, - stop: [String]? = nil - ) { - self.messages = messages - self.temperature = temperature - self.stream = stream - self.stop = stop - } -} - -// MARK: - Stream API - -extension AsyncSequence { - func toStream() -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - let task = Task { - do { - for try await element in self { - continuation.yield(element) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - - continuation.onTermination = { _ in - task.cancel() - } - } - } -} - -struct ChatCompletionsStreamDataChunk { - struct Delta { - var role: ChatCompletionsRequestBody.Message.Role? - var content: String? - } - - var id: String? - var object: String? - var model: String? - var message: Delta? - var finishReason: String? -} - -// MARK: - Non Stream API - -struct ChatCompletionResponseBody: Codable, Equatable { - typealias Message = ChatCompletionsRequestBody.Message - - var id: String? - var object: String - var message: Message - var otherChoices: [Message] - var finishReason: String -} - diff --git a/Tool/Sources/ChatAPIService/APIs/ResponseStream.swift b/Tool/Sources/ChatAPIService/APIs/ResponseStream.swift deleted file mode 100644 index ce28b7f9..00000000 --- a/Tool/Sources/ChatAPIService/APIs/ResponseStream.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -struct ResponseStream: AsyncSequence { - func makeAsyncIterator() -> Stream.AsyncIterator { - stream.makeAsyncIterator() - } - - typealias Stream = AsyncThrowingStream - typealias AsyncIterator = Stream.AsyncIterator - typealias Element = Chunk - - struct LineContent { - let chunk: Chunk? - let done: Bool - } - - let stream: Stream - - init(result: URLSession.AsyncBytes, lineExtractor: @escaping (String) throws -> LineContent) { - stream = AsyncThrowingStream { continuation in - let task = Task { - do { - for try await line in result.lines { - if Task.isCancelled { break } - let content = try lineExtractor(line) - if let chunk = content.chunk { - continuation.yield(chunk) - } - - if content.done { break } - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - result.task.cancel() - } - } - continuation.onTermination = { _ in - task.cancel() - result.task.cancel() - } - } - } -} - diff --git a/Tool/Sources/ChatAPIService/Debug/Debug.swift b/Tool/Sources/ChatAPIService/Debug/Debug.swift deleted file mode 100644 index 31864964..00000000 --- a/Tool/Sources/ChatAPIService/Debug/Debug.swift +++ /dev/null @@ -1,75 +0,0 @@ -import AppKit -import Foundation - -enum Debugger { - @TaskLocal - static var id: UUID? - - #if DEBUG - static func didSendRequestBody(body: ChatCompletionsRequestBody) { - do { - let json = try JSONEncoder().encode(body) - let center = NotificationCenter.default - center.post( - name: .init("ServiceDebugger.ChatRequestDebug.requestSent"), - object: nil, - userInfo: [ - "id": id ?? UUID(), - "data": json, - ] - ) - } catch { - print("Failed to encode request body: \(error)") - } - } - - static func didReceiveFunction(name: String, arguments: String) { - let center = NotificationCenter.default - center.post( - name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionCall"), - object: nil, - userInfo: [ - "id": id ?? UUID(), - "name": name, - "arguments": arguments, - ] - ) - } - - static func didReceiveFunctionResult(result: String) { - let center = NotificationCenter.default - center.post( - name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionResult"), - object: nil, - userInfo: [ - "id": id ?? UUID(), - "result": result, - ] - ) - } - - static func didReceiveResponse(content: String) { - let center = NotificationCenter.default - center.post( - name: .init("ServiceDebugger.ChatRequestDebug.responseReceived"), - object: nil, - userInfo: [ - "id": id ?? UUID(), - "response": content, - ] - ) - } - - static func didFinish() { - let center = NotificationCenter.default - center.post( - name: .init("ServiceDebugger.ChatRequestDebug.finished"), - object: nil, - userInfo: [ - "id": id ?? UUID(), - ] - ) - } - #endif -} - diff --git a/Tool/Sources/ChatAPIService/Memory/AutoManagedChatMemory.swift b/Tool/Sources/ChatAPIService/Memory/AutoManagedChatMemory.swift deleted file mode 100644 index 5460fb00..00000000 --- a/Tool/Sources/ChatAPIService/Memory/AutoManagedChatMemory.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import Logger -import Preferences -import ConversationServiceProvider - -@globalActor -public enum AutoManagedChatMemoryActor: GlobalActor { - public actor Actor {} - public static let shared = Actor() -} - -protocol AutoManagedChatMemoryStrategy { - func countToken(_ message: ChatMessage) async -> Int -} - -/// A memory that automatically manages the history according to max tokens and max message count. -public actor AutoManagedChatMemory: ChatMemory { - public struct ComposableMessages { - public var systemPromptMessage: ChatMessage - public var historyMessage: [ChatMessage] - public var retrievedContentMessage: ChatMessage - public var contextSystemPromptMessage: ChatMessage - public var newMessage: ChatMessage - } - - public typealias HistoryComposer = (ComposableMessages) -> [ChatMessage] - - public private(set) var history: [ChatMessage] = [] { - didSet { onHistoryChange() } - } - - public private(set) var remainingTokens: Int? - - public var systemPrompt: String - public var contextSystemPrompt: String - public var retrievedContent: [ConversationReference] = [] - - var onHistoryChange: () -> Void = {} - - let composeHistory: HistoryComposer - - public init( - systemPrompt: String, - composeHistory: @escaping HistoryComposer = { - /// Default Format: - /// ``` - /// [System Prompt] priority: high - /// [Functions] priority: high - /// [Retrieved Content] priority: low - /// [Retrieved Content A] - /// - /// [Retrieved Content B] - /// [Message History] priority: medium - /// [Context System Prompt] priority: high - /// [Latest Message] priority: high - /// ``` - [$0.systemPromptMessage] + - $0.historyMessage + - [$0.retrievedContentMessage, $0.contextSystemPromptMessage, $0.newMessage] - } - ) { - self.systemPrompt = systemPrompt - contextSystemPrompt = "" - self.composeHistory = composeHistory - } - - deinit { - history.removeAll() - onHistoryChange = {} - - retrievedContent.removeAll() - } - - public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) { - update(&history) - } - - public func mutateContextSystemPrompt(_ newPrompt: String) { - contextSystemPrompt = newPrompt - } - - public func mutateRetrievedContent(_ newContent: [ConversationReference]) { - retrievedContent = newContent - } - - public nonisolated - func observeHistoryChange(_ onChange: @escaping () -> Void) { - Task { - await setOnHistoryChangeBlock(onChange) - } - } - - func setOnHistoryChangeBlock(_ onChange: @escaping () -> Void) { - onHistoryChange = onChange - } -} - diff --git a/Tool/Sources/ChatAPIService/Memory/ChatMemory.swift b/Tool/Sources/ChatAPIService/Memory/ChatMemory.swift deleted file mode 100644 index 9bcbcf97..00000000 --- a/Tool/Sources/ChatAPIService/Memory/ChatMemory.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import ConversationServiceProvider - -public protocol ChatMemory { - /// The message history. - var history: [ChatMessage] { get async } - /// Update the message history. - func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async -} - -public extension ChatMemory { - /// Append a message to the history. - func appendMessage(_ message: ChatMessage) async { - await mutateHistory { history in - if let index = history.firstIndex(where: { $0.id == message.id }) { - history[index].mergeMessage(with: message) - } else { - history.append(message) - } - } - } - - /// Remove a message from the history. - func removeMessage(_ id: String) async { - await mutateHistory { - $0.removeAll { $0.id == id } - } - } - - /// Clear the history. - func clearHistory() async { - await mutateHistory { $0.removeAll() } - } -} - -extension ChatMessage { - mutating func mergeMessage(with message: ChatMessage) { - // merge content - self.content = self.content + message.content - - // merge references - var seen = Set() - // without duplicated and keep order - self.references = (self.references + message.references).filter { seen.insert($0).inserted } - - // merge followUp - self.followUp = message.followUp ?? self.followUp - - // merge suggested title - self.suggestedTitle = message.suggestedTitle ?? self.suggestedTitle - - // merge error message - self.errorMessages = self.errorMessages + message.errorMessages - - self.panelMessages = self.panelMessages + message.panelMessages - - // merge steps - if !message.steps.isEmpty { - var mergedSteps = self.steps - - for newStep in message.steps { - if let index = mergedSteps.firstIndex(where: { $0.id == newStep.id }) { - mergedSteps[index] = newStep - } else { - mergedSteps.append(newStep) - } - } - - self.steps = mergedSteps - } - - // merge agent steps - if !message.editAgentRounds.isEmpty { - let mergedAgentRounds = mergeEditAgentRounds( - oldRounds: self.editAgentRounds, - newRounds: message.editAgentRounds - ) - - self.editAgentRounds = mergedAgentRounds - } - - self.codeReviewRound = message.codeReviewRound - } - - private func mergeEditAgentRounds(oldRounds: [AgentRound], newRounds: [AgentRound]) -> [AgentRound] { - var mergedAgentRounds = oldRounds - - for newRound in newRounds { - if let index = mergedAgentRounds.firstIndex(where: { $0.roundId == newRound.roundId }) { - mergedAgentRounds[index].reply = mergedAgentRounds[index].reply + newRound.reply - - if newRound.toolCalls != nil, !newRound.toolCalls!.isEmpty { - var mergedToolCalls = mergedAgentRounds[index].toolCalls ?? [] - for newToolCall in newRound.toolCalls! { - if let toolCallIndex = mergedToolCalls.firstIndex(where: { $0.id == newToolCall.id }) { - mergedToolCalls[toolCallIndex].status = newToolCall.status - if let progressMessage = newToolCall.progressMessage, !progressMessage.isEmpty { - mergedToolCalls[toolCallIndex].progressMessage = newToolCall.progressMessage - } - if let error = newToolCall.error, !error.isEmpty { - mergedToolCalls[toolCallIndex].error = newToolCall.error - } - if let invokeParams = newToolCall.invokeParams { - mergedToolCalls[toolCallIndex].invokeParams = invokeParams - } - } else { - mergedToolCalls.append(newToolCall) - } - } - mergedAgentRounds[index].toolCalls = mergedToolCalls - } - } else { - mergedAgentRounds.append(newRound) - } - } - - return mergedAgentRounds - } -} diff --git a/Tool/Sources/ChatAPIService/Memory/ConversationChatMemory.swift b/Tool/Sources/ChatAPIService/Memory/ConversationChatMemory.swift deleted file mode 100644 index 8eece555..00000000 --- a/Tool/Sources/ChatAPIService/Memory/ConversationChatMemory.swift +++ /dev/null @@ -1,15 +0,0 @@ - -//import Foundation - -// Not used actor, commit it avoid chat message init error -//public actor ConversationChatMemory: ChatMemory { -// public var history: [ChatMessage] = [] -// -// public init(systemPrompt: String, systemMessageId: String = UUID().uuidString) { -// history.append(.init(id: systemMessageId, role: .system, content: systemPrompt)) -// } -// -// public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) { -// update(&history) -// } -//} diff --git a/Tool/Sources/ChatAPIService/Models.swift b/Tool/Sources/ChatAPIService/Models.swift deleted file mode 100644 index 6fbafba8..00000000 --- a/Tool/Sources/ChatAPIService/Models.swift +++ /dev/null @@ -1,232 +0,0 @@ -import CodableWrappers -import Foundation -import ConversationServiceProvider -import GitHubCopilotService - -// move here avoid circular reference -public struct ConversationReference: Codable, Equatable, Hashable { - public enum Kind: Codable, Equatable, Hashable { - case `class` - case `struct` - case `enum` - case `actor` - case `protocol` - case `extension` - case `case` - case property - case `typealias` - case function - case method - case text - case webpage - case other - // reference for turn - request - case fileReference(FileReference) - // reference from turn - response - case reference(Reference) - } - - public enum Status: String, Codable { - case included, blocked, notfound, empty - } - - public var uri: String - public var status: Status? - public var kind: Kind - - public var ext: String { - return url?.pathExtension ?? "" - } - - public var fileName: String { - return url?.lastPathComponent ?? "" - } - - public var filePath: String { - return url?.path ?? "" - } - - public var url: URL? { - return URL(string: uri) - } - - public init( - uri: String, - status: Status?, - kind: Kind - ) { - self.uri = uri - self.status = status - self.kind = kind - - } -} - - -public struct ChatMessage: Equatable, Codable { - public typealias ID = String - - public enum Role: String, Codable, Equatable { - case user - case assistant - case system - } - - /// The role of a message. - public var role: Role - - /// The content of the message, either the chat message, or a result of a function call. - public var content: String - - /// The attached image content of the message - public var contentImageReferences: [ImageReference] - - /// The id of the message. - public var id: ID - - /// The conversation id (not the CLS conversation id) - public var chatTabID: String - - /// The CLS turn id of the message which is from CLS. - public var clsTurnID: ID? - - /// Rate assistant message - public var rating: ConversationRating - - /// The references of this message. - public var references: [ConversationReference] - - /// The followUp question of this message - public var followUp: ConversationFollowUp? - - public var suggestedTitle: String? - - /// The error occurred during responding chat in server - public var errorMessages: [String] - - /// The steps of conversation progress - public var steps: [ConversationProgressStep] - - public var editAgentRounds: [AgentRound] - - public var panelMessages: [CopilotShowMessageParams] - - public var codeReviewRound: CodeReviewRound? - - /// The timestamp of the message. - public var createdAt: Date - public var updatedAt: Date - - public init( - id: String = UUID().uuidString, - chatTabID: String, - clsTurnID: String? = nil, - role: Role, - content: String, - contentImageReferences: [ImageReference] = [], - references: [ConversationReference] = [], - followUp: ConversationFollowUp? = nil, - suggestedTitle: String? = nil, - errorMessages: [String] = [], - rating: ConversationRating = .unrated, - steps: [ConversationProgressStep] = [], - editAgentRounds: [AgentRound] = [], - panelMessages: [CopilotShowMessageParams] = [], - codeReviewRound: CodeReviewRound? = nil, - createdAt: Date? = nil, - updatedAt: Date? = nil - ) { - self.role = role - self.content = content - self.contentImageReferences = contentImageReferences - self.id = id - self.chatTabID = chatTabID - self.clsTurnID = clsTurnID - self.references = references - self.followUp = followUp - self.suggestedTitle = suggestedTitle - self.errorMessages = errorMessages - self.rating = rating - self.steps = steps - self.editAgentRounds = editAgentRounds - self.panelMessages = panelMessages - self.codeReviewRound = codeReviewRound - - let now = Date.now - self.createdAt = createdAt ?? now - self.updatedAt = updatedAt ?? now - } - - public init( - userMessageWithId id: String, - chatTabId: String, - content: String, - contentImageReferences: [ImageReference] = [], - references: [ConversationReference] = [] - ) { - self.init( - id: id, - chatTabID: chatTabId, - role: .user, - content: content, - contentImageReferences: contentImageReferences, - references: references - ) - } - - public init( - assistantMessageWithId id: String, // TurnId - chatTabID: String, - content: String = "", - references: [ConversationReference] = [], - followUp: ConversationFollowUp? = nil, - suggestedTitle: String? = nil, - steps: [ConversationProgressStep] = [], - editAgentRounds: [AgentRound] = [], - codeReviewRound: CodeReviewRound? = nil - ) { - self.init( - id: id, - chatTabID: chatTabID, - clsTurnID: id, - role: .assistant, - content: content, - references: references, - followUp: followUp, - suggestedTitle: suggestedTitle, - steps: steps, - editAgentRounds: editAgentRounds, - codeReviewRound: codeReviewRound - ) - } - - public init( - errorMessageWithId id: String, // TurnId - chatTabID: String, - errorMessages: [String] = [], - panelMessages: [CopilotShowMessageParams] = [] - ) { - self.init( - id: id, - chatTabID: chatTabID, - clsTurnID: id, - role: .assistant, - content: "", - errorMessages: errorMessages, - panelMessages: panelMessages - ) - } -} - -extension ConversationReference { - public func getPathRelativeToHome() -> String { - guard !filePath.isEmpty else { return filePath} - - let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path - if !homeDirectory.isEmpty{ - return filePath.replacingOccurrences(of: homeDirectory, with: "~") - } - - return filePath - } -} diff --git a/Tool/Sources/ChatTab/ChatTab.swift b/Tool/Sources/ChatTab/ChatTab.swift deleted file mode 100644 index 0612cca5..00000000 --- a/Tool/Sources/ChatTab/ChatTab.swift +++ /dev/null @@ -1,303 +0,0 @@ -import ComposableArchitecture -import Foundation -import SwiftUI - -/// Preview info used in ChatHistoryView -public struct ChatTabPreviewInfo: Identifiable, Equatable, Codable { - public let id: String - public let title: String? - public let isSelected: Bool - public let updatedAt: Date - - public init(id: String, title: String?, isSelected: Bool, updatedAt: Date) { - self.id = id - self.title = title - self.isSelected = isSelected - self.updatedAt = updatedAt - } -} - -/// The information of a tab. -@ObservableState -public struct ChatTabInfo: Identifiable, Equatable, Codable { - public var id: String - public var title: String? = nil - public var isTitleSet: Bool { - if let title = title, !title.isEmpty { return true } - return false - } - public var focusTrigger: Int = 0 - public var isSelected: Bool - public var CLSConversationID: String? - public var createdAt: Date - // used in chat history view - // should be updated when chat tab info changed or chat message of it changed - public var updatedAt: Date - - // The `workspacePath` and `username` won't be save into database - private(set) public var workspacePath: String - private(set) public var username: String - - public init(id: String, title: String? = nil, isSelected: Bool = false, CLSConversationID: String? = nil, workspacePath: String, username: String) { - self.id = id - self.title = title - self.isSelected = isSelected - self.CLSConversationID = CLSConversationID - self.workspacePath = workspacePath - self.username = username - - let now = Date.now - self.createdAt = now - self.updatedAt = now - } - - // for restoring - public init(id: String, title: String? = nil, focusTrigger: Int = 0, isSelected: Bool, CLSConversationID: String? = nil, createdAt: Date, updatedAt: Date, workspacePath: String, username: String) { - self.id = id - self.title = title - self.focusTrigger = focusTrigger - self.isSelected = isSelected - self.CLSConversationID = CLSConversationID - self.createdAt = createdAt - self.updatedAt = updatedAt - self.workspacePath = workspacePath - self.username = username - } -} - -/// Every chat tab should conform to this type. -public typealias ChatTab = BaseChatTab & ChatTabType - -/// Defines a bunch of things a chat tab should implement. -public protocol ChatTabType { - /// The type of the external dependency required by this chat tab. - associatedtype ExternalDependency - /// Build the view for this chat tab. - @ViewBuilder - func buildView() -> any View - /// Build the tabItem for this chat tab. - @ViewBuilder - func buildTabItem() -> any View - /// Build the chatConversationItem - @ViewBuilder - func buildChatConversationItem() -> any View - /// Build the icon for this chat tab. - @ViewBuilder - func buildIcon() -> any View - /// Build the menu for this chat tab. - @ViewBuilder - func buildMenu() -> any View - /// The name of this chat tab type. - static var name: String { get } - /// Available builders for this chat tab. - /// It's used to generate a list of tab types for user to create. - static func chatBuilders(externalDependency: ExternalDependency) -> [ChatTabBuilder] - /// Restorable state - func restorableState() async -> Data - /// Restore state - static func restore( - from data: Data, - externalDependency: ExternalDependency - ) async throws -> any ChatTabBuilder - /// Whenever the body or menu is accessed, this method will be called. - /// It will be called only once so long as you don't call it yourself. - /// It will be called from MainActor. - func start() -} - -/// The base class for all chat tabs. -open class BaseChatTab { - /// A wrapper to support dynamic update of title in view. - struct ContentView: View { - var buildView: () -> any View - var body: some View { - AnyView(buildView()) - } - } - - public var id: String = "" - public var title: String = "" - /// The store for chat tab info. You should only access it after `start` is called. - public let chatTabStore: StoreOf - - private var didStart = false - private let storeObserver = NSObject() - - public init(store: StoreOf) { - chatTabStore = store - - storeObserver.observe { [weak self] in - guard let self else { return } - self.title = store.title ?? "" - self.id = store.id - } - } - - /// The view for this chat tab. - @ViewBuilder - public var body: some View { - let id = "ChatTabBody\(id)" - if let tab = self as? (any ChatTabType) { - ContentView(buildView: tab.buildView).id(id) - .onAppear { - Task { @MainActor in self.startIfNotStarted() } - } - } else { - EmptyView().id(id) - } - } - - /// The tab item for this chat tab. - @ViewBuilder - public var tabItem: some View { - let id = "ChatTabTab\(id)" - if let tab = self as? (any ChatTabType) { - ContentView(buildView: tab.buildTabItem).id(id) - .onAppear { - Task { @MainActor in self.startIfNotStarted() } - } - } else { - EmptyView().id(id) - } - } - - @ViewBuilder - public var chatConversationItem: some View { - let id = "ChatTabTab\(id)" - if let tab = self as? (any ChatTabType) { - ContentView(buildView: tab.buildChatConversationItem).id(id) - } else { - EmptyView().id(id) - } - } - - /// The icon for this chat tab. - @ViewBuilder - public var icon: some View { - let id = "ChatTabIcon\(id)" - if let tab = self as? (any ChatTabType) { - ContentView(buildView: tab.buildIcon).id(id) - } else { - EmptyView().id(id) - } - } - - /// The tab item for this chat tab. - @ViewBuilder - public var menu: some View { - let id = "ChatTabMenu\(id)" - if let tab = self as? (any ChatTabType) { - ContentView(buildView: tab.buildMenu).id(id) - .onAppear { - Task { @MainActor in self.startIfNotStarted() } - } - } else { - EmptyView().id(id) - } - } - - @MainActor - func startIfNotStarted() { - guard !didStart else { return } - didStart = true - - if let tab = self as? (any ChatTabType) { - tab.start() - } - } -} - -/// A factory of a chat tab. -public protocol ChatTabBuilder { - /// A visible title for user. - var title: String { get } - /// Build the chat tab. - func build(store: StoreOf) async -> (any ChatTab)? -} - -/// A chat tab builder that doesn't build. -public struct DisabledChatTabBuilder: ChatTabBuilder { - public var title: String - public func build(store: StoreOf) async -> (any ChatTab)? { - return nil - } - - public init(title: String) { - self.title = title - } -} - -public extension ChatTabType { - /// The name of this chat tab type. - var name: String { Self.name } -} - -public extension ChatTabType where ExternalDependency == Void { - /// Available builders for this chat tab. - /// It's used to generate a list of tab types for user to create. - static func chatBuilders() -> [ChatTabBuilder] { - chatBuilders(externalDependency: ()) - } -} - -/// A chat tab that does nothing. -public class EmptyChatTab: ChatTab { - public static var name: String { "Empty" } - - struct Builder: ChatTabBuilder { - let title: String - func build(store: StoreOf) async -> (any ChatTab)? { - EmptyChatTab(store: store) - } - } - - public static func chatBuilders(externalDependency: Void) -> [ChatTabBuilder] { - [Builder(title: "Empty")] - } - - public func buildView() -> any View { - VStack { - Text("Empty-\(id)") - } - .background(Color.blue) - } - - public func buildTabItem() -> any View { - Text("Empty-\(id)") - } - - public func buildChatConversationItem() -> any View { - Text("Empty-\(id)") - } - - public func buildIcon() -> any View { - Image(systemName: "square") - } - - public func buildMenu() -> any View { - Text("Empty-\(id)") - } - - public func restorableState() async -> Data { - return Data() - } - - public static func restore( - from data: Data, - externalDependency: Void - ) async throws -> any ChatTabBuilder { - return Builder(title: "Empty") - } - - public convenience init(id: String) { - self.init(store: .init( - initialState: .init(id: id, title: "Empty-\(id)", workspacePath: "", username: ""), - reducer: { ChatTabItem() } - )) - } - - public func start() { - chatTabStore.send(.updateTitle("Empty-\(id)")) - } -} - diff --git a/Tool/Sources/ChatTab/ChatTabItem.swift b/Tool/Sources/ChatTab/ChatTabItem.swift deleted file mode 100644 index 724cd810..00000000 --- a/Tool/Sources/ChatTab/ChatTabItem.swift +++ /dev/null @@ -1,52 +0,0 @@ -import ComposableArchitecture -import Foundation - -public struct AnyChatTabBuilder: Equatable { - public static func == (lhs: AnyChatTabBuilder, rhs: AnyChatTabBuilder) -> Bool { - true - } - - public let chatTabBuilder: any ChatTabBuilder - - public init(_ chatTabBuilder: any ChatTabBuilder) { - self.chatTabBuilder = chatTabBuilder - } -} - -@Reducer -public struct ChatTabItem { - public typealias State = ChatTabInfo - - public enum Action: Equatable { - case updateTitle(String) - case openNewTab(AnyChatTabBuilder) - case tabContentUpdated - case close - case focus - case setCLSConversationID(String) - } - - public init() {} - - public var body: some ReducerOf { - Reduce { state, action in - // the actions will be handled elsewhere in the ChatPanelFeature - switch action { - case .updateTitle: - return .none - case .openNewTab: - return .none - case .tabContentUpdated: - return .none - case .close: - return .none - case .focus: - state.focusTrigger += 1 - return .none - case .setCLSConversationID: - return .none - } - } - } -} - diff --git a/Tool/Sources/ChatTab/ChatTabPool.swift b/Tool/Sources/ChatTab/ChatTabPool.swift deleted file mode 100644 index 116070fd..00000000 --- a/Tool/Sources/ChatTab/ChatTabPool.swift +++ /dev/null @@ -1,57 +0,0 @@ -import ComposableArchitecture -import Dependencies -import Foundation -import SwiftUI - -/// A pool that stores all the available tabs. -public final class ChatTabPool { - public var createStore: (ChatTabInfo) -> StoreOf = { info in - .init( - initialState: info, - reducer: { ChatTabItem() } - ) - } - - private var pool: [String: any ChatTab] - - public init(_ pool: [String: any ChatTab] = [:]) { - self.pool = pool - } - - public func getTab(of id: String) -> (any ChatTab)? { - pool[id] - } - - public func setTab(_ tab: any ChatTab) { - pool[tab.id] = tab - } - - public func removeTab(of id: String) { - guard getTab(of: id) != nil else { return } - - pool.removeValue(forKey: id) - } -} - -public struct ChatTabPoolDependencyKey: DependencyKey { - public static let liveValue = ChatTabPool() -} - -public extension DependencyValues { - var chatTabPool: ChatTabPool { - get { self[ChatTabPoolDependencyKey.self] } - set { self[ChatTabPoolDependencyKey.self] = newValue } - } -} - -public struct ChatTabPoolEnvironmentKey: EnvironmentKey { - public static let defaultValue = ChatTabPool() -} - -public extension EnvironmentValues { - var chatTabPool: ChatTabPool { - get { self[ChatTabPoolEnvironmentKey.self] } - set { self[ChatTabPoolEnvironmentKey.self] = newValue } - } -} - diff --git a/Tool/Sources/Configs/Configurations.swift b/Tool/Sources/Configs/Configurations.swift deleted file mode 100644 index 5c6acec3..00000000 --- a/Tool/Sources/Configs/Configurations.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -private var teamIDPrefix: String { - Bundle.main.infoDictionary?["TEAM_ID_PREFIX"] as? String ?? "" -} - -private var bundleIdentifierBase: String { - Bundle.main.infoDictionary?["BUNDLE_IDENTIFIER_BASE"] as? String ?? "" -} - -public var userDefaultSuiteName: String { - "\(teamIDPrefix)group.\(bundleIdentifierBase).prefs" -} diff --git a/Tool/Sources/ConversationServiceProvider/CodeReview/CodeReviewRound.swift b/Tool/Sources/ConversationServiceProvider/CodeReview/CodeReviewRound.swift deleted file mode 100644 index d9e2c7e9..00000000 --- a/Tool/Sources/ConversationServiceProvider/CodeReview/CodeReviewRound.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import LanguageServerProtocol -import GitHelper - -public struct CodeReviewRequest: Equatable, Codable { - public struct FileChange: Equatable, Codable { - public let changes: [PRChange] - public var selectedChanges: [PRChange] - - public init(changes: [PRChange]) { - self.changes = changes - self.selectedChanges = changes - } - } - - public var fileChange: FileChange - - public var changedFileUris: [DocumentUri] { fileChange.changes.map { $0.uri } } - public var selectedFileUris: [DocumentUri] { fileChange.selectedChanges.map { $0.uri } } - - public init(fileChange: FileChange) { - self.fileChange = fileChange - } - - public static func from(_ changes: [PRChange]) -> CodeReviewRequest { - return .init(fileChange: .init(changes: changes)) - } - - public mutating func updateSelectedChanges(by fileUris: [DocumentUri]) { - fileChange.selectedChanges = fileChange.selectedChanges.filter { fileUris.contains($0.uri) } - } -} - -public struct CodeReviewResponse: Equatable, Codable { - public struct FileComment: Equatable, Codable, Hashable { - public let uri: DocumentUri - public let originalContent: String - public var comments: [ReviewComment] - - public var url: URL? { URL(string: uri) } - - public init(uri: DocumentUri, originalContent: String, comments: [ReviewComment]) { - self.uri = uri - self.originalContent = originalContent - self.comments = comments - } - } - - public var fileComments: [FileComment] - - public var allComments: [ReviewComment] { - fileComments.flatMap { $0.comments } - } - - public init(fileComments: [FileComment]) { - self.fileComments = fileComments - } - - public func merge(with other: CodeReviewResponse) -> CodeReviewResponse { - var mergedResponse = self - - for newFileComment in other.fileComments { - if let index = mergedResponse.fileComments.firstIndex(where: { $0.uri == newFileComment.uri }) { - // Merge comments for existing URI - var mergedComments = mergedResponse.fileComments[index].comments + newFileComment.comments - mergedComments.sortByEndLine() - mergedResponse.fileComments[index].comments = mergedComments - } else { - // Append new URI with sorted comments - var newReview = newFileComment - newReview.comments.sortByEndLine() - mergedResponse.fileComments.append(newReview) - } - } - - return mergedResponse - } -} - -public struct CodeReviewRound: Equatable, Codable { - public enum Status: Equatable, Codable { - case waitForConfirmation, accepted, running, completed, error, cancelled - - public func canTransitionTo(_ newStatus: Status) -> Bool { - switch (self, newStatus) { - case (.waitForConfirmation, .accepted): return true - case (.waitForConfirmation, .cancelled): return true - case (.accepted, .running): return true - case (.accepted, .cancelled): return true - case (.running, .completed): return true - case (.running, .error): return true - case (.running, .cancelled): return true - default: return false - } - } - } - - public let id: String - public let turnId: String - public var status: Status { - didSet { statusHistory.append(status) } - } - public private(set) var statusHistory: [Status] - public var request: CodeReviewRequest? - public var response: CodeReviewResponse? - public var error: String? - - public init( - id: String = UUID().uuidString, - turnId: String, - status: Status, - request: CodeReviewRequest? = nil, - response: CodeReviewResponse? = nil, - error: String? = nil - ) { - self.id = id - self.turnId = turnId - self.status = status - self.request = request - self.response = response - self.error = error - self.statusHistory = [status] - } - - public static func fromError(turnId: String, error: String) -> CodeReviewRound { - .init(turnId: turnId, status: .error, error: error) - } - - public func withResponse(_ response: CodeReviewResponse) -> CodeReviewRound { - var round = self - round.response = response - return round - } - - public func withStatus(_ status: Status) -> CodeReviewRound { - var round = self - round.status = status - return round - } - - public func withError(_ error: String) -> CodeReviewRound { - var round = self - round.error = error - round.status = .error - return round - } -} - -extension Array where Element == ReviewComment { - // Order in asc - public mutating func sortByEndLine() { - self.sort(by: { $0.range.end.line < $1.range.end.line }) - } -} diff --git a/Tool/Sources/ConversationServiceProvider/ConversationServiceProvider.swift b/Tool/Sources/ConversationServiceProvider/ConversationServiceProvider.swift deleted file mode 100644 index 12d51564..00000000 --- a/Tool/Sources/ConversationServiceProvider/ConversationServiceProvider.swift +++ /dev/null @@ -1,417 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import CodableWrappers -import LanguageServerProtocol - -public protocol ConversationServiceType { - func createConversation(_ request: ConversationRequest, workspace: WorkspaceInfo) async throws - func createTurn(with conversationId: String, request: ConversationRequest, workspace: WorkspaceInfo) async throws - func cancelProgress(_ workDoneToken: String, workspace: WorkspaceInfo) async throws - func rateConversation(turnId: String, rating: ConversationRating, workspace: WorkspaceInfo) async throws - func copyCode(request: CopyCodeRequest, workspace: WorkspaceInfo) async throws - func templates(workspace: WorkspaceInfo) async throws -> [ChatTemplate]? - func models(workspace: WorkspaceInfo) async throws -> [CopilotModel]? - func notifyDidChangeWatchedFiles(_ event: DidChangeWatchedFilesEvent, workspace: WorkspaceInfo) async throws - func agents(workspace: WorkspaceInfo) async throws -> [ChatAgent]? - func notifyChangeTextDocument(fileURL: URL, content: String, version: Int, workspace: WorkspaceInfo) async throws - func reviewChanges(workspace: WorkspaceInfo, params: ReviewChangesParams) async throws -> CodeReviewResult? -} - -public protocol ConversationServiceProvider { - func createConversation(_ request: ConversationRequest, workspaceURL: URL?) async throws - func createTurn(with conversationId: String, request: ConversationRequest, workspaceURL: URL?) async throws - func stopReceivingMessage(_ workDoneToken: String, workspaceURL: URL?) async throws - func rateConversation(turnId: String, rating: ConversationRating, workspaceURL: URL?) async throws - func copyCode(_ request: CopyCodeRequest, workspaceURL: URL?) async throws - func templates() async throws -> [ChatTemplate]? - func models() async throws -> [CopilotModel]? - func notifyDidChangeWatchedFiles(_ event: DidChangeWatchedFilesEvent, workspace: WorkspaceInfo) async throws - func agents() async throws -> [ChatAgent]? - func notifyChangeTextDocument(fileURL: URL, content: String, version: Int, workspaceURL: URL?) async throws - func reviewChanges(_ params: ReviewChangesParams) async throws -> CodeReviewResult? -} - -public struct FileReference: Hashable, Codable, Equatable { - public let url: URL - public let relativePath: String? - public let fileName: String? - public var isCurrentEditor: Bool = false - - public init(url: URL, relativePath: String?, fileName: String?, isCurrentEditor: Bool = false) { - self.url = url - self.relativePath = relativePath - self.fileName = fileName - self.isCurrentEditor = isCurrentEditor - } - - public init(url: URL, isCurrentEditor: Bool = false) { - self.url = url - self.relativePath = nil - self.fileName = nil - self.isCurrentEditor = isCurrentEditor - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(url) - hasher.combine(isCurrentEditor) - } - - public static func == (lhs: FileReference, rhs: FileReference) -> Bool { - return lhs.url == rhs.url && lhs.isCurrentEditor == rhs.isCurrentEditor - } -} - -extension FileReference { - public func getPathRelativeToHome() -> String { - let filePath = url.path - guard !filePath.isEmpty else { return "" } - - let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path - if !homeDirectory.isEmpty { - return filePath.replacingOccurrences(of: homeDirectory, with: "~") - } - - return filePath - } -} - -public enum ImageReferenceSource: String, Codable { - case file = "file" - case pasted = "pasted" - case screenshot = "screenshot" -} - -public struct ImageReference: Equatable, Codable, Hashable { - public var data: Data - public var fileUrl: URL? - public var source: ImageReferenceSource - - public init(data: Data, source: ImageReferenceSource) { - self.data = data - self.source = source - } - - public init(data: Data, fileUrl: URL) { - self.data = data - self.fileUrl = fileUrl - self.source = .file - } - - public func dataURL(imageType: String = "") -> String { - let base64String = data.base64EncodedString() - var type = imageType - if let url = fileUrl, imageType.isEmpty { - type = url.pathExtension - } - - let mimeType: String - switch type { - case "png": - mimeType = "image/png" - case "jpeg", "jpg": - mimeType = "image/jpeg" - case "bmp": - mimeType = "image/bmp" - case "gif": - mimeType = "image/gif" - case "webp": - mimeType = "image/webp" - case "tiff", "tif": - mimeType = "image/tiff" - default: - mimeType = "image/png" - } - - return "data:\(mimeType);base64,\(base64String)" - } -} - -public enum MessageContentType: String, Codable { - case text = "text" - case imageUrl = "image_url" -} - -public enum ImageDetail: String, Codable { - case low = "low" - case high = "high" -} - -public struct ChatCompletionImageURL: Codable,Equatable { - let url: String - let detail: ImageDetail? - - public init(url: String, detail: ImageDetail? = nil) { - self.url = url - self.detail = detail - } -} - -public struct ChatCompletionContentPartText: Codable, Equatable { - public let type: MessageContentType - public let text: String - - public init(text: String) { - self.type = .text - self.text = text - } -} - -public struct ChatCompletionContentPartImage: Codable, Equatable { - public let type: MessageContentType - public let imageUrl: ChatCompletionImageURL - - public init(imageUrl: ChatCompletionImageURL) { - self.type = .imageUrl - self.imageUrl = imageUrl - } - - public init(url: String, detail: ImageDetail? = nil) { - self.type = .imageUrl - self.imageUrl = ChatCompletionImageURL(url: url, detail: detail) - } -} - -public enum ChatCompletionContentPart: Codable, Equatable { - case text(ChatCompletionContentPartText) - case imageUrl(ChatCompletionContentPartImage) - - private enum CodingKeys: String, CodingKey { - case type - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(MessageContentType.self, forKey: .type) - - switch type { - case .text: - self = .text(try ChatCompletionContentPartText(from: decoder)) - case .imageUrl: - self = .imageUrl(try ChatCompletionContentPartImage(from: decoder)) - } - } - - public func encode(to encoder: Encoder) throws { - switch self { - case .text(let content): - try content.encode(to: encoder) - case .imageUrl(let content): - try content.encode(to: encoder) - } - } -} - -public enum MessageContent: Codable, Equatable { - case string(String) - case messageContentArray([ChatCompletionContentPart]) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let stringValue = try? container.decode(String.self) { - self = .string(stringValue) - } else if let arrayValue = try? container.decode([ChatCompletionContentPart].self) { - self = .messageContentArray(arrayValue) - } else { - throw DecodingError.typeMismatch(MessageContent.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected String or Array of MessageContent")) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .string(let value): - try container.encode(value) - case .messageContentArray(let value): - try container.encode(value) - } - } -} - -public struct TurnSchema: Codable { - public var request: MessageContent - public var response: String? - public var agentSlug: String? - public var turnId: String? - - public init(request: String, response: String? = nil, agentSlug: String? = nil, turnId: String? = nil) { - self.request = .string(request) - self.response = response - self.agentSlug = agentSlug - self.turnId = turnId - } - - public init( - request: [ChatCompletionContentPart], - response: String? = nil, - agentSlug: String? = nil, - turnId: String? = nil - ) { - self.request = .messageContentArray(request) - self.response = response - self.agentSlug = agentSlug - self.turnId = turnId - } - - public init(request: MessageContent, response: String? = nil, agentSlug: String? = nil, turnId: String? = nil) { - self.request = request - self.response = response - self.agentSlug = agentSlug - self.turnId = turnId - } -} - -public struct ConversationRequest { - public var workDoneToken: String - public var content: String - public var contentImages: [ChatCompletionContentPartImage] = [] - public var workspaceFolder: String - public var activeDoc: Doc? - public var skills: [String] - public var ignoredSkills: [String]? - public var references: [FileReference]? - public var model: String? - public var turns: [TurnSchema] - public var agentMode: Bool = false - public var userLanguage: String? = nil - public var turnId: String? = nil - - public init( - workDoneToken: String, - content: String, - contentImages: [ChatCompletionContentPartImage] = [], - workspaceFolder: String, - activeDoc: Doc? = nil, - skills: [String], - ignoredSkills: [String]? = nil, - references: [FileReference]? = nil, - model: String? = nil, - turns: [TurnSchema] = [], - agentMode: Bool = false, - userLanguage: String?, - turnId: String? = nil - ) { - self.workDoneToken = workDoneToken - self.content = content - self.contentImages = contentImages - self.workspaceFolder = workspaceFolder - self.activeDoc = activeDoc - self.skills = skills - self.ignoredSkills = ignoredSkills - self.references = references - self.model = model - self.turns = turns - self.agentMode = agentMode - self.userLanguage = userLanguage - self.turnId = turnId - } -} - -public struct CopyCodeRequest { - public var turnId: String - public var codeBlockIndex: Int - public var copyType: CopyKind - public var copiedCharacters: Int - public var totalCharacters: Int - public var copiedText: String - - init(turnId: String, codeBlockIndex: Int, copyType: CopyKind, copiedCharacters: Int, totalCharacters: Int, copiedText: String) { - self.turnId = turnId - self.codeBlockIndex = codeBlockIndex - self.copyType = copyType - self.copiedCharacters = copiedCharacters - self.totalCharacters = totalCharacters - self.copiedText = copiedText - } -} - -public enum ConversationRating: Int, Codable { - case unrated = 0 - case helpful = 1 - case unhelpful = -1 -} - -public enum CopyKind: Int, Codable { - case keyboard = 1 - case toolbar = 2 -} - - -public struct ConversationFollowUp: Codable, Equatable { - public var message: String - public var id: String - public var type: String - - public init(message: String, id: String, type: String) { - self.message = message - self.id = id - self.type = type - } -} - -public struct ConversationProgressStep: Codable, Equatable, Identifiable { - public enum StepStatus: String, Codable { - case running, completed, failed, cancelled - } - - public struct StepError: Codable, Equatable { - public let message: String - } - - public let id: String - public let title: String - public let description: String? - public var status: StepStatus - public let error: StepError? - - public init(id: String, title: String, description: String?, status: StepStatus, error: StepError?) { - self.id = id - self.title = title - self.description = description - self.status = status - self.error = error - } -} - -public struct DidChangeWatchedFilesEvent: Codable { - public var workspaceUri: String - public var changes: [FileEvent] - - public init(workspaceUri: String, changes: [FileEvent]) { - self.workspaceUri = workspaceUri - self.changes = changes - } -} - -public struct AgentRound: Codable, Equatable { - public let roundId: Int - public var reply: String - public var toolCalls: [AgentToolCall]? - - public init(roundId: Int, reply: String, toolCalls: [AgentToolCall]? = []) { - self.roundId = roundId - self.reply = reply - self.toolCalls = toolCalls - } -} - -public struct AgentToolCall: Codable, Equatable, Identifiable { - public let id: String - public let name: String - public var progressMessage: String? - public var status: ToolCallStatus - public var error: String? - public var invokeParams: InvokeClientToolParams? - - public enum ToolCallStatus: String, Codable { - case waitForConfirmation, accepted, running, completed, error, cancelled - } - - public init(id: String, name: String, progressMessage: String? = nil, status: ToolCallStatus, error: String? = nil, invokeParams: InvokeClientToolParams? = nil) { - self.id = id - self.name = name - self.progressMessage = progressMessage - self.status = status - self.error = error - self.invokeParams = invokeParams - } -} diff --git a/Tool/Sources/ConversationServiceProvider/LSPTypes.swift b/Tool/Sources/ConversationServiceProvider/LSPTypes.swift deleted file mode 100644 index a0c109f2..00000000 --- a/Tool/Sources/ConversationServiceProvider/LSPTypes.swift +++ /dev/null @@ -1,407 +0,0 @@ -import Foundation -import JSONRPC -import LanguageServerProtocol - -// MARK: Conversation template -public struct ChatTemplate: Codable, Equatable { - public var id: String - public var description: String - public var shortDescription: String - public var scopes: [PromptTemplateScope] - - public init(id: String, description: String, shortDescription: String, scopes: [PromptTemplateScope]=[]) { - self.id = id - self.description = description - self.shortDescription = shortDescription - self.scopes = scopes - } -} - -public enum PromptTemplateScope: String, Codable, Equatable { - case chatPanel = "chat-panel" - case editPanel = "edit-panel" - case agentPanel = "agent-panel" - case editor = "editor" - case inline = "inline" - case completion = "completion" -} - -public struct CopilotLanguageServerError: Codable { - public var code: Int? - public var message: String - public var responseIsIncomplete: Bool? - public var responseIsFiltered: Bool? -} - -// MARK: Copilot Model -public struct CopilotModel: Codable, Equatable { - public let modelFamily: String - public let modelName: String - public let id: String - public let modelPolicy: CopilotModelPolicy? - public let scopes: [PromptTemplateScope] - public let preview: Bool - public let isChatDefault: Bool - public let isChatFallback: Bool - public let capabilities: CopilotModelCapabilities - public let billing: CopilotModelBilling? -} - -public struct CopilotModelPolicy: Codable, Equatable { - public let state: String - public let terms: String -} - -public struct CopilotModelCapabilities: Codable, Equatable { - public let supports: CopilotModelCapabilitiesSupports -} - -public struct CopilotModelCapabilitiesSupports: Codable, Equatable { - public let vision: Bool -} - -public struct CopilotModelBilling: Codable, Equatable, Hashable { - public let isPremium: Bool - public let multiplier: Float -} - -// MARK: Conversation Agents -public struct ChatAgent: Codable, Equatable { - public let slug: String - public let name: String - public let description: String - public let avatarUrl: String? - - public init(slug: String, name: String, description: String, avatarUrl: String?) { - self.slug = slug - self.name = name - self.description = description - self.avatarUrl = avatarUrl - } -} - -// MARK: EditAgent - -public struct RegisterToolsParams: Codable, Equatable { - public let tools: [LanguageModelToolInformation] - - public init(tools: [LanguageModelToolInformation]) { - self.tools = tools - } -} - -public struct LanguageModelToolInformation: Codable, Equatable { - /// The name of the tool. - public let name: String - - /// A description of this tool that may be used by a language model to select it. - public let description: String - - /// A JSON schema for the input this tool accepts. The input must be an object at the top level. - /// A particular language model may not support all JSON schema features. - public let inputSchema: LanguageModelToolSchema? - - public let confirmationMessages: LanguageModelToolConfirmationMessages? - - public init(name: String, description: String, inputSchema: LanguageModelToolSchema?, confirmationMessages: LanguageModelToolConfirmationMessages? = nil) { - self.name = name - self.description = description - self.inputSchema = inputSchema - self.confirmationMessages = confirmationMessages - } -} - -public struct LanguageModelToolSchema: Codable, Equatable { - public let type: String - public let properties: [String: ToolInputPropertySchema] - public let required: [String] - - public init(type: String, properties: [String : ToolInputPropertySchema], required: [String]) { - self.type = type - self.properties = properties - self.required = required - } -} - -public struct ToolInputPropertySchema: Codable, Equatable { - public struct Items: Codable, Equatable { - public let type: String - - public init(type: String) { - self.type = type - } - } - - public let type: String - public let description: String - public let items: Items? - - public init(type: String, description: String, items: Items? = nil) { - self.type = type - self.description = description - self.items = items - } -} - -public struct LanguageModelToolConfirmationMessages: Codable, Equatable { - public let title: String - public let message: String - - public init(title: String, message: String) { - self.title = title - self.message = message - } -} - -public struct InvokeClientToolParams: Codable, Equatable { - /// The name of the tool to be invoked. - public let name: String - - /// The input to the tool. - public let input: [String: AnyCodable]? - - /// The ID of the conversation this tool invocation belongs to. - public let conversationId: String - - /// The ID of the turn this tool invocation belongs to. - public let turnId: String - - /// The ID of the round this tool invocation belongs to. - public let roundId: Int - - /// The unique ID for this specific tool call. - public let toolCallId: String - - /// The title of the tool confirmation. - public let title: String? - - /// The message of the tool confirmation. - public let message: String? -} - -/// A helper type to encode/decode `Any` values in JSON. -public struct AnyCodable: Codable, Equatable { - public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as [AnyCodable], rhs as [AnyCodable]): - return lhs == rhs - case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]): - return lhs == rhs - default: - return false - } - } - - public let value: Any - - public init(_ value: Any) { - self.value = value - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let intValue = try? container.decode(Int.self) { - value = intValue - } else if let doubleValue = try? container.decode(Double.self) { - value = doubleValue - } else if let stringValue = try? container.decode(String.self) { - value = stringValue - } else if let boolValue = try? container.decode(Bool.self) { - value = boolValue - } else if let arrayValue = try? container.decode([AnyCodable].self) { - value = arrayValue.map { $0.value } - } else if let dictionaryValue = try? container.decode([String: AnyCodable].self) { - value = dictionaryValue.mapValues { $0.value } - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type") - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - if let intValue = value as? Int { - try container.encode(intValue) - } else if let doubleValue = value as? Double { - try container.encode(doubleValue) - } else if let stringValue = value as? String { - try container.encode(stringValue) - } else if let boolValue = value as? Bool { - try container.encode(boolValue) - } else if let arrayValue = value as? [Any] { - try container.encode(arrayValue.map { AnyCodable($0) }) - } else if let dictionaryValue = value as? [String: Any] { - try container.encode(dictionaryValue.mapValues { AnyCodable($0) }) - } else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: container.codingPath, debugDescription: "Unsupported type")) - } - } -} - -public typealias InvokeClientToolRequest = JSONRPCRequest - -public enum ToolInvocationStatus: String, Codable { - case success - case error - case cancelled -} - -public struct LanguageModelToolResult: Codable, Equatable { - public struct Content: Codable, Equatable { - public let value: AnyCodable - - public init(value: Any) { - self.value = AnyCodable(value) - } - } - - public let status: ToolInvocationStatus - public let content: [Content] - - public init(status: ToolInvocationStatus = .success, content: [Content]) { - self.status = status - self.content = content - } -} - -public struct Doc: Codable { - var uri: String - - public init(uri: String) { - self.uri = uri - } -} - -public enum ToolConfirmationResult: String, Codable { - /// The user accepted the tool invocation. - case Accept = "accept" - /// The user dismissed the tool invocation. - case Dismiss = "dismiss" -} - -public struct LanguageModelToolConfirmationResult: Codable, Equatable { - /// The result of the confirmation. - public let result: ToolConfirmationResult - - public init(result: ToolConfirmationResult) { - self.result = result - } -} - -public typealias InvokeClientToolConfirmationRequest = JSONRPCRequest - -// MARK: CLS ShowMessage Notification -public struct CopilotShowMessageParams: Codable, Equatable, Hashable { - public var type: MessageType - public var title: String - public var message: String - public var actions: [CopilotMessageActionItem]? - public var location: CopilotMessageLocation - public var panelContext: CopilotMessagePanelContext? - - public init( - type: MessageType, - title: String, - message: String, - actions: [CopilotMessageActionItem]? = nil, - location: CopilotMessageLocation, - panelContext: CopilotMessagePanelContext? = nil - ) { - self.type = type - self.title = title - self.message = message - self.actions = actions - self.location = location - self.panelContext = panelContext - } -} - -public enum CopilotMessageLocation: String, Codable, Equatable, Hashable { - case Panel = "Panel" - case Inline = "Inline" -} - -public struct CopilotMessagePanelContext: Codable, Equatable, Hashable { - public var conversationId: String - public var turnId: String -} - -public struct CopilotMessageActionItem: Codable, Equatable, Hashable { - public var title: String - public var command: ActionCommand? -} - -public struct ActionCommand: Codable, Equatable, Hashable { - public var commandId: String - public var args: LSPAny? -} - -// MARK: - Copilot Code Review - -public struct ReviewChangesParams: Codable, Equatable { - public struct Change: Codable, Equatable { - public let uri: DocumentUri - public let path: String - // The original content of the file before changes were made. Will be empty string if the file is new. - public let baseContent: String - // The current content of the file with changes applied. Will be empty string if the file is deleted. - public let headContent: String - - public init(uri: DocumentUri, path: String, baseContent: String, headContent: String) { - self.uri = uri - self.path = path - self.baseContent = baseContent - self.headContent = headContent - } - } - - public let changes: [Change] - - public init(changes: [Change]) { - self.changes = changes - } -} - -public struct ReviewComment: Codable, Equatable, Hashable { - // Self-defined `id` for using in comment operation. Add an init value to bypass decoding - public let id: String = UUID().uuidString - public let uri: DocumentUri - public let range: LSPRange - public let message: String - // enum: bug, performance, consistency, documentation, naming, readability, style, other - public let kind: String - // enum: low, medium, high - public let severity: String - public let suggestion: String? - - public init( - uri: DocumentUri, - range: LSPRange, - message: String, - kind: String, - severity: String, - suggestion: String? - ) { - self.uri = uri - self.range = range - self.message = message - self.kind = kind - self.severity = severity - self.suggestion = suggestion - } -} - -public struct CodeReviewResult: Codable, Equatable { - public let comments: [ReviewComment] - - public init(comments: [ReviewComment]) { - self.comments = comments - } -} diff --git a/Tool/Sources/ConversationServiceProvider/ToolNames.swift b/Tool/Sources/ConversationServiceProvider/ToolNames.swift deleted file mode 100644 index 7b9d12c9..00000000 --- a/Tool/Sources/ConversationServiceProvider/ToolNames.swift +++ /dev/null @@ -1,9 +0,0 @@ - -public enum ToolName: String { - case runInTerminal = "run_in_terminal" - case getTerminalOutput = "get_terminal_output" - case getErrors = "get_errors" - case insertEditIntoFile = "insert_edit_into_file" - case createFile = "create_file" - case fetchWebPage = "fetch_webpage" -} diff --git a/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift b/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift deleted file mode 100644 index df296cc8..00000000 --- a/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation - -private actor TimedDebounceFunction { - let duration: TimeInterval - let block: (Element) async -> Void - - var task: Task? - var lastValue: Element? - var lastFireTime: Date = .init(timeIntervalSince1970: 0) - - init(duration: TimeInterval, block: @escaping (Element) async -> Void) { - self.duration = duration - self.block = block - } - - func callAsFunction(_ value: Element) async { - task?.cancel() - if lastFireTime.timeIntervalSinceNow < -duration { - await fire(value) - task = nil - } else { - lastValue = value - task = Task.detached { [weak self, duration] in - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) - await self?.fire(value) - } - } - } - - func finish() async { - task?.cancel() - if let lastValue { - await fire(lastValue) - } - } - - private func fire(_ value: Element) async { - lastFireTime = Date() - lastValue = nil - await block(value) - } -} - -public extension AsyncSequence { - /// Debounce, but only if the value is received within a certain time frame. - /// - /// In the future when we drop macOS 12 support we should just use chunked from AsyncAlgorithms. - func timedDebounce( - for duration: TimeInterval - ) -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - Task { - let function = TimedDebounceFunction(duration: duration) { value in - continuation.yield(value) - } - do { - for try await value in self { - await function(value) - } - await function.finish() - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - } - } -} - diff --git a/Tool/Sources/DebounceFunction/DebounceFunction.swift b/Tool/Sources/DebounceFunction/DebounceFunction.swift deleted file mode 100644 index 66a5fdd1..00000000 --- a/Tool/Sources/DebounceFunction/DebounceFunction.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -public actor DebounceFunction { - let duration: TimeInterval - let block: (T) async -> Void - - var task: Task? - - public init(duration: TimeInterval, block: @escaping (T) async -> Void) { - self.duration = duration - self.block = block - } - - public func cancel() { - task?.cancel() - } - - public func callAsFunction(_ t: T) async { - task?.cancel() - task = Task { [block, duration] in - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) - await block(t) - } - } -} - -public actor DebounceRunner { - let duration: TimeInterval - - var task: Task? - - public init(duration: TimeInterval) { - self.duration = duration - } - - public func cancel() { - task?.cancel() - } - - public func debounce(_ block: @escaping () async -> Void) { - task?.cancel() - task = Task { [duration] in - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) - await block() - } - } -} - diff --git a/Tool/Sources/DebounceFunction/ThrottleFunction.swift b/Tool/Sources/DebounceFunction/ThrottleFunction.swift deleted file mode 100644 index 3a0771c4..00000000 --- a/Tool/Sources/DebounceFunction/ThrottleFunction.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -public actor ThrottleFunction { - let duration: TimeInterval - let block: (T) async -> Void - - var task: Task? - var lastFinishTime: Date = .init(timeIntervalSince1970: 0) - var now: () -> Date = { Date() } - - public init(duration: TimeInterval, block: @escaping (T) async -> Void) { - self.duration = duration - self.block = block - } - - public func callAsFunction(_ t: T) async { - if task == nil { - scheduleTask(t, wait: now().timeIntervalSince(lastFinishTime) < duration) - } - } - - func scheduleTask(_ t: T, wait: Bool) { - task = Task.detached { [weak self] in - guard let self else { return } - do { - if wait { - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) - } - await block(t) - await finishTask() - } catch { - await finishTask() - } - } - } - - func finishTask() { - task = nil - lastFinishTime = now() - } -} - diff --git a/Tool/Sources/FileSystem/ByteString.swift b/Tool/Sources/FileSystem/ByteString.swift deleted file mode 100644 index af4a3b45..00000000 --- a/Tool/Sources/FileSystem/ByteString.swift +++ /dev/null @@ -1,160 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -import Foundation - -/// A `ByteString` represents a sequence of bytes. -/// -/// This struct provides useful operations for working with buffers of -/// bytes. Conceptually it is just a contiguous array of bytes (UInt8), but it -/// contains methods and default behavior suitable for common operations done -/// using bytes strings. -/// -/// This struct *is not* intended to be used for significant mutation of byte -/// strings, we wish to retain the flexibility to micro-optimize the memory -/// allocation of the storage (for example, by inlining the storage for small -/// strings or and by eliminating wasted space in growable arrays). For -/// construction of byte arrays, clients should use the `WritableByteStream` class -/// and then convert to a `ByteString` when complete. -public struct ByteString: ExpressibleByArrayLiteral, Hashable, Sendable { - /// The buffer contents. - @usableFromInline - internal var _bytes: [UInt8] - - /// Create an empty byte string. - @inlinable - public init() { - _bytes = [] - } - - /// Create a byte string from a byte array literal. - @inlinable - public init(arrayLiteral contents: UInt8...) { - _bytes = contents - } - - /// Create a byte string from an array of bytes. - @inlinable - public init(_ contents: [UInt8]) { - _bytes = contents - } - - /// Create a byte string from an array slice. - @inlinable - public init(_ contents: ArraySlice) { - _bytes = Array(contents) - } - - /// Create a byte string from an byte buffer. - @inlinable - public init (_ contents: S) where S.Iterator.Element == UInt8 { - _bytes = [UInt8](contents) - } - - /// Create a byte string from the UTF8 encoding of a string. - @inlinable - public init(encodingAsUTF8 string: String) { - _bytes = [UInt8](string.utf8) - } - - /// Access the byte string contents as an array. - @inlinable - public var contents: [UInt8] { - return _bytes - } - - /// Return the byte string size. - @inlinable - public var count: Int { - return _bytes.count - } - - /// Gives a non-escaping closure temporary access to an immutable `Data` instance wrapping the `ByteString` without - /// copying any memory around. - /// - /// - Parameters: - /// - closure: The closure that will have access to a `Data` instance for the duration of its lifetime. - @inlinable - public func withData(_ closure: (Data) throws -> T) rethrows -> T { - return try _bytes.withUnsafeBytes { pointer -> T in - let mutatingPointer = UnsafeMutableRawPointer(mutating: pointer.baseAddress!) - let data = Data(bytesNoCopy: mutatingPointer, count: pointer.count, deallocator: .none) - return try closure(data) - } - } - - /// Returns a `String` lowercase hexadecimal representation of the contents of the `ByteString`. - @inlinable - public var hexadecimalRepresentation: String { - _bytes.reduce("") { - var str = String($1, radix: 16) - // The above method does not do zero padding. - if str.count == 1 { - str = "0" + str - } - return $0 + str - } - } -} - -/// Conform to CustomDebugStringConvertible. -extension ByteString: CustomStringConvertible { - /// Return the string decoded as a UTF8 sequence, or traps if not possible. - public var description: String { - return cString - } - - /// Return the string decoded as a UTF8 sequence, if possible. - @inlinable - public var validDescription: String? { - // FIXME: This is very inefficient, we need a way to pass a buffer. It - // is also wrong if the string contains embedded '\0' characters. - let tmp = _bytes + [UInt8(0)] - return tmp.withUnsafeBufferPointer { ptr in - return String(validatingUTF8: unsafeBitCast(ptr.baseAddress, to: UnsafePointer.self)) - } - } - - /// Return the string decoded as a UTF8 sequence, substituting replacement - /// characters for ill-formed UTF8 sequences. - @inlinable - public var cString: String { - return String(decoding: _bytes, as: Unicode.UTF8.self) - } - - @available(*, deprecated, message: "use description or validDescription instead") - public var asString: String? { - return validDescription - } -} - -/// ByteStreamable conformance for a ByteString. -extension ByteString: ByteStreamable { - @inlinable - public func write(to stream: WritableByteStream) { - stream.write(_bytes) - } -} - -/// StringLiteralConvertable conformance for a ByteString. -extension ByteString: ExpressibleByStringLiteral { - public typealias UnicodeScalarLiteralType = StringLiteralType - public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType - - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - _bytes = [UInt8](value.utf8) - } - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - _bytes = [UInt8](value.utf8) - } - public init(stringLiteral value: StringLiteralType) { - _bytes = [UInt8](value.utf8) - } -} diff --git a/Tool/Sources/FileSystem/FileInfo.swift b/Tool/Sources/FileSystem/FileInfo.swift deleted file mode 100644 index 54a2e54c..00000000 --- a/Tool/Sources/FileSystem/FileInfo.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - */ - -import Foundation - -#if swift(<5.6) -extension FileAttributeType: UnsafeSendable {} -extension Date: UnsafeSendable {} -#endif - -/// File system information for a particular file. -public struct FileInfo: Equatable, Codable, Sendable { - - /// The device number. - public let device: UInt64 - - /// The inode number. - public let inode: UInt64 - - /// The size of the file. - public let size: UInt64 - - /// The modification time of the file. - public let modTime: Date - - /// Kind of file system entity. - public let posixPermissions: Int16 - - /// Kind of file system entity. - public let fileType: FileAttributeType - - public init(_ attrs: [FileAttributeKey : Any]) { - let device = (attrs[.systemNumber] as? NSNumber)?.uint64Value - assert(device != nil) - self.device = device! - - let inode = attrs[.systemFileNumber] as? UInt64 - assert(inode != nil) - self.inode = inode! - - let posixPermissions = (attrs[.posixPermissions] as? NSNumber)?.int16Value - assert(posixPermissions != nil) - self.posixPermissions = posixPermissions! - - let fileType = attrs[.type] as? FileAttributeType - assert(fileType != nil) - self.fileType = fileType! - - let size = attrs[.size] as? UInt64 - assert(size != nil) - self.size = size! - - let modTime = attrs[.modificationDate] as? Date - assert(modTime != nil) - self.modTime = modTime! - } -} - -extension FileAttributeType: Codable {} diff --git a/Tool/Sources/FileSystem/FileSystem.swift b/Tool/Sources/FileSystem/FileSystem.swift deleted file mode 100644 index 39f0bed6..00000000 --- a/Tool/Sources/FileSystem/FileSystem.swift +++ /dev/null @@ -1,1303 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - */ - -import Dispatch -import Foundation - -#if canImport(Glibc) -@_exported import Glibc -#elseif canImport(Musl) -@_exported import Musl -#elseif os(Windows) -@_exported import CRT -@_exported import WinSDK -#else -@_exported import Darwin.C -#endif - -public struct FileSystemError: Error, Equatable, Sendable { - public enum Kind: Equatable, Sendable { - /// Access to the path is denied. - /// - /// This is used when an operation cannot be completed because a component of - /// the path cannot be accessed. - /// - /// Used in situations that correspond to the POSIX EACCES error code. - case invalidAccess - - /// IO Error encoding - /// - /// This is used when an operation cannot be completed due to an otherwise - /// unspecified IO error. - case ioError(code: Int32) - - /// Is a directory - /// - /// This is used when an operation cannot be completed because a component - /// of the path which was expected to be a file was not. - /// - /// Used in situations that correspond to the POSIX EISDIR error code. - case isDirectory - - /// No such path exists. - /// - /// This is used when a path specified does not exist, but it was expected - /// to. - /// - /// Used in situations that correspond to the POSIX ENOENT error code. - case noEntry - - /// Not a directory - /// - /// This is used when an operation cannot be completed because a component - /// of the path which was expected to be a directory was not. - /// - /// Used in situations that correspond to the POSIX ENOTDIR error code. - case notDirectory - - /// Unsupported operation - /// - /// This is used when an operation is not supported by the concrete file - /// system implementation. - case unsupported - - /// An unspecific operating system error at a given path. - case unknownOSError - - /// File or folder already exists at destination. - /// - /// This is thrown when copying or moving a file or directory but the destination - /// path already contains a file or folder. - case alreadyExistsAtDestination - - /// If an unspecified error occurs when trying to change directories. - case couldNotChangeDirectory - - /// If a mismatch is detected in byte count when writing to a file. - case mismatchedByteCount(expected: Int, actual: Int) - } - - /// The kind of the error being raised. - public let kind: Kind - - /// The absolute path to the file associated with the error, if available. - public let path: AbsolutePath? - - public init(_ kind: Kind, _ path: AbsolutePath? = nil) { - self.kind = kind - self.path = path - } -} - -extension FileSystemError: CustomNSError { - public var errorUserInfo: [String: Any] { - return [NSLocalizedDescriptionKey: "\(self)"] - } -} - -public extension FileSystemError { - init(errno: Int32, _ path: AbsolutePath) { - switch errno { - case EACCES: - self.init(.invalidAccess, path) - case EISDIR: - self.init(.isDirectory, path) - case ENOENT: - self.init(.noEntry, path) - case ENOTDIR: - self.init(.notDirectory, path) - case EEXIST: - self.init(.alreadyExistsAtDestination, path) - default: - self.init(.ioError(code: errno), path) - } - } -} - -/// Defines the file modes. -public enum FileMode: Sendable { - public enum Option: Int, Sendable { - case recursive - case onlyFiles - } - - case userUnWritable - case userWritable - case executable - - public func setMode(_ originalMode: Int16) -> Int16 { - switch self { - case .userUnWritable: - // r-x rwx rwx - return originalMode & 0o577 - case .userWritable: - // -w- --- --- - return originalMode | 0o200 - case .executable: - // --x --x --x - return originalMode | 0o111 - } - } -} - -/// Extended file system attributes that can applied to a given file path. See also -/// ``FileSystem/hasAttribute(_:_:)``. -public enum FileSystemAttribute: RawRepresentable { - #if canImport(Darwin) - case quarantine - #endif - - public init?(rawValue: String) { - switch rawValue { - #if canImport(Darwin) - case "com.apple.quarantine": - self = .quarantine - #endif - default: - return nil - } - } - - public var rawValue: String { - switch self { - #if canImport(Darwin) - case .quarantine: - return "com.apple.quarantine" - #endif - } - } -} - -// FIXME: Design an asynchronous story? -// -/// Abstracted access to file system operations. -/// -/// This protocol is used to allow most of the codebase to interact with a -/// natural filesystem interface, while still allowing clients to transparently -/// substitute a virtual file system or redirect file system operations. -/// -/// - Note: All of these APIs are synchronous and can block. -public protocol FileSystem: Sendable { - /// Check whether the given path exists and is accessible. - @_disfavoredOverload - func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool - - /// Check whether the given path is accessible and a directory. - func isDirectory(_ path: AbsolutePath) -> Bool - - /// Check whether the given path is accessible and a file. - func isFile(_ path: AbsolutePath) -> Bool - - /// Check whether the given path is an accessible and executable file. - func isExecutableFile(_ path: AbsolutePath) -> Bool - - /// Check whether the given path is accessible and is a symbolic link. - func isSymlink(_ path: AbsolutePath) -> Bool - - /// Check whether the given path is accessible and readable. - func isReadable(_ path: AbsolutePath) -> Bool - - /// Check whether the given path is accessible and writable. - func isWritable(_ path: AbsolutePath) -> Bool - - /// Returns any known item replacement directories for a given path. These may be used by - /// platform-specific - /// libraries to handle atomic file system operations, such as deletion. - func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] - - @available(*, deprecated, message: "use `hasAttribute(_:_:)` instead") - func hasQuarantineAttribute(_ path: AbsolutePath) -> Bool - - /// Returns `true` if a given path has an attribute with a given name applied when file system - /// supports this - /// attribute. Returns `false` if such attribute is not applied or it isn't supported. - func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool - - // FIXME: Actual file system interfaces will allow more efficient access to - // more data than just the name here. - // - /// Get the contents of the given directory, in an undefined order. - func _getDirectoryContents( - _ path: AbsolutePath, - includingPropertiesForKeys: [URLResourceKey]?, - options: FileManager.DirectoryEnumerationOptions - ) throws -> [AbsolutePath] - - /// Get the current working directory (similar to `getcwd(3)`), which can be - /// different for different (virtualized) implementations of a FileSystem. - /// The current working directory can be empty if e.g. the directory became - /// unavailable while the current process was still working in it. - /// This follows the POSIX `getcwd(3)` semantics. - @_disfavoredOverload - var currentWorkingDirectory: AbsolutePath? { get } - - /// Change the current working directory. - /// - Parameters: - /// - path: The path to the directory to change the current working directory to. - func changeCurrentWorkingDirectory(to path: AbsolutePath) throws - - /// Get the home directory of current user - @_disfavoredOverload - var homeDirectory: AbsolutePath { get throws } - - /// Get the caches directory of current user - @_disfavoredOverload - var cachesDirectory: AbsolutePath? { get } - - /// Get the temp directory - @_disfavoredOverload - var tempDirectory: AbsolutePath { get throws } - - /// Create the given directory. - func createDirectory(_ path: AbsolutePath) throws - - /// Create the given directory. - /// - /// - recursive: If true, create missing parent directories if possible. - func createDirectory(_ path: AbsolutePath, recursive: Bool) throws - - /// Creates a symbolic link of the source path at the target path - /// - Parameters: - /// - path: The path at which to create the link. - /// - destination: The path to which the link points to. - /// - relative: If `relative` is true, the symlink contents will be a relative path, otherwise - /// it will be absolute. - func createSymbolicLink( - _ path: AbsolutePath, - pointingAt destination: AbsolutePath, - relative: Bool - ) throws - - func data(_ path: AbsolutePath) throws -> Data - - // FIXME: This is obviously not a very efficient or flexible API. - // - /// Get the contents of a file. - /// - /// - Returns: The file contents as bytes, or nil if missing. - func readFileContents(_ path: AbsolutePath) throws -> ByteString - - // FIXME: This is obviously not a very efficient or flexible API. - // - /// Write the contents of a file. - func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws - - // FIXME: This is obviously not a very efficient or flexible API. - // - /// Write the contents of a file. - func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws - - /// Recursively deletes the file system entity at `path`. - /// - /// If there is no file system entity at `path`, this function does nothing (in particular, this - /// is not considered - /// to be an error). - func removeFileTree(_ path: AbsolutePath) throws - - /// Change file mode. - func chmod(_ mode: FileMode, path: AbsolutePath, options: Set) throws - - /// Returns the file info of the given path. - /// - /// The method throws if the underlying stat call fails. - func getFileInfo(_ path: AbsolutePath) throws -> FileInfo - - /// Copy a file or directory. - func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws - - /// Move a file or directory. - func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws - - /// Execute the given block while holding the lock. - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () throws -> T - ) throws -> T - - /// Execute the given block while holding the lock. - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () async throws -> T - ) async throws -> T -} - -/// Convenience implementations (default arguments aren't permitted in protocol -/// methods). -public extension FileSystem { - /// exists override with default value. - @_disfavoredOverload - func exists(_ path: AbsolutePath) -> Bool { - return exists(path, followSymlink: true) - } - - /// Default implementation of createDirectory(_:) - func createDirectory(_ path: AbsolutePath) throws { - try createDirectory(path, recursive: false) - } - - // Change file mode. - func chmod(_ mode: FileMode, path: AbsolutePath) throws { - try chmod(mode, path: path, options: []) - } - - // Unless the file system type provides an override for this method, throw - // if `atomically` is `true`, otherwise fall back to whatever implementation already exists. - @_disfavoredOverload - func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws { - guard !atomically else { - throw FileSystemError(.unsupported, path) - } - try writeFileContents(path, bytes: bytes) - } - - /// Write to a file from a stream producer. - @_disfavoredOverload - func writeFileContents(_ path: AbsolutePath, body: (WritableByteStream) -> Void) throws { - let contents = BufferedOutputByteStream() - body(contents) - try createDirectory(path.parentDirectory, recursive: true) - try writeFileContents(path, bytes: contents.bytes) - } - - func getFileInfo(_ path: AbsolutePath) throws -> FileInfo { - throw FileSystemError(.unsupported, path) - } - - func withLock(on path: AbsolutePath, _ body: () throws -> T) throws -> T { - return try withLock(on: path, type: .exclusive, body) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - _ body: () throws -> T - ) throws -> T { - return try withLock(on: path, type: type, blocking: true, body) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () throws -> T - ) throws -> T { - throw FileSystemError(.unsupported, path) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - _ body: () async throws -> T - ) async throws -> T { - return try await withLock(on: path, type: type, blocking: true, body) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () async throws -> T - ) async throws -> T { - throw FileSystemError(.unsupported, path) - } - - func hasQuarantineAttribute(_: AbsolutePath) -> Bool { false } - - func hasAttribute(_: FileSystemAttribute, _: AbsolutePath) -> Bool { false } - - func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] { [] } -} - -/// Concrete FileSystem implementation which communicates with the local file system. -private struct LocalFileSystem: FileSystem { - func isExecutableFile(_ path: AbsolutePath) -> Bool { - // Our semantics doesn't consider directories. - return (isFile(path) || isSymlink(path)) && FileManager.default - .isExecutableFile(atPath: path.pathString) - } - - func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool { - if followSymlink { - return FileManager.default.fileExists(atPath: path.pathString) - } - return (try? FileManager.default.attributesOfItem(atPath: path.pathString)) != nil - } - - func isDirectory(_ path: AbsolutePath) -> Bool { - var isDirectory: ObjCBool = false - let exists: Bool = FileManager.default.fileExists( - atPath: path.pathString, - isDirectory: &isDirectory - ) - return exists && isDirectory.boolValue - } - - func isFile(_ path: AbsolutePath) -> Bool { - guard let path = try? resolveSymlinks(path) else { - return false - } - let attrs = try? FileManager.default.attributesOfItem(atPath: path.pathString) - return attrs?[.type] as? FileAttributeType == .typeRegular - } - - func isSymlink(_ path: AbsolutePath) -> Bool { - let url = NSURL(fileURLWithPath: path.pathString) - // We are intentionally using `NSURL.resourceValues(forKeys:)` here since it improves - // performance on Darwin platforms. - let result = try? url.resourceValues(forKeys: [.isSymbolicLinkKey]) - return (result?[.isSymbolicLinkKey] as? Bool) == true - } - - func isReadable(_ path: AbsolutePath) -> Bool { - FileManager.default.isReadableFile(atPath: path.pathString) - } - - func isWritable(_ path: AbsolutePath) -> Bool { - FileManager.default.isWritableFile(atPath: path.pathString) - } - - func getFileInfo(_ path: AbsolutePath) throws -> FileInfo { - let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString) - return FileInfo(attrs) - } - - func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool { - #if canImport(Darwin) - let bufLength = getxattr(path.pathString, name.rawValue, nil, 0, 0, 0) - - return bufLength > 0 - #else - return false - #endif - } - - var currentWorkingDirectory: AbsolutePath? { - let cwdStr = FileManager.default.currentDirectoryPath - - #if _runtime(_ObjC) - // The ObjC runtime indicates that the underlying Foundation has ObjC - // interoperability in which case the return type of - // `fileSystemRepresentation` is different from the Swift implementation - // of Foundation. - return try? AbsolutePath(validating: cwdStr) - #else - let fsr: UnsafePointer = cwdStr.fileSystemRepresentation - defer { fsr.deallocate() } - - return try? AbsolutePath(String(cString: fsr)) - #endif - } - - func changeCurrentWorkingDirectory(to path: AbsolutePath) throws { - guard isDirectory(path) else { - throw FileSystemError(.notDirectory, path) - } - - guard FileManager.default.changeCurrentDirectoryPath(path.pathString) else { - throw FileSystemError(.couldNotChangeDirectory, path) - } - } - - var homeDirectory: AbsolutePath { - get throws { - return try AbsolutePath(validating: NSHomeDirectory()) - } - } - - var cachesDirectory: AbsolutePath? { - return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - .flatMap { try? AbsolutePath(validating: $0.path) } - } - - var tempDirectory: AbsolutePath { - get throws { - return try AbsolutePath(validating: NSTemporaryDirectory()) - } - } - - func _getDirectoryContents( - _ path: AbsolutePath, - includingPropertiesForKeys: [URLResourceKey]?, - options: FileManager.DirectoryEnumerationOptions - ) throws -> [AbsolutePath] { - return try FileManager.default.contentsOfDirectory( - at: URL(fileURLWithPath: path.pathString), - includingPropertiesForKeys: includingPropertiesForKeys, - options: options - ).compactMap { try? AbsolutePath(validating: $0.path) } - } - - func createDirectory(_ path: AbsolutePath, recursive: Bool) throws { - // Don't fail if path is already a directory. - if isDirectory(path) { return } - - try FileManager.default.createDirectory( - atPath: path.pathString, - withIntermediateDirectories: recursive, - attributes: [:] - ) - } - - func createSymbolicLink( - _ path: AbsolutePath, - pointingAt destination: AbsolutePath, - relative: Bool - ) throws { - let destString = relative ? destination.relative(to: path.parentDirectory) - .pathString : destination.pathString - try FileManager.default.createSymbolicLink( - atPath: path.pathString, - withDestinationPath: destString - ) - } - - func data(_ path: AbsolutePath) throws -> Data { - try Data(contentsOf: URL(fileURLWithPath: path.pathString)) - } - - func readFileContents(_ path: AbsolutePath) throws -> ByteString { - // Open the file. - guard let fp = fopen(path.pathString, "rb") else { - throw FileSystemError(errno: errno, path) - } - defer { fclose(fp) } - - // Read the data one block at a time. - let data = BufferedOutputByteStream() - var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12) - while true { - let n = fread(&tmpBuffer, 1, tmpBuffer.count, fp) - if n < 0 { - if errno == EINTR { continue } - throw FileSystemError(.ioError(code: errno), path) - } - if n == 0 { - let errno = ferror(fp) - if errno != 0 { - throw FileSystemError(.ioError(code: errno), path) - } - break - } - data.send(tmpBuffer[0..) throws { - guard exists(path) else { return } - func setMode(path: String) throws { - let attrs = try FileManager.default.attributesOfItem(atPath: path) - // Skip if only files should be changed. - if options.contains(.onlyFiles) && attrs[.type] as? FileAttributeType != .typeRegular { - return - } - - // Compute the new mode for this file. - let currentMode = attrs[.posixPermissions] as! Int16 - let newMode = mode.setMode(currentMode) - guard newMode != currentMode else { return } - try FileManager.default.setAttributes( - [.posixPermissions: newMode], - ofItemAtPath: path - ) - } - - try setMode(path: path.pathString) - guard isDirectory(path) else { return } - - guard let traverse = FileManager.default.enumerator( - at: URL(fileURLWithPath: path.pathString), - includingPropertiesForKeys: nil - ) else { - throw FileSystemError(.noEntry, path) - } - - if !options.contains(.recursive) { - traverse.skipDescendants() - } - - while let path = traverse.nextObject() { - try setMode(path: (path as! URL).path) - } - } - - func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } - guard !exists(destinationPath) - else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } - try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL) - } - - func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } - guard !exists(destinationPath) - else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } - try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () throws -> T - ) throws -> T { - try FileLock.withLock(fileToLock: path, type: type, blocking: blocking, body: body) - } - - func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () async throws -> T - ) async throws -> T { - try await FileLock.withLock(fileToLock: path, type: type, blocking: blocking, body: body) - } - - func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] { - let result = try FileManager.default.url( - for: .itemReplacementDirectory, - in: .userDomainMask, - appropriateFor: path.asURL, - create: false - ) - let path = try AbsolutePath(validating: result.path) - // Foundation returns a path that is unique every time, so we return both that path, as well - // as its parent. - return [path, path.parentDirectory] - } -} - -/// Concrete FileSystem implementation which simulates an empty disk. -public final class InMemoryFileSystem: FileSystem { - /// Private internal representation of a file system node. - /// Not thread-safe. - private class Node { - /// The actual node data. - let contents: NodeContents - - init(_ contents: NodeContents) { - self.contents = contents - } - - /// Creates deep copy of the object. - func copy() -> Node { - return Node(contents.copy()) - } - } - - /// Private internal representation the contents of a file system node. - /// Not thread-safe. - private enum NodeContents { - case file(ByteString) - case directory(DirectoryContents) - case symlink(String) - - /// Creates deep copy of the object. - func copy() -> NodeContents { - switch self { - case let .file(bytes): - return .file(bytes) - case let .directory(contents): - return .directory(contents.copy()) - case let .symlink(path): - return .symlink(path) - } - } - } - - /// Private internal representation the contents of a directory. - /// Not thread-safe. - private final class DirectoryContents { - var entries: [String: Node] - - init(entries: [String: Node] = [:]) { - self.entries = entries - } - - /// Creates deep copy of the object. - func copy() -> DirectoryContents { - let contents = DirectoryContents() - for (key, node) in entries { - contents.entries[key] = node.copy() - } - return contents - } - } - - /// The root node of the filesystem. - private var root: Node - - /// Protects `root` and everything underneath it. - /// FIXME: Using a single lock for this is a performance problem, but in - /// reality, the only practical use for InMemoryFileSystem is for unit - /// tests. - private let lock = NSLock() - /// A map that keeps weak references to all locked files. - private var lockFiles = [AbsolutePath: WeakReference]() - /// Used to access lockFiles in a thread safe manner. - private let lockFilesLock = NSLock() - - /// Exclusive file system lock vended to clients through `withLock()`. - /// Used to ensure that DispatchQueues are released when they are no longer in use. - private struct WeakReference { - weak var reference: Value? - - init(_ value: Value?) { - reference = value - } - } - - public init() { - root = Node(.directory(DirectoryContents())) - } - - /// Creates deep copy of the object. - public func copy() -> InMemoryFileSystem { - return lock.withLock { - let fs = InMemoryFileSystem() - fs.root = root.copy() - return fs - } - } - - /// Private function to look up the node corresponding to a path. - /// Not thread-safe. - private func getNode(_ path: AbsolutePath, followSymlink: Bool = true) throws -> Node? { - func getNodeInternal(_ path: AbsolutePath) throws -> Node? { - // If this is the root node, return it. - if path.isRoot { - return root - } - - // Otherwise, get the parent node. - guard let parent = try getNodeInternal(path.parentDirectory) else { - return nil - } - - // If we didn't find a directory, this is an error. - guard case let .directory(contents) = parent.contents else { - throw FileSystemError(.notDirectory, path.parentDirectory) - } - - // Return the directory entry. - let node = contents.entries[path.basename] - - switch node?.contents { - case .directory, .file: - return node - case let .symlink(destination): - let destination = try AbsolutePath( - validating: destination, - relativeTo: path.parentDirectory - ) - return followSymlink ? try getNodeInternal(destination) : node - case .none: - return nil - } - } - - // Get the node that corresponds to the path. - return try getNodeInternal(path) - } - - // MARK: FileSystem Implementation - - public func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool { - return lock.withLock { - do { - switch try getNode(path, followSymlink: followSymlink)?.contents { - case .file, .directory, .symlink: return true - case .none: return false - } - } catch { - return false - } - } - } - - public func isDirectory(_ path: AbsolutePath) -> Bool { - return lock.withLock { - do { - if case .directory? = try getNode(path)?.contents { - return true - } - return false - } catch { - return false - } - } - } - - public func isFile(_ path: AbsolutePath) -> Bool { - return lock.withLock { - do { - if case .file? = try getNode(path)?.contents { - return true - } - return false - } catch { - return false - } - } - } - - public func isSymlink(_ path: AbsolutePath) -> Bool { - return lock.withLock { - do { - if case .symlink? = try getNode(path, followSymlink: false)?.contents { - return true - } - return false - } catch { - return false - } - } - } - - public func isReadable(_ path: AbsolutePath) -> Bool { - exists(path) - } - - public func isWritable(_ path: AbsolutePath) -> Bool { - exists(path) - } - - public func isExecutableFile(_: AbsolutePath) -> Bool { - // FIXME: Always return false until in-memory implementation - // gets permission semantics. - return false - } - - /// Virtualized current working directory. - public var currentWorkingDirectory: AbsolutePath? { - return try? AbsolutePath(validating: "/") - } - - public func changeCurrentWorkingDirectory(to path: AbsolutePath) throws { - throw FileSystemError(.unsupported, path) - } - - public var homeDirectory: AbsolutePath { - get throws { - // FIXME: Maybe we should allow setting this when creating the fs. - return try AbsolutePath(validating: "/home/user") - } - } - - public var cachesDirectory: AbsolutePath? { - return try? homeDirectory.appending(component: "caches") - } - - public var tempDirectory: AbsolutePath { - get throws { - return try AbsolutePath(validating: "/tmp") - } - } - - public func _getDirectoryContents( - _ path: AbsolutePath, - includingPropertiesForKeys: [URLResourceKey]?, - options: FileManager.DirectoryEnumerationOptions - ) throws -> [AbsolutePath] { - return try lock.withLock { - guard let node = try getNode(path) else { - throw FileSystemError(.noEntry, path) - } - guard case let .directory(contents) = node.contents else { - throw FileSystemError(.notDirectory, path) - } - - // FIXME: Perhaps we should change the protocol to allow lazy behavior. - return [String](contents.entries.keys).map { - path.appending(component: $0) - } - } - } - - /// Not thread-safe. - private func _createDirectory(_ path: AbsolutePath, recursive: Bool) throws { - // Ignore if client passes root. - guard !path.isRoot else { - return - } - // Get the parent directory node. - let parentPath = path.parentDirectory - guard let parent = try getNode(parentPath) else { - // If the parent doesn't exist, and we are recursive, then attempt - // to create the parent and retry. - if recursive && path != parentPath { - // Attempt to create the parent. - try _createDirectory(parentPath, recursive: true) - - // Re-attempt creation, non-recursively. - return try _createDirectory(path, recursive: false) - } else { - // Otherwise, we failed. - throw FileSystemError(.noEntry, parentPath) - } - } - - // Check that the parent is a directory. - guard case let .directory(contents) = parent.contents else { - // The parent isn't a directory, this is an error. - throw FileSystemError(.notDirectory, parentPath) - } - - // Check if the node already exists. - if let node = contents.entries[path.basename] { - // Verify it is a directory. - guard case .directory = node.contents else { - // The path itself isn't a directory, this is an error. - throw FileSystemError(.notDirectory, path) - } - - // We are done. - return - } - - // Otherwise, the node does not exist, create it. - contents.entries[path.basename] = Node(.directory(DirectoryContents())) - } - - public func createDirectory(_ path: AbsolutePath, recursive: Bool) throws { - return try lock.withLock { - try _createDirectory(path, recursive: recursive) - } - } - - public func createSymbolicLink( - _ path: AbsolutePath, - pointingAt destination: AbsolutePath, - relative: Bool - ) throws { - return try lock.withLock { - // Create directory to destination parent. - guard let destinationParent = try getNode(path.parentDirectory) else { - throw FileSystemError(.noEntry, path.parentDirectory) - } - - // Check that the parent is a directory. - guard case let .directory(contents) = destinationParent.contents else { - throw FileSystemError(.notDirectory, path.parentDirectory) - } - - guard contents.entries[path.basename] == nil else { - throw FileSystemError(.alreadyExistsAtDestination, path) - } - - let destination = relative ? destination.relative(to: path.parentDirectory) - .pathString : destination.pathString - - contents.entries[path.basename] = Node(.symlink(destination)) - } - } - - public func data(_ path: AbsolutePath) throws -> Data { - return try lock.withLock { - // Get the node. - guard let node = try getNode(path) else { - throw FileSystemError(.noEntry, path) - } - - // Check that the node is a file. - guard case let .file(contents) = node.contents else { - // The path is a directory, this is an error. - throw FileSystemError(.isDirectory, path) - } - - // Return the file contents. - return contents.withData { $0 } - } - } - - public func readFileContents(_ path: AbsolutePath) throws -> ByteString { - return try lock.withLock { - // Get the node. - guard let node = try getNode(path) else { - throw FileSystemError(.noEntry, path) - } - - // Check that the node is a file. - guard case let .file(contents) = node.contents else { - // The path is a directory, this is an error. - throw FileSystemError(.isDirectory, path) - } - - // Return the file contents. - return contents - } - } - - public func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws { - return try lock.withLock { - // It is an error if this is the root node. - let parentPath = path.parentDirectory - guard path != parentPath else { - throw FileSystemError(.isDirectory, path) - } - - // Get the parent node. - guard let parent = try getNode(parentPath) else { - throw FileSystemError(.noEntry, parentPath) - } - - // Check that the parent is a directory. - guard case let .directory(contents) = parent.contents else { - // The parent isn't a directory, this is an error. - throw FileSystemError(.notDirectory, parentPath) - } - - // Check if the node exists. - if let node = contents.entries[path.basename] { - // Verify it is a file. - guard case .file = node.contents else { - // The path is a directory, this is an error. - throw FileSystemError(.isDirectory, path) - } - } - - // Write the file. - contents.entries[path.basename] = Node(.file(bytes)) - } - } - - public func writeFileContents( - _ path: AbsolutePath, - bytes: ByteString, - atomically: Bool - ) throws { - // In memory file system's writeFileContents is already atomic, so ignore the parameter here - // and just call the base implementation. - try writeFileContents(path, bytes: bytes) - } - - public func removeFileTree(_ path: AbsolutePath) throws { - return lock.withLock { - // Ignore root and get the parent node's content if its a directory. - guard !path.isRoot, - let parent = try? getNode(path.parentDirectory), - case let .directory(contents) = parent.contents - else { - return - } - // Set it to nil to release the contents. - contents.entries[path.basename] = nil - } - } - - public func chmod(_ mode: FileMode, path: AbsolutePath, options: Set) throws { - // FIXME: We don't have these semantics in InMemoryFileSystem. - } - - /// Private implementation of core copying function. - /// Not thread-safe. - private func _copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - // Get the source node. - guard let source = try getNode(sourcePath) else { - throw FileSystemError(.noEntry, sourcePath) - } - - // Create directory to destination parent. - guard let destinationParent = try getNode(destinationPath.parentDirectory) else { - throw FileSystemError(.noEntry, destinationPath.parentDirectory) - } - - // Check that the parent is a directory. - guard case let .directory(contents) = destinationParent.contents else { - throw FileSystemError(.notDirectory, destinationPath.parentDirectory) - } - - guard contents.entries[destinationPath.basename] == nil else { - throw FileSystemError(.alreadyExistsAtDestination, destinationPath) - } - - contents.entries[destinationPath.basename] = source - } - - public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - return try lock.withLock { - try _copy(from: sourcePath, to: destinationPath) - } - } - - public func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { - return try lock.withLock { - // Get the source parent node. - guard let sourceParent = try getNode(sourcePath.parentDirectory) else { - throw FileSystemError(.noEntry, sourcePath.parentDirectory) - } - - // Check that the parent is a directory. - guard case let .directory(contents) = sourceParent.contents else { - throw FileSystemError(.notDirectory, sourcePath.parentDirectory) - } - - try _copy(from: sourcePath, to: destinationPath) - - contents.entries[sourcePath.basename] = nil - } - } - - public func withLock( - on path: AbsolutePath, - type: FileLock.LockType, - blocking: Bool, - _ body: () throws -> T - ) throws -> T { - if !blocking { - throw FileSystemError(.unsupported, path) - } - - let resolvedPath: AbsolutePath = try lock.withLock { - if case let .symlink(destination) = try getNode(path)?.contents { - return try AbsolutePath(validating: destination, relativeTo: path.parentDirectory) - } else { - return path - } - } - - let fileQueue: DispatchQueue = lockFilesLock.withLock { - if let queueReference = lockFiles[resolvedPath], let queue = queueReference.reference { - return queue - } else { - let queue = DispatchQueue( - label: "org.swift.swiftpm.in-memory-file-system.file-queue", - attributes: .concurrent - ) - lockFiles[resolvedPath] = WeakReference(queue) - return queue - } - } - - return try fileQueue.sync(flags: type == .exclusive ? .barrier : .init(), execute: body) - } -} - -// Internal state of `InMemoryFileSystem` is protected with a lock in all of its `public` methods. -#if compiler(>=5.7) -extension InMemoryFileSystem: @unchecked Sendable {} -#else -extension InMemoryFileSystem: UnsafeSendable {} -#endif - -private var _localFileSystem: FileSystem = LocalFileSystem() - -/// Public access to the local FS proxy. -public var localFileSystem: FileSystem { - return _localFileSystem -} - -public extension FileSystem { - /// Print the filesystem tree of the given path. - /// - /// For debugging only. - func dumpTree(at path: AbsolutePath = .root) { - print(".") - do { - try recurse(fs: self, path: path) - } catch { - print("\(error)") - } - } - - /// Write bytes to the path if the given contents are different. - func writeIfChanged(path: AbsolutePath, bytes: ByteString) throws { - try createDirectory(path.parentDirectory, recursive: true) - - // Return if the contents are same. - if isFile(path), try readFileContents(path) == bytes { - return - } - - try writeFileContents(path, bytes: bytes) - } - - func getDirectoryContents( - at path: AbsolutePath, - includingPropertiesForKeys: [URLResourceKey]? = nil, - options: FileManager.DirectoryEnumerationOptions = [] - ) throws -> [AbsolutePath] { - return try _getDirectoryContents( - path, - includingPropertiesForKeys: includingPropertiesForKeys, - options: options - ) - } - - /// Helper method to recurse and print the tree. - private func recurse(fs: FileSystem, path: AbsolutePath, prefix: String = "") throws { - let contents = (try fs.getDirectoryContents(at: path)).map(\.basename) - - for (idx, entry) in contents.enumerated() { - let isLast = idx == contents.count - 1 - let line = prefix + (isLast ? "└── " : "├── ") + entry - print(line) - - let entryPath = path.appending(component: entry) - if fs.isDirectory(entryPath) { - let childPrefix = prefix + (isLast ? " " : "│ ") - try recurse(fs: fs, path: entryPath, prefix: String(childPrefix)) - } - } - } -} - diff --git a/Tool/Sources/FileSystem/Lock.swift b/Tool/Sources/FileSystem/Lock.swift deleted file mode 100644 index 695494af..00000000 --- a/Tool/Sources/FileSystem/Lock.swift +++ /dev/null @@ -1,214 +0,0 @@ -import Foundation - -public enum ProcessLockError: Error { - case unableToAquireLock(errno: Int32) -} - -extension ProcessLockError: CustomNSError { - public var errorUserInfo: [String : Any] { - return [NSLocalizedDescriptionKey: "\(self)"] - } -} - -/// Provides functionality to acquire a lock on a file via POSIX's flock() method. -/// It can be used for things like serializing concurrent mutations on a shared resource -/// by multiple instances of a process. The `FileLock` is not thread-safe. -public final class FileLock { - - public enum LockType { - case exclusive - case shared - } - - /// File descriptor to the lock file. - #if os(Windows) - private var handle: HANDLE? - #else - private var fileDescriptor: CInt? - #endif - - /// Path to the lock file. - private let lockFile: AbsolutePath - - /// Create an instance of FileLock at the path specified - /// - /// Note: The parent directory path should be a valid directory. - internal init(at lockFile: AbsolutePath) { - self.lockFile = lockFile - } - - @available(*, deprecated, message: "use init(at:) instead") - public convenience init(name: String, cachePath: AbsolutePath) { - self.init(at: cachePath.appending(component: name + ".lock")) - } - - /// Try to acquire a lock. This method will block until lock the already aquired by other process. - /// - /// Note: This method can throw if underlying POSIX methods fail. - public func lock(type: LockType = .exclusive, blocking: Bool = true) throws { - #if os(Windows) - if handle == nil { - let h: HANDLE = lockFile.pathString.withCString(encodedAs: UTF16.self, { - CreateFileW( - $0, - UInt32(GENERIC_READ) | UInt32(GENERIC_WRITE), - UInt32(FILE_SHARE_READ) | UInt32(FILE_SHARE_WRITE), - nil, - DWORD(OPEN_ALWAYS), - DWORD(FILE_ATTRIBUTE_NORMAL), - nil - ) - }) - if h == INVALID_HANDLE_VALUE { - throw FileSystemError(errno: Int32(GetLastError()), lockFile) - } - self.handle = h - } - var overlapped = OVERLAPPED() - overlapped.Offset = 0 - overlapped.OffsetHigh = 0 - overlapped.hEvent = nil - var dwFlags = Int32(0) - switch type { - case .exclusive: dwFlags |= LOCKFILE_EXCLUSIVE_LOCK - case .shared: break - } - if !blocking { - dwFlags |= LOCKFILE_FAIL_IMMEDIATELY - } - if !LockFileEx(handle, DWORD(dwFlags), 0, - UInt32.max, UInt32.max, &overlapped) { - throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError())) - } - #else - // Open the lock file. - if fileDescriptor == nil { - let fd = open(lockFile.pathString, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666) - if fd == -1 { - throw FileSystemError(errno: errno, lockFile) - } - self.fileDescriptor = fd - } - var flags = Int32(0) - switch type { - case .exclusive: flags = LOCK_EX - case .shared: flags = LOCK_SH - } - if !blocking { - flags |= LOCK_NB - } - // Aquire lock on the file. - while true { - if flock(fileDescriptor!, flags) == 0 { - break - } - // Retry if interrupted. - if errno == EINTR { continue } - throw ProcessLockError.unableToAquireLock(errno: errno) - } - #endif - } - - /// Unlock the held lock. - public func unlock() { - #if os(Windows) - var overlapped = OVERLAPPED() - overlapped.Offset = 0 - overlapped.OffsetHigh = 0 - overlapped.hEvent = nil - UnlockFileEx(handle, 0, UInt32.max, UInt32.max, &overlapped) - #else - guard let fd = fileDescriptor else { return } - flock(fd, LOCK_UN) - #endif - } - - deinit { - #if os(Windows) - guard let handle = handle else { return } - CloseHandle(handle) - #else - guard let fd = fileDescriptor else { return } - close(fd) - #endif - } - - /// Execute the given block while holding the lock. - public func withLock(type: LockType = .exclusive, blocking: Bool = true, _ body: () throws -> T) throws -> T { - try lock(type: type, blocking: blocking) - defer { unlock() } - return try body() - } - - /// Execute the given block while holding the lock. - public func withLock(type: LockType = .exclusive, blocking: Bool = true, _ body: () async throws -> T) async throws -> T { - try lock(type: type, blocking: blocking) - defer { unlock() } - return try await body() - } - - public static func prepareLock( - fileToLock: AbsolutePath, - at lockFilesDirectory: AbsolutePath? = nil - ) throws -> FileLock { - // unless specified, we use the tempDirectory to store lock files - let lockFilesDirectory = try lockFilesDirectory ?? localFileSystem.tempDirectory - if !localFileSystem.exists(lockFilesDirectory) { - throw FileSystemError(.noEntry, lockFilesDirectory) - } - if !localFileSystem.isDirectory(lockFilesDirectory) { - throw FileSystemError(.notDirectory, lockFilesDirectory) - } - // use the parent path to generate unique filename in temp - var lockFileName = try (resolveSymlinks(fileToLock.parentDirectory) - .appending(component: fileToLock.basename)) - .components.joined(separator: "_") - .replacingOccurrences(of: ":", with: "_") + ".lock" -#if os(Windows) - // NTFS has an ARC limit of 255 codepoints - var lockFileUTF16 = lockFileName.utf16.suffix(255) - while String(lockFileUTF16) == nil { - lockFileUTF16 = lockFileUTF16.dropFirst() - } - lockFileName = String(lockFileUTF16) ?? lockFileName -#else - if lockFileName.hasPrefix(AbsolutePath.root.pathString) { - lockFileName = String(lockFileName.dropFirst(AbsolutePath.root.pathString.count)) - } - // back off until it occupies at most `NAME_MAX` UTF-8 bytes but without splitting scalars - // (we might split clusters but it's not worth the effort to keep them together as long as we get a valid file name) - var lockFileUTF8 = lockFileName.utf8.suffix(Int(NAME_MAX)) - while String(lockFileUTF8) == nil { - // in practice this will only be a few iterations - lockFileUTF8 = lockFileUTF8.dropFirst() - } - // we will never end up with nil since we have ASCII characters at the end - lockFileName = String(lockFileUTF8) ?? lockFileName -#endif - let lockFilePath = lockFilesDirectory.appending(component: lockFileName) - - return FileLock(at: lockFilePath) - } - - public static func withLock( - fileToLock: AbsolutePath, - lockFilesDirectory: AbsolutePath? = nil, - type: LockType = .exclusive, - blocking: Bool = true, - body: () throws -> T - ) throws -> T { - let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory) - return try lock.withLock(type: type, blocking: blocking, body) - } - - public static func withLock( - fileToLock: AbsolutePath, - lockFilesDirectory: AbsolutePath? = nil, - type: LockType = .exclusive, - blocking: Bool = true, - body: () async throws -> T - ) async throws -> T { - let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory) - return try await lock.withLock(type: type, blocking: blocking, body) - } -} diff --git a/Tool/Sources/FileSystem/Misc.swift b/Tool/Sources/FileSystem/Misc.swift deleted file mode 100644 index f016cfdc..00000000 --- a/Tool/Sources/FileSystem/Misc.swift +++ /dev/null @@ -1,426 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -#if canImport(Glibc) -@_exported import Glibc -#elseif canImport(Musl) -@_exported import Musl -#elseif os(Windows) -@_exported import CRT -@_exported import WinSDK -#else -@_exported import Darwin.C -#endif - -/// `CStringArray` represents a C null-terminated array of pointers to C strings. -/// -/// The lifetime of the C strings will correspond to the lifetime of the `CStringArray` -/// instance so be careful about copying the buffer as it may contain dangling pointers. -public final class CStringArray { - /// The null-terminated array of C string pointers. - public let cArray: [UnsafeMutablePointer?] - - /// Creates an instance from an array of strings. - public init(_ array: [String]) { -#if os(Windows) - cArray = array.map({ $0.withCString({ _strdup($0) }) }) + [nil] -#else - cArray = array.map({ $0.withCString({ strdup($0) }) }) + [nil] -#endif - } - - deinit { - for case let element? in cArray { - free(element) - } - } -} - -import Foundation -#if os(Windows) -import WinSDK -#endif - -#if os(Windows) -public let executableFileSuffix = ".exe" -#else -public let executableFileSuffix = "" -#endif - -#if os(Windows) -private func quote(_ arguments: [String]) -> String { - func quote(argument: String) -> String { - if !argument.contains(where: { " \t\n\"".contains($0) }) { - return argument - } - - // To escape the command line, we surround the argument with quotes. - // However, the complication comes due to how the Windows command line - // parser treats backslashes (\) and quotes ("). - // - // - \ is normally treated as a literal backslash - // e.g. alpha\beta\gamma => alpha\beta\gamma - // - The sequence \" is treated as a literal " - // e.g. alpha\"beta => alpha"beta - // - // But then what if we are given a path that ends with a \? - // - // Surrounding alpha\beta\ with " would be "alpha\beta\" which would be - // an unterminated string since it ends on a literal quote. To allow - // this case the parser treats: - // - // - \\" as \ followed by the " metacharacter - // - \\\" as \ followed by a literal " - // - // In general: - // - 2n \ followed by " => n \ followed by the " metacharacter - // - 2n + 1 \ followed by " => n \ followed by a literal " - - var quoted = "\"" - var unquoted = argument.unicodeScalars - - while !unquoted.isEmpty { - guard let firstNonBS = unquoted.firstIndex(where: { $0 != "\\" }) else { - // String ends with a backslash (e.g. first\second\), escape all - // the backslashes then add the metacharacter ". - let count = unquoted.count - quoted.append(String(repeating: "\\", count: 2 * count)) - break - } - - let count = unquoted.distance(from: unquoted.startIndex, to: firstNonBS) - if unquoted[firstNonBS] == "\"" { - // This is a string of \ followed by a " (e.g. first\"second). - // Escape the backslashes and the quote. - quoted.append(String(repeating: "\\", count: 2 * count + 1)) - } else { - // These are just literal backslashes - quoted.append(String(repeating: "\\", count: count)) - } - - quoted.append(String(unquoted[firstNonBS])) - - // Drop the backslashes and the following character - unquoted.removeFirst(count + 1) - } - quoted.append("\"") - - return quoted - } - return arguments.map(quote(argument:)).joined(separator: " ") -} -#endif - -/// Replace the current process image with a new process image. -/// -/// - Parameters: -/// - path: Absolute path to the executable. -/// - args: The executable arguments. -public func exec(path: String, args: [String]) throws -> Never { - let cArgs = CStringArray(args) - #if os(Windows) - var hJob: HANDLE - - hJob = CreateJobObjectA(nil, nil) - if hJob == HANDLE(bitPattern: 0) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - defer { CloseHandle(hJob) } - - let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1) - if hPort == HANDLE(bitPattern: 0) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - - var acpAssociation: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = JOBOBJECT_ASSOCIATE_COMPLETION_PORT() - acpAssociation.CompletionKey = hJob - acpAssociation.CompletionPort = hPort - if !SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, - &acpAssociation, DWORD(MemoryLayout.size)) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - - var eliLimits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() - eliLimits.BasicLimitInformation.LimitFlags = - DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) - if !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &eliLimits, - DWORD(MemoryLayout.size)) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - - - var siInfo: STARTUPINFOW = STARTUPINFOW() - siInfo.cb = DWORD(MemoryLayout.size) - - var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() - - try quote(args).withCString(encodedAs: UTF16.self) { pwszCommandLine in - if !CreateProcessW(nil, - UnsafeMutablePointer(mutating: pwszCommandLine), - nil, nil, false, - DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP), - nil, nil, &siInfo, &piInfo) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - } - - defer { CloseHandle(piInfo.hThread) } - defer { CloseHandle(piInfo.hProcess) } - - if !AssignProcessToJobObject(hJob, piInfo.hProcess) { - throw SystemError.exec(Int32(GetLastError()), path: path, args: args) - } - - _ = ResumeThread(piInfo.hThread) - - var dwCompletionCode: DWORD = 0 - var ulCompletionKey: ULONG_PTR = 0 - var lpOverlapped: LPOVERLAPPED? - repeat { - } while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey, - &lpOverlapped, INFINITE) && - !(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) && - dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) - - var dwExitCode: DWORD = DWORD(bitPattern: -1) - _ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode) - _exit(Int32(bitPattern: dwExitCode)) - #elseif (!canImport(Darwin) || os(macOS)) - guard execv(path, cArgs.cArray) != -1 else { - throw SystemError.exec(errno, path: path, args: args) - } - fatalError("unreachable") - #else - fatalError("not implemented") - #endif -} - -@_disfavoredOverload -@available(*, deprecated, message: "Use the overload which returns Never") -public func exec(path: String, args: [String]) throws { - try exec(path: path, args: args) -} - -// MARK: TSCUtility function for searching for executables - -/// Create a list of AbsolutePath search paths from a string, such as the PATH environment variable. -/// -/// - Parameters: -/// - pathString: The path string to parse. -/// - currentWorkingDirectory: The current working directory, the relative paths will be converted to absolute paths -/// based on this path. -/// - Returns: List of search paths. -public func getEnvSearchPaths( - pathString: String?, - currentWorkingDirectory: AbsolutePath? -) -> [AbsolutePath] { - // Compute search paths from PATH variable. -#if os(Windows) - let pathSeparator: Character = ";" -#else - let pathSeparator: Character = ":" -#endif - return (pathString ?? "").split(separator: pathSeparator).map(String.init).compactMap({ pathString in - if let cwd = currentWorkingDirectory { - return try? AbsolutePath(validating: pathString, relativeTo: cwd) - } - return try? AbsolutePath(validating: pathString) - }) -} - -/// Lookup an executable path from an environment variable value, current working -/// directory or search paths. Only return a value that is both found and executable. -/// -/// This method searches in the following order: -/// * If env value is a valid absolute path, return it. -/// * If env value is relative path, first try to locate it in current working directory. -/// * Otherwise, in provided search paths. -/// -/// - Parameters: -/// - filename: The name of the file to find. -/// - currentWorkingDirectory: The current working directory to look in. -/// - searchPaths: The additional search paths to look in if not found in cwd. -/// - Returns: Valid path to executable if present, otherwise nil. -public func lookupExecutablePath( - filename value: String?, - currentWorkingDirectory: AbsolutePath? = localFileSystem.currentWorkingDirectory, - searchPaths: [AbsolutePath] = [] -) -> AbsolutePath? { - - // We should have a value to continue. - guard let value = value, !value.isEmpty else { - return nil - } - - var paths: [AbsolutePath] = [] - - if let cwd = currentWorkingDirectory, let path = try? AbsolutePath(validating: value, relativeTo: cwd) { - // We have a value, but it could be an absolute or a relative path. - paths.append(path) - } else if let absPath = try? AbsolutePath(validating: value) { - // Current directory not being available is not a problem - // for the absolute-specified paths. - paths.append(absPath) - } - - // Ensure the value is not a path. - if !value.contains("/") { - // Try to locate in search paths. - paths.append(contentsOf: searchPaths.map({ $0.appending(component: value) })) - } - - return paths.first(where: { localFileSystem.isExecutableFile($0) }) -} - -/// A wrapper for Range to make it Codable. -/// -/// Technically, we can use conditional conformance and make -/// stdlib's Range Codable but since extensions leak out, it -/// is not a good idea to extend types that you don't own. -/// -/// Range conformance will be added soon to stdlib so we can remove -/// this type in the future. -public struct CodableRange where Bound: Comparable & Codable { - - /// The underlying range. - public let range: Range - - /// Create a CodableRange instance. - public init(_ range: Range) { - self.range = range - } -} - -extension CodableRange: Sendable where Bound: Sendable {} - -extension CodableRange: Codable { - private enum CodingKeys: String, CodingKey { - case lowerBound, upperBound - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(range.lowerBound, forKey: .lowerBound) - try container.encode(range.upperBound, forKey: .upperBound) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let lowerBound = try container.decode(Bound.self, forKey: .lowerBound) - let upperBound = try container.decode(Bound.self, forKey: .upperBound) - self.init(Range(uncheckedBounds: (lowerBound, upperBound))) - } -} - -extension AbsolutePath { - /// File URL created from the normalized string representation of the path. - public var asURL: Foundation.URL { - return URL(fileURLWithPath: pathString) - } - - public init(_ url: URL) throws { - try self.init(validating: url.path) - } -} - -// FIXME: Eliminate or find a proper place for this. -public enum SystemError: Error { - case chdir(Int32, String) - case close(Int32) - case exec(Int32, path: String, args: [String]) - case pipe(Int32) - case posix_spawn(Int32, [String]) - case read(Int32) - case setenv(Int32, String) - case stat(Int32, String) - case symlink(Int32, String, dest: String) - case unsetenv(Int32, String) - case waitpid(Int32) -} - -extension SystemError: CustomStringConvertible { - public var description: String { - func strerror(_ errno: Int32) -> String { - #if os(Windows) - let cap = 128 - var buf = [Int8](repeating: 0, count: cap) - let _ = strerror_s(&buf, 128, errno) - return "\(String(cString: buf)) (\(errno))" - #else - var cap = 64 - while cap <= 16 * 1024 { - var buf = [Int8](repeating: 0, count: cap) - let err = strerror_r(errno, &buf, buf.count) - if err == EINVAL { - return "Unknown error \(errno)" - } - if err == ERANGE { - cap *= 2 - continue - } - if err != 0 { - fatalError("strerror_r error: \(err)") - } - return "\(String(cString: buf)) (\(errno))" - } - fatalError("strerror_r error: \(ERANGE)") - #endif - } - - switch self { - case .chdir(let errno, let path): - return "chdir error: \(strerror(errno)): \(path)" - case .close(let err): - let errorMessage: String - if err == -1 { // if the return code is -1, we need to consult the global `errno` - errorMessage = strerror(errno) - } else { - errorMessage = strerror(err) - } - return "close error: \(errorMessage)" - case .exec(let errno, let path, let args): - let joinedArgs = args.joined(separator: " ") - return "exec error: \(strerror(errno)): \(path) \(joinedArgs)" - case .pipe(let errno): - return "pipe error: \(strerror(errno))" - case .posix_spawn(let errno, let args): - return "posix_spawn error: \(strerror(errno)), `\(args)`" - case .read(let errno): - return "read error: \(strerror(errno))" - case .setenv(let errno, let key): - return "setenv error: \(strerror(errno)): \(key)" - case .stat(let errno, _): - return "stat error: \(strerror(errno))" - case .symlink(let errno, let path, let dest): - return "symlink error: \(strerror(errno)): \(path) -> \(dest)" - case .unsetenv(let errno, let key): - return "unsetenv error: \(strerror(errno)): \(key)" - case .waitpid(let errno): - return "waitpid error: \(strerror(errno))" - } - } -} - -extension SystemError: CustomNSError { - public var errorUserInfo: [String : Any] { - return [NSLocalizedDescriptionKey: self.description] - } -} - -/// Memoizes a costly computation to a cache variable. -func memoize(to cache: inout T?, build: () throws -> T) rethrows -> T { - if let value = cache { - return value - } else { - let value = try build() - cache = value - return value - } -} diff --git a/Tool/Sources/FileSystem/Path.swift b/Tool/Sources/FileSystem/Path.swift deleted file mode 100644 index b65a22b9..00000000 --- a/Tool/Sources/FileSystem/Path.swift +++ /dev/null @@ -1,1058 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ -#if os(Windows) -import Foundation -import WinSDK -#endif - -#if os(Windows) -private typealias PathImpl = WindowsPath -#else -private typealias PathImpl = UNIXPath -#endif - -import protocol Foundation.CustomNSError -import var Foundation.NSLocalizedDescriptionKey - -/// Represents an absolute file system path, independently of what (or whether -/// anything at all) exists at that path in the file system at any given time. -/// An absolute path always starts with a `/` character, and holds a normalized -/// string representation. This normalization is strictly syntactic, and does -/// not access the file system in any way. -/// -/// The absolute path string is normalized by: -/// - Collapsing `..` path components -/// - Removing `.` path components -/// - Removing any trailing path separator -/// - Removing any redundant path separators -/// -/// This string manipulation may change the meaning of a path if any of the -/// path components are symbolic links on disk. However, the file system is -/// never accessed in any way when initializing an AbsolutePath. -/// -/// Note that `~` (home directory resolution) is *not* done as part of path -/// normalization, because it is normally the responsibility of the shell and -/// not the program being invoked (e.g. when invoking `cd ~`, it is the shell -/// that evaluates the tilde; the `cd` command receives an absolute path). -public struct AbsolutePath: Hashable, Sendable { - /// Check if the given name is a valid individual path component. - /// - /// This only checks with regard to the semantics enforced by `AbsolutePath` - /// and `RelativePath`; particular file systems may have their own - /// additional requirements. - static func isValidComponent(_ name: String) -> Bool { - return PathImpl.isValidComponent(name) - } - - /// Private implementation details, shared with the RelativePath struct. - private let _impl: PathImpl - - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl - } - - /// Initializes an AbsolutePath from a string that may be either absolute - /// or relative; if relative, `basePath` is used as the anchor; if absolute, - /// it is used as is, and in this case `basePath` is ignored. - public init(validating str: String, relativeTo basePath: AbsolutePath) throws { - if PathImpl(string: str).isAbsolute { - try self.init(validating: str) - } else { -#if os(Windows) - assert(!basePath.pathString.isEmpty) - guard !str.isEmpty else { - self.init(basePath._impl) - return - } - - let base: UnsafePointer = - basePath.pathString.fileSystemRepresentation - defer { base.deallocate() } - - let path: UnsafePointer = str.fileSystemRepresentation - defer { path.deallocate() } - - var pwszResult: PWSTR! - _ = String(cString: base).withCString(encodedAs: UTF16.self) { pwszBase in - String(cString: path).withCString(encodedAs: UTF16.self) { pwszPath in - PathAllocCombine(pwszBase, pwszPath, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &pwszResult) - } - } - defer { LocalFree(pwszResult) } - - self.init(String(decodingCString: pwszResult, as: UTF16.self)) -#else - try self.init(basePath, RelativePath(validating: str)) -#endif - } - } - - /// Initializes the AbsolutePath by concatenating a relative path to an - /// existing absolute path, and renormalizing if necessary. - public init(_ absPath: AbsolutePath, _ relPath: RelativePath) { - self.init(absPath._impl.appending(relativePath: relPath._impl)) - } - - /// Convenience initializer that appends a string to a relative path. - public init(_ absPath: AbsolutePath, validating relStr: String) throws { - try self.init(absPath, RelativePath(validating: relStr)) - } - - /// Initializes the AbsolutePath from `absStr`, which must be an absolute - /// path (i.e. it must begin with a path separator; this initializer does - /// not interpret leading `~` characters as home directory specifiers). - /// The input string will be normalized if needed, as described in the - /// documentation for AbsolutePath. - public init(validating path: String) throws { - try self.init(PathImpl(validatingAbsolutePath: path)) - } - - /// Directory component. An absolute path always has a non-empty directory - /// component (the directory component of the root path is the root itself). - public var dirname: String { - return _impl.dirname - } - - /// Last path component (including the suffix, if any). it is never empty. - public var basename: String { - return _impl.basename - } - - /// Returns the basename without the extension. - public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename - } - - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } - - /// Extension of the give path's basename. This follow same rules as - /// suffix except that it doesn't include leading `.` character. - public var `extension`: String? { - return _impl.extension - } - - /// Absolute path of parent directory. This always returns a path, because - /// every directory has a parent (the parent directory of the root directory - /// is considered to be the root directory itself). - public var parentDirectory: AbsolutePath { - return AbsolutePath(_impl.parentDirectory) - } - - /// True if the path is the root directory. - public var isRoot: Bool { - return _impl.isRoot - } - - /// Returns the absolute path with the relative path applied. - public func appending(_ subpath: RelativePath) -> AbsolutePath { - return AbsolutePath(self, subpath) - } - - /// Returns the absolute path with an additional literal component appended. - /// - /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> AbsolutePath { - return AbsolutePath(_impl.appending(component: component)) - } - - /// Returns the absolute path with additional literal components appended. - /// - /// This method should only be used in cases where the input is guaranteed - /// to be a valid path component (i.e., it cannot be empty, contain a path - /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> AbsolutePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) - } - - public func appending(components names: String...) -> AbsolutePath { - appending(components: names) - } - - /// NOTE: We will most likely want to add other `appending()` methods, such - /// as `appending(suffix:)`, and also perhaps `replacing()` methods, - /// such as `replacing(suffix:)` or `replacing(basename:)` for some - /// of the more common path operations. - - /// NOTE: We may want to consider adding operators such as `+` for appending - /// a path component. - - /// NOTE: We will want to add a method to return the lowest common ancestor - /// path. - - /// Root directory (whose string representation is just a path separator). - public static let root = AbsolutePath(PathImpl.root) - - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string - } - - /// Returns an array of strings that make up the path components of the - /// absolute path. This is the same sequence of strings as the basenames - /// of each successive path component, starting from the root. Therefore - /// the first path component of an absolute path is always `/`. - public var components: [String] { - return _impl.components - } -} - -/// Represents a relative file system path. A relative path never starts with -/// a `/` character, and holds a normalized string representation. As with -/// AbsolutePath, the normalization is strictly syntactic, and does not access -/// the file system in any way. -/// -/// The relative path string is normalized by: -/// - Collapsing `..` path components that aren't at the beginning -/// - Removing extraneous `.` path components -/// - Removing any trailing path separator -/// - Removing any redundant path separators -/// - Replacing a completely empty path with a `.` -/// -/// This string manipulation may change the meaning of a path if any of the -/// path components are symbolic links on disk. However, the file system is -/// never accessed in any way when initializing a RelativePath. -public struct RelativePath: Hashable, Sendable { - /// Private implementation details, shared with the AbsolutePath struct. - fileprivate let _impl: PathImpl - - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl - } - - /// Convenience initializer that verifies that the path is relative. - public init(validating path: String) throws { - try self.init(PathImpl(validatingRelativePath: path)) - } - - /// Directory component. For a relative path without any path separators, - /// this is the `.` string instead of the empty string. - public var dirname: String { - return _impl.dirname - } - - /// Last path component (including the suffix, if any). It is never empty. - public var basename: String { - return _impl.basename - } - - /// Returns the basename without the extension. - public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename - } - - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } - - /// Extension of the give path's basename. This follow same rules as - /// suffix except that it doesn't include leading `.` character. - public var `extension`: String? { - return _impl.extension - } - - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string - } - - /// Returns an array of strings that make up the path components of the - /// relative path. This is the same sequence of strings as the basenames - /// of each successive path component. Therefore the returned array of - /// path components is never empty; even an empty path has a single path - /// component: the `.` string. - public var components: [String] { - return _impl.components - } - - /// Returns the relative path with the given relative path applied. - public func appending(_ subpath: RelativePath) -> RelativePath { - return RelativePath(_impl.appending(relativePath: subpath._impl)) - } - - /// Returns the relative path with an additional literal component appended. - /// - /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> RelativePath { - return RelativePath(_impl.appending(component: component)) - } - - /// Returns the relative path with additional literal components appended. - /// - /// This method should only be used in cases where the input is guaranteed - /// to be a valid path component (i.e., it cannot be empty, contain a path - /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> RelativePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) - } - - public func appending(components names: String...) -> RelativePath { - appending(components: names) - } -} - -extension AbsolutePath: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(pathString) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - try self.init(validating: container.decode(String.self)) - } -} - -extension RelativePath: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(pathString) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - try self.init(validating: container.decode(String.self)) - } -} - -// Make absolute paths Comparable. -extension AbsolutePath: Comparable { - public static func < (lhs: AbsolutePath, rhs: AbsolutePath) -> Bool { - return lhs.pathString < rhs.pathString - } -} - -/// Make absolute paths CustomStringConvertible and CustomDebugStringConvertible. -extension AbsolutePath: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return pathString - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Make relative paths CustomStringConvertible and CustomDebugStringConvertible. -extension RelativePath: CustomStringConvertible { - public var description: String { - return _impl.string - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Private implementation shared between AbsolutePath and RelativePath. -protocol Path: Hashable { - - /// Root directory. - static var root: Self { get } - - /// Checks if a string is a valid component. - static func isValidComponent(_ name: String) -> Bool - - /// Normalized string of the (absolute or relative) path. Never empty. - var string: String { get } - - /// Returns whether the path is the root path. - var isRoot: Bool { get } - - /// Returns whether the path is an absolute path. - var isAbsolute: Bool { get } - - /// Returns the directory part of the stored path (relying on the fact that it has been normalized). Returns a - /// string consisting of just `.` if there is no directory part (which is the case if and only if there is no path - /// separator). - var dirname: String { get } - - /// Returns the last past component. - var basename: String { get } - - /// Returns the components of the path between each path separator. - var components: [String] { get } - - /// Path of parent directory. This always returns a path, because every directory has a parent (the parent - /// directory of the root directory is considered to be the root directory itself). - var parentDirectory: Self { get } - - /// Creates a path from its normalized string representation. - init(string: String) - - /// Creates a path from a string representation, validates that it is a valid absolute path and normalizes it. - init(validatingAbsolutePath: String) throws - - /// Creates a path from a string representation, validates that it is a valid relative path and normalizes it. - init(validatingRelativePath: String) throws - - /// Returns suffix with leading `.` if withDot is true otherwise without it. - func suffix(withDot: Bool) -> String? - - /// Returns a new Path by appending the path component. - func appending(component: String) -> Self - - /// Returns a path by concatenating a relative path and renormalizing if necessary. - func appending(relativePath: Self) -> Self -} - -extension Path { - var suffix: String? { - return suffix(withDot: true) - } - - var `extension`: String? { - return suffix(withDot: false) - } -} - -#if os(Windows) -private struct WindowsPath: Path, Sendable { - let string: String - - // NOTE: this is *NOT* a root path. It is a drive-relative path that needs - // to be specified due to assumptions in the APIs. Use the platform - // specific path separator as we should be normalizing the path normally. - // This is required to make the `InMemoryFileSystem` correctly iterate - // paths. - static let root = Self(string: "\\") - - static func isValidComponent(_ name: String) -> Bool { - return name != "" && name != "." && name != ".." && !name.contains("/") - } - - static func isAbsolutePath(_ path: String) -> Bool { - return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) - } - - var dirname: String { - let fsr: UnsafePointer = self.string.fileSystemRepresentation - defer { fsr.deallocate() } - - let path: String = String(cString: fsr) - return path.withCString(encodedAs: UTF16.self) { - let data = UnsafeMutablePointer(mutating: $0) - PathCchRemoveFileSpec(data, path.count) - return String(decodingCString: data, as: UTF16.self) - } - } - - var isAbsolute: Bool { - return Self.isAbsolutePath(self.string) - } - - public var isRoot: Bool { - return self.string.withCString(encodedAs: UTF16.self, PathCchIsRoot) - } - - var basename: String { - let path: String = self.string - return path.withCString(encodedAs: UTF16.self) { - PathStripPathW(UnsafeMutablePointer(mutating: $0)) - return String(decodingCString: $0, as: UTF16.self) - } - } - - // FIXME: We should investigate if it would be more efficient to instead - // return a path component iterator that does all its work lazily, moving - // from one path separator to the next on-demand. - // - var components: [String] { - let normalized: UnsafePointer = string.fileSystemRepresentation - defer { normalized.deallocate() } - - return String(cString: normalized).components(separatedBy: "\\").filter { !$0.isEmpty } - } - - var parentDirectory: Self { - return self == .root ? self : Self(string: dirname) - } - - init(string: String) { - if string.first?.isASCII ?? false, string.first?.isLetter ?? false, string.first?.isLowercase ?? false, - string.count > 1, string[string.index(string.startIndex, offsetBy: 1)] == ":" - { - self.string = "\(string.first!.uppercased())\(string.dropFirst(1))" - } else { - self.string = string - } - } - - private static func repr(_ path: String) -> String { - guard !path.isEmpty else { return "" } - let representation: UnsafePointer = path.fileSystemRepresentation - defer { representation.deallocate() } - return String(cString: representation) - } - - init(validatingAbsolutePath path: String) throws { - let realpath = Self.repr(path) - if !Self.isAbsolutePath(realpath) { - throw PathValidationError.invalidAbsolutePath(path) - } - self.init(string: realpath) - } - - init(validatingRelativePath path: String) throws { - if path.isEmpty || path == "." { - self.init(string: ".") - } else { - let realpath: String = Self.repr(path) - // Treat a relative path as an invalid relative path... - if Self.isAbsolutePath(realpath) || realpath.first == "\\" { - throw PathValidationError.invalidRelativePath(path) - } - self.init(string: realpath) - } - } - - func suffix(withDot: Bool) -> String? { - return self.string.withCString(encodedAs: UTF16.self) { - if let pointer = PathFindExtensionW($0) { - let substring = String(decodingCString: pointer, as: UTF16.self) - guard substring.length > 0 else { return nil } - return withDot ? substring : String(substring.dropFirst(1)) - } - return nil - } - } - - func appending(component name: String) -> Self { - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - name.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return Self(string: String(decodingCString: result!, as: UTF16.self)) - } - - func appending(relativePath: Self) -> Self { - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - relativePath.string.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return Self(string: String(decodingCString: result!, as: UTF16.self)) - } -} -#else -private struct UNIXPath: Path, Sendable { - let string: String - - static let root = Self(string: "/") - - static func isValidComponent(_ name: String) -> Bool { - return name != "" && name != "." && name != ".." && !name.contains("/") - } - - var dirname: String { - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the directory name is `.`. - return "." - } - // Check if it's the only one in the string. - if idx == string.startIndex { - // Just one path separator, so the directory name is `/`. - return "/" - } - // Otherwise, it's the string up to (but not including) the last path - // separator. - return String(string.prefix(upTo: idx)) - } - - var isAbsolute: Bool { - return string.hasPrefix("/") - } - - var isRoot: Bool { - return self == Self.root - } - - var basename: String { - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the basename is the whole string. - return string - } - // Otherwise, it's the string from (but not including) the last path - // separator. - return String(string.suffix(from: string.index(after: idx))) - } - - // FIXME: We should investigate if it would be more efficient to instead - // return a path component iterator that does all its work lazily, moving - // from one path separator to the next on-demand. - // - var components: [String] { - // FIXME: This isn't particularly efficient; needs optimization, and - // in fact, it might well be best to return a custom iterator so we - // don't have to allocate everything up-front. It would be backed by - // the path string and just return a slice at a time. - let components = string.components(separatedBy: "/").filter({ !$0.isEmpty }) - - if string.hasPrefix("/") { - return ["/"] + components - } else { - return components - } - } - - var parentDirectory: Self { - return self == .root ? self : Self(string: dirname) - } - - init(string: String) { - self.string = string - } - - init(normalizingAbsolutePath path: String) { - precondition(path.first == "/", "Failure normalizing \(path), absolute paths should start with '/'") - - // At this point we expect to have a path separator as first character. - assert(path.first == "/") - // Fast path. - if !mayNeedNormalization(absolute: path) { - self.init(string: path) - } - - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If there's a previous part, drop it; otherwise, do nothing. - if let prev = parts.last { - parts.removeLast() - capacity -= prev.count - } - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } - } - capacity += max(parts.count, 1) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - result.append("/") - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) - } - } - - // Sanity-check the result (including the capacity we reserved). - assert(!result.isEmpty, "unexpected empty string") - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // Use the result as our stored string. - self.init(string: result) - } - - init(normalizingRelativePath path: String) { - precondition(path.first != "/") - - // FIXME: Here we should also keep track of whether anything actually has - // to be changed in the string, and if not, just return the existing one. - - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If at beginning, fall through to treat the `..` literally. - guard let prev = parts.last else { - fallthrough - } - // If previous component is anything other than `..`, drop it. - if !(prev.count == 2 && prev.first == "." && prev.last == ".") { - parts.removeLast() - capacity -= prev.count - continue - } - // Otherwise, fall through to treat the `..` literally. - fallthrough - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } - } - capacity += max(parts.count - 1, 0) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) - } - } - - // Sanity-check the result (including the capacity we reserved). - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // If the result is empty, return `.`, otherwise we return it as a string. - self.init(string: result.isEmpty ? "." : result) - } - - init(validatingAbsolutePath path: String) throws { - switch path.first { - case "/": - self.init(normalizingAbsolutePath: path) - case "~": - throw PathValidationError.startsWithTilde(path) - default: - throw PathValidationError.invalidAbsolutePath(path) - } - } - - init(validatingRelativePath path: String) throws { - switch path.first { - case "/": - throw PathValidationError.invalidRelativePath(path) - default: - self.init(normalizingRelativePath: path) - } - } - - func suffix(withDot: Bool) -> String? { - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator, if any. - let sIdx = string.lastIndex(of: "/") - // Find the start of the basename. - let bIdx = (sIdx != nil) ? string.index(after: sIdx!) : string.startIndex - // Find the last `.` (if any), starting from the second character of - // the basename (a leading `.` does not make the whole path component - // a suffix). - let fIdx = string.index(bIdx, offsetBy: 1, limitedBy: string.endIndex) ?? string.startIndex - if let idx = string[fIdx...].lastIndex(of: ".") { - // Unless it's just a `.` at the end, we have found a suffix. - if string.distance(from: idx, to: string.endIndex) > 1 { - let fromIndex = withDot ? idx : string.index(idx, offsetBy: 1) - return String(string.suffix(from: fromIndex)) - } else { - return nil - } - } - // If we get this far, there is no suffix. - return nil - } - - func appending(component name: String) -> Self { - assert(!name.contains("/"), "\(name) is invalid path component") - - // Handle pseudo paths. - switch name { - case "", ".": - return self - case "..": - return self.parentDirectory - default: - break - } - - if self == Self.root { - return Self(string: "/" + name) - } else { - return Self(string: string + "/" + name) - } - } - - func appending(relativePath: Self) -> Self { - // Both paths are already normalized. The only case in which we have - // to renormalize their concatenation is if the relative path starts - // with a `..` path component. - var newPathString = string - if self != .root { - newPathString.append("/") - } - - let relativePathString = relativePath.string - newPathString.append(relativePathString) - - // If the relative string starts with `.` or `..`, we need to normalize - // the resulting string. - // FIXME: We can actually optimize that case, since we know that the - // normalization of a relative path can leave `..` path components at - // the beginning of the path only. - if relativePathString.hasPrefix(".") { - if newPathString.hasPrefix("/") { - return Self(normalizingAbsolutePath: newPathString) - } else { - return Self(normalizingRelativePath: newPathString) - } - } else { - return Self(string: newPathString) - } - } -} -#endif - -/// Describes the way in which a path is invalid. -public enum PathValidationError: Error { - case startsWithTilde(String) - case invalidAbsolutePath(String) - case invalidRelativePath(String) -} - -extension PathValidationError: CustomStringConvertible { - public var description: String { - switch self { - case .startsWithTilde(let path): - return "invalid absolute path '\(path)'; absolute path must begin with '/'" - case .invalidAbsolutePath(let path): - return "invalid absolute path '\(path)'" - case .invalidRelativePath(let path): - return "invalid relative path '\(path)'; relative path should not begin with '\(AbsolutePath.root.pathString)'" - } - } -} - -extension AbsolutePath { - /// Returns a relative path that, when concatenated to `base`, yields the - /// callee path itself. If `base` is not an ancestor of the callee, the - /// returned path will begin with one or more `..` path components. - /// - /// Because both paths are absolute, they always have a common ancestor - /// (the root path, if nothing else). Therefore, any path can be made - /// relative to any other path by using a sufficient number of `..` path - /// components. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. Therefore, it does not take symbolic links into account. - public func relative(to base: AbsolutePath) -> RelativePath { - let result: RelativePath - // Split the two paths into their components. - // FIXME: The is needs to be optimized to avoid unncessary copying. - let pathComps = self.components - let baseComps = base.components - - // It's common for the base to be an ancestor, so try that first. - if pathComps.starts(with: baseComps) { - // Special case, which is a plain path without `..` components. It - // might be an empty path (when self and the base are equal). - let relComps = pathComps.dropFirst(baseComps.count) -#if os(Windows) - let pathString = relComps.joined(separator: "\\") -#else - let pathString = relComps.joined(separator: "/") -#endif - do { - result = try RelativePath(validating: pathString) - } catch { - preconditionFailure("invalid relative path computed from \(pathString)") - } - - } else { - // General case, in which we might well need `..` components to go - // "up" before we can go "down" the directory tree. - var newPathComps = ArraySlice(pathComps) - var newBaseComps = ArraySlice(baseComps) - while newPathComps.prefix(1) == newBaseComps.prefix(1) { - // First component matches, so drop it. - newPathComps = newPathComps.dropFirst() - newBaseComps = newBaseComps.dropFirst() - } - // Now construct a path consisting of as many `..`s as are in the - // `newBaseComps` followed by what remains in `newPathComps`. - var relComps = Array(repeating: "..", count: newBaseComps.count) - relComps.append(contentsOf: newPathComps) -#if os(Windows) - let pathString = relComps.joined(separator: "\\") -#else - let pathString = relComps.joined(separator: "/") -#endif - do { - result = try RelativePath(validating: pathString) - } catch { - preconditionFailure("invalid relative path computed from \(pathString)") - } - } - - assert(AbsolutePath(base, result) == self) - return result - } - - /// Returns true if the path contains the given path. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. - @available(*, deprecated, renamed: "isDescendantOfOrEqual(to:)") - public func contains(_ other: AbsolutePath) -> Bool { - return isDescendantOfOrEqual(to: other) - } - - /// Returns true if the path is an ancestor of the given path. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. - public func isAncestor(of descendant: AbsolutePath) -> Bool { - return descendant.components.dropLast().starts(with: self.components) - } - - /// Returns true if the path is an ancestor of or equal to the given path. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. - public func isAncestorOfOrEqual(to descendant: AbsolutePath) -> Bool { - return descendant.components.starts(with: self.components) - } - - /// Returns true if the path is a descendant of the given path. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. - public func isDescendant(of ancestor: AbsolutePath) -> Bool { - return self.components.dropLast().starts(with: ancestor.components) - } - - /// Returns true if the path is a descendant of or equal to the given path. - /// - /// This method is strictly syntactic and does not access the file system - /// in any way. - public func isDescendantOfOrEqual(to ancestor: AbsolutePath) -> Bool { - return self.components.starts(with: ancestor.components) - } -} - -extension PathValidationError: CustomNSError { - public var errorUserInfo: [String : Any] { - return [NSLocalizedDescriptionKey: self.description] - } -} - -// FIXME: We should consider whether to merge the two `normalize()` functions. -// The argument for doing so is that some of the code is repeated; the argument -// against doing so is that some of the details are different, and since any -// given path is either absolute or relative, it's wasteful to keep checking -// for whether it's relative or absolute. Possibly we can do both by clever -// use of generics that abstract away the differences. - -/// Fast check for if a string might need normalization. -/// -/// This assumes that paths containing dotfiles are rare: -private func mayNeedNormalization(absolute string: String) -> Bool { - var last = UInt8(ascii: "0") - for c in string.utf8 { - switch c { - case UInt8(ascii: "/") where last == UInt8(ascii: "/"): - return true - case UInt8(ascii: ".") where last == UInt8(ascii: "/"): - return true - default: - break - } - last = c - } - if last == UInt8(ascii: "/") { - return true - } - return false -} - -// MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period. - -extension AbsolutePath { - @_disfavoredOverload - @available(*, deprecated, message: "use throwing `init(validating:)` variant instead") - public init(_ absStr: String) { - try! self.init(validating: absStr) - } - - @_disfavoredOverload - @available(*, deprecated, message: "use throwing `init(validating:relativeTo:)` variant instead") - public init(_ str: String, relativeTo basePath: AbsolutePath) { - try! self.init(validating: str, relativeTo: basePath) - } - - @_disfavoredOverload - @available(*, deprecated, message: "use throwing variant instead") - public init(_ absPath: AbsolutePath, _ relStr: String) { - try! self.init(absPath, validating: relStr) - } -} - -// MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period. - -extension RelativePath { - @_disfavoredOverload - @available(*, deprecated, message: "use throwing variant instead") - public init(_ string: String) { - try! self.init(validating: string) - } -} diff --git a/Tool/Sources/FileSystem/PathShim.swift b/Tool/Sources/FileSystem/PathShim.swift deleted file mode 100644 index aacf2fca..00000000 --- a/Tool/Sources/FileSystem/PathShim.swift +++ /dev/null @@ -1,229 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - ------------------------------------------------------------------------- - - This file contains temporary shim functions for use during the adoption of - AbsolutePath and RelativePath. The eventual plan is to use the FileSystem - API for all of this, at which time this file will go way. But since it is - important to have a quality FileSystem API, we will evolve it slowly. - - Meanwhile this file bridges the gap to let call sites be as clean as possible, - while making it fairly easy to find those calls later. - */ - -import Foundation - -#if canImport(Glibc) -@_exported import Glibc -#elseif canImport(Musl) -@_exported import Musl -#elseif os(Windows) -@_exported import CRT -@_exported import WinSDK -#else -@_exported import Darwin.C -#endif - -/// Returns the "real path" corresponding to `path` by resolving any symbolic links. -public func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath { - #if os(Windows) - let handle: HANDLE = path.pathString.withCString(encodedAs: UTF16.self) { - CreateFileW( - $0, - GENERIC_READ, - DWORD(FILE_SHARE_READ), - nil, - DWORD(OPEN_EXISTING), - DWORD(FILE_FLAG_BACKUP_SEMANTICS), - nil - ) - } - if handle == INVALID_HANDLE_VALUE { return path } - defer { CloseHandle(handle) } - return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: 261) { - let dwLength: DWORD = - GetFinalPathNameByHandleW( - handle, - $0.baseAddress!, - DWORD($0.count), - DWORD(FILE_NAME_NORMALIZED) - ) - let path = String(decodingCString: $0.baseAddress!, as: UTF16.self) - return try AbsolutePath(path) - } - #else - let pathStr = path.pathString - - // FIXME: We can't use FileManager's destinationOfSymbolicLink because - // that implements readlink and not realpath. - if let resultPtr = realpath(pathStr, nil) { - let result = String(cString: resultPtr) - // If `resolved_path` is specified as NULL, then `realpath` uses - // malloc(3) to allocate a buffer [...]. The caller should deallocate - // this buffer using free(3). - // - // String.init(cString:) creates a new string by copying the - // null-terminated UTF-8 data referenced by the given pointer. - resultPtr.deallocate() - // FIXME: We should measure if it's really more efficient to compare the strings first. - return result == pathStr ? path : try AbsolutePath(validating: result) - } - - return path - #endif -} - -/// Creates a new, empty directory at `path`. If needed, any non-existent ancestor paths are also -/// created. If there is -/// already a directory at `path`, this function does nothing (in particular, this is not considered -/// to be an error). -public func makeDirectories(_ path: AbsolutePath) throws { - try FileManager.default.createDirectory( - atPath: path.pathString, - withIntermediateDirectories: true, - attributes: [:] - ) -} - -/// Creates a symbolic link at `path` whose content points to `dest`. If `relative` is true, the -/// symlink contents will -/// be a relative path, otherwise it will be absolute. -@available(*, deprecated, renamed: "localFileSystem.createSymbolicLink") -public func createSymlink( - _ path: AbsolutePath, - pointingAt dest: AbsolutePath, - relative: Bool = true -) throws { - let destString = relative ? dest.relative(to: path.parentDirectory).pathString : dest.pathString - try FileManager.default.createSymbolicLink( - atPath: path.pathString, - withDestinationPath: destString - ) -} - -/** - - Returns: a generator that walks the specified directory producing all - files therein. If recursively is true will enter any directories - encountered recursively. - - - Warning: directories that cannot be entered due to permission problems - are silently ignored. So keep that in mind. - - - Warning: Symbolic links that point to directories are *not* followed. - - - Note: setting recursively to `false` still causes the generator to feed - you the directory; just not its contents. - */ -public func walk( - _ path: AbsolutePath, - fileSystem: FileSystem = localFileSystem, - recursively: Bool = true -) throws -> RecursibleDirectoryContentsGenerator { - return try RecursibleDirectoryContentsGenerator( - path: path, - fileSystem: fileSystem, - recursionFilter: { _ in recursively } - ) -} - -/** - - Returns: a generator that walks the specified directory producing all - files therein. Directories are recursed based on the return value of - `recursing`. - - - Warning: directories that cannot be entered due to permissions problems - are silently ignored. So keep that in mind. - - - Warning: Symbolic links that point to directories are *not* followed. - - - Note: returning `false` from `recursing` still produces that directory - from the generator; just not its contents. - */ -public func walk( - _ path: AbsolutePath, - fileSystem: FileSystem = localFileSystem, - recursing: @escaping (AbsolutePath) -> Bool -) throws -> RecursibleDirectoryContentsGenerator { - return try RecursibleDirectoryContentsGenerator( - path: path, - fileSystem: fileSystem, - recursionFilter: recursing - ) -} - -/** - Produced by `walk`. - */ -public class RecursibleDirectoryContentsGenerator: IteratorProtocol, Sequence { - private var current: (path: AbsolutePath, iterator: IndexingIterator<[String]>) - private var towalk = [AbsolutePath]() - - private let shouldRecurse: (AbsolutePath) -> Bool - private let fileSystem: FileSystem - - fileprivate init( - path: AbsolutePath, - fileSystem: FileSystem, - recursionFilter: @escaping (AbsolutePath) -> Bool - ) throws { - self.fileSystem = fileSystem - // FIXME: getDirectoryContents should have an iterator version. - current = try ( - path, - fileSystem.getDirectoryContents(at: path).map(\.basename).makeIterator() - ) - shouldRecurse = recursionFilter - } - - public func next() -> AbsolutePath? { - outer: while true { - guard let entry = current.iterator.next() else { - while !towalk.isEmpty { - // FIXME: This looks inefficient. - let path = towalk.removeFirst() - guard shouldRecurse(path) else { continue } - // Ignore if we can't get content for this path. - guard let current = try? fileSystem.getDirectoryContents(at: path) - .map(\.basename) - .makeIterator() else { continue } - self.current = (path, current) - continue outer - } - return nil - } - - let path = current.path.appending(component: entry) - if fileSystem.isDirectory(path) && !fileSystem.isSymlink(path) { - towalk.append(path) - } - return path - } - } -} - -public extension AbsolutePath { - /// Returns a path suitable for display to the user (if possible, it is made - /// to be relative to the current working directory). - func prettyPath(cwd: AbsolutePath? = localFileSystem.currentWorkingDirectory) -> String { - guard let dir = cwd else { - // No current directory, display as is. - return pathString - } - // FIXME: Instead of string prefix comparison we should add a proper API - // to AbsolutePath to determine ancestry. - if self == dir { - return "." - } else if pathString.hasPrefix(dir.pathString + "/") { - return "./" + relative(to: dir).pathString - } else { - return pathString - } - } -} - diff --git a/Tool/Sources/FileSystem/WritableByteStream.swift b/Tool/Sources/FileSystem/WritableByteStream.swift deleted file mode 100644 index 94dd033d..00000000 --- a/Tool/Sources/FileSystem/WritableByteStream.swift +++ /dev/null @@ -1,846 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -/// Closable entity is one that manages underlying resources and needs to be closed for cleanup -/// The intent of this method is for the sole owner of the refernece/handle of the resource to close it completely, comapred to releasing a shared resource. -public protocol Closable { - func close() throws -} - -import Dispatch - -#if canImport(Glibc) -@_exported import Glibc -#elseif canImport(Musl) -@_exported import Musl -#elseif os(Windows) -@_exported import CRT -@_exported import WinSDK -#else -@_exported import Darwin.C -#endif - -/// Convert an integer in 0..<16 to its hexadecimal ASCII character. -private func hexdigit(_ value: UInt8) -> UInt8 { - return value < 10 ? (0x30 + value) : (0x41 + value - 10) -} - -/// Describes a type which can be written to a byte stream. -public protocol ByteStreamable { - func write(to stream: WritableByteStream) -} - -/// An output byte stream. -/// -/// This protocol is designed to be able to support efficient streaming to -/// different output destinations, e.g., a file or an in memory buffer. This is -/// loosely modeled on LLVM's llvm::raw_ostream class. -/// -/// The stream is generally used in conjunction with the `appending` function. -/// For example: -/// -/// let stream = BufferedOutputByteStream() -/// stream.appending("Hello, world!") -/// -/// would write the UTF8 encoding of "Hello, world!" to the stream. -/// -/// The stream accepts a number of custom formatting operators which are defined -/// in the `Format` struct (used for namespacing purposes). For example: -/// -/// let items = ["hello", "world"] -/// stream.appending(Format.asSeparatedList(items, separator: " ")) -/// -/// would write each item in the list to the stream, separating them with a -/// space. -public protocol WritableByteStream: AnyObject, TextOutputStream, Closable { - /// The current offset within the output stream. - var position: Int { get } - - /// Write an individual byte to the buffer. - func write(_ byte: UInt8) - - /// Write a collection of bytes to the buffer. - func write(_ bytes: C) where C.Element == UInt8 - - /// Flush the stream's buffer. - func flush() -} - -// Default noop implementation of close to avoid source-breaking downstream dependents with the addition of the close -// API. -public extension WritableByteStream { - func close() throws { } -} - -// Public alias to the old name to not introduce API compatibility. -public typealias OutputByteStream = WritableByteStream - -#if os(Android) || canImport(Musl) -public typealias FILEPointer = OpaquePointer -#else -public typealias FILEPointer = UnsafeMutablePointer -#endif - -extension WritableByteStream { - /// Write a sequence of bytes to the buffer. - public func write(sequence: S) where S.Iterator.Element == UInt8 { - // Iterate the sequence and append byte by byte since sequence's append - // is not performant anyway. - for byte in sequence { - write(byte) - } - } - - /// Write a string to the buffer (as UTF8). - public func write(_ string: String) { - // FIXME(performance): Use `string.utf8._copyContents(initializing:)`. - write(string.utf8) - } - - /// Write a string (as UTF8) to the buffer, with escaping appropriate for - /// embedding within a JSON document. - /// - /// - Note: This writes the literal data applying JSON string escaping, but - /// does not write any other characters (like the quotes that would surround - /// a JSON string). - public func writeJSONEscaped(_ string: String) { - // See RFC7159 for reference: https://tools.ietf.org/html/rfc7159 - for character in string.utf8 { - // Handle string escapes; we use constants here to directly match the RFC. - switch character { - // Literal characters. - case 0x20...0x21, 0x23...0x5B, 0x5D...0xFF: - write(character) - - // Single-character escaped characters. - case 0x22: // '"' - write(0x5C) // '\' - write(0x22) // '"' - case 0x5C: // '\\' - write(0x5C) // '\' - write(0x5C) // '\' - case 0x08: // '\b' - write(0x5C) // '\' - write(0x62) // 'b' - case 0x0C: // '\f' - write(0x5C) // '\' - write(0x66) // 'b' - case 0x0A: // '\n' - write(0x5C) // '\' - write(0x6E) // 'n' - case 0x0D: // '\r' - write(0x5C) // '\' - write(0x72) // 'r' - case 0x09: // '\t' - write(0x5C) // '\' - write(0x74) // 't' - - // Multi-character escaped characters. - default: - write(0x5C) // '\' - write(0x75) // 'u' - write(hexdigit(0)) - write(hexdigit(0)) - write(hexdigit(character >> 4)) - write(hexdigit(character & 0xF)) - } - } - } - - // MARK: helpers that return `self` - - // FIXME: This override shouldn't be necesary but removing it causes a 30% performance regression. This problem is - // tracked by the following bug: https://bugs.swift.org/browse/SR-8535 - @discardableResult - public func send(_ value: ArraySlice) -> WritableByteStream { - value.write(to: self) - return self - } - - @discardableResult - public func send(_ value: ByteStreamable) -> WritableByteStream { - value.write(to: self) - return self - } - - @discardableResult - public func send(_ value: CustomStringConvertible) -> WritableByteStream { - value.description.write(to: self) - return self - } - - @discardableResult - public func send(_ value: ByteStreamable & CustomStringConvertible) -> WritableByteStream { - value.write(to: self) - return self - } -} - -/// The `WritableByteStream` base class. -/// -/// This class provides a base and efficient implementation of the `WritableByteStream` -/// protocol. It can not be used as is-as subclasses as several functions need to be -/// implemented in subclasses. -public class _WritableByteStreamBase: WritableByteStream { - /// If buffering is enabled - @usableFromInline let _buffered : Bool - - /// The data buffer. - /// - Note: Minimum Buffer size should be one. - @usableFromInline var _buffer: [UInt8] - - /// Default buffer size of the data buffer. - private static let bufferSize = 1024 - - /// Queue to protect mutating operation. - fileprivate let queue = DispatchQueue(label: "org.swift.swiftpm.basic.stream") - - init(buffered: Bool) { - self._buffered = buffered - self._buffer = [] - - // When not buffered we still reserve 1 byte, as it is used by the - // by the single byte write() variant. - self._buffer.reserveCapacity(buffered ? _WritableByteStreamBase.bufferSize : 1) - } - - // MARK: Data Access API - - /// The current offset within the output stream. - public var position: Int { - return _buffer.count - } - - /// Currently available buffer size. - @usableFromInline var _availableBufferSize: Int { - return _buffer.capacity - _buffer.count - } - - /// Clears the buffer maintaining current capacity. - @usableFromInline func _clearBuffer() { - _buffer.removeAll(keepingCapacity: true) - } - - // MARK: Data Output API - - public final func flush() { - writeImpl(ArraySlice(_buffer)) - _clearBuffer() - flushImpl() - } - - @usableFromInline func flushImpl() { - // Do nothing. - } - - public final func close() throws { - try closeImpl() - } - - @usableFromInline func closeImpl() throws { - fatalError("Subclasses must implement this") - } - - @usableFromInline func writeImpl(_ bytes: C) where C.Iterator.Element == UInt8 { - fatalError("Subclasses must implement this") - } - - @usableFromInline func writeImpl(_ bytes: ArraySlice) { - fatalError("Subclasses must implement this") - } - - /// Write an individual byte to the buffer. - public final func write(_ byte: UInt8) { - guard _buffered else { - _buffer.append(byte) - writeImpl(ArraySlice(_buffer)) - flushImpl() - _clearBuffer() - return - } - - // If buffer is full, write and clear it. - if _availableBufferSize == 0 { - writeImpl(ArraySlice(_buffer)) - _clearBuffer() - } - - // This will need to change change if we ever have unbuffered stream. - precondition(_availableBufferSize > 0) - _buffer.append(byte) - } - - /// Write a collection of bytes to the buffer. - @inlinable public final func write(_ bytes: C) where C.Element == UInt8 { - guard _buffered else { - if let b = bytes as? ArraySlice { - // Fast path for unbuffered ArraySlice - writeImpl(b) - } else if let b = bytes as? Array { - // Fast path for unbuffered Array - writeImpl(ArraySlice(b)) - } else { - // generic collection unfortunately must be temporarily buffered - writeImpl(bytes) - } - flushImpl() - return - } - - // This is based on LLVM's raw_ostream. - let availableBufferSize = self._availableBufferSize - let byteCount = Int(bytes.count) - - // If we have to insert more than the available space in buffer. - if byteCount > availableBufferSize { - // If buffer is empty, start writing and keep the last chunk in buffer. - if _buffer.isEmpty { - let bytesToWrite = byteCount - (byteCount % availableBufferSize) - let writeUptoIndex = bytes.index(bytes.startIndex, offsetBy: numericCast(bytesToWrite)) - writeImpl(bytes.prefix(upTo: writeUptoIndex)) - - // If remaining bytes is more than buffer size write everything. - let bytesRemaining = byteCount - bytesToWrite - if bytesRemaining > availableBufferSize { - writeImpl(bytes.suffix(from: writeUptoIndex)) - return - } - // Otherwise keep remaining in buffer. - _buffer += bytes.suffix(from: writeUptoIndex) - return - } - - let writeUptoIndex = bytes.index(bytes.startIndex, offsetBy: numericCast(availableBufferSize)) - // Append whatever we can accommodate. - _buffer += bytes.prefix(upTo: writeUptoIndex) - - writeImpl(ArraySlice(_buffer)) - _clearBuffer() - - // FIXME: We should start again with remaining chunk but this doesn't work. Write everything for now. - //write(collection: bytes.suffix(from: writeUptoIndex)) - writeImpl(bytes.suffix(from: writeUptoIndex)) - return - } - _buffer += bytes - } -} - -/// The thread-safe wrapper around output byte streams. -/// -/// This class wraps any `WritableByteStream` conforming type to provide a type-safe -/// access to its operations. If the provided stream inherits from `_WritableByteStreamBase`, -/// it will also ensure it is type-safe will all other `ThreadSafeOutputByteStream` instances -/// around the same stream. -public final class ThreadSafeOutputByteStream: WritableByteStream { - private static let defaultQueue = DispatchQueue(label: "org.swift.swiftpm.basic.thread-safe-output-byte-stream") - public let stream: WritableByteStream - private let queue: DispatchQueue - - public var position: Int { - return queue.sync { - stream.position - } - } - - public init(_ stream: WritableByteStream) { - self.stream = stream - self.queue = (stream as? _WritableByteStreamBase)?.queue ?? ThreadSafeOutputByteStream.defaultQueue - } - - public func write(_ byte: UInt8) { - queue.sync { - stream.write(byte) - } - } - - public func write(_ bytes: C) where C.Element == UInt8 { - queue.sync { - stream.write(bytes) - } - } - - public func flush() { - queue.sync { - stream.flush() - } - } - - public func write(sequence: S) where S.Iterator.Element == UInt8 { - queue.sync { - stream.write(sequence: sequence) - } - } - - public func writeJSONEscaped(_ string: String) { - queue.sync { - stream.writeJSONEscaped(string) - } - } - - public func close() throws { - try queue.sync { - try stream.close() - } - } -} - - -#if swift(<5.6) -extension ThreadSafeOutputByteStream: UnsafeSendable {} -#else -extension ThreadSafeOutputByteStream: @unchecked Sendable {} -#endif - -/// Define an output stream operator. We need it to be left associative, so we -/// use `<<<`. -infix operator <<< : StreamingPrecedence -precedencegroup StreamingPrecedence { - associativity: left -} - -// MARK: Output Operator Implementations - -// FIXME: This override shouldn't be necesary but removing it causes a 30% performance regression. This problem is -// tracked by the following bug: https://bugs.swift.org/browse/SR-8535 - -@available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") -@discardableResult -public func <<< (stream: WritableByteStream, value: ArraySlice) -> WritableByteStream { - value.write(to: stream) - return stream -} - -@available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") -@discardableResult -public func <<< (stream: WritableByteStream, value: ByteStreamable) -> WritableByteStream { - value.write(to: stream) - return stream -} - -@available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") -@discardableResult -public func <<< (stream: WritableByteStream, value: CustomStringConvertible) -> WritableByteStream { - value.description.write(to: stream) - return stream -} - -@available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") -@discardableResult -public func <<< (stream: WritableByteStream, value: ByteStreamable & CustomStringConvertible) -> WritableByteStream { - value.write(to: stream) - return stream -} - -extension UInt8: ByteStreamable { - public func write(to stream: WritableByteStream) { - stream.write(self) - } -} - -extension Character: ByteStreamable { - public func write(to stream: WritableByteStream) { - stream.write(String(self)) - } -} - -extension String: ByteStreamable { - public func write(to stream: WritableByteStream) { - stream.write(self.utf8) - } -} - -extension Substring: ByteStreamable { - public func write(to stream: WritableByteStream) { - stream.write(self.utf8) - } -} - -extension StaticString: ByteStreamable { - public func write(to stream: WritableByteStream) { - withUTF8Buffer { stream.write($0) } - } -} - -extension Array: ByteStreamable where Element == UInt8 { - public func write(to stream: WritableByteStream) { - stream.write(self) - } -} - -extension ArraySlice: ByteStreamable where Element == UInt8 { - public func write(to stream: WritableByteStream) { - stream.write(self) - } -} - -extension ContiguousArray: ByteStreamable where Element == UInt8 { - public func write(to stream: WritableByteStream) { - stream.write(self) - } -} - -// MARK: Formatted Streaming Output - -/// Provides operations for returning derived streamable objects to implement various forms of formatted output. -public struct Format { - /// Write the input boolean encoded as a JSON object. - static public func asJSON(_ value: Bool) -> ByteStreamable { - return JSONEscapedBoolStreamable(value: value) - } - private struct JSONEscapedBoolStreamable: ByteStreamable { - let value: Bool - - func write(to stream: WritableByteStream) { - stream.send(value ? "true" : "false") - } - } - - /// Write the input integer encoded as a JSON object. - static public func asJSON(_ value: Int) -> ByteStreamable { - return JSONEscapedIntStreamable(value: value) - } - private struct JSONEscapedIntStreamable: ByteStreamable { - let value: Int - - func write(to stream: WritableByteStream) { - // FIXME: Diagnose integers which cannot be represented in JSON. - stream.send(value.description) - } - } - - /// Write the input double encoded as a JSON object. - static public func asJSON(_ value: Double) -> ByteStreamable { - return JSONEscapedDoubleStreamable(value: value) - } - private struct JSONEscapedDoubleStreamable: ByteStreamable { - let value: Double - - func write(to stream: WritableByteStream) { - // FIXME: What should we do about NaN, etc.? - // - // FIXME: Is Double.debugDescription the best representation? - stream.send(value.debugDescription) - } - } - - /// Write the input CustomStringConvertible encoded as a JSON object. - static public func asJSON(_ value: T) -> ByteStreamable { - return JSONEscapedStringStreamable(value: value.description) - } - /// Write the input string encoded as a JSON object. - static public func asJSON(_ string: String) -> ByteStreamable { - return JSONEscapedStringStreamable(value: string) - } - private struct JSONEscapedStringStreamable: ByteStreamable { - let value: String - - func write(to stream: WritableByteStream) { - stream.send(UInt8(ascii: "\"")) - stream.writeJSONEscaped(value) - stream.send(UInt8(ascii: "\"")) - } - } - - /// Write the input string list encoded as a JSON object. - static public func asJSON(_ items: [T]) -> ByteStreamable { - return JSONEscapedStringListStreamable(items: items.map({ $0.description })) - } - /// Write the input string list encoded as a JSON object. - // - // FIXME: We might be able to make this more generic through the use of a "JSONEncodable" protocol. - static public func asJSON(_ items: [String]) -> ByteStreamable { - return JSONEscapedStringListStreamable(items: items) - } - private struct JSONEscapedStringListStreamable: ByteStreamable { - let items: [String] - - func write(to stream: WritableByteStream) { - stream.send(UInt8(ascii: "[")) - for (i, item) in items.enumerated() { - if i != 0 { stream.send(",") } - stream.send(Format.asJSON(item)) - } - stream.send(UInt8(ascii: "]")) - } - } - - /// Write the input dictionary encoded as a JSON object. - static public func asJSON(_ items: [String: String]) -> ByteStreamable { - return JSONEscapedDictionaryStreamable(items: items) - } - private struct JSONEscapedDictionaryStreamable: ByteStreamable { - let items: [String: String] - - func write(to stream: WritableByteStream) { - stream.send(UInt8(ascii: "{")) - for (offset: i, element: (key: key, value: value)) in items.enumerated() { - if i != 0 { stream.send(",") } - stream.send(Format.asJSON(key)).send(":").send(Format.asJSON(value)) - } - stream.send(UInt8(ascii: "}")) - } - } - - /// Write the input list (after applying a transform to each item) encoded as a JSON object. - // - // FIXME: We might be able to make this more generic through the use of a "JSONEncodable" protocol. - static public func asJSON(_ items: [T], transform: @escaping (T) -> String) -> ByteStreamable { - return JSONEscapedTransformedStringListStreamable(items: items, transform: transform) - } - private struct JSONEscapedTransformedStringListStreamable: ByteStreamable { - let items: [T] - let transform: (T) -> String - - func write(to stream: WritableByteStream) { - stream.send(UInt8(ascii: "[")) - for (i, item) in items.enumerated() { - if i != 0 { stream.send(",") } - stream.send(Format.asJSON(transform(item))) - } - stream.send(UInt8(ascii: "]")) - } - } - - /// Write the input list to the stream with the given separator between items. - static public func asSeparatedList(_ items: [T], separator: String) -> ByteStreamable { - return SeparatedListStreamable(items: items, separator: separator) - } - private struct SeparatedListStreamable: ByteStreamable { - let items: [T] - let separator: String - - func write(to stream: WritableByteStream) { - for (i, item) in items.enumerated() { - // Add the separator, if necessary. - if i != 0 { - stream.send(separator) - } - - stream.send(item) - } - } - } - - /// Write the input list to the stream (after applying a transform to each item) with the given separator between - /// items. - static public func asSeparatedList( - _ items: [T], - transform: @escaping (T) -> ByteStreamable, - separator: String - ) -> ByteStreamable { - return TransformedSeparatedListStreamable(items: items, transform: transform, separator: separator) - } - private struct TransformedSeparatedListStreamable: ByteStreamable { - let items: [T] - let transform: (T) -> ByteStreamable - let separator: String - - func write(to stream: WritableByteStream) { - for (i, item) in items.enumerated() { - if i != 0 { stream.send(separator) } - stream.send(transform(item)) - } - } - } - - static public func asRepeating(string: String, count: Int) -> ByteStreamable { - return RepeatingStringStreamable(string: string, count: count) - } - private struct RepeatingStringStreamable: ByteStreamable { - let string: String - let count: Int - - init(string: String, count: Int) { - precondition(count >= 0, "Count should be >= zero") - self.string = string - self.count = count - } - - func write(to stream: WritableByteStream) { - for _ in 0..(_ bytes: C) where C.Iterator.Element == UInt8 { - contents += bytes - } - override final func writeImpl(_ bytes: ArraySlice) { - contents += bytes - } - - override final func closeImpl() throws { - // Do nothing. The protocol does not require to stop receiving writes, close only signals that resources could - // be released at this point should we need to. - } -} - -/// Represents a stream which is backed to a file. Not for instantiating. -public class FileOutputByteStream: _WritableByteStreamBase { - - public override final func closeImpl() throws { - flush() - try fileCloseImpl() - } - - /// Closes the file flushing any buffered data. - func fileCloseImpl() throws { - fatalError("fileCloseImpl() should be implemented by a subclass") - } -} - -/// Implements file output stream for local file system. -public final class LocalFileOutputByteStream: FileOutputByteStream { - - /// The pointer to the file. - let filePointer: FILEPointer - - /// Set to an error value if there were any IO error during writing. - private var error: FileSystemError? - - /// Closes the file on deinit if true. - private var closeOnDeinit: Bool - - /// Path to the file this stream should operate on. - private let path: AbsolutePath? - - /// Instantiate using the file pointer. - public init(filePointer: FILEPointer, closeOnDeinit: Bool = true, buffered: Bool = true) throws { - self.filePointer = filePointer - self.closeOnDeinit = closeOnDeinit - self.path = nil - super.init(buffered: buffered) - } - - /// Opens the file for writing at the provided path. - /// - /// - Parameters: - /// - path: Path to the file this stream should operate on. - /// - closeOnDeinit: If true closes the file on deinit. clients can use - /// close() if they want to close themselves or catch - /// errors encountered during writing to the file. - /// Default value is true. - /// - buffered: If true buffers writes in memory until full or flush(). - /// Otherwise, writes are processed and flushed immediately. - /// Default value is true. - /// - /// - Throws: FileSystemError - public init(_ path: AbsolutePath, closeOnDeinit: Bool = true, buffered: Bool = true) throws { - guard let filePointer = fopen(path.pathString, "wb") else { - throw FileSystemError(errno: errno, path) - } - self.path = path - self.filePointer = filePointer - self.closeOnDeinit = closeOnDeinit - super.init(buffered: buffered) - } - - deinit { - if closeOnDeinit { - fclose(filePointer) - } - } - - func errorDetected(code: Int32?) { - if let code = code { - error = .init(.ioError(code: code), path) - } else { - error = .init(.unknownOSError, path) - } - } - - override final func writeImpl(_ bytes: C) where C.Iterator.Element == UInt8 { - // FIXME: This will be copying bytes but we don't have option currently. - var contents = [UInt8](bytes) - while true { - let n = fwrite(&contents, 1, contents.count, filePointer) - if n < 0 { - if errno == EINTR { continue } - errorDetected(code: errno) - } else if n != contents.count { - errorDetected(code: nil) - } - break - } - } - - override final func writeImpl(_ bytes: ArraySlice) { - bytes.withUnsafeBytes { bytesPtr in - while true { - let n = fwrite(bytesPtr.baseAddress!, 1, bytesPtr.count, filePointer) - if n < 0 { - if errno == EINTR { continue } - errorDetected(code: errno) - } else if n != bytesPtr.count { - errorDetected(code: nil) - } - break - } - } - } - - override final func flushImpl() { - fflush(filePointer) - } - - override final func fileCloseImpl() throws { - defer { - fclose(filePointer) - // If clients called close we shouldn't call fclose again in deinit. - closeOnDeinit = false - } - // Throw if errors were found during writing. - if let error = error { - throw error - } - } -} - -/// Public stdout stream instance. -public var stdoutStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( - filePointer: stdout, - closeOnDeinit: false)) - -/// Public stderr stream instance. -public var stderrStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( - filePointer: stderr, - closeOnDeinit: false)) diff --git a/Tool/Sources/GitHelper/CurrentChange.swift b/Tool/Sources/GitHelper/CurrentChange.swift deleted file mode 100644 index d7680f25..00000000 --- a/Tool/Sources/GitHelper/CurrentChange.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation -import LanguageServerProtocol - -public struct PRChange: Equatable, Codable { - public let uri: DocumentUri - public let path: String - public let baseContent: String - public let headContent: String - - public var originalContent: String { headContent } -} - -public enum CurrentChangeService { - public static func getPRChanges( - _ repositoryURL: URL, - group: GitDiffGroup, - shouldIncludeFile: (URL) -> Bool - ) async -> [PRChange] { - let gitStats = await GitDiff.getDiffFiles(repositoryURL: repositoryURL, group: group) - - var changes: [PRChange] = [] - - for stat in gitStats { - guard shouldIncludeFile(stat.url) else { continue } - - guard let content = try? String(contentsOf: stat.url, encoding: .utf8) - else { continue } - let uri = stat.url.absoluteString - - let relativePath = Self.getRelativePath(fileURL: stat.url, repositoryURL: repositoryURL) - - switch stat.status { - case .untracked, .indexAdded: - changes.append(.init(uri: uri, path: relativePath, baseContent: "", headContent: content)) - - case .modified: - guard let originalContent = GitShow.showHeadContent(of: relativePath, repositoryURL: repositoryURL) else { - continue - } - changes.append(.init(uri: uri, path: relativePath, baseContent: originalContent, headContent: content)) - - case .deleted, .indexRenamed: - continue - } - } - - // Include untracked files - if group == .workingTree { - let untrackedGitStats = GitStatus.getStatus(repositoryURL: repositoryURL, untrackedFilesOption: .all) - for stat in untrackedGitStats { - guard !changes.contains(where: { $0.uri == stat.url.absoluteString }), - let content = try? String(contentsOf: stat.url, encoding: .utf8) - else { continue } - - let relativePath = Self.getRelativePath(fileURL: stat.url, repositoryURL: repositoryURL) - changes.append( - .init(uri: stat.url.absoluteString, path: relativePath, baseContent: "", headContent: content) - ) - } - } - - return changes - } - - // TODO: Handle cases of multi-project and referenced file - private static func getRelativePath(fileURL: URL, repositoryURL: URL) -> String { - var relativePath = fileURL.path.replacingOccurrences(of: repositoryURL.path, with: "") - if relativePath.starts(with: "/") { - relativePath = String(relativePath.dropFirst()) - } - - return relativePath - } -} diff --git a/Tool/Sources/GitHelper/GitDiff.swift b/Tool/Sources/GitHelper/GitDiff.swift deleted file mode 100644 index b8cf4a00..00000000 --- a/Tool/Sources/GitHelper/GitDiff.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation -import SystemUtils - -public enum GitDiffGroup { - case index // Staged - case workingTree // Unstaged -} - -public struct GitDiff { - public static func getDiff(of filePath: String, repositoryURL: URL, group: GitDiffGroup) async -> String { - var arguments = ["diff"] - if group == .index { - arguments.append("--cached") - } - arguments.append(contentsOf: ["--", filePath]) - - let result = try? SystemUtils.executeCommand( - inDirectory: repositoryURL.path, - path: GitPath, - arguments: arguments - ) - - return result ?? "" - } - - public static func getDiffFiles(repositoryURL: URL, group: GitDiffGroup) async -> [GitChange] { - var arguments = ["diff", "--name-status", "-z", "--diff-filter=ADMR"] - if group == .index { - arguments.append("--cached") - } - - let result = try? SystemUtils.executeCommand( - inDirectory: repositoryURL.path, - path: GitPath, - arguments: arguments - ) - - return result == nil - ? [] - : Self.parseDiff(repositoryURL: repositoryURL, raw: result!) - } - - private static func parseDiff(repositoryURL: URL, raw: String) -> [GitChange] { - var index = 0 - var result: [GitChange] = [] - let segments = raw.trimmingCharacters(in: .whitespacesAndNewlines) - .split(separator: "\0") - .map(String.init) - .filter { !$0.isEmpty } - - segmentsLoop: while index < segments.count - 1 { - let change = segments[index] - index += 1 - - let resourcePath = segments[index] - index += 1 - - if change.isEmpty || resourcePath.isEmpty { - break - } - - let originalURL: URL - if resourcePath.hasPrefix("/") { - originalURL = URL(fileURLWithPath: resourcePath) - } else { - originalURL = repositoryURL.appendingPathComponent(resourcePath) - } - - var url = originalURL - var status = GitFileStatus.untracked - - // Copy or Rename status comes with a number (ex: 'R100'). - // We don't need the number, we use only first character of the status. - switch change.first { - case "A": - status = .indexAdded - - case "M": - status = .modified - - case "D": - status = .deleted - - // Rename contains two paths, the second one is what the file is renamed/copied to. - case "R": - if index >= segments.count { - break - } - - let newPath = segments[index] - index += 1 - - if newPath.isEmpty { - break - } - - status = .indexRenamed - if newPath.hasPrefix("/") { - url = URL(fileURLWithPath: newPath) - } else { - url = repositoryURL.appendingPathComponent(newPath) - } - - default: - // Unknown status - break segmentsLoop - } - - result.append(.init(url: url, originalURL: originalURL, status: status)) - } - - return result - } -} diff --git a/Tool/Sources/GitHelper/GitHunk.swift b/Tool/Sources/GitHelper/GitHunk.swift deleted file mode 100644 index 2939dd99..00000000 --- a/Tool/Sources/GitHelper/GitHunk.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation - -public struct GitHunk { - public let startDeletedLine: Int // 1-based - public let deletedLines: Int - public let startAddedLine: Int // 1-based - public let addedLines: Int - public let additions: [(start: Int, length: Int)] - public let diffText: String - - public init( - startDeletedLine: Int, - deletedLines: Int, - startAddedLine: Int, - addedLines: Int, - additions: [(start: Int, length: Int)], - diffText: String - ) { - self.startDeletedLine = startDeletedLine - self.deletedLines = deletedLines - self.startAddedLine = startAddedLine - self.addedLines = addedLines - self.additions = additions - self.diffText = diffText - } -} - -public extension GitHunk { - static func parseDiff(_ diff: String) -> [GitHunk] { - var hunkTexts = diff.components(separatedBy: "\n@@") - - if !hunkTexts.isEmpty, hunkTexts.last?.hasSuffix("\n") == true { - hunkTexts[hunkTexts.count - 1] = String(hunkTexts.last!.dropLast()) - } - - let hunks: [GitHunk] = hunkTexts.compactMap { chunk -> GitHunk? in - let rangePattern = #"-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?"# - let regex = try! NSRegularExpression(pattern: rangePattern) - let nsString = chunk as NSString - - guard let match = regex.firstMatch( - in: chunk, - options: [], - range: NSRange(location: 0, length: nsString.length) - ) - else { return nil } - - var startDeletedLine = Int(nsString.substring(with: match.range(at: 1))) ?? 0 - let deletedLines = match.range(at: 2).location != NSNotFound - ? Int(nsString.substring(with: match.range(at: 2))) ?? 1 - : 1 - var startAddedLine = Int(nsString.substring(with: match.range(at: 3))) ?? 0 - let addedLines = match.range(at: 4).location != NSNotFound - ? Int(nsString.substring(with: match.range(at: 4))) ?? 1 - : 1 - - var additions: [(start: Int, length: Int)] = [] - let lines = Array(chunk.components(separatedBy: "\n").dropFirst()) - var d = 0 - var addStart: Int? - - for line in lines { - let ch = line.first ?? Character(" ") - - if ch == "+" { - if addStart == nil { - addStart = startAddedLine + d - } - d += 1 - } else { - if let start = addStart { - additions.append((start: start, length: startAddedLine + d - start)) - addStart = nil - } - if ch == " " { - d += 1 - } - } - } - - if let start = addStart { - additions.append((start: start, length: startAddedLine + d - start)) - } - - if startDeletedLine == 0 { - startDeletedLine = 1 - } - - if startAddedLine == 0 { - startAddedLine = 1 - } - - return GitHunk( - startDeletedLine: startDeletedLine, - deletedLines: deletedLines, - startAddedLine: startAddedLine, - addedLines: addedLines, - additions: additions, - diffText: lines.joined(separator: "\n") - ) - } - - return hunks - } -} diff --git a/Tool/Sources/GitHelper/GitShow.swift b/Tool/Sources/GitHelper/GitShow.swift deleted file mode 100644 index 6eaf858f..00000000 --- a/Tool/Sources/GitHelper/GitShow.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import SystemUtils - -public struct GitShow { - public static func showHeadContent(of filePath: String, repositoryURL: URL) -> String? { - let escapedFilePath = Self.escapePath(filePath) - let arguments = ["show", "HEAD:\(escapedFilePath)"] - - let result = try? SystemUtils.executeCommand( - inDirectory: repositoryURL.path, - path: GitPath, - arguments: arguments - ) - - return result - } - - private static func escapePath(_ string: String) -> String { - let charactersToEscape = CharacterSet(charactersIn: " '\"&()[]{}$`\\|;<>*?~") - return string.unicodeScalars.map { scalar in - charactersToEscape.contains(scalar) ? "\\\(Character(scalar))" : String(Character(scalar)) - }.joined() - } -} diff --git a/Tool/Sources/GitHelper/GitStatus.swift b/Tool/Sources/GitHelper/GitStatus.swift deleted file mode 100644 index eb769403..00000000 --- a/Tool/Sources/GitHelper/GitStatus.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import SystemUtils - -public enum UntrackedFilesOption: String { - case all, no, normal -} - -public struct GitStatus { - static let unTrackedFilePrefix = "?? " - - public static func getStatus(repositoryURL: URL, untrackedFilesOption: UntrackedFilesOption = .all) -> [GitChange] { - let arguments = ["status", "--porcelain", "--untracked-files=\(untrackedFilesOption.rawValue)"] - - let result = try? SystemUtils.executeCommand( - inDirectory: repositoryURL.path, - path: GitPath, - arguments: arguments - ) - - if let result = result { - return Self.parseStatus(statusOutput: result, repositoryURL: repositoryURL) - } else { - return [] - } - } - - private static func parseStatus(statusOutput: String, repositoryURL: URL) -> [GitChange] { - var changes: [GitChange] = [] - let fileManager = FileManager.default - - let lines = statusOutput.components(separatedBy: .newlines) - for line in lines { - if line.hasPrefix(unTrackedFilePrefix) { - let fileRelativePath = String(line.dropFirst(unTrackedFilePrefix.count)) - let fileURL = repositoryURL.appendingPathComponent(fileRelativePath) - - guard fileManager.fileExists(atPath: fileURL.path) else { continue } - - changes.append( - .init(url: fileURL, originalURL: fileURL, status: .untracked) - ) - } - } - - return changes - } -} diff --git a/Tool/Sources/GitHelper/types.swift b/Tool/Sources/GitHelper/types.swift deleted file mode 100644 index 26adcec7..00000000 --- a/Tool/Sources/GitHelper/types.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -let GitPath = "/usr/bin/git" - -public enum GitFileStatus { - case untracked - case indexAdded - case modified - case deleted - case indexRenamed -} - -public struct GitChange { - public let url: URL - public let originalURL: URL - public let status: GitFileStatus - - public init(url: URL, originalURL: URL, status: GitFileStatus) { - self.url = url - self.originalURL = originalURL - self.status = status - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/ClientToolHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/ClientToolHandler.swift deleted file mode 100644 index 46f92ee5..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/ClientToolHandler.swift +++ /dev/null @@ -1,27 +0,0 @@ -import JSONRPC -import ConversationServiceProvider -import Combine - -public protocol ClientToolHandler { - var onClientToolInvokeEvent: PassthroughSubject<(InvokeClientToolRequest, (AnyJSONRPCResponse) -> Void), Never> { get } - func invokeClientTool(_ params: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) - - var onClientToolConfirmationEvent: PassthroughSubject<(InvokeClientToolConfirmationRequest, (AnyJSONRPCResponse) -> Void), Never> { get } - func invokeClientToolConfirmation(_ params: InvokeClientToolConfirmationRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) -} - -public final class ClientToolHandlerImpl: ClientToolHandler { - - public static let shared = ClientToolHandlerImpl() - - public let onClientToolInvokeEvent: PassthroughSubject<(InvokeClientToolRequest, (AnyJSONRPCResponse) -> Void), Never> = .init() - public let onClientToolConfirmationEvent: PassthroughSubject<(InvokeClientToolConfirmationRequest, (AnyJSONRPCResponse) -> Void), Never> = .init() - - public func invokeClientTool(_ request: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) { - onClientToolInvokeEvent.send((request, completion)) - } - - public func invokeClientToolConfirmation(_ request: InvokeClientToolConfirmationRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) { - onClientToolConfirmationEvent.send((request, completion)) - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/ConversationContextHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/ConversationContextHandler.swift deleted file mode 100644 index bd8ad82d..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/ConversationContextHandler.swift +++ /dev/null @@ -1,17 +0,0 @@ -import JSONRPC -import Combine - -public protocol ConversationContextHandler { - var onConversationContext: PassthroughSubject<(ConversationContextRequest, (AnyJSONRPCResponse) -> Void), Never> { get } - func handleConversationContext(_ request: ConversationContextRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) -} - -public final class ConversationContextHandlerImpl: ConversationContextHandler { - public static let shared = ConversationContextHandlerImpl() - - public var onConversationContext = PassthroughSubject<(ConversationContextRequest, (AnyJSONRPCResponse) -> Void), Never>() - - public func handleConversationContext(_ request: ConversationContextRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) { - onConversationContext.send((request, completion)) - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/ConversationProgressHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/ConversationProgressHandler.swift deleted file mode 100644 index 4a7c559b..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/ConversationProgressHandler.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Combine -import Foundation -import JSONRPC -import LanguageServerProtocol -import Logger - -public enum ProgressKind: String { - case begin, report, end -} - -public protocol ConversationProgressHandler { - var onBegin: PassthroughSubject<(String, ConversationProgressBegin), Never> { get } - var onProgress: PassthroughSubject<(String, ConversationProgressReport), Never> { get } - var onEnd: PassthroughSubject<(String, ConversationProgressEnd), Never> { get } - func handleConversationProgress(_ progressParams: ProgressParams) -} - -public final class ConversationProgressHandlerImpl: ConversationProgressHandler { - public static let shared = ConversationProgressHandlerImpl() - - public var onBegin = PassthroughSubject<(String, ConversationProgressBegin), Never>() - public var onProgress = PassthroughSubject<(String, ConversationProgressReport), Never>() - public var onEnd = PassthroughSubject<(String, ConversationProgressEnd), Never>() - - private var cancellables = Set() - - public func handleConversationProgress(_ progressParams: ProgressParams) { - guard let token = getValueAsString(from: progressParams.token), - let data = try? JSONEncoder().encode(progressParams.value) else { - print("Error encountered while parsing conversation progress params") - Logger.gitHubCopilot.error("Error encountered while parsing conversation progress params") - return - } - - let progress = try? JSONDecoder().decode(ConversationProgressContainer.self, from: data) - switch progress { - case .begin(let begin): - onBegin.send((token, begin)) - case .report(let report): - onProgress.send((token, report)) - case .end(let end): - onEnd.send((token, end)) - default: - print("Invalid progress kind") - return - } -} - - private func getValueAsString(from token: ProgressToken) -> String? { - switch token { - case .optionA(let intValue): - return String(intValue) - case .optionB(let stringValue): - return stringValue - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/MCPOAuthRequestHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/MCPOAuthRequestHandler.swift deleted file mode 100644 index 47ea5017..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/MCPOAuthRequestHandler.swift +++ /dev/null @@ -1,67 +0,0 @@ -import JSONRPC -import Foundation -import Combine -import Logger -import AppKit - -public protocol MCPOAuthRequestHandler { - func handleShowOAuthMessage( - _ request: MCPOAuthRequest, - completion: @escaping ( - AnyJSONRPCResponse - ) -> Void - ) -} - -public final class MCPOAuthRequestHandlerImpl: MCPOAuthRequestHandler { - public static let shared = MCPOAuthRequestHandlerImpl() - - public func handleShowOAuthMessage(_ request: MCPOAuthRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) { - guard let params = request.params else { return } - Logger.gitHubCopilot.debug("Received MCP OAuth Request: \(params)") - Task { @MainActor in - let confirmResult = showMCPOAuthAlert(params) - let jsonResult = try? JSONEncoder().encode(MCPOAuthResponse(confirm: confirmResult)) - let jsonValue = (try? JSONDecoder().decode(JSONValue.self, from: jsonResult ?? Data())) ?? JSONValue.null - completion(AnyJSONRPCResponse(id: request.id, result: jsonValue)) - } - } - - @MainActor - func showMCPOAuthAlert(_ params: MCPOAuthRequestParams) -> Bool { - let alert = NSAlert() - let mcpConfigString = UserDefaults.shared.value(for: \.gitHubCopilotMCPConfig) - - var serverName = params.mcpServer // Default fallback - - if let mcpConfigData = mcpConfigString.data(using: .utf8), - let mcpConfig = try? JSONDecoder().decode(JSONValue.self, from: mcpConfigData) { - // Iterate through the servers to find a match for the mcpServer URL - if case .hash(let serversDict) = mcpConfig { - for (userDefinedName, serverConfig) in serversDict { - if let url = serverConfig["url"]?.stringValue { - // Check if the mcpServer URL matches the configured URL - if params.mcpServer.contains(url) || url.contains(params.mcpServer) { - serverName = userDefinedName - break - } - } - } - } - } - - alert.messageText = "GitHub Copilot" - alert.informativeText = "The MCP Server Definition '\(serverName)' wants to authenticate to \(params.authLabel)." - alert.alertStyle = .informational - - alert.addButton(withTitle: "Continue") - alert.addButton(withTitle: "Cancel") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - return true - } else { - return false - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/ShowMessageRequestHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/ShowMessageRequestHandler.swift deleted file mode 100644 index cf137aa3..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/ShowMessageRequestHandler.swift +++ /dev/null @@ -1,22 +0,0 @@ -import JSONRPC -import Combine - -public protocol ShowMessageRequestHandler { - var onShowMessage: PassthroughSubject<(ShowMessageRequest, (AnyJSONRPCResponse) -> Void), Never> { get } - func handleShowMessage( - _ request: ShowMessageRequest, - completion: @escaping ( - AnyJSONRPCResponse - ) -> Void - ) -} - -public final class ShowMessageRequestHandlerImpl: ShowMessageRequestHandler { - public static let shared = ShowMessageRequestHandlerImpl() - - public let onShowMessage: PassthroughSubject<(ShowMessageRequest, (AnyJSONRPCResponse) -> Void), Never> = .init() - - public func handleShowMessage(_ request: ShowMessageRequest, completion: @escaping (AnyJSONRPCResponse) -> Void) { - onShowMessage.send((request, completion)) - } -} diff --git a/Tool/Sources/GitHubCopilotService/Conversation/WatchedFilesHandler.swift b/Tool/Sources/GitHubCopilotService/Conversation/WatchedFilesHandler.swift deleted file mode 100644 index 281b534d..00000000 --- a/Tool/Sources/GitHubCopilotService/Conversation/WatchedFilesHandler.swift +++ /dev/null @@ -1,80 +0,0 @@ -import JSONRPC -import Combine -import Workspace -import XcodeInspector -import Foundation -import ConversationServiceProvider - -public protocol WatchedFilesHandler { - func handleWatchedFiles(_ request: WatchedFilesRequest, workspaceURL: URL, completion: @escaping (AnyJSONRPCResponse) -> Void, service: GitHubCopilotService?) -} - -public final class WatchedFilesHandlerImpl: WatchedFilesHandler { - public static let shared = WatchedFilesHandlerImpl() - - public func handleWatchedFiles(_ request: WatchedFilesRequest, workspaceURL: URL, completion: @escaping (AnyJSONRPCResponse) -> Void, service: GitHubCopilotService?) { - guard let params = request.params, params.workspaceFolder.uri != "/" else { return } - - let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL(workspaceURL: workspaceURL, documentURL: nil) ?? workspaceURL - - let files = WorkspaceFile.getWatchedFiles( - workspaceURL: workspaceURL, - projectURL: projectURL, - excludeGitIgnoredFiles: params.excludeGitignoredFiles, - excludeIDEIgnoredFiles: params.excludeIDEIgnoredFiles - ) - WorkspaceFileIndex.shared.setFiles(files, for: workspaceURL) - - let fileUris = files.prefix(10000).map { $0.url.absoluteString } // Set max number of indexing file to 10000 - - let batchSize = BatchingFileChangeWatcher.maxEventPublishSize - /// only `batchSize`(100) files to complete this event for setup watching workspace in CLS side - let jsonResult: JSONValue = .array(fileUris.prefix(batchSize).map { .hash(["uri": .string($0)]) }) - let jsonValue: JSONValue = .hash(["files": jsonResult]) - - completion(AnyJSONRPCResponse(id: request.id, result: jsonValue)) - - Task { - if fileUris.count > batchSize { - for startIndex in stride(from: batchSize, to: fileUris.count, by: batchSize) { - let endIndex = min(startIndex + batchSize, fileUris.count) - let batch = Array(fileUris[startIndex.. 15 * 1024 * 1024 - { return } - - Task { - let content: String - do { - content = try String(contentsOf: documentURL, encoding: .utf8) - } catch { - Logger.extension.info("Failed to read \(documentURL.lastPathComponent): \(error)") - return - } - - do { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.notifyOpenTextDocument(fileURL: documentURL, content: content) - } catch { - Logger.gitHubCopilot.info(error.localizedDescription) - } - } - } - - public func workspace(_ workspace: WorkspaceInfo, didSaveDocumentAt documentURL: URL) { - guard isLanguageServerInUse else { return } - Task { - do { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.notifySaveTextDocument(fileURL: documentURL) - } catch { - Logger.gitHubCopilot.info(error.localizedDescription) - } - } - } - - public func workspace(_ workspace: WorkspaceInfo, didCloseDocumentAt documentURL: URL) { - guard isLanguageServerInUse else { return } - Task { - do { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.notifyCloseTextDocument(fileURL: documentURL) - } catch { - Logger.gitHubCopilot.info(error.localizedDescription) - } - } - } - - public func workspace( - _ workspace: WorkspaceInfo, - didUpdateDocumentAt documentURL: URL, - content: String? - ) { - guard isLanguageServerInUse else { return } - // check if file size is larger than 15MB, if so, return immediately - if let attrs = try? FileManager.default - .attributesOfItem(atPath: documentURL.path), - let fileSize = attrs[FileAttributeKey.size] as? UInt64, - fileSize > 15 * 1024 * 1024 - { return } - - Task { - guard let content else { return } - guard let service = await serviceLocator.getService(from: workspace) else { return } - do { - try await service.notifyChangeTextDocument( - fileURL: documentURL, - content: content, - version: 0 - ) - } catch let error as ServerError { - switch error { - case .serverError(-32602, _, _): // parameter incorrect - Logger.gitHubCopilot.error(error.localizedDescription) - // Reopen document if it's not found in the language server - self.workspace(workspace, didOpenDocumentAt: documentURL) - default: - Logger.gitHubCopilot.info(error.localizedDescription) - } - } catch { - Logger.gitHubCopilot.info(error.localizedDescription) - } - } - } - - public func extensionUsageDidChange(_ usage: ExtensionUsage) { - extensionUsage = usage - if !usage.isChatServiceInUse && !usage.isSuggestionServiceInUse { - terminate() - } - } - - public func terminate() { - for workspace in workspacePool.workspaces.values { - guard let plugin = workspace.plugin(for: GitHubCopilotWorkspacePlugin.self) - else { continue } - plugin.terminate() - } - } -} - -protocol ServiceLocatorType { - func getService(from workspace: WorkspaceInfo) async -> GitHubCopilotService? -} - -final class ServiceLocator: ServiceLocatorType { - let workspacePool: WorkspacePool - - init(workspacePool: WorkspacePool) { - self.workspacePool = workspacePool - } - - func getService(from workspace: WorkspaceInfo) async -> GitHubCopilotService? { - guard let workspace = workspacePool.workspaces[workspace.workspaceURL], - let plugin = workspace.plugin(for: GitHubCopilotWorkspacePlugin.self) - else { - return nil - } - return plugin.gitHubCopilotService - } -} diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift deleted file mode 100644 index cf73d46d..00000000 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotWorkspacePlugin.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import Logger -import Workspace - -public final class GitHubCopilotWorkspacePlugin: WorkspacePlugin { - public var gitHubCopilotService: GitHubCopilotService? - - public override init(workspace: Workspace) { - super.init(workspace: workspace) - do { - gitHubCopilotService = try createGitHubCopilotService() - } catch { - Logger.gitHubCopilot.error("Failed to create GitHub Copilot service: \(error)") - } - } - - deinit { - if let gitHubCopilotService { - Task { await gitHubCopilotService.terminate() } - } - } - - func createGitHubCopilotService() throws -> GitHubCopilotService { - let newService = try GitHubCopilotService(projectRootURL: projectRootURL, workspaceURL: workspaceURL) - Task { - try await Task.sleep(nanoseconds: 1_000_000_000) - finishLaunchingService() - } - return newService - } - - func finishLaunchingService() { - guard let workspace, let gitHubCopilotService else { return } - Task { - for (_, filespace) in workspace.filespaces { - let documentURL = filespace.fileURL - guard let content = try? String(contentsOf: documentURL) else { continue } - try? await gitHubCopilotService.notifyOpenTextDocument( - fileURL: documentURL, - content: content - ) - } - } - } - - func terminate() { - gitHubCopilotService = nil - } -} - diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CLSErrorInfo.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CLSErrorInfo.swift deleted file mode 100644 index ea745a66..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CLSErrorInfo.swift +++ /dev/null @@ -1,46 +0,0 @@ -import LanguageServerProtocol - -public enum CLSErrorCode: Int { - // defined by JSON-RPC - case parseError = -32700 - case invalidRequest = -32600 - case methodNotFound = -32601 - case invalidParams = -32602 - case internalError = -32603 - - // defined by LSP (see https://microsoft.github.io/language-server-protocol/specification/#responseMessage) - case serverNotInitialized = -32002 - case requestFailed = -32803; - case serverCancelled = -32802; - case contentModified = -32801; - case requestCancelled = -32800; - - // used by the Copilot Language Server - case noCopilotToken = 1000 - case deviceFlowFailed = 1001 - case copilotNotAvailable = 1002 -} - -public struct CLSErrorInfo { - public let code: Int - public let message: String - public let data: Codable? - - public init?(for error: ServerError) { - if case .serverError(let code, let message, let data) = error { - self.code = code - self.message = message - self.data = data - } else { - return nil - } - } - - public var clsErrorCode: CLSErrorCode? { - CLSErrorCode(rawValue: code) - } - - public var affectsAuthStatus: Bool { - clsErrorCode == CLSErrorCode.noCopilotToken - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/ClientToolRegistry.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/ClientToolRegistry.swift deleted file mode 100644 index e78c9cde..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/ClientToolRegistry.swift +++ /dev/null @@ -1,127 +0,0 @@ - -import ConversationServiceProvider - -func registerClientTools(server: GitHubCopilotConversationServiceType) async { - var tools: [LanguageModelToolInformation] = [] - let runInTerminalTool = LanguageModelToolInformation( - name: ToolName.runInTerminal.rawValue, - description: "Run a shell command in a terminal. State is persistent across tool calls.\n- Use this tool instead of printing a shell codeblock and asking the user to run it.\n- If the command is a long-running background process, you MUST pass isBackground=true. Background terminals will return a terminal ID which you can use to check the output of a background process with get_terminal_output.\n- If a command may use a pager, you must something to disable it. For example, you can use `git --no-pager`. Otherwise you should add something like ` | cat`. Examples: git, less, man, etc.", - inputSchema: LanguageModelToolSchema( - type: "object", - properties: [ - "command": ToolInputPropertySchema( - type: "string", - description: "The command to run in the terminal."), - "explanation": ToolInputPropertySchema( - type: "string", - description: "A one-sentence description of what the command does. This will be shown to the user before the command is run."), - "isBackground": ToolInputPropertySchema( - type: "boolean", - description: "Whether the command starts a background process. If true, the command will run in the background and you will not see the output. If false, the tool call will block on the command finishing, and then you will get the output. Examples of background processes: building in watch mode, starting a server. You can check the output of a background process later on by using get_terminal_output.") - ], - required: [ - "command", - "explanation", - "isBackground" - ]), - confirmationMessages: LanguageModelToolConfirmationMessages( - title: "Run command In Terminal", - message: "Run command In Terminal" - ) - ) - let getErrorsTool: LanguageModelToolInformation = .init( - name: ToolName.getErrors.rawValue, - description: "Get any compile or lint errors in a code file. If the user mentions errors or problems in a file, they may be referring to these. Use the tool to see the same errors that the user is seeing. Also use this tool after editing a file to validate the change.", - inputSchema: .init( - type: "object", - properties: [ - "filePaths": .init( - type: "array", - description: "The absolute paths to the files to check for errors.", - items: .init(type: "string") - ) - ], - required: ["filePaths"] - ) - ) - - let getTerminalOutputTool = LanguageModelToolInformation( - name: ToolName.getTerminalOutput.rawValue, - description: "Get the output of a terminal command previously started using run_in_terminal", - inputSchema: LanguageModelToolSchema( - type: "object", - properties: [ - "id": ToolInputPropertySchema( - type: "string", - description: "The ID of the terminal command output to check." - ) - ], - required: [ - "id" - ]) - ) - - let createFileTool: LanguageModelToolInformation = .init( - name: ToolName.createFile.rawValue, - description: "This is a tool for creating a new file in the workspace. The file will be created with the specified content.", - inputSchema: .init( - type: "object", - properties: [ - "filePath": .init( - type: "string", - description: "The absolute path to the file to create." - ), - "content": .init( - type: "string", - description: "The content to write to the file." - ) - ], - required: ["filePath", "content"] - ) - ) - - let insertEditIntoFileTool: LanguageModelToolInformation = .init( - name: ToolName.insertEditIntoFile.rawValue, - description: "Insert new code into an existing file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the \"explanation\" property first.\nThe system is very smart and can understand how to apply your edits to the files, you just need to provide minimal hints.\nAvoid repeating existing code, instead use comments to represent regions of unchanged code. Be as concise as possible. For example:\n// ...existing code...\n{ changed code }\n// ...existing code...\n{ changed code }\n// ...existing code...\n\nHere is an example of how you should use format an edit to an existing Person class:\nclass Person {\n\t// ...existing code...\n\tage: number;\n\t// ...existing code...\n\tgetAge() {\n\treturn this.age;\n\t}\n}", - inputSchema: .init( - type: "object", - properties: [ - "filePath": .init(type: "string", description: "An absolute path to the file to edit."), - "code": .init(type: "string", description: "The code change to apply to the file.\nThe system is very smart and can understand how to apply your edits to the files, you just need to provide minimal hints.\nAvoid repeating existing code, instead use comments to represent regions of unchanged code. Be as concise as possible. For example:\n// ...existing code...\n{ changed code }\n// ...existing code...\n{ changed code }\n// ...existing code...\n\nHere is an example of how you should use format an edit to an existing Person class:\nclass Person {\n\t// ...existing code...\n\tage: number;\n\t// ...existing code...\n\tgetAge() {\n\t\treturn this.age;\n\t}\n}"), - "explanation": .init(type: "string", description: "A short explanation of the edit being made.") - ], - required: ["filePath", "code", "explanation"] - ) - ) - - let fetchWebPageTool: LanguageModelToolInformation = .init( - name: ToolName.fetchWebPage.rawValue, - description: "Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.", - inputSchema: .init( - type: "object", - properties: [ - "urls": .init( - type: "array", - description: "An array of web page URLs to fetch content from.", - items: .init(type: "string") - ), - ], - required: ["urls"] - ), - confirmationMessages: LanguageModelToolConfirmationMessages( - title: "Fetch Web Page", - message: "Web content may contain malicious code or attempt prompt injection attacks." - ) - ) - - tools.append(runInTerminalTool) - tools.append(getTerminalOutputTool) - tools.append(getErrorsTool) - tools.append(insertEditIntoFileTool) - tools.append(createFileTool) - tools.append(fetchWebPageTool) - - if !tools.isEmpty { - try? await server.registerTools(tools: tools) - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotAuthStatusWatcher.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotAuthStatusWatcher.swift deleted file mode 100644 index ab8b9590..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotAuthStatusWatcher.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -class CopilotAuthStatusWatcher { - static let pollInterval: TimeInterval = 30 - private var timer: Timer? - - public init(_ service: GitHubCopilotService) { - Task { @MainActor in - self.timer = Timer.scheduledTimer(withTimeInterval: Self.pollInterval, repeats: true) { [weak service] _ in - service?.updateStatusInBackground() - } - } - } - - deinit { - let t = timer - Task { @MainActor in - t?.invalidate() - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift deleted file mode 100644 index 29e33d35..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift +++ /dev/null @@ -1,334 +0,0 @@ -import Combine -import Foundation -import JSONRPC -import LanguageClient -import LanguageServerProtocol -import Logger -import ProcessEnv -import Status - -public enum ServerError: LocalizedError { - case handlerUnavailable(String) - case unhandledMethod(String) - case notificationDispatchFailed(Error) - case requestDispatchFailed(Error) - case clientDataUnavailable(Error) - case serverUnavailable - case missingExpectedParameter - case missingExpectedResult - case unableToDecodeRequest(Error) - case unableToSendRequest(Error) - case unableToSendNotification(Error) - case serverError(code: Int, message: String, data: Codable?) - case invalidRequest(Error?) - case timeout - case unknownError(Error) - - static func responseError(_ error: AnyJSONRPCResponseError) -> ServerError { - return ServerError.serverError(code: error.code, - message: error.message, - data: error.data) - } - - static func convertToServerError(error: any Error) -> ServerError { - if let serverError = error as? ServerError { - return serverError - } else if let jsonRPCError = error as? AnyJSONRPCResponseError { - return responseError(jsonRPCError) - } - - return .unknownError(error) - } -} - -public typealias LSPResponse = Decodable & Sendable - -/// A clone of the `LocalProcessServer`. -/// We need it because the original one does not allow us to handle custom notifications. -class CopilotLocalProcessServer { - public var notificationPublisher: PassthroughSubject = PassthroughSubject() - - private var process: Process? - private var wrappedServer: CustomJSONRPCServerConnection? - - private var cancellables = Set() - @MainActor var ongoingCompletionRequestIDs: [JSONId] = [] - @MainActor var ongoingConversationRequestIDs = [String: JSONId]() - - public convenience init( - path: String, - arguments: [String], - environment: [String: String]? = nil - ) { - let params = Process.ExecutionParameters( - path: path, - arguments: arguments, - environment: environment - ) - - self.init(executionParameters: params) - } - - init(executionParameters parameters: Process.ExecutionParameters) { - do { - let channel: DataChannel = try startLocalProcess(parameters: parameters, terminationHandler: processTerminated) - let noop: @Sendable (Data) async -> Void = { _ in } - let newChannel = DataChannel.tap(channel: channel.withMessageFraming(), onRead: noop, onWrite: onWriteRequest) - - self.wrappedServer = CustomJSONRPCServerConnection(dataChannel: newChannel, notificationHandler: handleNotification) - } catch { - Logger.gitHubCopilot.error("Failed to start local CLS process: \(error)") - } - } - - deinit { - self.process?.terminate() - } - - private func startLocalProcess(parameters: Process.ExecutionParameters, - terminationHandler: @escaping @Sendable () -> Void) throws -> DataChannel { - let (channel, process) = try DataChannel.localProcessChannel(parameters: parameters, terminationHandler: terminationHandler) - - // Create a serial queue to synchronize writes - let writeQueue = DispatchQueue(label: "DataChannel.writeQueue") - let stdinPipe: Pipe = process.standardInput as! Pipe - self.process = process - let handler: DataChannel.WriteHandler = { data in - try writeQueue.sync { - // write is not thread-safe, so we need to use queue to ensure it thread-safe - try stdinPipe.fileHandleForWriting.write(contentsOf: data) - } - } - - let wrappedChannel = DataChannel( - writeHandler: handler, - dataSequence: channel.dataSequence - ) - - return wrappedChannel - } - - @Sendable - private func onWriteRequest(data: Data) { - guard let request = try? JSONDecoder().decode(JSONRPCRequest.self, from: data) else { - return - } - - if request.method == "getCompletionsCycling" { - Task { @MainActor [weak self] in - self?.ongoingCompletionRequestIDs.append(request.id) - } - } else if request.method == "conversation/create" { - Task { @MainActor [weak self] in - if let paramsData = try? JSONEncoder().encode(request.params) { - do { - let params = try JSONDecoder().decode(ConversationCreateParams.self, from: paramsData) - self?.ongoingConversationRequestIDs[params.workDoneToken] = request.id - } catch { - // Handle decoding error - Logger.gitHubCopilot.error("Error decoding ConversationCreateParams: \(error)") - } - } - } - } else if request.method == "conversation/turn" { - Task { @MainActor [weak self] in - if let paramsData = try? JSONEncoder().encode(request.params) { - do { - let params = try JSONDecoder().decode(TurnCreateParams.self, from: paramsData) - self?.ongoingConversationRequestIDs[params.workDoneToken] = request.id - } catch { - // Handle decoding error - Logger.gitHubCopilot.error("Error decoding TurnCreateParams: \(error)") - } - } - } - } - } - - @Sendable - private func processTerminated() { - // releasing the server here will short-circuit any pending requests, - // which might otherwise take a while to time out, if ever. - wrappedServer = nil - } - - private func handleNotification( - _ anyNotification: AnyJSONRPCNotification, - data: Data - ) -> Bool { - let methodName = anyNotification.method - let debugDescription = encodeJSONParams(params: anyNotification.params) - if let method = ServerNotification.Method(rawValue: methodName) { - switch method { - case .windowLogMessage: - Logger.gitHubCopilot.info("\(anyNotification.method): \(debugDescription)") - return true - case .protocolProgress: - notificationPublisher.send(anyNotification) - return true - default: - return false - } - } else { - switch methodName { - case "LogMessage": - Logger.gitHubCopilot.info("\(anyNotification.method): \(debugDescription)") - return true - case "didChangeStatus": - Logger.gitHubCopilot.info("\(anyNotification.method): \(debugDescription)") - if let payload = GitHubCopilotNotification.StatusNotification.decode(fromParams: anyNotification.params) { - Task { - await Status.shared - .updateCLSStatus( - payload.kind.clsStatus, - busy: payload.busy, - message: payload.message ?? "" - ) - } - } - return true - case "copilot/didChangeFeatureFlags": - notificationPublisher.send(anyNotification) - return true - case "copilot/mcpTools": - notificationPublisher.send(anyNotification) - return true - case "copilot/mcpRuntimeLogs": - notificationPublisher.send(anyNotification) - return true - case "conversation/preconditionsNotification", "statusNotification": - // Ignore - return true - default: - return false - } - } - } -} - -extension CopilotLocalProcessServer: ServerConnection { - var eventSequence: EventSequence { - guard let server = wrappedServer else { - let result = EventSequence.makeStream() - result.continuation.finish() - return result.stream - } - - return server.eventSequence - } - - public func sendNotification(_ notif: ClientNotification) async throws { - guard let server = wrappedServer, let process = process, process.isRunning else { - throw ServerError.serverUnavailable - } - - do { - try await server.sendNotification(notif) - } catch { - throw ServerError.unableToSendNotification(error) - } - } - - /// send copilot specific notification - public func sendCopilotNotification(_ notif: CopilotClientNotification) async throws -> Void { - guard let server = wrappedServer, let process = process, process.isRunning else { - throw ServerError.serverUnavailable - } - - let method = notif.method.rawValue - - switch notif { - case .copilotDidChangeWatchedFiles(let params): - do { - try await server.sendNotification(params, method: method) - } catch { - throw ServerError.unableToSendNotification(error) - } - } - } - - /// Cancel ongoing completion requests. - public func cancelOngoingTasks() async { - let task = Task { @MainActor in - for id in ongoingCompletionRequestIDs { - await cancelTask(id) - } - self.ongoingCompletionRequestIDs = [] - } - await task.value - } - - public func cancelOngoingTask(_ workDoneToken: String) async { - let task = Task { @MainActor in - guard let id = ongoingConversationRequestIDs[workDoneToken] else { return } - await cancelTask(id) - } - await task.value - } - - public func cancelTask(_ id: JSONId) async { - guard let server = wrappedServer, let process = process, process.isRunning else { - return - } - - switch id { - case let .numericId(id): - try? await server.sendNotification(.protocolCancelRequest(.init(id: id))) - case let .stringId(id): - try? await server.sendNotification(.protocolCancelRequest(.init(id: id))) - } - } - - public func sendRequest( - _ request: ClientRequest - ) async throws -> Response { - guard let server = wrappedServer, let process = process, process.isRunning else { - throw ServerError.serverUnavailable - } - - do { - return try await server.sendRequest(request) - } catch { - throw ServerError.convertToServerError(error: error) - } - } -} - -func encodeJSONParams(params: JSONValue?) -> String { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - if let jsonData = try? encoder.encode(params), - let text = String(data: jsonData, encoding: .utf8) - { - return text - } - return "N/A" -} - -// MARK: - Copilot custom notification - -public struct CopilotDidChangeWatchedFilesParams: Codable, Hashable { - /// The CLS need an additional parameter `workspaceUri` for "workspace/didChangeWatchedFiles" event - public var workspaceUri: String - public var changes: [FileEvent] - - public init(workspaceUri: String, changes: [FileEvent]) { - self.workspaceUri = workspaceUri - self.changes = changes - } -} - -public enum CopilotClientNotification { - public enum Method: String { - case workspaceDidChangeWatchedFiles = "workspace/didChangeWatchedFiles" - } - - case copilotDidChangeWatchedFiles(CopilotDidChangeWatchedFilesParams) - - public var method: Method { - switch self { - case .copilotDidChangeWatchedFiles: - return .workspaceDidChangeWatchedFiles - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotMCPToolManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotMCPToolManager.swift deleted file mode 100644 index a2baecbc..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotMCPToolManager.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import Logger - -public extension Notification.Name { - static let gitHubCopilotMCPToolsDidChange = Notification - .Name("com.github.CopilotForXcode.CopilotMCPToolsDidChange") -} - -public class CopilotMCPToolManager { - private static var availableMCPServerTools: [MCPServerToolsCollection]? - - public static func updateMCPTools(_ serverToolsCollections: [MCPServerToolsCollection]) { - let sortedMCPServerTools = serverToolsCollections.sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) - guard sortedMCPServerTools != availableMCPServerTools else { return } - availableMCPServerTools = sortedMCPServerTools - DispatchQueue.main.async { - Logger.client.info("Notify about MCP tools change: \(getToolsSummary())") - DistributedNotificationCenter.default().post(name: .gitHubCopilotMCPToolsDidChange, object: nil) - } - } - - private static func getToolsSummary() -> String { - var summary = "" - guard let tools = availableMCPServerTools else { return summary } - for server in tools { - summary += "Server: \(server.name) with \(server.tools.count) tools (\(server.tools.filter { $0._status == .enabled }.count) enabled, \(server.tools.filter { $0._status == .disabled }.count) disabled). " - } - - return summary - } - - public static func getAvailableMCPTools() -> [MCPTool]? { - // Flatten all tools from all servers into a single array - return availableMCPServerTools?.flatMap { $0.tools } - } - - public static func getAvailableMCPServerToolsCollections() -> [MCPServerToolsCollection]? { - return availableMCPServerTools - } - - public static func hasMCPTools() -> Bool { - return availableMCPServerTools != nil && !availableMCPServerTools!.isEmpty - } - - public static func clearMCPTools() { - availableMCPServerTools = [] - DispatchQueue.main.async { - DistributedNotificationCenter.default().post(name: .gitHubCopilotMCPToolsDidChange, object: nil) - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotModelManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotModelManager.swift deleted file mode 100644 index 898dd5b0..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotModelManager.swift +++ /dev/null @@ -1,43 +0,0 @@ -import ConversationServiceProvider -import Foundation - -public extension Notification.Name { - static let gitHubCopilotModelsDidChange = Notification - .Name("com.github.CopilotForXcode.CopilotModelsDidChange") - static let gitHubCopilotShouldSwitchFallbackModel = Notification - .Name("com.github.CopilotForXcode.CopilotShouldSwitchFallbackModel") -} - -public class CopilotModelManager { - private static var availableLLMs: [CopilotModel] = [] - private static var fallbackLLMs: [CopilotModel] = [] - - public static func updateLLMs(_ models: [CopilotModel]) { - let sortedModels = models.sorted(by: { $0.modelName.lowercased() < $1.modelName.lowercased() }) - guard sortedModels != availableLLMs else { return } - availableLLMs = sortedModels - fallbackLLMs = models.filter({ $0.isChatFallback}) - NotificationCenter.default.post(name: .gitHubCopilotModelsDidChange, object: nil) - } - - public static func getAvailableLLMs() -> [CopilotModel] { - return availableLLMs - } - - public static func hasLLMs() -> Bool { - return !availableLLMs.isEmpty - } - - public static func getFallbackLLM(scope: PromptTemplateScope) -> CopilotModel? { - return fallbackLLMs.first(where: { $0.scopes.contains(scope) && $0.billing?.isPremium == false}) - } - - public static func switchToFallbackModel() { - NotificationCenter.default.post(name: .gitHubCopilotShouldSwitchFallbackModel, object: nil) - } - - public static func clearLLMs() { - availableLLMs = [] - NotificationCenter.default.post(name: .gitHubCopilotModelsDidChange, object: nil) - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CustomJSONRPCServerConnection.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CustomJSONRPCServerConnection.swift deleted file mode 100644 index d65e9c4c..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CustomJSONRPCServerConnection.swift +++ /dev/null @@ -1,378 +0,0 @@ -import Foundation -import LanguageClient -import JSONRPC -import LanguageServerProtocol - -/// A clone of the `JSONRPCServerConnection`. -/// We need it because the original one does not allow us to handle custom notifications. -public actor CustomJSONRPCServerConnection: ServerConnection { - public let eventSequence: EventSequence - private let eventContinuation: EventSequence.Continuation - - private let session: JSONRPCSession - - /// NOTE: The channel will wrapped with message framing - public init(dataChannel: DataChannel, notificationHandler: ((AnyJSONRPCNotification, Data) -> Bool)? = nil) { - self.notificationHandler = notificationHandler - self.session = JSONRPCSession(channel: dataChannel) - - (self.eventSequence, self.eventContinuation) = EventSequence.makeStream() - - Task { - await startMonitoringSession() - } - } - - deinit { - eventContinuation.finish() - } - - private func startMonitoringSession() async { - let seq = await session.eventSequence - - for await event in seq { - - switch event { - case let .notification(notification, data): - self.handleNotification(notification, data: data) - case let .request(request, handler, data): - self.handleRequest(request, data: data, handler: handler) - case .error: - break // TODO? - } - - } - - eventContinuation.finish() - } - - public func sendNotification(_ notif: ClientNotification) async throws { - let method = notif.method.rawValue - - switch notif { - case .initialized(let params): - try await session.sendNotification(params, method: method) - case .exit: - try await session.sendNotification(method: method) - case .textDocumentDidChange(let params): - try await session.sendNotification(params, method: method) - case .textDocumentDidOpen(let params): - try await session.sendNotification(params, method: method) - case .textDocumentDidClose(let params): - try await session.sendNotification(params, method: method) - case .textDocumentWillSave(let params): - try await session.sendNotification(params, method: method) - case .textDocumentDidSave(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidChangeWatchedFiles(let params): - try await session.sendNotification(params, method: method) - case .protocolCancelRequest(let params): - try await session.sendNotification(params, method: method) - case .protocolSetTrace(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidChangeWorkspaceFolders(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidChangeConfiguration(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidCreateFiles(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidRenameFiles(let params): - try await session.sendNotification(params, method: method) - case .workspaceDidDeleteFiles(let params): - try await session.sendNotification(params, method: method) - case .windowWorkDoneProgressCancel(let params): - try await session.sendNotification(params, method: method) - } - } - - public func sendRequest(_ request: ClientRequest) async throws -> Response - where Response: Decodable & Sendable { - let method = request.method.rawValue - - switch request { - case .initialize(let params, _): - return try await session.response(to: method, params: params) - case .shutdown: - return try await session.response(to: method) - case .workspaceExecuteCommand(let params, _): - return try await session.response(to: method, params: params) - case .workspaceInlayHintRefresh: - return try await session.response(to: method) - case .workspaceWillCreateFiles(let params, _): - return try await session.response(to: method, params: params) - case .workspaceWillRenameFiles(let params, _): - return try await session.response(to: method, params: params) - case .workspaceWillDeleteFiles(let params, _): - return try await session.response(to: method, params: params) - case .workspaceSymbol(let params, _): - return try await session.response(to: method, params: params) - case .workspaceSymbolResolve(let params, _): - return try await session.response(to: method, params: params) - case .textDocumentWillSaveWaitUntil(let params, _): - return try await session.response(to: method, params: params) - case .completion(let params, _): - return try await session.response(to: method, params: params) - case .completionItemResolve(let params, _): - return try await session.response(to: method, params: params) - case .hover(let params, _): - return try await session.response(to: method, params: params) - case .signatureHelp(let params, _): - return try await session.response(to: method, params: params) - case .declaration(let params, _): - return try await session.response(to: method, params: params) - case .definition(let params, _): - return try await session.response(to: method, params: params) - case .typeDefinition(let params, _): - return try await session.response(to: method, params: params) - case .implementation(let params, _): - return try await session.response(to: method, params: params) - case .documentHighlight(let params, _): - return try await session.response(to: method, params: params) - case .documentSymbol(let params, _): - return try await session.response(to: method, params: params) - case .codeAction(let params, _): - return try await session.response(to: method, params: params) - case .codeActionResolve(let params, _): - return try await session.response(to: method, params: params) - case .codeLens(let params, _): - return try await session.response(to: method, params: params) - case .codeLensResolve(let params, _): - return try await session.response(to: method, params: params) - case .selectionRange(let params, _): - return try await session.response(to: method, params: params) - case .linkedEditingRange(let params, _): - return try await session.response(to: method, params: params) - case .prepareCallHierarchy(let params, _): - return try await session.response(to: method, params: params) - case .prepareRename(let params, _): - return try await session.response(to: method, params: params) - case .prepareTypeHierarchy(let params, _): - return try await session.response(to: method, params: params) - case .rename(let params, _): - return try await session.response(to: method, params: params) - case .inlayHint(let params, _): - return try await session.response(to: method, params: params) - case .inlayHintResolve(let params, _): - return try await session.response(to: method, params: params) - case .diagnostics(let params, _): - return try await session.response(to: method, params: params) - case .documentLink(let params, _): - return try await session.response(to: method, params: params) - case .documentLinkResolve(let params, _): - return try await session.response(to: method, params: params) - case .documentColor(let params, _): - return try await session.response(to: method, params: params) - case .colorPresentation(let params, _): - return try await session.response(to: method, params: params) - case .formatting(let params, _): - return try await session.response(to: method, params: params) - case .rangeFormatting(let params, _): - return try await session.response(to: method, params: params) - case .onTypeFormatting(let params, _): - return try await session.response(to: method, params: params) - case .references(let params, _): - return try await session.response(to: method, params: params) - case .foldingRange(let params, _): - return try await session.response(to: method, params: params) - case .moniker(let params, _): - return try await session.response(to: method, params: params) - case .semanticTokensFull(let params, _): - return try await session.response(to: method, params: params) - case .semanticTokensFullDelta(let params, _): - return try await session.response(to: method, params: params) - case .semanticTokensRange(let params, _): - return try await session.response(to: method, params: params) - case .callHierarchyIncomingCalls(let params, _): - return try await session.response(to: method, params: params) - case .callHierarchyOutgoingCalls(let params, _): - return try await session.response(to: method, params: params) - case let .custom(method, params, _): - return try await session.response(to: method, params: params) - } - } - - private func decodeNotificationParams(_ type: Params.Type, from data: Data) throws - -> Params where Params: Decodable - { - let note = try JSONDecoder().decode(JSONRPCNotification.self, from: data) - - guard let params = note.params else { - throw ProtocolError.missingParams - } - - return params - } - - private func yield(_ notification: ServerNotification) { - eventContinuation.yield(.notification(notification)) - } - - private func yield(id: JSONId, request: ServerRequest) { - eventContinuation.yield(.request(id: id, request: request)) - } - - private func handleNotification(_ anyNotification: AnyJSONRPCNotification, data: Data) { - // MARK: Handle custom notifications here. - if let handler = notificationHandler, handler(anyNotification, data) { - return - } - // MARK: End of custom notification handling. - - let methodName = anyNotification.method - - do { - guard let method = ServerNotification.Method(rawValue: methodName) else { - throw ProtocolError.unrecognizedMethod(methodName) - } - - switch method { - case .windowLogMessage: - let params = try decodeNotificationParams(LogMessageParams.self, from: data) - - yield(.windowLogMessage(params)) - case .windowShowMessage: - let params = try decodeNotificationParams(ShowMessageParams.self, from: data) - - yield(.windowShowMessage(params)) - case .textDocumentPublishDiagnostics: - let params = try decodeNotificationParams(PublishDiagnosticsParams.self, from: data) - - yield(.textDocumentPublishDiagnostics(params)) - case .telemetryEvent: - let params = anyNotification.params ?? .null - - yield(.telemetryEvent(params)) - case .protocolCancelRequest: - let params = try decodeNotificationParams(CancelParams.self, from: data) - - yield(.protocolCancelRequest(params)) - case .protocolProgress: - let params = try decodeNotificationParams(ProgressParams.self, from: data) - - yield(.protocolProgress(params)) - case .protocolLogTrace: - let params = try decodeNotificationParams(LogTraceParams.self, from: data) - - yield(.protocolLogTrace(params)) - } - } catch { - // should we backchannel this to the client somehow? - print("failed to relay notification: \(error)") - } - } - - private func decodeRequestParams(_ type: Params.Type, from data: Data) throws -> Params - where Params: Decodable { - let req = try JSONDecoder().decode(JSONRPCRequest.self, from: data) - - guard let params = req.params else { - throw ProtocolError.missingParams - } - - return params - } - - private nonisolated func makeErrorOnlyHandler(_ handler: @escaping JSONRPCEvent.RequestHandler) - -> ServerRequest.ErrorOnlyHandler - { - return { - if let error = $0 { - await handler(.failure(error)) - } else { - await handler(.success(JSONValue.null)) - } - } - } - - private nonisolated func makeHandler(_ handler: @escaping JSONRPCEvent.RequestHandler) - -> ServerRequest.Handler - { - return { - let loweredResult = $0.map({ $0 as Encodable & Sendable }) - - await handler(loweredResult) - } - } - - private func handleRequest( - _ anyRequest: AnyJSONRPCRequest, data: Data, handler: @escaping JSONRPCEvent.RequestHandler - ) { - let methodName = anyRequest.method - let id = anyRequest.id - - do { - - let method = ServerRequest.Method(rawValue: methodName) ?? .custom - switch method { - case .workspaceConfiguration: - let params = try decodeRequestParams(ConfigurationParams.self, from: data) - let reqHandler: ServerRequest.Handler<[LSPAny]> = makeHandler(handler) - - yield(id: id, request: ServerRequest.workspaceConfiguration(params, reqHandler)) - case .workspaceFolders: - let reqHandler: ServerRequest.Handler = makeHandler( - handler) - - yield(id: id, request: ServerRequest.workspaceFolders(reqHandler)) - case .workspaceApplyEdit: - let params = try decodeRequestParams(ApplyWorkspaceEditParams.self, from: data) - let reqHandler: ServerRequest.Handler = makeHandler( - handler) - - yield(id: id, request: ServerRequest.workspaceApplyEdit(params, reqHandler)) - case .clientRegisterCapability: - let params = try decodeRequestParams(RegistrationParams.self, from: data) - let reqHandler = makeErrorOnlyHandler(handler) - - yield(id: id, request: ServerRequest.clientRegisterCapability(params, reqHandler)) - case .clientUnregisterCapability: - let params = try decodeRequestParams(UnregistrationParams.self, from: data) - let reqHandler = makeErrorOnlyHandler(handler) - - yield(id: id, request: ServerRequest.clientUnregisterCapability(params, reqHandler)) - case .workspaceCodeLensRefresh: - let reqHandler = makeErrorOnlyHandler(handler) - - yield(id: id, request: ServerRequest.workspaceCodeLensRefresh(reqHandler)) - case .workspaceSemanticTokenRefresh: - let reqHandler = makeErrorOnlyHandler(handler) - - yield(id: id, request: ServerRequest.workspaceSemanticTokenRefresh(reqHandler)) - case .windowShowMessageRequest: - let params = try decodeRequestParams(ShowMessageRequestParams.self, from: data) - let reqHandler: ServerRequest.Handler = makeHandler( - handler) - - yield(id: id, request: ServerRequest.windowShowMessageRequest(params, reqHandler)) - case .windowShowDocument: - let params = try decodeRequestParams(ShowDocumentParams.self, from: data) - let reqHandler: ServerRequest.Handler = makeHandler(handler) - - yield(id: id, request: ServerRequest.windowShowDocument(params, reqHandler)) - case .windowWorkDoneProgressCreate: - let params = try decodeRequestParams(WorkDoneProgressCreateParams.self, from: data) - let reqHandler = makeErrorOnlyHandler(handler) - - yield( - id: id, request: ServerRequest.windowWorkDoneProgressCreate(params, reqHandler)) - case .custom: - let params = try decodeRequestParams(LSPAny.self, from: data) - let reqHandler: ServerRequest.Handler = makeHandler(handler) - - yield(id: id, request: ServerRequest.custom(methodName, params, reqHandler)) - - } - - } catch { - // should we backchannel this to the client somehow? - print("failed to relay request: \(error)") - } - } - - // MARK: New properties/methods to handle custom copilot notifications - private var notificationHandler: ((AnyJSONRPCNotification, Data) -> Bool)? - - public func sendNotification(_ params: Note, method: String) async throws where Note: Encodable { - try await self.session.sendNotification(params, method: method) - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotAccountStatus.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotAccountStatus.swift deleted file mode 100644 index 2a10ed5c..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotAccountStatus.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public enum GitHubCopilotAccountStatus: String, Codable, CustomStringConvertible { - case alreadySignedIn = "AlreadySignedIn" - case maybeOk = "MaybeOk" - case notAuthorized = "NotAuthorized" - case notSignedIn = "NotSignedIn" - case ok = "OK" - case failedToGetToken = "FailedToGetToken" - - public var description: String { - switch self { - case .alreadySignedIn: - return "Already Signed In" - case .maybeOk: - return "Unknown" - case .notAuthorized: - return "No Subscription" - case .notSignedIn: - return "Not Signed In" - case .ok: - return "Active" - case .failedToGetToken: - return "Failed to Get Token" - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Conversation.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Conversation.swift deleted file mode 100644 index 4c1ca9e7..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Conversation.swift +++ /dev/null @@ -1,172 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import LanguageServerProtocol -import SuggestionBasic -import ConversationServiceProvider -import JSONRPC -import Logger - -enum ConversationSource: String, Codable { - case panel, inline -} - -public struct Reference: Codable, Equatable, Hashable { - public var type: String = "file" - public let uri: String - public let position: Position? - public let visibleRange: SuggestionBasic.CursorRange? - public let selection: SuggestionBasic.CursorRange? - public let openedAt: String? - public let activeAt: String? -} - -struct ConversationCreateParams: Codable { - var workDoneToken: String - var turns: [TurnSchema] - var capabilities: Capabilities - var textDocument: Doc? - var references: [Reference]? - var computeSuggestions: Bool? - var source: ConversationSource? - var workspaceFolder: String? - var workspaceFolders: [WorkspaceFolder]? - var ignoredSkills: [String]? - var model: String? - var chatMode: String? - var needToolCallConfirmation: Bool? - var userLanguage: String? - - struct Capabilities: Codable { - var skills: [String] - var allSkills: Bool? - } -} - -// MARK: Conversation Progress - -public enum ConversationProgressKind: String, Codable { - case begin, report, end -} - -protocol BaseConversationProgress: Codable { - var kind: ConversationProgressKind { get } - var conversationId: String { get } - var turnId: String { get } -} - -public struct ConversationProgressBegin: BaseConversationProgress { - public let kind: ConversationProgressKind - public let conversationId: String - public let turnId: String -} - -public struct ConversationProgressReport: BaseConversationProgress { - - public let kind: ConversationProgressKind - public let conversationId: String - public let turnId: String - public let reply: String? - public let references: [Reference]? - public let steps: [ConversationProgressStep]? - public let editAgentRounds: [AgentRound]? -} - -public struct ConversationProgressEnd: BaseConversationProgress { - public let kind: ConversationProgressKind - public let conversationId: String - public let turnId: String - public let error: CopilotLanguageServerError? - public let followUp: ConversationFollowUp? - public let suggestedTitle: String? -} - -enum ConversationProgressContainer: Decodable { - case begin(ConversationProgressBegin) - case report(ConversationProgressReport) - case end(end: ConversationProgressEnd) - - enum CodingKeys: String, CodingKey { - case kind - } - - init(from decoder: Decoder) throws { - do { - let container = try decoder.container(keyedBy: CodingKeys.self) - let kind = try container.decode(ConversationProgressKind.self, forKey: .kind) - - switch kind { - case .begin: - let begin = try ConversationProgressBegin(from: decoder) - self = .begin(begin) - case .report: - let report = try ConversationProgressReport(from: decoder) - self = .report(report) - case .end: - let end = try ConversationProgressEnd(from: decoder) - self = .end(end: end) - } - } catch { - Logger.gitHubCopilot.error("Error decoding ConversationProgressContainer: \(error)") - throw error - } - } - } - -// MARK: Conversation rating - -struct ConversationRatingParams: Codable { - var turnId: String - var rating: ConversationRating - var doc: Doc? - var source: ConversationSource? -} - -// MARK: Conversation turn -struct TurnCreateParams: Codable { - var workDoneToken: String - var conversationId: String - var turnId: String? - var message: MessageContent - var textDocument: Doc? - var ignoredSkills: [String]? - var references: [Reference]? - var model: String? - var workspaceFolder: String? - var workspaceFolders: [WorkspaceFolder]? - var chatMode: String? - var needToolCallConfirmation: Bool? -} - -// MARK: Copy - -struct CopyCodeParams: Codable { - var turnId: String - var codeBlockIndex: Int - var copyType: CopyKind - var copiedCharacters: Int - var totalCharacters: Int - var copiedText: String - var doc: Doc? - var source: ConversationSource? -} - -// MARK: Conversation context - -public struct ConversationContextParams: Codable { - public var conversationId: String - public var turnId: String - public var skillId: String -} - -public typealias ConversationContextRequest = JSONRPCRequest - - -// MARK: Watched Files - -public struct WatchedFilesParams: Codable { - public var workspaceFolder: WorkspaceFolder - public var excludeGitignoredFiles: Bool - public var excludeIDEIgnoredFiles: Bool -} - -public typealias WatchedFilesRequest = JSONRPCRequest diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+MCP.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+MCP.swift deleted file mode 100644 index 1ad669a1..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+MCP.swift +++ /dev/null @@ -1,172 +0,0 @@ -import Foundation -import JSONRPC -import LanguageServerProtocol - -public enum MCPServerStatus: String, Codable, Equatable, Hashable { - case running = "running" - case stopped = "stopped" - case error = "error" -} - -public enum MCPToolStatus: String, Codable, Equatable, Hashable { - case enabled = "enabled" - case disabled = "disabled" -} - -public struct InputSchema: Codable, Equatable, Hashable { - public var type: String = "object" - public var properties: [String: JSONValue]? - - public init(properties: [String: JSONValue]? = nil) { - self.properties = properties - } - - // Custom coding for handling `properties` as Any - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - type = try container.decode(String.self, forKey: .type) - - if let propertiesData = try? container.decode(Data.self, forKey: .properties), - let props = try? JSONSerialization.jsonObject(with: propertiesData) as? [String: JSONValue] { - properties = props - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - - if let props = properties, - let propertiesData = try? JSONSerialization.data(withJSONObject: props) { - try container.encode(propertiesData, forKey: .properties) - } - } - - enum CodingKeys: String, CodingKey { - case type - case properties - } -} - -public struct ToolAnnotations: Codable, Equatable, Hashable { - public var title: String? - public var readOnlyHint: Bool? - public var destructiveHint: Bool? - public var idempotentHint: Bool? - public var openWorldHint: Bool? - - public init( - title: String? = nil, - readOnlyHint: Bool? = nil, - destructiveHint: Bool? = nil, - idempotentHint: Bool? = nil, - openWorldHint: Bool? = nil - ) { - self.title = title - self.readOnlyHint = readOnlyHint - self.destructiveHint = destructiveHint - self.idempotentHint = idempotentHint - self.openWorldHint = openWorldHint - } - - enum CodingKeys: String, CodingKey { - case title - case readOnlyHint - case destructiveHint - case idempotentHint - case openWorldHint - } -} - -public struct MCPTool: Codable, Equatable, Hashable { - public let name: String - public let description: String? - public let _status: MCPToolStatus - public let inputSchema: InputSchema - public var annotations: ToolAnnotations? - - public init( - name: String, - description: String? = nil, - _status: MCPToolStatus, - inputSchema: InputSchema, - annotations: ToolAnnotations? = nil - ) { - self.name = name - self.description = description - self._status = _status - self.inputSchema = inputSchema - self.annotations = annotations - } - - enum CodingKeys: String, CodingKey { - case name - case description - case _status - case inputSchema - case annotations - } -} - -public struct MCPServerToolsCollection: Codable, Equatable, Hashable { - public let name: String - public let status: MCPServerStatus - public let tools: [MCPTool] - public let error: String? - - public init(name: String, status: MCPServerStatus, tools: [MCPTool], error: String? = nil) { - self.name = name - self.status = status - self.tools = tools - self.error = error - } -} - -public struct GetAllToolsParams: Codable, Hashable { - public var servers: [MCPServerToolsCollection] - - public static func decode(fromParams params: JSONValue?) -> GetAllToolsParams? { - try? JSONDecoder().decode(Self.self, from: (try? JSONEncoder().encode(params)) ?? Data()) - } -} - -public struct UpdatedMCPToolsStatus: Codable, Hashable { - public var name: String - public var status: MCPToolStatus - - public init(name: String, status: MCPToolStatus) { - self.name = name - self.status = status - } -} - -public struct UpdateMCPToolsStatusServerCollection: Codable, Hashable { - public var name: String - public var tools: [UpdatedMCPToolsStatus] - - public init(name: String, tools: [UpdatedMCPToolsStatus]) { - self.name = name - self.tools = tools - } -} - -public struct UpdateMCPToolsStatusParams: Codable, Hashable { - public var servers: [UpdateMCPToolsStatusServerCollection] - - public init(servers: [UpdateMCPToolsStatusServerCollection]) { - self.servers = servers - } -} - -public typealias CopilotMCPToolsRequest = JSONRPCRequest - -public struct MCPOAuthRequestParams: Codable, Hashable { - public var mcpServer: String - public var authLabel: String -} - -public struct MCPOAuthResponse: Codable, Hashable { - public var confirm: Bool -} - -public typealias MCPOAuthRequest = JSONRPCRequest diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Telemetry.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Telemetry.swift deleted file mode 100644 index 8d1b580e..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest+Telemetry.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import TelemetryServiceProvider - -struct TelemetryExceptionParams: Codable { - public let transaction: String? - public let stacktrace: String? - public let properties: [String: String]? - public let platform: String? - public let exceptionDetail: [ExceptionDetail]? - - public init( - transaction: String? = nil, - stacktrace: String? = nil, - properties: [String: String]? = nil, - platform: String? = nil, - exceptionDetail: [ExceptionDetail]? = nil - ) { - self.transaction = transaction - self.stacktrace = stacktrace - self.properties = properties - self.platform = platform - self.exceptionDetail = exceptionDetail - } - - enum CodingKeys: String, CodingKey { - case transaction - case stacktrace - case properties - case platform - case exceptionDetail = "exception_detail" - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift deleted file mode 100644 index 2a352118..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ /dev/null @@ -1,520 +0,0 @@ -import Foundation -import JSONRPC -import LanguageServerProtocol -import Status -import SuggestionBasic -import ConversationServiceProvider - -struct GitHubCopilotDoc: Codable { - var source: String - var tabSize: Int - var indentSize: Int - var insertSpaces: Bool - var path: String - var uri: String - var relativePath: String - var languageId: CodeLanguage - var position: Position - /// Buffer version. Not sure what this is for, not sure how to get it - var version: Int = 0 -} - -protocol GitHubCopilotRequestType { - associatedtype Response: Codable - var request: ClientRequest { get } -} - -public struct GitHubCopilotCodeSuggestion: Codable, Equatable { - public init( - text: String, - position: CursorPosition, - uuid: String, - range: CursorRange, - displayText: String - ) { - self.text = text - self.position = position - self.uuid = uuid - self.range = range - self.displayText = displayText - } - - /// The new code to be inserted and the original code on the first line. - public var text: String - /// The position of the cursor before generating the completion. - public var position: CursorPosition - /// An id. - public var uuid: String - /// The range of the original code that should be replaced. - public var range: CursorRange - /// The new code to be inserted. - public var displayText: String -} - -public func editorConfiguration(includeMCP: Bool) -> JSONValue { - var proxyAuthorization: String? { - let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) - if username.isEmpty { return nil } - let password = UserDefaults.shared.value(for: \.gitHubCopilotProxyPassword) - return "\(username):\(password)" - } - - var http: JSONValue? { - var d: [String: JSONValue] = [:] - let proxy = UserDefaults.shared.value(for: \.gitHubCopilotProxyUrl) - if !proxy.isEmpty { - d["proxy"] = .string(proxy) - } - if let proxyAuthorization = proxyAuthorization { - d["proxyAuthorization"] = .string(proxyAuthorization) - } - let proxyStrictSSL = UserDefaults.shared.value(for: \.gitHubCopilotUseStrictSSL) - d["proxyStrictSSL"] = .bool(proxyStrictSSL) - if proxy.isEmpty && proxyStrictSSL == false { - // Setting the proxy to an empty string avoids the lanaguage server - // ignoring the proxyStrictSSL setting. - d["proxy"] = .string("") - } - return .hash(d) - } - - var authProvider: JSONValue? { - let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) - return .hash([ "uri": .string(enterpriseURI) ]) - } - - var mcp: JSONValue? { - let mcpConfig = UserDefaults.shared.value(for: \.gitHubCopilotMCPConfig) - return JSONValue.string(mcpConfig) - } - - var customInstructions: JSONValue? { - let instructions = UserDefaults.shared.value(for: \.globalCopilotInstructions) - return .string(instructions) - } - - var d: [String: JSONValue] = [:] - if let http { d["http"] = http } - if let authProvider { d["github-enterprise"] = authProvider } - if (includeMCP && mcp != nil) || customInstructions != nil { - var github: [String: JSONValue] = [:] - var copilot: [String: JSONValue] = [:] - if includeMCP { - copilot["mcp"] = mcp - } - copilot["globalCopilotInstructions"] = customInstructions - github["copilot"] = .hash(copilot) - d["github"] = .hash(github) - } - return .hash(d) -} - -public enum SignInInitiateStatus: String, Codable { - case promptUserDeviceFlow = "PromptUserDeviceFlow" - case alreadySignedIn = "AlreadySignedIn" -} - -enum GitHubCopilotRequest { - struct GetVersion: GitHubCopilotRequestType { - struct Response: Codable { - var version: String - } - - var request: ClientRequest { - .custom("getVersion", .hash([:]), ClientRequest.NullHandler) - } - } - - struct CheckStatus: GitHubCopilotRequestType { - struct Response: Codable { - var status: GitHubCopilotAccountStatus - var user: String? - } - - var request: ClientRequest { - .custom("checkStatus", .hash([:]), ClientRequest.NullHandler) - } - } - - struct CheckQuota: GitHubCopilotRequestType { - typealias Response = GitHubCopilotQuotaInfo - - var request: ClientRequest { - .custom("checkQuota", .hash([:]), ClientRequest.NullHandler) - } - } - - struct SignInInitiate: GitHubCopilotRequestType { - struct Response: Codable { - var status: SignInInitiateStatus - var userCode: String? - var verificationUri: String? - var expiresIn: Int? - var interval: Int? - var user: String? - } - - var request: ClientRequest { - .custom("signInInitiate", .hash([:]), ClientRequest.NullHandler) - } - } - - struct SignInConfirm: GitHubCopilotRequestType { - struct Response: Codable { - var status: GitHubCopilotAccountStatus - var user: String - } - - var userCode: String - - var request: ClientRequest { - .custom("signInConfirm", .hash([ - "userCode": .string(userCode), - ]), ClientRequest.NullHandler) - } - } - - struct SignOut: GitHubCopilotRequestType { - struct Response: Codable { - var status: GitHubCopilotAccountStatus - } - - var request: ClientRequest { - .custom("signOut", .hash([:]), ClientRequest.NullHandler) - } - } - - struct GetCompletions: GitHubCopilotRequestType { - struct Response: Codable { - var completions: [GitHubCopilotCodeSuggestion] - } - - var doc: GitHubCopilotDoc - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(doc)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("getCompletions", .hash([ - "doc": dict, - ]), ClientRequest.NullHandler) - } - } - - struct GetCompletionsCycling: GitHubCopilotRequestType { - struct Response: Codable { - var completions: [GitHubCopilotCodeSuggestion] - } - - var doc: GitHubCopilotDoc - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(doc)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("getCompletionsCycling", .hash([ - "doc": dict, - ]), ClientRequest.NullHandler) - } - } - - struct InlineCompletion: GitHubCopilotRequestType { - struct Response: Codable { - var items: [InlineCompletionItem] - } - - struct InlineCompletionItem: Codable { - var insertText: String - var filterText: String? - var range: Range? - var command: Command? - - struct Range: Codable { - var start: Position - var end: Position - } - - struct Command: Codable { - var title: String - var command: String - var arguments: [String]? - } - } - - var doc: Input - - struct Input: Codable { - var textDocument: _TextDocument; struct _TextDocument: Codable { - var uri: String - var version: Int - } - - var position: Position - var formattingOptions: FormattingOptions - var context: _Context; struct _Context: Codable { - enum TriggerKind: Int, Codable { - case invoked = 1 - case automatic = 2 - } - - var triggerKind: TriggerKind - } - } - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(doc)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("textDocument/inlineCompletion", dict, ClientRequest.NullHandler) - } - } - - struct GetPanelCompletions: GitHubCopilotRequestType { - struct Response: Codable { - var completions: [GitHubCopilotCodeSuggestion] - } - - var doc: GitHubCopilotDoc - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(doc)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("getPanelCompletions", .hash([ - "doc": dict, - ]), ClientRequest.NullHandler) - } - } - - struct NotifyShown: GitHubCopilotRequestType { - struct Response: Codable {} - - var completionUUID: String - - var request: ClientRequest { - .custom("notifyShown", .hash([ - "uuid": .string(completionUUID), - ]), ClientRequest.NullHandler) - } - } - - struct NotifyAccepted: GitHubCopilotRequestType { - struct Response: Codable {} - - var completionUUID: String - - var acceptedLength: Int? - - var request: ClientRequest { - var dict: [String: JSONValue] = [ - "uuid": .string(completionUUID), - ] - if let acceptedLength { - dict["acceptedLength"] = .number(Double(acceptedLength)) - } - - return .custom("notifyAccepted", .hash(dict), ClientRequest.NullHandler) - } - } - - struct NotifyRejected: GitHubCopilotRequestType { - struct Response: Codable {} - - var completionUUIDs: [String] - - var request: ClientRequest { - .custom("notifyRejected", .hash([ - "uuids": .array(completionUUIDs.map(JSONValue.string)), - ]), ClientRequest.NullHandler) - } - } - - // MARK: Conversation - - struct CreateConversation: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: ConversationCreateParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/create", dict, ClientRequest.NullHandler) - } - } - - // MARK: Conversation turn - - struct CreateTurn: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: TurnCreateParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/turn", dict, ClientRequest.NullHandler) - } - } - - // MARK: Conversation rating - - struct ConversationRating: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: ConversationRatingParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/rating", dict, ClientRequest.NullHandler) - } - } - - // MARK: Conversation templates - - struct GetTemplates: GitHubCopilotRequestType { - typealias Response = Array - - var request: ClientRequest { - .custom("conversation/templates", .hash([:]), ClientRequest.NullHandler) - } - } - - struct CopilotModels: GitHubCopilotRequestType { - typealias Response = Array - - var request: ClientRequest { - .custom("copilot/models", .hash([:]), ClientRequest.NullHandler) - } - } - - // MARK: MCP Tools - - struct UpdatedMCPToolsStatus: GitHubCopilotRequestType { - typealias Response = Array - - var params: UpdateMCPToolsStatusParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("mcp/updateToolsStatus", dict, ClientRequest.NullHandler) - } - } - - // MARK: - Conversation Agents - - struct GetAgents: GitHubCopilotRequestType { - typealias Response = Array - - var request: ClientRequest { - .custom("conversation/agents", .hash([:]), ClientRequest.NullHandler) - } - } - - // MARK: - Code Review - - struct ReviewChanges: GitHubCopilotRequestType { - typealias Response = CodeReviewResult - - var params: ReviewChangesParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("copilot/codeReview/reviewChanges", dict, ClientRequest.NullHandler) - } - } - - struct RegisterTools: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: RegisterToolsParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/registerTools", dict, ClientRequest.NullHandler) - } - } - - // MARK: Copy code - - struct CopyCode: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: CopyCodeParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/copyCode", dict, ClientRequest.NullHandler) - } - } - - // MARK: Telemetry - - struct TelemetryException: GitHubCopilotRequestType { - struct Response: Codable {} - - var params: TelemetryExceptionParams - - var request: ClientRequest { - let data = (try? JSONEncoder().encode(params)) ?? Data() - let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("telemetry/exception", dict, ClientRequest.NullHandler) - } - } -} - -// MARK: Notifications - -public enum GitHubCopilotNotification { - - public struct StatusNotification: Codable { - public enum StatusKind : String, Codable { - case normal = "Normal" - case error = "Error" - case warning = "Warning" - case inactive = "Inactive" - - public var clsStatus: CLSStatus.Status { - switch self { - case .normal: - .normal - case .error: - .error - case .warning: - .warning - case .inactive: - .inactive - } - } - } - - public var kind: StatusKind - public var busy: Bool - public var message: String? - - public static func decode(fromParams params: JSONValue?) -> StatusNotification? { - try? JSONDecoder().decode(Self.self, from: (try? JSONEncoder().encode(params)) ?? Data()) - } - } - - - public struct MCPRuntimeNotification: Codable { - public enum MCPRuntimeLogLevel: String, Codable { - case Info = "info" - case Warning = "warning" - case Error = "error" - } - - public var level: MCPRuntimeLogLevel - public var message: String - public var server: String - public var tool: String? - public var time: Double - - public static func decode(fromParams params: JSONValue?) -> MCPRuntimeNotification? { - try? JSONDecoder().decode(Self.self, from: (try? JSONEncoder().encode(params)) ?? Data()) - } - } - -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift deleted file mode 100644 index 139fd42b..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ /dev/null @@ -1,1282 +0,0 @@ -import AppKit -import TelemetryServiceProvider -import Combine -import ConversationServiceProvider -import Foundation -import JSONRPC -import LanguageClient -import LanguageServerProtocol -import Logger -import Preferences -import Status -import SuggestionBasic -import SystemUtils -import Persist - -public protocol GitHubCopilotAuthServiceType { - func checkStatus() async throws -> GitHubCopilotAccountStatus - func checkQuota() async throws -> GitHubCopilotQuotaInfo - func signInInitiate() async throws -> (status: SignInInitiateStatus, verificationUri: String?, userCode: String?, user: String?) - func signInConfirm(userCode: String) async throws - -> (username: String, status: GitHubCopilotAccountStatus) - func signOut() async throws -> GitHubCopilotAccountStatus - func version() async throws -> String -} - -public protocol GitHubCopilotSuggestionServiceType { - func getCompletions( - fileURL: URL, - content: String, - originalContent: String, - cursorPosition: CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool - ) async throws -> [CodeSuggestion] - func notifyShown(_ completion: CodeSuggestion) async - func notifyAccepted(_ completion: CodeSuggestion, acceptedLength: Int?) async - func notifyRejected(_ completions: [CodeSuggestion]) async - func notifyOpenTextDocument(fileURL: URL, content: String) async throws - func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws - func notifyCloseTextDocument(fileURL: URL) async throws - func notifySaveTextDocument(fileURL: URL) async throws - func cancelRequest() async - func terminate() async -} - -public protocol GitHubCopilotTelemetryServiceType { - func sendError(transaction: String?, - stacktrace: String?, - properties: [String: String]?, - platform: String?, - exceptionDetail: [ExceptionDetail]?) async throws -} - -public protocol GitHubCopilotConversationServiceType { - func createConversation(_ message: MessageContent, - workDoneToken: String, - workspaceFolder: String, - workspaceFolders: [WorkspaceFolder]?, - activeDoc: Doc?, - skills: [String], - ignoredSkills: [String]?, - references: [FileReference], - model: String?, - turns: [TurnSchema], - agentMode: Bool, - userLanguage: String?) async throws - func createTurn(_ message: MessageContent, - workDoneToken: String, - conversationId: String, - turnId: String?, - activeDoc: Doc?, - ignoredSkills: [String]?, - references: [FileReference], - model: String?, - workspaceFolder: String, - workspaceFolders: [WorkspaceFolder]?, - agentMode: Bool) async throws - func rateConversation(turnId: String, rating: ConversationRating) async throws - func copyCode(turnId: String, codeBlockIndex: Int, copyType: CopyKind, copiedCharacters: Int, totalCharacters: Int, copiedText: String) async throws - func cancelProgress(token: String) async - func templates() async throws -> [ChatTemplate] - func models() async throws -> [CopilotModel] - func registerTools(tools: [LanguageModelToolInformation]) async throws -} - -protocol GitHubCopilotLSP { - var eventSequence: ServerConnection.EventSequence { get } - func sendRequest(_ endpoint: E) async throws -> E.Response - func sendNotification(_ notif: ClientNotification) async throws -} - -protocol GitHubCopilotLSPNotification { - func sendCopilotNotification(_ notif: CopilotClientNotification) async throws -} - -public enum GitHubCopilotError: Error, LocalizedError { - case languageServerNotInstalled - case languageServerError(ServerError) - case failedToInstallStartScript - - public var errorDescription: String? { - switch self { - case .languageServerNotInstalled: - return "Language server is not installed." - case .failedToInstallStartScript: - return "Failed to install start script." - case let .languageServerError(error): - switch error { - case let .handlerUnavailable(handler): - return "Language server error: Handler \(handler) unavailable" - case let .unhandledMethod(method): - return "Language server error: Unhandled method \(method)" - case let .notificationDispatchFailed(error): - return "Language server error: Notification dispatch failed: \(error)" - case let .requestDispatchFailed(error): - return "Language server error: Request dispatch failed: \(error)" - case let .clientDataUnavailable(error): - return "Language server error: Client data unavailable: \(error)" - case .serverUnavailable: - return "Language server error: Server unavailable, please make sure that:\n1. The path to node is correctly set.\n2. The node is not a shim executable.\n3. the node version is high enough." - case .missingExpectedParameter: - return "Language server error: Missing expected parameter" - case .missingExpectedResult: - return "Language server error: Missing expected result" - case let .unableToDecodeRequest(error): - return "Language server error: Unable to decode request: \(error)" - case let .unableToSendRequest(error): - return "Language server error: Unable to send request: \(error)" - case let .unableToSendNotification(error): - return "Language server error: Unable to send notification: \(error)" - case let .serverError(code: code, message: message, data: data): - return "Language server error: Server error: \(code) \(message) \(String(describing: data))" - case .invalidRequest: - return "Language server error: Invalid request" - case .timeout: - return "Language server error: Timeout, please try again later" - case .unknownError: - return "Language server error: An unknown error occurred: \(error)" - } - } - } -} - -public extension Notification.Name { - static let gitHubCopilotShouldRefreshEditorInformation = Notification - .Name("com.github.CopilotForXcode.GitHubCopilotShouldRefreshEditorInformation") -} - -public class GitHubCopilotBaseService { - let projectRootURL: URL - var server: GitHubCopilotLSP - var localProcessServer: CopilotLocalProcessServer? - let sessionId: String - - init(designatedServer: GitHubCopilotLSP) { - projectRootURL = URL(fileURLWithPath: "/") - server = designatedServer - sessionId = UUID().uuidString - } - - init(projectRootURL: URL, workspaceURL: URL = URL(fileURLWithPath: "/")) throws { - self.projectRootURL = projectRootURL - self.sessionId = UUID().uuidString - let (server, localServer) = try { - let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() - var path = SystemUtils.shared.getXcodeBinaryPath() - var args = ["--stdio"] - let home = ProcessInfo.processInfo.homePath - - var environment: [String: String] = ["HOME": home] - let envVarNamesToFetch = ["PATH", "NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED"] - let terminalEnvVars = getTerminalEnvironmentVariables(envVarNamesToFetch) - - for varName in envVarNamesToFetch { - if let value = terminalEnvVars[varName] ?? ProcessInfo.processInfo.environment[varName] { - environment[varName] = value - Logger.gitHubCopilot.info("Setting env \(varName): \(value)") - } - } - - environment["PATH"] = SystemUtils.shared.appendCommonBinPaths(path: environment["PATH"] ?? "") - - let versionNumber = JSONValue( - stringLiteral: SystemUtils.editorPluginVersion ?? "" - ) - let xcodeVersion = JSONValue( - stringLiteral: SystemUtils.xcodeVersion ?? "" - ) - let watchedFiles = JSONValue( - booleanLiteral: projectRootURL.path == "/" ? false : true - ) - - #if DEBUG - // Use local language server if set and available - if let languageServerPath = Bundle.main.infoDictionary?["LANGUAGE_SERVER_PATH"] as? String { - let jsPath = URL(fileURLWithPath: NSString(string: languageServerPath).expandingTildeInPath) - .appendingPathComponent("dist") - .appendingPathComponent("language-server.js") - let nodePath = Bundle.main.infoDictionary?["NODE_PATH"] as? String ?? "node" - if FileManager.default.fileExists(atPath: jsPath.path) { - path = "/usr/bin/env" - if projectRootURL.path == "/" { - args = [nodePath, jsPath.path, "--stdio"] - } else { - args = [nodePath, "--inspect", jsPath.path, "--stdio"] - } - Logger.debug.info("Using local language server \(path) \(args)") - } - } - // Add debug-specific environment variables - environment["GH_COPILOT_DEBUG_UI_PORT"] = "8180" - environment["GH_COPILOT_VERBOSE"] = "true" - #else - // Add release-specific environment variables - if UserDefaults.shared.value(for: \.verboseLoggingEnabled) { - environment["GH_COPILOT_VERBOSE"] = "true" - } - #endif - - let executionParams = Process.ExecutionParameters( - path: path, - arguments: args, - environment: environment, - currentDirectoryURL: urls.supportURL - ) - - Logger.gitHubCopilot.info("Starting language server in \(urls.supportURL), \(environment)") - Logger.gitHubCopilot.info("Running on Xcode \(xcodeVersion), extension version \(versionNumber)") - - let localServer = CopilotLocalProcessServer(executionParameters: executionParams) - - let initializeParamsProvider = { @Sendable () -> InitializeParams in - let capabilities = ClientCapabilities( - workspace: .init( - applyEdit: false, - workspaceEdit: nil, - didChangeConfiguration: nil, - didChangeWatchedFiles: nil, - symbol: nil, - executeCommand: nil, - /// enable for "watchedFiles capability", set others to default value - workspaceFolders: true, - configuration: nil, - semanticTokens: nil - ), - textDocument: nil, - window: nil, - general: nil, - experimental: nil - ) - - return InitializeParams( - processId: Int(ProcessInfo.processInfo.processIdentifier), - locale: nil, - rootPath: projectRootURL.path, - rootUri: projectRootURL.path, - initializationOptions: [ - "editorInfo": [ - "name": "Xcode", - "version": xcodeVersion, - ], - "editorPluginInfo": [ - "name": "copilot-xcode", - "version": versionNumber, - ], - "copilotCapabilities": [ - /// The editor has support for watching files over LSP - "watchedFiles": watchedFiles, - "didChangeFeatureFlags": true - ] - ], - capabilities: capabilities, - trace: .off, - workspaceFolders: [WorkspaceFolder( - uri: projectRootURL.absoluteString, - name: projectRootURL.lastPathComponent - )] - ) - } - - let server = SafeInitializingServer(InitializingServer(server: localServer, initializeParamsProvider: initializeParamsProvider)) - - return (server, localServer) - }() - - self.server = server - localProcessServer = localServer - } - - - - public static func createFoldersIfNeeded() throws -> ( - applicationSupportURL: URL, - gitHubCopilotURL: URL, - executableURL: URL, - supportURL: URL - ) { - guard let supportURL = FileManager.default.urls( - for: .applicationSupportDirectory, - in: .userDomainMask - ).first?.appendingPathComponent( - Bundle.main - .object(forInfoDictionaryKey: "APPLICATION_SUPPORT_FOLDER") as? String - ?? "com.github.CopilotForXcode" - ) else { - throw CancellationError() - } - - if !FileManager.default.fileExists(atPath: supportURL.path) { - try? FileManager.default - .createDirectory(at: supportURL, withIntermediateDirectories: false) - } - let gitHubCopilotFolderURL = supportURL.appendingPathComponent("GitHub Copilot") - if !FileManager.default.fileExists(atPath: gitHubCopilotFolderURL.path) { - try? FileManager.default - .createDirectory(at: gitHubCopilotFolderURL, withIntermediateDirectories: false) - } - let supportFolderURL = gitHubCopilotFolderURL.appendingPathComponent("support") - if !FileManager.default.fileExists(atPath: supportFolderURL.path) { - try? FileManager.default - .createDirectory(at: supportFolderURL, withIntermediateDirectories: false) - } - let executableFolderURL = gitHubCopilotFolderURL.appendingPathComponent("executable") - if !FileManager.default.fileExists(atPath: executableFolderURL.path) { - try? FileManager.default - .createDirectory(at: executableFolderURL, withIntermediateDirectories: false) - } - - return (supportURL, gitHubCopilotFolderURL, executableFolderURL, supportFolderURL) - } - - public func getSessionId() -> String { - return sessionId - } -} - -func getTerminalEnvironmentVariables(_ variableNames: [String]) -> [String: String] { - var results = [String: String]() - guard !variableNames.isEmpty else { return results } - - let userShell: String? = { - if let shell = ProcessInfo.processInfo.environment["SHELL"] { - return shell - } - - // Check for zsh executable - if FileManager.default.fileExists(atPath: "/bin/zsh") { - Logger.gitHubCopilot.info("SHELL not found, falling back to /bin/zsh") - return "/bin/zsh" - } - // Check for bash executable - if FileManager.default.fileExists(atPath: "/bin/bash") { - Logger.gitHubCopilot.info("SHELL not found, falling back to /bin/bash") - return "/bin/bash" - } - - Logger.gitHubCopilot.info("Cannot determine user's shell, returning empty environment") - return nil // No shell found - }() - - guard let shell = userShell else { - return results - } - - if let env = SystemUtils.shared.getLoginShellEnvironment(shellPath: shell) { - variableNames.forEach { varName in - if let value = env[varName] { - results[varName] = value - } - } - } - - return results -} - -@globalActor public enum GitHubCopilotSuggestionActor { - public actor TheActor {} - public static let shared = TheActor() -} - -public final class GitHubCopilotService: - GitHubCopilotBaseService, - GitHubCopilotSuggestionServiceType, - GitHubCopilotConversationServiceType, - GitHubCopilotAuthServiceType, - GitHubCopilotTelemetryServiceType -{ - private var ongoingTasks = Set>() - private var serverNotificationHandler: ServerNotificationHandler = ServerNotificationHandlerImpl.shared - private var serverRequestHandler: ServerRequestHandler = ServerRequestHandlerImpl.shared - private var cancellables = Set() - private var statusWatcher: CopilotAuthStatusWatcher? - private static var services: [GitHubCopilotService] = [] // cache all alive copilot service instances - private var isMCPInitialized = false - private var unrestoredMcpServers: [String] = [] - private var mcpRuntimeLogFileName: String = "" - private var lastSentConfiguration: JSONValue? - - override init(designatedServer: any GitHubCopilotLSP) { - super.init(designatedServer: designatedServer) - } - - override public init(projectRootURL: URL = URL(fileURLWithPath: "/"), workspaceURL: URL = URL(fileURLWithPath: "/")) throws { - do { - try super.init(projectRootURL: projectRootURL, workspaceURL: workspaceURL) - - self.handleSendWorkspaceDidChangeNotifications() - - localProcessServer?.notificationPublisher.sink(receiveValue: { [weak self] notification in - if notification.method == "copilot/mcpTools" && projectRootURL.path != "/" { - DispatchQueue.main.async { [weak self] in - guard let self else { return } - Task { @MainActor in - await self.handleMCPToolsNotification(notification) - } - } - } - - if notification.method == "copilot/mcpRuntimeLogs" && projectRootURL.path != "/" { - DispatchQueue.main.async { [weak self] in - guard let self else { return } - Task { @MainActor in - await self.handleMCPRuntimeLogsNotification(notification) - } - } - } - - self?.serverNotificationHandler.handleNotification(notification) - }).store(in: &cancellables) - - Task { - for await event in server.eventSequence { - switch event { - case let .request(id, request): - switch request { - case let .custom(method, params, callback): - if method == "copilot/mcpOAuth" && projectRootURL.path == "/" { - continue - } - self.serverRequestHandler.handleRequest(.init(id: id, method: method, params: params), workspaceURL: workspaceURL, callback: callback, service: self) - default: - break - } - default: - break - } - } - } - - updateStatusInBackground() - - GitHubCopilotService.services.append(self) - - Task { - await registerClientTools(server: self) - } - } catch { - Logger.gitHubCopilot.error(error) - throw error - } - - } - - deinit { - GitHubCopilotService.services.removeAll { $0 === self } - } - - @GitHubCopilotSuggestionActor - public func getCompletions( - fileURL: URL, - content: String, - originalContent: String, - cursorPosition: SuggestionBasic.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool - ) async throws -> [CodeSuggestion] { - ongoingTasks.forEach { $0.cancel() } - ongoingTasks.removeAll() - await localProcessServer?.cancelOngoingTasks() - - func sendRequest(maxTry: Int = 5) async throws -> [CodeSuggestion] { - do { - let completions = try await self - .sendRequest(GitHubCopilotRequest.InlineCompletion(doc: .init( - textDocument: .init(uri: fileURL.absoluteString, version: 1), - position: cursorPosition, - formattingOptions: .init( - tabSize: tabSize, - insertSpaces: !usesTabsForIndentation - ), - context: .init(triggerKind: .invoked) - ))) - .items - .compactMap { (item: _) -> CodeSuggestion? in - guard let range = item.range else { return nil } - let suggestion = CodeSuggestion( - id: item.command?.arguments?.first ?? UUID().uuidString, - text: item.insertText, - position: cursorPosition, - range: .init(start: range.start, end: range.end) - ) - return suggestion - } - try Task.checkCancellation() - return completions - } catch let error as ServerError { - switch error { - case .serverError: - // sometimes the content inside language server is not new enough, which can - // lead to an version mismatch error. We can try a few times until the content - // is up to date. - if maxTry <= 0 { - Logger.gitHubCopilot.error( - "Max retry for getting suggestions reached: \(GitHubCopilotError.languageServerError(error).localizedDescription)" - ) - break - } - Logger.gitHubCopilot.info( - "Try getting suggestions again: \(GitHubCopilotError.languageServerError(error).localizedDescription)" - ) - try await Task.sleep(nanoseconds: 200_000_000) - return try await sendRequest(maxTry: maxTry - 1) - default: - break - } - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - func recoverContent() async { - try? await notifyChangeTextDocument( - fileURL: fileURL, - content: originalContent, - version: 0 - ) - } - - // since when the language server is no longer using the passed in content to generate - // suggestions, we will need to update the content to the file before we do any request. - // - // And sometimes the language server's content was not up to date and may generate - // weird result when the cursor position exceeds the line. - let task = Task { @GitHubCopilotSuggestionActor in - try? await notifyChangeTextDocument( - fileURL: fileURL, - content: content, - version: 1 - ) - - do { - try Task.checkCancellation() - return try await sendRequest() - } catch let error as CancellationError { - if ongoingTasks.isEmpty { - await recoverContent() - } - throw error - } catch { - await recoverContent() - throw error - } - } - - ongoingTasks.insert(task) - - return try await task.value - } - - @GitHubCopilotSuggestionActor - public func createConversation(_ message: MessageContent, - workDoneToken: String, - workspaceFolder: String, - workspaceFolders: [WorkspaceFolder]? = nil, - activeDoc: Doc?, - skills: [String], - ignoredSkills: [String]?, - references: [FileReference], - model: String?, - turns: [TurnSchema], - agentMode: Bool, - userLanguage: String?) async throws { - var conversationCreateTurns: [TurnSchema] = [] - // invoke conversation history - if turns.count > 0 { - conversationCreateTurns.append( - contentsOf: turns.map { - TurnSchema( - request: $0.request, - response: $0.response, - agentSlug: $0.agentSlug, - turnId: $0.turnId - ) - } - ) - } - conversationCreateTurns.append(TurnSchema(request: message)) - let params = ConversationCreateParams(workDoneToken: workDoneToken, - turns: conversationCreateTurns, - capabilities: ConversationCreateParams.Capabilities( - skills: skills, - allSkills: false), - textDocument: activeDoc, - references: references.map { - Reference(uri: $0.url.absoluteString, - position: nil, - visibleRange: nil, - selection: nil, - openedAt: nil, - activeAt: nil) - }, - source: .panel, - workspaceFolder: workspaceFolder, - workspaceFolders: workspaceFolders, - ignoredSkills: ignoredSkills, - model: model, - chatMode: agentMode ? "Agent" : nil, - needToolCallConfirmation: true, - userLanguage: userLanguage) - do { - _ = try await sendRequest( - GitHubCopilotRequest.CreateConversation(params: params)) - } catch { - print("Failed to create conversation. Error: \(error)") - throw error - } - } - - @GitHubCopilotSuggestionActor - public func createTurn(_ message: MessageContent, - workDoneToken: String, - conversationId: String, - turnId: String?, - activeDoc: Doc?, - ignoredSkills: [String]?, - references: [FileReference], - model: String?, - workspaceFolder: String, - workspaceFolders: [WorkspaceFolder]? = nil, - agentMode: Bool) async throws { - do { - let params = TurnCreateParams(workDoneToken: workDoneToken, - conversationId: conversationId, - turnId: turnId, - message: message, - textDocument: activeDoc, - ignoredSkills: ignoredSkills, - references: references.map { - Reference(uri: $0.url.absoluteString, - position: nil, - visibleRange: nil, - selection: nil, - openedAt: nil, - activeAt: nil) - }, - model: model, - workspaceFolder: workspaceFolder, - workspaceFolders: workspaceFolders, - chatMode: agentMode ? "Agent" : nil, - needToolCallConfirmation: true) - _ = try await sendRequest( - GitHubCopilotRequest.CreateTurn(params: params)) - } catch { - print("Failed to create turn. Error: \(error)") - throw error - } - } - - @GitHubCopilotSuggestionActor - public func templates() async throws -> [ChatTemplate] { - do { - let response = try await sendRequest( - GitHubCopilotRequest.GetTemplates() - ) - return response - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func models() async throws -> [CopilotModel] { - do { - let response = try await sendRequest( - GitHubCopilotRequest.CopilotModels() - ) - return response - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func agents() async throws -> [ChatAgent] { - do { - let response = try await sendRequest( - GitHubCopilotRequest.GetAgents() - ) - return response - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func reviewChanges(params: ReviewChangesParams) async throws -> CodeReviewResult { - do { - let response = try await sendRequest( - GitHubCopilotRequest.ReviewChanges(params: params) - ) - return response - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func registerTools(tools: [LanguageModelToolInformation]) async throws { - do { - _ = try await sendRequest( - GitHubCopilotRequest.RegisterTools(params: RegisterToolsParams(tools: tools)) - ) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func updateMCPToolsStatus(params: UpdateMCPToolsStatusParams) async throws -> [MCPServerToolsCollection] { - do { - let response = try await sendRequest( - GitHubCopilotRequest.UpdatedMCPToolsStatus(params: params) - ) - return response - } catch { - throw error - } - } - - - @GitHubCopilotSuggestionActor - public func rateConversation(turnId: String, rating: ConversationRating) async throws { - do { - let params = ConversationRatingParams(turnId: turnId, rating: rating) - let _ = try await sendRequest( - GitHubCopilotRequest.ConversationRating(params: params) - ) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func copyCode(turnId: String, codeBlockIndex: Int, copyType: CopyKind, copiedCharacters: Int, totalCharacters: Int, copiedText: String) async throws { - let params = CopyCodeParams(turnId: turnId, codeBlockIndex: codeBlockIndex, copyType: copyType, copiedCharacters: copiedCharacters, totalCharacters: totalCharacters, copiedText: copiedText) - do { - let _ = try await sendRequest( - GitHubCopilotRequest.CopyCode(params: params) - ) - } catch { - print("Failed to register copied code block. Error: \(error)") - throw error - } - } - - @GitHubCopilotSuggestionActor - public func cancelRequest() async { - ongoingTasks.forEach { $0.cancel() } - ongoingTasks.removeAll() - await localProcessServer?.cancelOngoingTasks() - } - - @GitHubCopilotSuggestionActor - public func cancelProgress(token: String) async { - await localProcessServer?.cancelOngoingTask(token) - } - - @GitHubCopilotSuggestionActor - public func notifyShown(_ completion: CodeSuggestion) async { - _ = try? await sendRequest( - GitHubCopilotRequest.NotifyShown(completionUUID: completion.id) - ) - } - - @GitHubCopilotSuggestionActor - public func notifyAccepted(_ completion: CodeSuggestion, acceptedLength: Int? = nil) async { - _ = try? await sendRequest( - GitHubCopilotRequest.NotifyAccepted(completionUUID: completion.id, acceptedLength: acceptedLength) - ) - } - - @GitHubCopilotSuggestionActor - public func notifyRejected(_ completions: [CodeSuggestion]) async { - _ = try? await sendRequest( - GitHubCopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.id)) - ) - } - - @GitHubCopilotSuggestionActor - public func notifyOpenTextDocument( - fileURL: URL, - content: String - ) async throws { - let languageId = languageIdentifierFromFileURL(fileURL) - let uri = "file://\(fileURL.path)" - // Logger.service.debug("Open \(uri), \(content.count)") - try await server.sendNotification( - .textDocumentDidOpen( - DidOpenTextDocumentParams( - textDocument: .init( - uri: uri, - languageId: languageId.rawValue, - version: 0, - text: content - ) - ) - ) - ) - } - - @GitHubCopilotSuggestionActor - public func notifyChangeTextDocument( - fileURL: URL, - content: String, - version: Int - ) async throws { - let uri = "file://\(fileURL.path)" - // Logger.service.debug("Change \(uri), \(content.count)") - try await server.sendNotification( - .textDocumentDidChange( - DidChangeTextDocumentParams( - uri: uri, - version: version, - contentChange: .init( - range: nil, - rangeLength: nil, - text: content - ) - ) - ) - ) - } - - @GitHubCopilotSuggestionActor - public func notifySaveTextDocument(fileURL: URL) async throws { - let uri = "file://\(fileURL.path)" - // Logger.service.debug("Save \(uri)") - try await server.sendNotification(.textDocumentDidSave(.init(uri: uri))) - } - - @GitHubCopilotSuggestionActor - public func notifyCloseTextDocument(fileURL: URL) async throws { - let uri = "file://\(fileURL.path)" - // Logger.service.debug("Close \(uri)") - try await server.sendNotification(.textDocumentDidClose(.init(uri: uri))) - } - - @GitHubCopilotSuggestionActor - public func notifyDidChangeWatchedFiles(_ event: DidChangeWatchedFilesEvent) async throws { -// Logger.service.debug("notifyDidChangeWatchedFiles \(event)") - try await sendCopilotNotification(.copilotDidChangeWatchedFiles(.init(workspaceUri: event.workspaceUri, changes: event.changes))) - } - - @GitHubCopilotSuggestionActor - public func terminate() async { - // automatically handled - } - - @GitHubCopilotSuggestionActor - public func checkStatus() async throws -> GitHubCopilotAccountStatus { - do { - let response = try await sendRequest(GitHubCopilotRequest.CheckStatus()) - await updateServiceAuthStatus(response) - return response.status - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func checkQuota() async throws -> GitHubCopilotQuotaInfo { - do { - let response = try await sendRequest(GitHubCopilotRequest.CheckQuota()) - await Status.shared.updateQuotaInfo(response) - return response - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - public func updateStatusInBackground() { - Task { @GitHubCopilotSuggestionActor in - try? await checkStatus() - } - } - - private func updateServiceAuthStatus(_ status: GitHubCopilotRequest.CheckStatus.Response) async { - Logger.gitHubCopilot.info("check status response: \(status)") - if status.status == .ok || status.status == .maybeOk { - await Status.shared.updateAuthStatus(.loggedIn, username: status.user) - if !CopilotModelManager.hasLLMs() { - Logger.gitHubCopilot.info("No models found, fetching models...") - let models = try? await models() - if let models = models, !models.isEmpty { - CopilotModelManager.updateLLMs(models) - } - } - await unwatchAuthStatus() - } else if status.status == .notAuthorized { - await Status.shared - .updateAuthStatus( - .notAuthorized, - username: status.user, - message: status.status.description - ) - await watchAuthStatus() - } else { - await Status.shared.updateAuthStatus(.notLoggedIn, message: status.status.description) - await watchAuthStatus() - } - } - - @GitHubCopilotSuggestionActor - private func watchAuthStatus() { - guard statusWatcher == nil else { return } - statusWatcher = CopilotAuthStatusWatcher(self) - } - - @GitHubCopilotSuggestionActor - private func unwatchAuthStatus() { - statusWatcher = nil - } - - @GitHubCopilotSuggestionActor - public func signInInitiate() async throws -> ( - status: SignInInitiateStatus, - verificationUri: String?, - userCode: String?, - user: String? - ) { - do { - let result = try await sendRequest(GitHubCopilotRequest.SignInInitiate()) - switch result.status { - case .promptUserDeviceFlow: - guard let verificationUri = result.verificationUri, - let userCode = result.userCode else { - throw GitHubCopilotError.languageServerError(.missingExpectedResult) - } - return (status: .promptUserDeviceFlow, verificationUri: verificationUri, userCode: userCode, user: nil) - case .alreadySignedIn: - guard let user = result.user else { - throw GitHubCopilotError.languageServerError(.missingExpectedResult) - } - return (status: .alreadySignedIn, verificationUri: nil, userCode: nil, user: user) - } - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func signInConfirm(userCode: String) async throws - -> (username: String, status: GitHubCopilotAccountStatus) - { - do { - let result = try await sendRequest(GitHubCopilotRequest.SignInConfirm(userCode: userCode)) - return (result.user, result.status) - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func signOut() async throws -> GitHubCopilotAccountStatus { - do { - return try await sendRequest(GitHubCopilotRequest.SignOut()).status - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func version() async throws -> String { - do { - return try await sendRequest(GitHubCopilotRequest.GetVersion()).version - } catch let error as ServerError { - throw GitHubCopilotError.languageServerError(error) - } catch { - throw error - } - } - - @GitHubCopilotSuggestionActor - public func shutdown() async throws { - GitHubCopilotService.services.removeAll { $0 === self } - if let localProcessServer { - try await localProcessServer.shutdown() - } else { - throw GitHubCopilotError.languageServerError(ServerError.serverUnavailable) - } - } - - @GitHubCopilotSuggestionActor - public func exit() async throws { - GitHubCopilotService.services.removeAll { $0 === self } - if let localProcessServer { - try await localProcessServer.exit() - } else { - throw GitHubCopilotError.languageServerError(ServerError.serverUnavailable) - } - } - - @GitHubCopilotSuggestionActor - public func sendError( - transaction: String?, - stacktrace: String?, - properties: [String: String]?, - platform: String?, - exceptionDetail: [ExceptionDetail]? - ) async throws { - let params = TelemetryExceptionParams( - transaction: transaction, - stacktrace: stacktrace, - properties: properties, - platform: platform ?? "macOS", - exceptionDetail: exceptionDetail - ) - do { - let _ = try await sendRequest( - GitHubCopilotRequest.TelemetryException(params: params) - ) - } catch { - print("Failed to send telemetry exception. Error: \(error)") - throw error - } - } - - private func sendRequest(_ endpoint: E, timeout: TimeInterval? = nil) async throws -> E.Response { - do { - return try await server.sendRequest(endpoint) - } catch { - let error = ServerError.convertToServerError(error: error) - if let info = CLSErrorInfo(for: error) { - // update the auth status if the error indicates it may have changed, and then rethrow - if info.affectsAuthStatus && !(endpoint is GitHubCopilotRequest.CheckStatus) { - updateStatusInBackground() - } - } - let methodName: String - switch endpoint.request { - case .custom(let method, _, _): - methodName = method - default: - methodName = endpoint.request.method.rawValue - } - if methodName != "telemetry/exception" { // ignore telemetry request - Logger.gitHubCopilot.error( - "Failed to send request \(methodName). Error: \(GitHubCopilotError.languageServerError(error).localizedDescription)" - ) - } - throw error - } - } - - public static func signOutAll() async throws { - var signoutError: Error? = nil - for service in services { - do { - let _ = try await service.signOut() - } catch let error as ServerError { - signoutError = GitHubCopilotError.languageServerError(error) - } catch { - signoutError = error - } - } - - if let signoutError { - throw signoutError - } else { - CopilotModelManager.clearLLMs() - } - } - - public static func updateAllClsMCP(collections: [UpdateMCPToolsStatusServerCollection]) async { - var updateError: Error? = nil - var servers: [MCPServerToolsCollection] = [] - - for service in services { - if service.projectRootURL.path == "/" { - continue // Skip services with root project URL - } - - do { - servers = try await service.updateMCPToolsStatus( - params: .init(servers: collections) - ) - } catch let error as ServerError { - updateError = GitHubCopilotError.languageServerError(error) - } catch { - updateError = error - } - } - - CopilotMCPToolManager.updateMCPTools(servers) - Logger.gitHubCopilot.info("Updated All MCPTools: \(servers.count) servers") - - if let updateError { - Logger.gitHubCopilot.error("Failed to update MCP Tools status: \(updateError)") - } - } - - private func loadUnrestoredMCPServers() -> [String] { - if let savedJSON = AppState.shared.get(key: "mcpToolsStatus"), - let data = try? JSONEncoder().encode(savedJSON), - let savedStatus = try? JSONDecoder().decode([UpdateMCPToolsStatusServerCollection].self, from: data) { - return savedStatus - .filter { !$0.tools.isEmpty } - .map { $0.name } - } - - return [] - } - - private func restoreMCPToolsStatus(_ mcpServers: [String]) async -> [MCPServerToolsCollection]? { - guard let savedJSON = AppState.shared.get(key: "mcpToolsStatus"), - let data = try? JSONEncoder().encode(savedJSON), - let savedStatus = try? JSONDecoder().decode([UpdateMCPToolsStatusServerCollection].self, from: data) else { - Logger.gitHubCopilot.info("Failed to get MCP Tools status") - return nil - } - - do { - let savedServers = savedStatus.filter { mcpServers.contains($0.name) } - if savedServers.isEmpty { - return nil - } else { - return try await updateMCPToolsStatus( - params: .init(servers: savedServers) - ) - } - } catch let error as ServerError { - Logger.gitHubCopilot.error("Failed to update MCP Tools status: \(GitHubCopilotError.languageServerError(error))") - } catch { - Logger.gitHubCopilot.error("Failed to update MCP Tools status: \(error)") - } - - return nil - } - - public func handleMCPToolsNotification(_ notification: AnyJSONRPCNotification) async { - defer { - self.isMCPInitialized = true - } - - if !self.isMCPInitialized { - self.unrestoredMcpServers = self.loadUnrestoredMCPServers() - } - - if let payload = GetAllToolsParams.decode(fromParams: notification.params) { - if !self.unrestoredMcpServers.isEmpty { - // Find servers that need to be restored - let toRestore = payload.servers.filter { !$0.tools.isEmpty } - .filter { self.unrestoredMcpServers.contains($0.name) } - .map { $0.name } - self.unrestoredMcpServers.removeAll { toRestore.contains($0) } - - if let tools = await self.restoreMCPToolsStatus(toRestore) { - Logger.gitHubCopilot.info("Restore MCP tools status for servers: \(toRestore)") - CopilotMCPToolManager.updateMCPTools(tools) - return - } - } - - CopilotMCPToolManager.updateMCPTools(payload.servers) - } - } - - public func handleMCPRuntimeLogsNotification(_ notification: AnyJSONRPCNotification) async { - let debugDescription = encodeJSONParams(params: notification.params) - Logger.mcp.info("[\(self.projectRootURL.path)] copilot/mcpRuntimeLogs: \(debugDescription)") - - if let payload = GitHubCopilotNotification.MCPRuntimeNotification.decode( - fromParams: notification.params - ) { - if mcpRuntimeLogFileName.isEmpty { - mcpRuntimeLogFileName = mcpLogFileNameFromURL(projectRootURL) - } - Logger - .logMCPRuntime( - logFileName: mcpRuntimeLogFileName, - level: payload.level.rawValue, - message: payload.message, - server: payload.server, - tool: payload.tool, - time: payload.time - ) - } - } - - private func mcpLogFileNameFromURL(_ projectRootURL: URL) -> String { - // Create a unique key from workspace URL that's safe for filesystem - let workspaceName = projectRootURL.lastPathComponent - .replacingOccurrences(of: ".xcworkspace", with: "") - .replacingOccurrences(of: ".xcodeproj", with: "") - .replacingOccurrences(of: ".playground", with: "") - let workspacePath = projectRootURL.path - - // Use a combination of name and hash of path for uniqueness - let pathHash = String(workspacePath.hash.magnitude, radix: 36).prefix(6) - return "\(workspaceName)-\(pathHash)" - } - - public func handleSendWorkspaceDidChangeNotifications() { - Task { - if projectRootURL.path != "/" { - try? await self.server.sendNotification( - .workspaceDidChangeWorkspaceFolders( - .init(event: .init(added: [.init(uri: projectRootURL.absoluteString, name: projectRootURL.lastPathComponent)], removed: [])) - ) - ) - } - - // Send initial configuration after initialize - await sendConfigurationUpdate() - - // Combine both notification streams - let combinedNotifications = Publishers.Merge( - NotificationCenter.default.publisher(for: .gitHubCopilotShouldRefreshEditorInformation).map { _ in "editorInfo" }, - FeatureFlagNotifierImpl.shared.featureFlagsDidChange.map { _ in "featureFlags" } - ) - - for await _ in combinedNotifications.values { - await sendConfigurationUpdate() - } - } - } - - private func sendConfigurationUpdate() async { - let includeMCP = projectRootURL.path != "/" && - FeatureFlagNotifierImpl.shared.featureFlags.agentMode && - FeatureFlagNotifierImpl.shared.featureFlags.mcp - - let newConfiguration = editorConfiguration(includeMCP: includeMCP) - - // Only send the notification if the configuration has actually changed - guard self.lastSentConfiguration != newConfiguration else { return } - - _ = try? await self.server.sendNotification( - .workspaceDidChangeConfiguration( - .init(settings: newConfiguration) - ) - ) - - // Cache the sent configuration - self.lastSentConfiguration = newConfiguration - } -} - -extension SafeInitializingServer: GitHubCopilotLSP { - func sendRequest(_ endpoint: E) async throws -> E.Response { - try await sendRequest(endpoint.request) - } -} - -extension GitHubCopilotService { - func sendCopilotNotification(_ notif: CopilotClientNotification) async throws { - try await localProcessServer?.sendCopilotNotification(notif) - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GithubCopilotRequest+Message.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GithubCopilotRequest+Message.swift deleted file mode 100644 index b43ec840..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GithubCopilotRequest+Message.swift +++ /dev/null @@ -1,4 +0,0 @@ -import JSONRPC -import LanguageServerProtocol - -public typealias ShowMessageRequest = JSONRPCRequest diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/SafeInitializingServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/SafeInitializingServer.swift deleted file mode 100644 index 49cbbeb8..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/SafeInitializingServer.swift +++ /dev/null @@ -1,62 +0,0 @@ -import LanguageClient -import LanguageServerProtocol - -public actor SafeInitializingServer { - private let underlying: InitializingServer - private var initTask: Task? = nil - - public init(_ server: InitializingServer) { - self.underlying = server - } - - // Ensure initialize request is sent by once - public func initializeIfNeeded() async throws -> InitializationResponse { - if let task = initTask { - return try await task.value - } - - let task = Task { - try await underlying.initializeIfNeeded() - } - initTask = task - - do { - let result = try await task.value - return result - } catch { - // Retryable failure - initTask = nil - throw error - } - } - - public func shutdownAndExit() async throws { - try await underlying.shutdownAndExit() - } - - public func sendNotification(_ notif: ClientNotification) async throws { - _ = try await initializeIfNeeded() - try await underlying.sendNotification(notif) - } - - public func sendRequest(_ request: ClientRequest) async throws -> Response { - _ = try await initializeIfNeeded() - return try await underlying.sendRequest(request) - } - - public var capabilities: ServerCapabilities? { - get async { - await underlying.capabilities - } - } - - public var serverInfo: ServerInfo? { - get async { - await underlying.serverInfo - } - } - - public nonisolated var eventSequence: ServerConnection.EventSequence { - underlying.eventSequence - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/ServerNotificationHandler.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/ServerNotificationHandler.swift deleted file mode 100644 index 39c2c4a5..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/ServerNotificationHandler.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Combine -import Foundation -import JSONRPC -import LanguageServerProtocol - -protocol ServerNotificationHandler { - var protocolProgressSubject: PassthroughSubject { get } - func handleNotification(_ notification: AnyJSONRPCNotification) -} - -class ServerNotificationHandlerImpl: ServerNotificationHandler { - public static let shared = ServerNotificationHandlerImpl() - var protocolProgressSubject: PassthroughSubject - var conversationProgressHandler: ConversationProgressHandler = ConversationProgressHandlerImpl.shared - var featureFlagNotifier: FeatureFlagNotifier = FeatureFlagNotifierImpl.shared - - init() { - self.protocolProgressSubject = PassthroughSubject() - } - - func handleNotification(_ notification: AnyJSONRPCNotification) { - let methodName = notification.method - - if let method = ServerNotification.Method(rawValue: methodName) { - switch method { - case .windowLogMessage: - break - case .protocolProgress: - if let data = try? JSONEncoder().encode(notification.params), - let progress = try? JSONDecoder().decode(ProgressParams.self, from: data) { - conversationProgressHandler.handleConversationProgress(progress) - } - default: - break - } - } else { - switch methodName { - case "copilot/didChangeFeatureFlags": - if let data = try? JSONEncoder().encode(notification.params), - let didChangeFeatureFlagsParams = try? JSONDecoder().decode( - DidChangeFeatureFlagsParams.self, - from: data - ) { - featureFlagNotifier.handleFeatureFlagNotification(didChangeFeatureFlagsParams) - } - break - default: - break - } - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/ServerRequestHandler.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/ServerRequestHandler.swift deleted file mode 100644 index 7b28b73b..00000000 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/ServerRequestHandler.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation -import ConversationServiceProvider -import Combine -import JSONRPC -import LanguageClient -import LanguageServerProtocol -import Logger - -public typealias ResponseHandler = ServerRequest.Handler -public typealias LegacyResponseHandler = (AnyJSONRPCResponse) -> Void - -protocol ServerRequestHandler { - func handleRequest(_ request: AnyJSONRPCRequest, workspaceURL: URL, callback: @escaping ResponseHandler, service: GitHubCopilotService?) -} - -class ServerRequestHandlerImpl : ServerRequestHandler { - public static let shared = ServerRequestHandlerImpl() - private let conversationContextHandler: ConversationContextHandler = ConversationContextHandlerImpl.shared - private let watchedFilesHandler: WatchedFilesHandler = WatchedFilesHandlerImpl.shared - private let showMessageRequestHandler: ShowMessageRequestHandler = ShowMessageRequestHandlerImpl.shared - private let mcpOAuthRequestHandler: MCPOAuthRequestHandler = MCPOAuthRequestHandlerImpl.shared - - func handleRequest(_ request: AnyJSONRPCRequest, workspaceURL: URL, callback: @escaping ResponseHandler, service: GitHubCopilotService?) { - let methodName = request.method - let legacyResponseHandler = toLegacyResponseHandler(callback) - do { - switch methodName { - case "conversation/context": - let params = try JSONEncoder().encode(request.params) - let contextParams = try JSONDecoder().decode(ConversationContextParams.self, from: params) - conversationContextHandler.handleConversationContext( - ConversationContextRequest(id: request.id, method: request.method, params: contextParams), - completion: legacyResponseHandler) - - case "copilot/watchedFiles": - let params = try JSONEncoder().encode(request.params) - let watchedFilesParams = try JSONDecoder().decode(WatchedFilesParams.self, from: params) - watchedFilesHandler.handleWatchedFiles(WatchedFilesRequest(id: request.id, method: request.method, params: watchedFilesParams), workspaceURL: workspaceURL, completion: legacyResponseHandler, service: service) - - case "window/showMessageRequest": - let params = try JSONEncoder().encode(request.params) - let showMessageRequestParams = try JSONDecoder().decode(ShowMessageRequestParams.self, from: params) - showMessageRequestHandler - .handleShowMessage( - ShowMessageRequest( - id: request.id, - method: request.method, - params: showMessageRequestParams - ), - completion: legacyResponseHandler - ) - - case "conversation/invokeClientTool": - let params = try JSONEncoder().encode(request.params) - let invokeParams = try JSONDecoder().decode(InvokeClientToolParams.self, from: params) - ClientToolHandlerImpl.shared.invokeClientTool(InvokeClientToolRequest(id: request.id, method: request.method, params: invokeParams), completion: legacyResponseHandler) - - case "conversation/invokeClientToolConfirmation": - let params = try JSONEncoder().encode(request.params) - let invokeParams = try JSONDecoder().decode(InvokeClientToolParams.self, from: params) - ClientToolHandlerImpl.shared.invokeClientToolConfirmation(InvokeClientToolConfirmationRequest(id: request.id, method: request.method, params: invokeParams), completion: legacyResponseHandler) - - case "copilot/mcpOAuth": - let params = try JSONEncoder().encode(request.params) - let mcpOAuthRequestParams = try JSONDecoder().decode(MCPOAuthRequestParams.self, from: params) - mcpOAuthRequestHandler.handleShowOAuthMessage( - MCPOAuthRequest( - id: request.id, - method: request.method, - params: mcpOAuthRequestParams - ), - completion: legacyResponseHandler - ) - - default: - break - } - } catch { - handleError(request, error: error, callback: legacyResponseHandler) - } - } - - private func handleError(_ request: AnyJSONRPCRequest, error: Error, callback: @escaping (AnyJSONRPCResponse) -> Void) { - callback( - AnyJSONRPCResponse( - id: request.id, - result: JSONValue.array([ - JSONValue.null, - JSONValue.hash([ - "code": .number(-32602/* Invalid params */), - "message": .string("Error: \(error.localizedDescription)")]) - ]) - ) - ) - Logger.gitHubCopilot.error(error) - } - - /// Converts a new Handler to work with old code that expects LegacyResponseHandler - private func toLegacyResponseHandler( - _ newHandler: @escaping ResponseHandler - ) -> LegacyResponseHandler { - return { response in - Task { - if let error = response.error { - await newHandler(.failure(error)) - } else if let result = response.result { - await newHandler(.success(result)) - } - } - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/Services/FeatureFlagNotifier.swift b/Tool/Sources/GitHubCopilotService/Services/FeatureFlagNotifier.swift deleted file mode 100644 index a4248e8f..00000000 --- a/Tool/Sources/GitHubCopilotService/Services/FeatureFlagNotifier.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Combine -import SwiftUI -import JSONRPC - -public extension Notification.Name { - static let gitHubCopilotFeatureFlagsDidChange = Notification - .Name("com.github.CopilotForXcode.CopilotFeatureFlagsDidChange") -} - -public enum ExperimentValue: Hashable, Codable { - case string(String) - case number(Double) - case boolean(Bool) - case stringArray([String]) -} - -public typealias ActiveExperimentForFeatureFlags = [String: ExperimentValue] - -public struct DidChangeFeatureFlagsParams: Hashable, Codable { - let envelope: [String: JSONValue] - let token: [String: String] - let activeExps: ActiveExperimentForFeatureFlags -} - -public struct FeatureFlags: Hashable, Codable { - public var restrictedTelemetry: Bool - public var snippy: Bool - public var chat: Bool - public var inlineChat: Bool - public var projectContext: Bool - public var agentMode: Bool - public var mcp: Bool - public var ccr: Bool // Copilot Code Review - public var activeExperimentForFeatureFlags: ActiveExperimentForFeatureFlags - - public init( - restrictedTelemetry: Bool = true, - snippy: Bool = true, - chat: Bool = true, - inlineChat: Bool = true, - projectContext: Bool = true, - agentMode: Bool = true, - mcp: Bool = true, - ccr: Bool = true, - activeExperimentForFeatureFlags: ActiveExperimentForFeatureFlags = [:] - ) { - self.restrictedTelemetry = restrictedTelemetry - self.snippy = snippy - self.chat = chat - self.inlineChat = inlineChat - self.projectContext = projectContext - self.agentMode = agentMode - self.mcp = mcp - self.ccr = ccr - self.activeExperimentForFeatureFlags = activeExperimentForFeatureFlags - } -} - -public protocol FeatureFlagNotifier { - var didChangeFeatureFlagsParams: DidChangeFeatureFlagsParams { get } - var featureFlagsDidChange: PassthroughSubject { get } - func handleFeatureFlagNotification(_ didChangeFeatureFlagsParams: DidChangeFeatureFlagsParams) -} - -public class FeatureFlagNotifierImpl: FeatureFlagNotifier { - public var didChangeFeatureFlagsParams: DidChangeFeatureFlagsParams - public var featureFlags: FeatureFlags - public static let shared = FeatureFlagNotifierImpl() - public var featureFlagsDidChange: PassthroughSubject - - init( - didChangeFeatureFlagsParams: DidChangeFeatureFlagsParams = .init(envelope: [:], token: [:], activeExps: [:]), - featureFlags: FeatureFlags = FeatureFlags(), - featureFlagsDidChange: PassthroughSubject = PassthroughSubject() - ) { - self.didChangeFeatureFlagsParams = didChangeFeatureFlagsParams - self.featureFlags = featureFlags - self.featureFlagsDidChange = featureFlagsDidChange - } - - private func updateFeatureFlags() { - let xcodeChat = self.didChangeFeatureFlagsParams.envelope["xcode_chat"]?.boolValue != false - let chatEnabled = self.didChangeFeatureFlagsParams.envelope["chat_enabled"]?.boolValue != false - self.featureFlags.restrictedTelemetry = self.didChangeFeatureFlagsParams.token["rt"] != "0" - self.featureFlags.snippy = self.didChangeFeatureFlagsParams.token["sn"] != "0" - self.featureFlags.chat = xcodeChat && chatEnabled - self.featureFlags.inlineChat = chatEnabled - self.featureFlags.agentMode = self.didChangeFeatureFlagsParams.token["agent_mode"] != "0" - self.featureFlags.mcp = self.didChangeFeatureFlagsParams.token["mcp"] != "0" - self.featureFlags.ccr = self.didChangeFeatureFlagsParams.token["ccr"] != "0" - self.featureFlags.activeExperimentForFeatureFlags = self.didChangeFeatureFlagsParams.activeExps - } - - public func handleFeatureFlagNotification(_ didChangeFeatureFlagsParams: DidChangeFeatureFlagsParams) { - self.didChangeFeatureFlagsParams = didChangeFeatureFlagsParams - updateFeatureFlags() - DispatchQueue.main.async { [weak self] in - guard let self else { return } - self.featureFlagsDidChange.send(self.featureFlags) - DistributedNotificationCenter.default().post(name: .gitHubCopilotFeatureFlagsDidChange, object: nil) - } - } -} diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotConversationService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotConversationService.swift deleted file mode 100644 index b6f19132..00000000 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotConversationService.swift +++ /dev/null @@ -1,125 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import ConversationServiceProvider -import BuiltinExtension -import Workspace -import LanguageServerProtocol - -public final class GitHubCopilotConversationService: ConversationServiceType { - public func notifyChangeTextDocument(fileURL: URL, content: String, version: Int, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.notifyChangeTextDocument(fileURL: fileURL, content: content, version: version) - } - - private let serviceLocator: ServiceLocator - - init(serviceLocator: ServiceLocator) { - self.serviceLocator = serviceLocator - } - - private func getWorkspaceFolders(workspace: WorkspaceInfo) -> [WorkspaceFolder] { - let projects = WorkspaceFile.getProjects(workspace: workspace) - return projects.map { project in - WorkspaceFolder(uri: project.uri, name: project.name) - } - } - - private func getMessageContent(_ request: ConversationRequest) -> MessageContent { - let contentImages = request.contentImages - let message: MessageContent - if contentImages.count > 0 { - var chatCompletionContentParts: [ChatCompletionContentPart] = contentImages.map { - .imageUrl($0) - } - chatCompletionContentParts.append(.text(ChatCompletionContentPartText(text: request.content))) - message = .messageContentArray(chatCompletionContentParts) - } else { - message = .string(request.content) - } - - return message - } - - public func createConversation(_ request: ConversationRequest, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - - let message = getMessageContent(request) - - return try await service.createConversation(message, - workDoneToken: request.workDoneToken, - workspaceFolder: workspace.projectURL.absoluteString, - workspaceFolders: getWorkspaceFolders(workspace: workspace), - activeDoc: request.activeDoc, - skills: request.skills, - ignoredSkills: request.ignoredSkills, - references: request.references ?? [], - model: request.model, - turns: request.turns, - agentMode: request.agentMode, - userLanguage: request.userLanguage) - } - - public func createTurn(with conversationId: String, request: ConversationRequest, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - - let message = getMessageContent(request) - - return try await service.createTurn(message, - workDoneToken: request.workDoneToken, - conversationId: conversationId, - turnId: request.turnId, - activeDoc: request.activeDoc, - ignoredSkills: request.ignoredSkills, - references: request.references ?? [], - model: request.model, - workspaceFolder: workspace.projectURL.absoluteString, - workspaceFolders: getWorkspaceFolders(workspace: workspace), - agentMode: request.agentMode) - } - - public func cancelProgress(_ workDoneToken: String, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - - await service.cancelProgress(token: workDoneToken) - } - - public func rateConversation(turnId: String, rating: ConversationRating, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.rateConversation(turnId: turnId, rating: rating) - } - - public func copyCode(request: CopyCodeRequest, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - try await service.copyCode(turnId: request.turnId, codeBlockIndex: request.codeBlockIndex, copyType: request.copyType, copiedCharacters: request.copiedCharacters, totalCharacters: request.totalCharacters, copiedText: request.copiedText) - } - - public func templates(workspace: WorkspaceInfo) async throws -> [ChatTemplate]? { - guard let service = await serviceLocator.getService(from: workspace) else { return nil } - return try await service.templates() - } - - public func models(workspace: WorkspaceInfo) async throws -> [CopilotModel]? { - guard let service = await serviceLocator.getService(from: workspace) else { return nil } - return try await service.models() - } - - public func notifyDidChangeWatchedFiles(_ event: DidChangeWatchedFilesEvent, workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { - return - } - - return try await service.notifyDidChangeWatchedFiles(.init(workspaceUri: event.workspaceUri, changes: event.changes)) - } - - public func agents(workspace: WorkspaceInfo) async throws -> [ChatAgent]? { - guard let service = await serviceLocator.getService(from: workspace) else { return nil } - return try await service.agents() - } - - public func reviewChanges(workspace: WorkspaceInfo, params: ReviewChangesParams) async throws -> CodeReviewResult? { - guard let service = await serviceLocator.getService(from: workspace) else { return nil } - - return try await service.reviewChanges(params: params) - } -} - diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift deleted file mode 100644 index f9f8a9b5..00000000 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift +++ /dev/null @@ -1,107 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import SuggestionBasic -import Workspace - -public final class GitHubCopilotSuggestionService: SuggestionServiceType { - public var configuration: SuggestionServiceConfiguration { - .init( - acceptsRelevantCodeSnippets: true, - mixRelevantCodeSnippetsInSource: true, - acceptsRelevantSnippetsFromOpenedFiles: false - ) - } - - let serviceLocator: ServiceLocatorType - - init(serviceLocator: ServiceLocatorType) { - self.serviceLocator = serviceLocator - } - - public func getSuggestions( - _ request: SuggestionRequest, - workspace: WorkspaceInfo - ) async throws -> [CopilotForXcodeKit.CodeSuggestion] { - guard let service = await serviceLocator.getService(from: workspace) else { return [] } - return try await service.getCompletions( - fileURL: request.fileURL, - content: request.content, - originalContent: request.originalContent, - cursorPosition: .init( - line: request.cursorPosition.line, - character: request.cursorPosition.character - ), - tabSize: request.tabSize, - indentSize: request.indentSize, - usesTabsForIndentation: request.usesTabsForIndentation - ).map(Self.convert) - } - - public func notifyAccepted( - _ suggestion: CopilotForXcodeKit.CodeSuggestion, - workspace: WorkspaceInfo - ) async { - guard let service = await serviceLocator.getService(from: workspace) else { return } - await service.notifyAccepted(Self.convert(suggestion)) - } - - public func notifyRejected( - _ suggestions: [CopilotForXcodeKit.CodeSuggestion], - workspace: WorkspaceInfo - ) async { - guard let service = await serviceLocator.getService(from: workspace) else { return } - await service.notifyRejected(suggestions.map(Self.convert)) - } - - public func cancelRequest(workspace: WorkspaceInfo) async { - guard let service = await serviceLocator.getService(from: workspace) else { return } - await service.cancelRequest() - } - - static func convert( - _ suggestion: SuggestionBasic.CodeSuggestion - ) -> CopilotForXcodeKit.CodeSuggestion { - .init( - id: suggestion.id, - text: suggestion.text, - position: .init( - line: suggestion.position.line, - character: suggestion.position.character - ), - range: .init( - start: .init( - line: suggestion.range.start.line, - character: suggestion.range.start.character - ), - end: .init( - line: suggestion.range.end.line, - character: suggestion.range.end.character - ) - ) - ) - } - - static func convert( - _ suggestion: CopilotForXcodeKit.CodeSuggestion - ) -> SuggestionBasic.CodeSuggestion { - .init( - id: suggestion.id, - text: suggestion.text, - position: .init( - line: suggestion.position.line, - character: suggestion.position.character - ), - range: .init( - start: .init( - line: suggestion.range.start.line, - character: suggestion.range.start.character - ), - end: .init( - line: suggestion.range.end.line, - character: suggestion.range.end.character - ) - ) - ) - } -} - diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotTelemetryService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotTelemetryService.swift deleted file mode 100644 index a38cbf82..00000000 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotTelemetryService.swift +++ /dev/null @@ -1,30 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import TelemetryServiceProvider -import BuiltinExtension - -public final class GitHubCopilotTelemetryService: TelemetryServiceType { - - private let serviceLocator: ServiceLocator - - init(serviceLocator: ServiceLocator) { - self.serviceLocator = serviceLocator - } - - public func sendError(_ request: TelemetryExceptionRequest, - workspace: WorkspaceInfo) async throws { - guard let service = await serviceLocator.getService(from: workspace) else { return } - let sessionId = service.getSessionId() - var properties = request.properties ?? [:] - properties.updateValue(sessionId, forKey: "common_vscodesessionid") - properties.updateValue(sessionId, forKey: "client_sessionid") - - try await service.sendError( - transaction: request.transaction, - stacktrace: request.stacktrace, - properties: properties, - platform: request.platform, - exceptionDetail: request.exceptionDetail - ) - } -} diff --git a/Tool/Sources/HostAppActivator/HostAppActivator.swift b/Tool/Sources/HostAppActivator/HostAppActivator.swift deleted file mode 100644 index 81658337..00000000 --- a/Tool/Sources/HostAppActivator/HostAppActivator.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import AppKit -import Logger - -public let HostAppURL = locateHostBundleURL(url: Bundle.main.bundleURL) - -public extension Notification.Name { - static let openSettingsWindowRequest = Notification - .Name("com.github.CopilotForXcode.OpenSettingsWindowRequest") - static let openMCPSettingsWindowRequest = Notification - .Name("com.github.CopilotForXcode.OpenMCPSettingsWindowRequest") -} - -public enum GitHubCopilotForXcodeSettingsLaunchError: Error, LocalizedError { - case appNotFound - case openFailed(errorDescription: String) - - public var errorDescription: String? { - switch self { - case .appNotFound: - return "\(hostAppName()) settings application not found" - case let .openFailed(errorDescription): - return "Failed to launch \(hostAppName()) settings (\(errorDescription))" - } - } -} - -public func getRunningHostApp() -> NSRunningApplication? { - return NSWorkspace.shared.runningApplications.first(where: { - $0.bundleIdentifier == (Bundle.main.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String) - }) -} - -public func launchHostAppSettings() throws { - // Try the AppleScript approach first, but only if app is already running - if let hostApp = getRunningHostApp() { - let activated = hostApp.activate(options: [.activateIgnoringOtherApps]) - Logger.ui.info("\(hostAppName()) activated: \(activated)") - - let scriptSuccess = tryLaunchWithAppleScript() - - // If AppleScript fails, fall back to notification center - if !scriptSuccess { - DistributedNotificationCenter.default().postNotificationName( - .openSettingsWindowRequest, - object: nil - ) - Logger.ui.info("\(hostAppName()) settings notification sent after activation") - return - } - } else { - // If app is not running, launch it with the settings flag - try launchHostAppWithArgs(args: ["--settings"]) - } -} - -public func launchHostAppMCPSettings() throws { - // Try the AppleScript approach first, but only if app is already running - if let hostApp = getRunningHostApp() { - let activated = hostApp.activate(options: [.activateIgnoringOtherApps]) - Logger.ui.info("\(hostAppName()) activated: \(activated)") - - _ = tryLaunchWithAppleScript() - - DistributedNotificationCenter.default().postNotificationName( - .openMCPSettingsWindowRequest, - object: nil - ) - Logger.ui.info("\(hostAppName()) MCP settings notification sent after activation") - return - } else { - // If app is not running, launch it with the settings flag - try launchHostAppWithArgs(args: ["--mcp"]) - } -} - -private func tryLaunchWithAppleScript() -> Bool { - // Try to launch settings using AppleScript - let script = """ - tell application "\(hostAppName())" - activate - tell application "System Events" - keystroke "," using command down - end tell - end tell - """ - - var error: NSDictionary? - if let scriptObject = NSAppleScript(source: script) { - scriptObject.executeAndReturnError(&error) - - // Log the result - if let error = error { - Logger.ui.info("\(hostAppName()) settings script error: \(error)") - return false - } - - Logger.ui.info("\(hostAppName()) settings opened successfully via AppleScript") - return true - } - - return false -} - -public func launchHostAppDefault() throws { - try launchHostAppWithArgs(args: nil) -} - -func launchHostAppWithArgs(args: [String]?) throws { - guard let appURL = HostAppURL else { - throw GitHubCopilotForXcodeSettingsLaunchError.appNotFound - } - - Task { - let configuration = NSWorkspace.OpenConfiguration() - if let args { - configuration.arguments = args - } - configuration.activates = true - - try await NSWorkspace.shared - .openApplication(at: appURL, configuration: configuration) - } -} - -func locateHostBundleURL(url: URL) -> URL? { - var nextURL = url - while nextURL.path != "/" { - nextURL = nextURL.deletingLastPathComponent() - if nextURL.lastPathComponent.hasSuffix(".app") { - return nextURL - } - } - let devAppURL = url - .deletingLastPathComponent() - .appendingPathComponent("GitHub Copilot for Xcode Dev.app") - return devAppURL -} - -func hostAppName() -> String { - return Bundle.main.object(forInfoDictionaryKey: "HOST_APP_NAME") as? String - ?? "GitHub Copilot for Xcode" -} diff --git a/Tool/Sources/Logger/FileLogger.swift b/Tool/Sources/Logger/FileLogger.swift deleted file mode 100644 index 92d51161..00000000 --- a/Tool/Sources/Logger/FileLogger.swift +++ /dev/null @@ -1,205 +0,0 @@ -import Foundation -import System - -public final class FileLoggingLocation { - public static let path = { - FilePath(stringLiteral: NSHomeDirectory()) - .appending("Library") - .appending("Logs") - .appending("GitHubCopilot") - }() - - public static let mcpRuntimeLogsPath = path.appending("MCPRuntimeLogs") -} - -final class FileLogger { - private let timestampFormat = Date.ISO8601FormatStyle.iso8601 - .year() - .month() - .day() - .timeZone(separator: .omitted).time(includingFractionalSeconds: true) - private let pid = "\(ProcessInfo.processInfo.processIdentifier)" - private static let implementation = FileLoggerImplementation() - - private func timestamp() -> String { - return Date().formatted(timestampFormat) - } - - public func log(level: LogLevel, category: String, message: String) { - let log = "[\(timestamp())] [\(level)] [\(category)] [\(pid)] \(message)\(message.hasSuffix("\n") ? "" : "\n")" - - Task { - await FileLogger.implementation.logToFile(log) - } - } -} - -actor FileLoggerImplementation { - private let baseLogger: BaseFileLoggerImplementation - - public init() { - baseLogger = BaseFileLoggerImplementation( - logDir: FileLoggingLocation.path - ) - } - - public func logToFile(_ log: String) async { - await baseLogger.logToFile(log) - } -} - -// MARK: - Shared Base File Logger -actor BaseFileLoggerImplementation { - #if DEBUG - private let logBaseName = "github-copilot-for-xcode-dev" - #else - private let logBaseName = "github-copilot-for-xcode" - #endif - private let logExtension = "log" - private let maxLogSize: Int - private let logOverflowLimit: Int - private let maxLogs: Int - private let maxLockTime: Int - - private let logDir: FilePath - private let logName: String - private let lockFilePath: FilePath - private var logStream: OutputStream? - private var logHandle: FileHandle? - - init( - logDir: FilePath, - logFileName: String? = nil, - maxLogSize: Int = 5_000_000, - logOverflowLimit: Int? = nil, - maxLogs: Int = 10, - maxLockTime: Int = 3_600 - ) { - self.logDir = logDir - self.logName = (logFileName ?? logBaseName) + "." + logExtension - self.lockFilePath = logDir.appending(logName + ".lock") - self.maxLogSize = maxLogSize - self.logOverflowLimit = logOverflowLimit ?? maxLogSize * 2 - self.maxLogs = maxLogs - self.maxLockTime = maxLockTime - } - - func logToFile(_ log: String) async { - if let stream = logAppender() { - let data = [UInt8](log.utf8) - stream.write(data, maxLength: data.count) - } - } - - private func logAppender() -> OutputStream? { - if logStream == nil { - reopenLogFile() - } - - if rotateIfNeeded() > logOverflowLimit { - return nil // do not exceed the overflow limit - } - - return logStream - } - - private func reopenLogFile() { - if !FileManager.default.fileExists(atPath: logDir.string) { - let success: ()? = try? FileManager.default.createDirectory(atPath: logDir.string, withIntermediateDirectories: true) - guard success != nil else { return } - } - - let fileName = logDir.appending(logName).string - logStream = OutputStream(toFileAtPath: fileName, append: true) - logStream?.open() - - logHandle = FileHandle(forReadingAtPath: fileName) - } - - private func logSize() -> UInt64{ - return logHandle?.seekToEndOfFile() ?? 0 - } - - /// @returns The resulting size of the log file - private func rotateIfNeeded() -> UInt64 { - let size = logSize() - - if size > maxLogSize { - rotateLogs() - return logSize() // return the new size of the log file - } - - return size - } - - private func rotateLogs() { - // attempt to acquire a lock for rotating logs - let fd = try? FileDescriptor.open( - lockFilePath, - .readWrite, - options: .init([.create, .exclusiveCreate]), - permissions: .init(rawValue: 0o666) - ) - guard fd != nil else { - // if we can't get the lock, another process is already rotating - checkLockValidity() // prevents stale locks - return // write to the existing log while rotation is happening - } - - defer { - try? fd?.close() - try? FileManager.default.removeItem(atPath: lockFilePath.string) - } - - // check the log size again. if it's under the limit, another process already rotated the logs - let fileName = logDir.appending(logName).string - let attributes = try? FileManager.default.attributesOfItem(atPath: fileName) - let size = (attributes?[FileAttributeKey.size] ?? 0) as! Int - - if (size > maxLogSize) { - let formatter = DateFormatter() - formatter.dateFormat = "yyyyMMddHHmmss" - let archiveName = "\(logBaseName)-\(formatter.string(from: Date())).\(logExtension)" - let newName = logDir.appending(archiveName).string - - // moving the log file does not affect any open file handles. they continue writing to the new location. - try? FileManager.default.moveItem(atPath: fileName, toPath: newName) - - cleanupOldLogs() - } - - reopenLogFile() - } - - /// Note: This is only safe to call if the caller has already obtained a lock on the log directory - private func cleanupOldLogs() { - let logFiles = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logDir.string), includingPropertiesForKeys: nil) - .filter { $0.pathExtension == logExtension && $0.lastPathComponent != logName } - .sorted { $0.lastPathComponent > $1.lastPathComponent } - - if let oldLogFiles = logFiles, oldLogFiles.count > maxLogs { - for fileURL in oldLogFiles[maxLogs...] { - try? FileManager.default.removeItem(at: fileURL) - } - } - } - - /// Checks the lock file's creation time and removes it if it is stale. - /// - /// If a process hangs or crashes while rotating logs, the lock file will - /// be left behind, preventing other processes from rotating logs. To - /// prevent this, an lock file older than the lock limit (1 hour) is - /// considered stale and removed. - /// - /// The pending log entry will still be written to the existing log, but - /// by removing the lock file, rotation will resume the next time an entry - /// is logged. - private func checkLockValidity() { - let attributes = try? FileManager.default.attributesOfItem(atPath: lockFilePath.string) - let ctime = (attributes?[FileAttributeKey.creationDate] ?? NSDate()) as! NSDate - - if ctime.timeIntervalSinceNow < -TimeInterval(maxLockTime) { - try? FileManager.default.removeItem(atPath: lockFilePath.string) - } - } -} diff --git a/Tool/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift deleted file mode 100644 index a23f33b2..00000000 --- a/Tool/Sources/Logger/Logger.swift +++ /dev/null @@ -1,208 +0,0 @@ -import Foundation -import os.log - -enum LogLevel: String { - case debug - case info - case error -} - -public final class Logger { - private let subsystem: String - private let category: String - private let osLog: OSLog - private let fileLogger = FileLogger() - private static let mcpRuntimeFileLogger = MCPRuntimeFileLogger() - - public static let service = Logger(category: "Service") - public static let ui = Logger(category: "UI") - public static let client = Logger(category: "Client") - public static let updateChecker = Logger(category: "UpdateChecker") - public static let gitHubCopilot = Logger(category: "GitHubCopilot") - public static let langchain = Logger(category: "LangChain") - public static let retrieval = Logger(category: "Retrieval") - public static let license = Logger(category: "License") - public static let `extension` = Logger(category: "Extension") - public static let communicationBridge = Logger(category: "CommunicationBridge") - public static let workspacePool = Logger(category: "WorkspacePool") - public static let mcp = Logger(category: "MCP") - public static let debug = Logger(category: "Debug") - public static var telemetryLogger: TelemetryLoggerProvider? = nil - #if DEBUG - /// Use a temp logger to log something temporary. I won't be available in release builds. - public static let temp = Logger(category: "Temp") - #endif - - public init(subsystem: String = "com.github.CopilotForXcode", category: String) { - self.subsystem = subsystem - self.category = category - osLog = OSLog(subsystem: subsystem, category: category) - } - - func log( - level: LogLevel, - message: String, - error: Error? = nil, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function, - callStackSymbols: [String] = [] - ) { - let osLogType: OSLogType - switch level { - case .debug: - osLogType = .debug - case .info: - osLogType = .info - case .error: - osLogType = .error - } - - os_log("%{public}@", log: osLog, type: osLogType, message as CVarArg) - if category != "MCP" { - fileLogger.log(level: level, category: category, message: message) - } - - if osLogType == .error { - if let error = error { - Logger.telemetryLogger?.sendError( - error: error, - category: category, - file: file, - line: line, - function: function, - callStackSymbols: callStackSymbols - ) - } else { - Logger.telemetryLogger?.sendError( - message: message, - category: category, - file: file, - line: line, - function: function, - callStackSymbols: callStackSymbols - ) - } - } - } - - public func debug( - _ message: String, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function - ) { - log(level: .debug, message: """ - \(message) - file: \(file) - line: \(line) - function: \(function) - """, file: file, line: line, function: function) - } - - public func info( - _ message: String, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function - ) { - log(level: .info, message: message, file: file, line: line, function: function) - } - - public func error( - _ message: String, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function, - callStackSymbols: [String] = [] - ) { - log( - level: .error, - message: message, - file: file, - line: line, - function: function, - callStackSymbols: callStackSymbols - ) - } - - public func error( - _ error: Error, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function, - callStackSymbols: [String] = Thread.callStackSymbols - ) { - log( - level: .error, - message: error.localizedDescription, - error: error, - file: file, - line: line, - function: function, - callStackSymbols: callStackSymbols - ) - } - - public static func logMCPRuntime( - logFileName: String, - level: String, - message: String, - server: String, - tool: String? = nil, - time: Double - ) { - mcpRuntimeFileLogger - .log( - logFileName: logFileName, - level: level, - message: message, - server: server, - tool: tool, - time: time - ) - } - - public func signpostBegin( - name: StaticString, - file: StaticString = #file, - line: UInt = #line, - function: StaticString = #function - ) -> Signposter { - let poster = OSSignposter(logHandle: osLog) - let id = poster.makeSignpostID() - let state = poster.beginInterval(name, id: id) - return .init(log: osLog, id: id, name: name, signposter: poster, beginState: state) - } - - public struct Signposter { - let log: OSLog - let id: OSSignpostID - let name: StaticString - let signposter: OSSignposter - let state: OSSignpostIntervalState - - init( - log: OSLog, - id: OSSignpostID, - name: StaticString, - signposter: OSSignposter, - beginState: OSSignpostIntervalState - ) { - self.id = id - self.log = log - self.name = name - self.signposter = signposter - state = beginState - } - - public func end() { - signposter.endInterval(name, state) - } - - public func event(_ text: String) { - signposter.emitEvent(name, id: id, "\(text, privacy: .public)") - } - } -} - diff --git a/Tool/Sources/Logger/MCPRuntimeLogger.swift b/Tool/Sources/Logger/MCPRuntimeLogger.swift deleted file mode 100644 index 36527e43..00000000 --- a/Tool/Sources/Logger/MCPRuntimeLogger.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import System - -public final class MCPRuntimeFileLogger { - private let timestampFormat = Date.ISO8601FormatStyle.iso8601 - .year() - .month() - .day() - .timeZone(separator: .omitted).time(includingFractionalSeconds: true) - private static let implementation = MCPRuntimeFileLoggerImplementation() - - /// Converts a timestamp in milliseconds since the Unix epoch to a formatted date string. - private func timestamp(timeStamp: Double) -> String { - return Date(timeIntervalSince1970: timeStamp/1000).formatted(timestampFormat) - } - - public func log( - logFileName: String, - level: String, - message: String, - server: String, - tool: String? = nil, - time: Double - ) { - let log = "[\(timestamp(timeStamp: time))] [\(level)] [\(server)\(tool == nil ? "" : "-\(tool!))")] \(message)\(message.hasSuffix("\n") ? "" : "\n")" - - Task { - await MCPRuntimeFileLogger.implementation.logToFile(logFileName: logFileName, log: log) - } - } -} - -actor MCPRuntimeFileLoggerImplementation { - private let logDir: FilePath - private var workspaceLoggers: [String: BaseFileLoggerImplementation] = [:] - - public init() { - logDir = FileLoggingLocation.mcpRuntimeLogsPath - } - - public func logToFile(logFileName: String, log: String) async { - if workspaceLoggers[logFileName] == nil { - workspaceLoggers[logFileName] = BaseFileLoggerImplementation( - logDir: logDir, - logFileName: logFileName - ) - } - - if let logger = workspaceLoggers[logFileName] { - await logger.logToFile(log) - } - } -} diff --git a/Tool/Sources/Logger/TelemetryLoggerProvider.swift b/Tool/Sources/Logger/TelemetryLoggerProvider.swift deleted file mode 100644 index db580619..00000000 --- a/Tool/Sources/Logger/TelemetryLoggerProvider.swift +++ /dev/null @@ -1,18 +0,0 @@ -public protocol TelemetryLoggerProvider { - func sendError( - message: String, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - callStackSymbols: [String] - ) - func sendError( - error: Error, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - callStackSymbols: [String] - ) -} diff --git a/Tool/Sources/Persist/AppState.swift b/Tool/Sources/Persist/AppState.swift deleted file mode 100644 index 3b7f8cc2..00000000 --- a/Tool/Sources/Persist/AppState.swift +++ /dev/null @@ -1,113 +0,0 @@ -import CryptoKit -import Foundation -import JSONRPC -import Logger -import Status - -public extension JSONValue { - subscript(key: String) -> JSONValue? { - if case .hash(let dict) = self { - return dict[key] - } - return nil - } - - var stringValue: String? { - if case .string(let value) = self { - return value - } - return nil - } - - var boolValue: Bool? { - if case .bool(let value) = self { - return value - } - return nil - } - - static func convertToJSONValue(_ object: T) -> JSONValue? { - do { - let data = try JSONEncoder().encode(object) - let jsonValue = try JSONDecoder().decode(JSONValue.self, from: data) - return jsonValue - } catch { - Logger.client.info("Error converting to JSONValue: \(error)") - return nil - } - } -} - -public class AppState { - public static let shared = AppState() - - private var cache: [String: [String: JSONValue]] = [:] - private let cacheFileName = "appstate.json" - private let queue = DispatchQueue(label: "com.github.AppStateCacheQueue") - private var loadStatus: [String: Bool] = [:] - - private init() { - cache[""] = [:] // initialize a default cache if no user exists - initCacheForUserIfNeeded() - } - - func toHash(contents: String, _ length: Int = 16) -> String { - let data = Data(contents.utf8) - let hashData = SHA256.hash(data: data) - let hashValue = hashData.compactMap { String(format: "%02x", $0 ) }.joined() - let index = hashValue.index(hashValue.startIndex, offsetBy: length) - return String(hashValue[..(key: String, value: T) { - queue.async { - let userName = Status.currentUser() ?? "" - self.initCacheForUserIfNeeded(userName) - self.cache[userName]![key] = JSONValue.convertToJSONValue(value) - self.saveCacheForUser(userName) - } - } - - public func get(key: String) -> JSONValue? { - return queue.sync { - let userName = Status.currentUser() ?? "" - initCacheForUserIfNeeded(userName) - return (self.cache[userName] ?? [:])[key] - } - } - - private func configFilePath(userName: String) -> URL { - return ConfigPathUtils.configFilePath(userName: userName, fileName: cacheFileName) - } - - private func saveCacheForUser(_ userName: String? = nil) { - if let user = userName ?? Status.currentUser(), !user.isEmpty { // save cache for non-empty user - let cacheFilePath = configFilePath(userName: user) - do { - let data = try JSONEncoder().encode(self.cache[user] ?? [:]) - try data.write(to: cacheFilePath) - } catch { - Logger.client.info("Failed to save AppState cache: \(error)") - } - } - } - - private func initCacheForUserIfNeeded(_ userName: String? = nil) { - if let user = userName ?? Status.currentUser(), !user.isEmpty, - loadStatus[user] != true { // load cache for non-empty user - self.loadStatus[user] = true - self.cache[user] = [:] - let cacheFilePath = configFilePath(userName: user) - guard FileManager.default.fileExists(atPath: cacheFilePath.path) else { - return - } - - do { - let data = try Data(contentsOf: cacheFilePath) - self.cache[user] = try JSONDecoder().decode([String: JSONValue].self, from: data) - } catch { - Logger.client.info("Failed to load AppState cache: \(error)") - } - } - } -} diff --git a/Tool/Sources/Persist/ConfigPathUtils.swift b/Tool/Sources/Persist/ConfigPathUtils.swift deleted file mode 100644 index 603581ba..00000000 --- a/Tool/Sources/Persist/ConfigPathUtils.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import CryptoKit -import Logger - -let BaseAppDirectory = "github-copilot/xcode" - -/// String extension for hashing functionality -extension String { - /// Generates a SHA256 hash of the string - /// - Parameter length: The length of the hash to return, defaults to 16 characters - /// - Returns: The hashed string - func hashed(_ length: Int = 16) -> String { - let data = Data(self.utf8) - let hashData = SHA256.hash(data: data) - let hashValue = hashData.compactMap { String(format: "%02x", $0 ) }.joined() - let index = hashValue.index(hashValue.startIndex, offsetBy: length) - return String(hashValue[.. URL { - if let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], - xdgConfigHome.hasPrefix("/") { - return URL(fileURLWithPath: xdgConfigHome) - } - return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config") - } - - /// Generates a config file path for a specific user. - /// - Parameters: - /// - userName: The user name to generate a path for - /// - appDirectory: The application directory name, defaults to "github-copilot/xcode" - /// - fileName: The file name to append to the path - /// - Returns: The complete URL for the config file - static func configFilePath( - userName: String, - baseDirectory: String = BaseAppDirectory, - subDirectory: String? = nil, - fileName: String - ) -> URL { - var baseURL: URL = getXdgConfigHome() - .appendingPathComponent(baseDirectory) - .appendingPathComponent(toHash(contents: userName)) - - if let subDirectory = subDirectory { - baseURL = baseURL.appendingPathComponent(subDirectory) - } - - ensureDirectoryExists(at: baseURL) - return baseURL.appendingPathComponent(fileName) - } - - /// Ensures a directory exists at the specified URL, creating it if necessary. - /// - Parameter url: The directory URL - private static func ensureDirectoryExists(at url: URL) { - let fileManager = FileManager.default - if !fileManager.fileExists(atPath: url.path) { - do { - try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) - } catch let error as NSError { - if error.domain == NSPOSIXErrorDomain && error.code == EACCES { - Logger.client.error("Permission denied when trying to create directory: \(url.path)") - } else { - Logger.client.info("Failed to create directory: \(error)") - } - } - } - } - - /// Generates a hash from a string using SHA256. - /// - Parameters: - /// - contents: The string to hash - /// - length: The length of the hash to return, defaults to 16 characters - /// - Returns: The hashed string - static func toHash(contents: String, _ length: Int = 16) -> String { - let data = Data(contents.utf8) - let hashData = SHA256.hash(data: data) - let hashValue = hashData.compactMap { String(format: "%02x", $0 ) }.joined() - let index = hashValue.index(hashValue.startIndex, offsetBy: length) - return String(hashValue[.. [TurnItem] - func fetchConversationItems(_ type: ConversationFetchType) throws -> [ConversationItem] - func operate(_ request: OperationRequest) throws -} - -public final class ConversationStorage: ConversationStorageProtocol { - static let BusyTimeout: Double = 5 // error after 5 seconds - private var path: String - private var db: Connection? - - let conversationTable = ConversationTable() - let turnTable = TurnTable() - - public init(_ path: String) throws { - guard !path.isEmpty else { throw DatabaseError.invalidPath(path) } - self.path = path - - do { - let db = try Connection(path) - db.busyTimeout = ConversationStorage.BusyTimeout - self.db = db - } catch { - throw DatabaseError.connectionFailed(error.localizedDescription) - } - } - - deinit { db = nil } - - private func withDB(_ operation: (Connection) throws -> T) throws -> T { - guard let db = self.db else { - throw DatabaseError.connectionLost - } - return try operation(db) - } - - private func withDBTransaction(_ operation: (Connection) throws -> Void) throws { - guard let db = self.db else { - throw DatabaseError.connectionLost - } - try db.transaction { - try operation(db) - } - } - - public func createTableIfNeeded() throws { - try withDB { db in - try db.execute(""" - BEGIN TRANSACTION; - CREATE TABLE IF NOT EXISTS Conversation ( - id TEXT NOT NULL PRIMARY KEY, - title TEXT, - isSelected INTEGER NOT NULL, - CLSConversationID TEXT, - data BLOB NOT NULL, - createdAt REAL DEFAULT (strftime('%s','now')), - updatedAt REAL DEFAULT (strftime('%s','now')) - ); - CREATE TABLE IF NOT EXISTS Turn ( - rowID INTEGER PRIMARY KEY AUTOINCREMENT, - id TEXT NOT NULL UNIQUE, - conversationID TEXT NOT NULL, - CLSTurnID TEXT, - role TEXT NOT NULL, - data BLOB NOT NULL, - createdAt REAL DEFAULT (strftime('%s','now')), - updatedAt REAL DEFAULT (strftime('%s','now')), - UNIQUE (conversationID, id) - ); - COMMIT TRANSACTION; - """) - } - } - - public func operate(_ request: OperationRequest) throws { - guard request.operations.count > 0 else { return } - - try withDBTransaction { db in - - let now = Date().timeIntervalSince1970 - - for operation in request.operations { - switch operation { - case .upsertConversation(let conversationItems): - for conversationItems in conversationItems { - try db.run( - conversationTable.table.upsert( - conversationTable.column.id <- conversationItems.id, - conversationTable.column.title <- conversationItems.title, - conversationTable.column.isSelected <- conversationItems.isSelected, - conversationTable.column.CLSConversationID <- conversationItems.CLSConversationID ?? "", - conversationTable.column.data <- conversationItems.data.toBlob(), - conversationTable.column.createdAt <- conversationItems.createdAt.timeIntervalSince1970, - conversationTable.column.updatedAt <- conversationItems.updatedAt.timeIntervalSince1970, - onConflictOf: conversationTable.column.id - ) - ) - } - case .upsertTurn(let turnItems): - for turnItem in turnItems { - try db.run( - turnTable.table.upsert( - turnTable.column.conversationID <- turnItem.conversationID, - turnTable.column.id <- turnItem.id, - turnTable.column.CLSTurnID <- turnItem.CLSTurnID ?? "", - turnTable.column.role <- turnItem.role, - turnTable.column.data <- turnItem.data.toBlob(), - turnTable.column.createdAt <- turnItem.createdAt.timeIntervalSince1970, - turnTable.column.updatedAt <- turnItem.updatedAt.timeIntervalSince1970, - onConflictOf: SQLite.Expression(literal: "\"conversationID\", \"id\"") - ) - ) - } - case .delete(let deleteItems): - for deleteItem in deleteItems { - switch deleteItem { - case let .conversation(id): - try db.run(conversationTable.table.filter(conversationTable.column.id == id).delete()) - case .turn(let id): - try db.run(turnTable.table.filter(conversationTable.column.id == id).delete()) - case .turnByConversationID(let conversationID): - try db.run(turnTable.table.filter(turnTable.column.conversationID == conversationID).delete()) - } - } - } - } - } - } - - public func fetchTurnItems(for conversationID: String) throws -> [TurnItem] { - var items: [TurnItem] = [] - - try withDB { db in - let table = turnTable.table - let column = turnTable.column - - var query = table - .filter(column.conversationID == conversationID) - .order(column.rowID.asc) - let rowIterator = try db.prepareRowIterator(query) - items = try rowIterator.map { row in - TurnItem( - id: row[column.id], - conversationID: row[column.conversationID], - CLSTurnID: row[column.CLSTurnID], - role: row[column.role], - data: row[column.data].toString(), - createdAt: row[column.createdAt].toDate(), - updatedAt: row[column.updatedAt].toDate() - ) - } - } - - return items - } - - public func fetchConversationItems(_ type: ConversationFetchType) throws -> [ConversationItem] { - var items: [ConversationItem] = [] - - try withDB { db in - let table = conversationTable.table - let column = conversationTable.column - var query = table - - switch type { - case .all: - query = query.order(column.updatedAt.desc) - case .selected: - query = query - .filter(column.isSelected == true) - .limit(1) - case .latest: - query = query - .order(column.updatedAt.desc) - .limit(1) - case .id(let id): - query = query - .filter(conversationTable.column.id == id) - .limit(1) - } - - let rowIterator = try db.prepareRowIterator(query) - items = try rowIterator.map { row in - ConversationItem( - id: row[column.id], - title: row[column.title], - isSelected: row[column.isSelected], - CLSConversationID: row[column.CLSConversationID], - data: row[column.data].toString(), - createdAt: row[column.createdAt].toDate(), - updatedAt: row[column.updatedAt].toDate() - ) - } - } - - return items - } - - public func fetchConversationPreviewItems() throws -> [ConversationPreviewItem] { - var items: [ConversationPreviewItem] = [] - - try withDB { db in - let table = conversationTable.table - let column = conversationTable.column - let query = table - .select(column.id, column.title, column.isSelected, column.updatedAt) - .order(column.updatedAt.desc) - - let rowIterator = try db.prepareRowIterator(query) - items = try rowIterator.map { row in - ConversationPreviewItem( - id: row[column.id], - title: row[column.title], - isSelected: row[column.isSelected], - updatedAt: row[column.updatedAt].toDate() - ) - } - } - - return items - } -} - - -extension String { - func toBlob() -> Blob { - let data = self.data(using: .utf8) ?? Data() // TODO: handle exception - return Blob(bytes: [UInt8](data)) - } -} - -extension Blob { - func toString() -> String { - return String(data: Data(bytes), encoding: .utf8) ?? "" - } -} - -extension Double { - func toDate() -> Date { - return Date(timeIntervalSince1970: self) - } -} diff --git a/Tool/Sources/Persist/Storage/ConversationStorage/Model.swift b/Tool/Sources/Persist/Storage/ConversationStorage/Model.swift deleted file mode 100644 index 6193f4d5..00000000 --- a/Tool/Sources/Persist/Storage/ConversationStorage/Model.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation - -public struct TurnItem: Codable, Equatable { - public let id: String - public let conversationID: String - public let CLSTurnID: String? - public let role: String - public let data: String - public let createdAt: Date - public let updatedAt: Date - - public init(id: String, conversationID: String, CLSTurnID: String?, role: String, data: String, createdAt: Date, updatedAt: Date) { - self.id = id - self.conversationID = conversationID - self.CLSTurnID = CLSTurnID - self.role = role - self.data = data - self.createdAt = createdAt - self.updatedAt = updatedAt - } -} - -public struct ConversationItem: Codable, Equatable { - public let id: String - public let title: String? - public let isSelected: Bool - public let CLSConversationID: String? - public let data: String - public let createdAt: Date - public let updatedAt: Date - - public init(id: String, title: String?, isSelected: Bool, CLSConversationID: String?, data: String, createdAt: Date, updatedAt: Date) { - self.id = id - self.title = title - self.isSelected = isSelected - self.CLSConversationID = CLSConversationID - self.data = data - self.createdAt = createdAt - self.updatedAt = updatedAt - } -} - -public struct ConversationPreviewItem: Codable, Equatable { - public let id: String - public let title: String? - public let isSelected: Bool - public let updatedAt: Date -} - -public enum DeleteType { - case conversation(id: String) - case turn(id: String) - case turnByConversationID(conversationID: String) -} - -public enum OperationType { - case upsertTurn([TurnItem]) - case upsertConversation([ConversationItem]) - case delete([DeleteType]) -} - -public struct OperationRequest { - - var operations: [OperationType] - - public init(_ operations: [OperationType]) { - self.operations = operations - } -} - -public enum ConversationFetchType { - case all, selected, latest, id(String) -} diff --git a/Tool/Sources/Persist/Storage/ConversationStorage/Table.swift b/Tool/Sources/Persist/Storage/ConversationStorage/Table.swift deleted file mode 100644 index c6932c83..00000000 --- a/Tool/Sources/Persist/Storage/ConversationStorage/Table.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SQLite - -struct ConversationTable { - let table = Table("Conversation") - - // Column - struct Column { - let id = SQLite.Expression("id") - let title = SQLite.Expression("title") - // 0 -> false, 1 -> true - let isSelected = SQLite.Expression("isSelected") - let CLSConversationID = SQLite.Expression("CLSConversationID") - // for extensibility purpose - let data = SQLite.Expression("data") - let createdAt = SQLite.Expression("createdAt") - let updatedAt = SQLite.Expression("updatedAt") - } - - let column = Column() -} - -struct TurnTable { - let table = Table("Turn") - - // Column - struct Column { - // an auto-incremental id genrated by SQLite - let rowID = SQLite.Expression("rowID") - let id = SQLite.Expression("id") - let conversationID = SQLite.Expression("conversationID") - let CLSTurnID = SQLite.Expression("CLSTurnID") - let role = SQLite.Expression("role") - // for extensibility purpose - let data = SQLite.Expression("data") - let createdAt = SQLite.Expression("createdAt") - let updatedAt = SQLite.Expression("updatedAt") - } - - let column = Column() -} diff --git a/Tool/Sources/Persist/Storage/ConversationStorageService.swift b/Tool/Sources/Persist/Storage/ConversationStorageService.swift deleted file mode 100644 index 113eafa2..00000000 --- a/Tool/Sources/Persist/Storage/ConversationStorageService.swift +++ /dev/null @@ -1,142 +0,0 @@ -import Foundation -import CryptoKit -import Logger - -extension String { - - func appendingPathComponents(_ components: String...) -> String { - var url = URL(fileURLWithPath: self) - components.forEach { component in - url = url.appendingPathComponent(component) - } - - return url.path - } -} - -protocol ConversationStorageServiceProtocol { - func fetchConversationItems(_ type: ConversationFetchType, metadata: StorageMetadata) -> [ConversationItem] - func fetchTurnItems(for conversationID: String, metadata: StorageMetadata) -> [TurnItem] - - func operate(_ request: OperationRequest, metadata: StorageMetadata) - - func terminate() -} - -public struct StorageMetadata: Hashable { - public var workspacePath: String - public var username: String - - public init(workspacePath: String, username: String) { - self.workspacePath = workspacePath - self.username = username - } -} - -public final class ConversationStorageService: ConversationStorageServiceProtocol { - private var conversationStoragePool: [StorageMetadata: ConversationStorage] = [:] - public static let shared = ConversationStorageService() - private init() { } - - // The storage path would be xdgConfigHome/usernameHash/conversations/workspacePathHash.db - private func getPersistenceFile(_ metadata: StorageMetadata) -> String { - let fileName = "\(ConfigPathUtils.toHash(contents: metadata.workspacePath)).db" - let persistenceFileURL = ConfigPathUtils.configFilePath( - userName: metadata.username, - subDirectory: "conversations", - fileName: fileName - ) - - return persistenceFileURL.path - } - - private func getConversationStorage(_ metadata: StorageMetadata) throws -> ConversationStorage { - if let existConversationStorage = conversationStoragePool[metadata] { - return existConversationStorage - } - - let persistenceFile = getPersistenceFile(metadata) - - let conversationStorage = try ConversationStorage(persistenceFile) - try conversationStorage.createTableIfNeeded() - conversationStoragePool[metadata] = conversationStorage - return conversationStorage - } - - private func ensurePathExists(_ path: String) -> Bool { - - do { - let fileManager = FileManager.default - let pathURL = URL(fileURLWithPath: path) - if !fileManager.fileExists(atPath: path) { - try fileManager.createDirectory(at: pathURL, withIntermediateDirectories: true) - } - } catch { - Logger.client.error("Failed to create persistence path: \(error)") - return false - } - - return true - } - - private func withStorage(_ metadata: StorageMetadata, operation: (ConversationStorage) throws -> T) throws -> T { - let storage = try getConversationStorage(metadata) - return try operation(storage) - } - - public func fetchConversationItems(_ type: ConversationFetchType, metadata: StorageMetadata) -> [ConversationItem] { - var items: [ConversationItem] = [] - do { - try withStorage(metadata) { conversationStorage in - items = try conversationStorage.fetchConversationItems(type) - } - } catch { - Logger.client.error("Failed to fetch conversation items: \(error)") - } - - return items - } - - public func fetchConversationPreviewItems(metadata: StorageMetadata) -> [ConversationPreviewItem] { - var items: [ConversationPreviewItem] = [] - - do { - try withStorage(metadata) { conversationStorage in - items = try conversationStorage.fetchConversationPreviewItems() - } - } catch { - Logger.client.error("Failed to fetch conversation preview items: \(error)") - } - - return items - } - - public func fetchTurnItems(for conversationID: String, metadata: StorageMetadata) -> [TurnItem] { - var items: [TurnItem] = [] - - do { - try withStorage(metadata) { conversationStorage in - items = try conversationStorage.fetchTurnItems(for: conversationID) - } - } catch { - Logger.client.error("Failed to fetch turn items: \(error)") - } - - return items - } - - public func operate(_ request: OperationRequest, metadata: StorageMetadata) { - do { - try withStorage(metadata) { conversationStorage in - try conversationStorage.operate(request) - } - - } catch { - Logger.client.error("Failed to operate database request: \(error)") - } - } - - public func terminate() { - conversationStoragePool = [:] - } -} diff --git a/Tool/Sources/Persist/Storage/Storage.swift b/Tool/Sources/Persist/Storage/Storage.swift deleted file mode 100644 index b0770b20..00000000 --- a/Tool/Sources/Persist/Storage/Storage.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -public enum DatabaseError: Error { - case connectionFailed(String) - case invalidPath(String) - case connectionLost -} diff --git a/Tool/Sources/Preferences/AppStorage.swift b/Tool/Sources/Preferences/AppStorage.swift deleted file mode 100644 index a5b3b214..00000000 --- a/Tool/Sources/Preferences/AppStorage.swift +++ /dev/null @@ -1,262 +0,0 @@ -import Foundation - -#if canImport(SwiftUI) - -import SwiftUI - -public extension AppStorage { - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Bool { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Double { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == URL { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Data { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value: RawRepresentable, Value.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value: RawRepresentable, Value.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } -} - -public extension AppStorage where Value: ExpressibleByNilLiteral { - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Bool? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == String? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Double? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Int? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == URL? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == Data? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } -} - -public extension AppStorage { - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == R?, R: RawRepresentable, R.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - init( - _ keyPath: KeyPath - ) where K.Value == Value, Value == R?, R: RawRepresentable, R.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } -} - -// MARK: - Deprecated Key Accessor - -public extension AppStorage { - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Bool { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Double { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == URL { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Data { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value: RawRepresentable, Value.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value: RawRepresentable, Value.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(wrappedValue: key.defaultValue, key.key, store: .shared) - } -} - -public extension AppStorage where Value: ExpressibleByNilLiteral { - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Bool? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == String? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Double? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Int? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == URL? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == Data? { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } -} - -public extension AppStorage { - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == R?, R: RawRepresentable, R.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } - - @available(*, deprecated, message: "This preference key is deprecated.") - init( - _ keyPath: KeyPath> - ) where Value == R?, R: RawRepresentable, R.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - self.init(key.key, store: .shared) - } -} - -#endif - diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift deleted file mode 100644 index c4296fc7..00000000 --- a/Tool/Sources/Preferences/Keys.swift +++ /dev/null @@ -1,590 +0,0 @@ -import Foundation - -public protocol UserDefaultPreferenceKey { - associatedtype Value - var defaultValue: Value { get } - var key: String { get } -} - -public struct PreferenceKey: UserDefaultPreferenceKey { - public let defaultValue: T - public let key: String - - public init(defaultValue: T, key: String) { - self.defaultValue = defaultValue - self.key = key - } -} - -public struct DeprecatedPreferenceKey { - public let defaultValue: T - public let key: String - - public init(defaultValue: T, key: String) { - self.defaultValue = defaultValue - self.key = key - } -} - -public struct FeatureFlag: UserDefaultPreferenceKey { - public let defaultValue: Bool - public let key: String - - public init(defaultValue: Bool, key: String) { - self.defaultValue = defaultValue - self.key = key - } -} - -public struct UserDefaultPreferenceKeys { - public init() {} - - // MARK: Quit XPC Service On Xcode And App Quit - - public let quitXPCServiceOnXcodeAndAppQuit = PreferenceKey( - defaultValue: true, - key: "QuitXPCServiceOnXcodeAndAppQuit" - ) - - // MARK: Suggestion Widget Position Mode - - public let suggestionWidgetPositionMode = PreferenceKey( - defaultValue: SuggestionWidgetPositionMode.fixedToBottom, - key: "SuggestionWidgetPositionMode" - ) - - // MARK: Widget Color Scheme - - public let widgetColorScheme = PreferenceKey( - defaultValue: WidgetColorScheme.system, - key: "WidgetColorScheme" - ) - - // MARK: Force Order Widget to Front - - public let forceOrderWidgetToFront = PreferenceKey( - defaultValue: true, - key: "ForceOrderWidgetToFront" - ) - - // MARK: Prefer Widget to Stay Inside Editor When Width Greater Than - - public let preferWidgetToStayInsideEditorWhenWidthGreaterThan = PreferenceKey( - defaultValue: 1400 as Double, - key: "PreferWidgetToStayInsideEditorWhenWidthGreaterThan" - ) - - // MARK: Hide Circular Widget - - public let hideCircularWidget = PreferenceKey( - defaultValue: true, - key: "HideCircularWidget" - ) - - public let showHideWidgetShortcutGlobally = PreferenceKey( - defaultValue: false, - key: "ShowHideWidgetShortcutGlobally" - ) - - // MARK: Update Channel - - public let installPrereleases = PreferenceKey( - defaultValue: false, - key: "InstallPrereleases" - ) - - // MARK: Completion Hint Shown - - public let completionHintShown = PreferenceKey( - defaultValue: false, - key: "CompletionHintShown" - ) - - // MARK: First Time Intro Interface - - public let introLastShownVersion = PreferenceKey( - defaultValue: "", - key: "IntroLastShownVersion" - ) - - public let hideIntro = PreferenceKey( - defaultValue: false, - key: "HideIntro" - ) - - public let extensionPermissionShown = PreferenceKey( - defaultValue: false, - key: "ExtensionPermissionShown" - ) - - public let capturePermissionShown = PreferenceKey( - defaultValue: false, - key: "CapturePermissionShown" - ) -} - -// MARK: - Prompt to Code - -public extension UserDefaultPreferenceKeys { - - var promptToCodeGenerateDescription: PreferenceKey { - .init(defaultValue: true, key: "PromptToCodeGenerateDescription") - } - - var promptToCodeGenerateDescriptionInUserPreferredLanguage: PreferenceKey { - .init(defaultValue: true, key: "PromptToCodeGenerateDescriptionInUserPreferredLanguage") - } - - var enableSenseScopeByDefaultInPromptToCode: PreferenceKey { - .init(defaultValue: false, key: "EnableSenseScopeByDefaultInPromptToCode") - } - - var promptToCodeCodeFontSize: PreferenceKey { - .init(defaultValue: 13, key: "PromptToCodeCodeFontSize") - } - - var hideCommonPrecedingSpacesInPromptToCode: PreferenceKey { - .init(defaultValue: true, key: "HideCommonPrecedingSpacesInPromptToCode") - } - - var wrapCodeInPromptToCode: PreferenceKey { - .init(defaultValue: true, key: "WrapCodeInPromptToCode") - } -} - -// MARK: - Suggestion - -public extension UserDefaultPreferenceKeys { - var oldSuggestionFeatureProvider: DeprecatedPreferenceKey { - .init(defaultValue: .gitHubCopilot, key: "SuggestionFeatureProvider") - } - - var suggestionFeatureProvider: PreferenceKey { - .init(defaultValue: .builtIn(.gitHubCopilot), key: "NewSuggestionFeatureProvider") - } - - var realtimeSuggestionToggle: PreferenceKey { - .init(defaultValue: true, key: "RealtimeSuggestionToggle") - } - - var suggestionDisplayCompactMode: PreferenceKey { - .init(defaultValue: true, key: "SuggestionDisplayCompactMode") - } - - var suggestionCodeFontSize: PreferenceKey { - .init(defaultValue: 13, key: "SuggestionCodeFontSize") - } - - var disableSuggestionFeatureGlobally: PreferenceKey { - .init(defaultValue: false, key: "DisableSuggestionFeatureGlobally") - } - - var suggestionFeatureEnabledProjectList: PreferenceKey<[String]> { - .init(defaultValue: [], key: "SuggestionFeatureEnabledProjectList") - } - - var suggestionFeatureDisabledLanguageList: PreferenceKey<[String]> { - .init(defaultValue: [], key: "SuggestionFeatureDisabledLanguageList") - } - - var hideCommonPrecedingSpacesInSuggestion: PreferenceKey { - .init(defaultValue: true, key: "HideCommonPrecedingSpacesInSuggestion") - } - - var suggestionPresentationMode: PreferenceKey { - .init(defaultValue: .nearbyTextCursor, key: "SuggestionPresentationMode") - } - - var realtimeSuggestionDebounce: PreferenceKey { - .init(defaultValue: 0.2, key: "RealtimeSuggestionDebounce") - } - - var acceptSuggestionWithTab: PreferenceKey { - .init(defaultValue: true, key: "AcceptSuggestionWithTab") - } - - var acceptSuggestionWithModifierCommand: PreferenceKey { - .init(defaultValue: false, key: "SuggestionWithModifierCommand") - } - - var acceptSuggestionWithModifierOption: PreferenceKey { - .init(defaultValue: false, key: "SuggestionWithModifierOption") - } - - var acceptSuggestionWithModifierControl: PreferenceKey { - .init(defaultValue: false, key: "SuggestionWithModifierControl") - } - - var acceptSuggestionWithModifierShift: PreferenceKey { - .init(defaultValue: false, key: "SuggestionWithModifierShift") - } - - var acceptSuggestionWithModifierOnlyForSwift: PreferenceKey { - .init(defaultValue: false, key: "SuggestionWithModifierOnlyForSwift") - } - - var dismissSuggestionWithEsc: PreferenceKey { - .init(defaultValue: true, key: "DismissSuggestionWithEsc") - } - - var isSuggestionSenseEnabled: PreferenceKey { - .init(defaultValue: false, key: "IsSuggestionSenseEnabled") - } - - var isSuggestionTypeInTheMiddleEnabled: PreferenceKey { - .init(defaultValue: true, key: "IsSuggestionTypeInTheMiddleEnabled") - } - - var clsWarningDismissedUntilRelaunch: PreferenceKey { - .init(defaultValue: false, key: "CLSWarningDismissedUntilRelaunch") - } -} - -// MARK: - Chat - -public extension UserDefaultPreferenceKeys { - - var chatFontSize: PreferenceKey { - .init(defaultValue: 13, key: "ChatFontSize") - } - - var chatCodeFontSize: PreferenceKey { - .init(defaultValue: 12, key: "ChatCodeFontSize") - } - - var useGlobalChat: PreferenceKey { - .init(defaultValue: true, key: "UseGlobalChat") - } - - var embedFileContentInChatContextIfNoSelection: PreferenceKey { - .init(defaultValue: false, key: "EmbedFileContentInChatContextIfNoSelection") - } - - var maxFocusedCodeLineCount: PreferenceKey { - .init(defaultValue: 100, key: "MaxEmbeddableFileInChatContextLineCount") - } - - var useCodeScopeByDefaultInChatContext: DeprecatedPreferenceKey { - .init(defaultValue: true, key: "UseSelectionScopeByDefaultInChatContext") - } - - - var wrapCodeInChatCodeBlock: PreferenceKey { - .init(defaultValue: true, key: "WrapCodeInChatCodeBlock") - } - - var enableFileScopeByDefaultInChatContext: PreferenceKey { - .init(defaultValue: true, key: "EnableFileScopeByDefaultInChatContext") - } - - var enableCodeScopeByDefaultInChatContext: PreferenceKey { - .init(defaultValue: true, key: "UseSelectionScopeByDefaultInChatContext") - } - - var enableSenseScopeByDefaultInChatContext: PreferenceKey { - .init(defaultValue: false, key: "EnableSenseScopeByDefaultInChatContext") - } - - var enableProjectScopeByDefaultInChatContext: PreferenceKey { - .init(defaultValue: false, key: "EnableProjectScopeByDefaultInChatContext") - } - - var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { - .init(defaultValue: true, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") - } - - var keepFloatOnTopIfChatPanelAndXcodeOverlaps: PreferenceKey { - .init(defaultValue: true, key: "KeepFloatOnTopIfChatPanelAndXcodeOverlaps") - } - - var enableCurrentEditorContext: PreferenceKey { - .init(defaultValue: true, key: "EnableCurrentEditorContext") - } - - var chatResponseLocale: PreferenceKey { - .init(defaultValue: "en", key: "ChatResponseLocale") - } - - var globalCopilotInstructions: PreferenceKey { - .init(defaultValue: "", key: "GlobalCopilotInstructions") - } - - var autoAttachChatToXcode: PreferenceKey { - .init(defaultValue: true, key: "AutoAttachChatToXcode") - } -} - -// MARK: - Theme - -public extension UserDefaultPreferenceKeys { - var syncSuggestionHighlightTheme: PreferenceKey { - .init(defaultValue: true, key: "SyncSuggestionHighlightTheme") - } - - var syncPromptToCodeHighlightTheme: PreferenceKey { - .init(defaultValue: false, key: "SyncPromptToCodeHighlightTheme") - } - - var syncChatCodeHighlightTheme: PreferenceKey { - .init(defaultValue: false, key: "SyncChatCodeHighlightTheme") - } - - var codeForegroundColorLight: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CodeForegroundColorLight") - } - - var codeForegroundColorDark: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CodeForegroundColorDark") - } - - var codeBackgroundColorLight: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CodeBackgroundColorLight") - } - - var codeBackgroundColorDark: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CodeBackgroundColorDark") - } - - var currentLineBackgroundColorLight: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CurrentLineBackgroundColorLight") - } - - var currentLineBackgroundColorDark: PreferenceKey> { - .init(defaultValue: .init(nil), key: "CurrentLineBackgroundColorDark") - } - - var codeFontLight: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "CodeFontLight" - ) - } - - var codeFontDark: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "CodeFontDark" - ) - } - - var suggestionCodeFont: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "SuggestionCodeFont" - ) - } - - var promptToCodeCodeFont: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "PromptToCodeCodeFont" - ) - } - - var chatCodeFont: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "ChatCodeFont" - ) - } - - var terminalFont: PreferenceKey> { - .init( - defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "TerminalCodeFont" - ) - } -} - -// MARK: - Bing Search - -public extension UserDefaultPreferenceKeys { - var bingSearchSubscriptionKey: PreferenceKey { - .init(defaultValue: "", key: "BingSearchSubscriptionKey") - } - - var bingSearchEndpoint: PreferenceKey { - .init( - defaultValue: "https://api.bing.microsoft.com/v7.0/search/", - key: "BingSearchEndpoint" - ) - } -} - -// MARK: - Custom Commands - -public extension UserDefaultPreferenceKeys { - var customCommands: PreferenceKey<[CustomCommand]> { - .init(defaultValue: [ - .init( - commandId: "BuiltInCustomCommandExplainSelection", - name: "Explain Selection", - feature: .chatWithSelection( - extraSystemPrompt: "", - prompt: "Explain the selected code concisely, step-by-step.", - useExtraSystemPrompt: true - ) - ), - .init( - commandId: "BuiltInCustomCommandAddDocumentationToSelection", - name: "Add Documentation to Selection", - feature: .promptToCode( - extraSystemPrompt: "", - prompt: "Add documentation on top of the code. Use triple slash if the language supports it.", - continuousMode: false, - generateDescription: true - ) - ) - ], key: "CustomCommands") - } - - var customChatCommands: PreferenceKey<[CustomCommand]> { - .init(defaultValue: [ - .init( - commandId: "BuiltInCustomCommandSendCodeToChat", - name: "Send Selected Code to Chat", - feature: .chatWithSelection( - extraSystemPrompt: "", - prompt: """ - ```{{active_editor_language}} - {{selected_code}} - ``` - """, - useExtraSystemPrompt: true - ) - ) - ], key: "CustomChatCommands") - } -} - -// MARK: - Feature Flags - -public extension UserDefaultPreferenceKeys { - var disableLazyVStack: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-DisableLazyVStack") - } - - var preCacheOnFileOpen: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-PreCacheOnFileOpen") - } - - var runNodeWithInteractiveLoggedInShell: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-RunNodeWithInteractiveLoggedInShell") - } - - var useCustomScrollViewWorkaround: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-UseCustomScrollViewWorkaround") - } - - var triggerActionWithAccessibilityAPI: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-TriggerActionWithAccessibilityAPI") - } - - var alwaysAcceptSuggestionWithAccessibilityAPI: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-AlwaysAcceptSuggestionWithAccessibilityAPI") - } - - var animationACrashSuggestion: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-AnimationACrashSuggestion") - } - - var animationBCrashSuggestion: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-AnimationBCrashSuggestion") - } - - var animationCCrashSuggestion: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-AnimationCCrashSuggestion") - } - - var enableXcodeInspectorDebugMenu: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-EnableXcodeInspectorDebugMenu") - } - - var disableFunctionCalling: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-DisableFunctionCalling") - } - - var useUserDefaultsBaseAPIKeychain: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-UseUserDefaultsBaseAPIKeychain") - } - - var disableGitHubCopilotSettingsAutoRefreshOnAppear: FeatureFlag { - .init( - defaultValue: false, - key: "FeatureFlag-DisableGitHubCopilotSettingsAutoRefreshOnAppear" - ) - } - - var disableEnhancedWorkspace: FeatureFlag { - .init( - defaultValue: false, - key: "FeatureFlag-DisableEnhancedWorkspace" - ) - } - - var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning: FeatureFlag { - .init( - defaultValue: false, - key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioning" - ) - } - - var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer: FeatureFlag { - .init( - defaultValue: true, - key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer" - ) - } - - var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted: FeatureFlag { - .init( - defaultValue: false, - key: "FeatureFlag-ToastForTheReasonWhyXcodeInspectorNeedsToBeRestarted" - ) - } - - var observeToAXNotificationWithDefaultMode: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-observeToAXNotificationWithDefaultMode") - } - - var useCloudflareDomainNameForLicenseCheck: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-UseCloudflareDomainNameForLicenseCheck") - } -} - -// MARK: - Advanced Features - -public extension UserDefaultPreferenceKeys { - - var gitHubCopilotProxyUrl: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotProxyUrl") - } - - var gitHubCopilotUseStrictSSL: PreferenceKey { - .init(defaultValue: true, key: "GitHubCopilotUseStrictSSL") - } - - var gitHubCopilotProxyUsername: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotProxyUsername") - } - - var gitHubCopilotProxyPassword: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotProxyPassword") - } - - var gitHubCopilotMCPConfig: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotMCPConfig") - } - - var gitHubCopilotMCPUpdatedStatus: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotMCPUpdatedStatus") - } - - var gitHubCopilotEnterpriseURI: PreferenceKey { - .init(defaultValue: "", key: "GitHubCopilotEnterpriseURI") - } - - var verboseLoggingEnabled: PreferenceKey { - .init(defaultValue: false, key: "VerboseLoggingEnabled") - } -} diff --git a/Tool/Sources/Preferences/Types/CustomCommand.swift b/Tool/Sources/Preferences/Types/CustomCommand.swift deleted file mode 100644 index b462e8a3..00000000 --- a/Tool/Sources/Preferences/Types/CustomCommand.swift +++ /dev/null @@ -1,75 +0,0 @@ -import CryptoKit -import Foundation - -public struct CustomCommand: Codable, Equatable { - /// The custom command feature. - /// - /// Keep everything optional so nothing will break when the format changes. - public enum Feature: Codable, Equatable { - /// Prompt to code. - case promptToCode( - extraSystemPrompt: String?, - prompt: String?, - continuousMode: Bool?, - generateDescription: Bool? - ) - /// Send message. - case chatWithSelection( - extraSystemPrompt: String?, - prompt: String?, - useExtraSystemPrompt: Bool? - ) - /// Custom chat. - case customChat(systemPrompt: String?, prompt: String?) - /// Single round dialog. - case singleRoundDialog( - systemPrompt: String?, - overwriteSystemPrompt: Bool?, - prompt: String?, - receiveReplyInNotification: Bool? - ) - } - - public var id: String { commandId ?? legacyId } - public var commandId: String? - public var name: String - public var feature: Feature - - public init(commandId: String, name: String, feature: Feature) { - self.commandId = commandId - self.name = name - self.feature = feature - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - commandId = try container.decodeIfPresent(String.self, forKey: .commandId) - name = try container.decode(String.self, forKey: .name) - feature = (try? container - .decode(CustomCommand.Feature.self, forKey: .feature)) ?? .chatWithSelection( - extraSystemPrompt: "", - prompt: "", - useExtraSystemPrompt: false - ) - } - - var legacyId: String { - name.sha1HexString - } -} - -private extension Digest { - var bytes: [UInt8] { Array(makeIterator()) } - var data: Data { Data(bytes) } - - var hexStr: String { - bytes.map { String(format: "%02X", $0) }.joined() - } -} - -private extension String { - var sha1HexString: String { - Insecure.SHA1.hash(data: data(using: .utf8) ?? Data()).hexStr - } -} - diff --git a/Tool/Sources/Preferences/Types/Locale.swift b/Tool/Sources/Preferences/Types/Locale.swift deleted file mode 100644 index 6b50d82d..00000000 --- a/Tool/Sources/Preferences/Types/Locale.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public extension Locale { - static var availableLocalizedLocales: [String] { - let localizedLocales = Locale.isoLanguageCodes.compactMap { - Locale(identifier: "en-US").localizedString(forLanguageCode: $0) - } - .sorted() - return localizedLocales - } - - var languageName: String { - localizedString(forLanguageCode: languageCode ?? "") ?? "" - } -} diff --git a/Tool/Sources/Preferences/Types/PresentationMode.swift b/Tool/Sources/Preferences/Types/PresentationMode.swift deleted file mode 100644 index 66fd9a76..00000000 --- a/Tool/Sources/Preferences/Types/PresentationMode.swift +++ /dev/null @@ -1,4 +0,0 @@ -public enum PresentationMode: Int, CaseIterable { - case nearbyTextCursor = 0 - case floatingWidget = 1 -} diff --git a/Tool/Sources/Preferences/Types/StorableColors.swift b/Tool/Sources/Preferences/Types/StorableColors.swift deleted file mode 100644 index b5c2d6cb..00000000 --- a/Tool/Sources/Preferences/Types/StorableColors.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -public struct StorableColor: Codable, Equatable { - public var red: Double - public var green: Double - public var blue: Double - public var alpha: Double - - public init(red: Double, green: Double, blue: Double, alpha: Double) { - self.red = red - self.green = green - self.blue = blue - self.alpha = alpha - } -} - -#if canImport(SwiftUI) -import SwiftUI -public extension StorableColor { - var swiftUIColor: SwiftUI.Color { - SwiftUI.Color(CGColor(red: red, green: green, blue: blue, alpha: alpha)) - } -} -#endif - -#if canImport(AppKit) -import AppKit -public extension StorableColor { - var nsColor: NSColor { - NSColor( - srgbRed: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) - } -} -#endif - diff --git a/Tool/Sources/Preferences/Types/StorableFont.swift b/Tool/Sources/Preferences/Types/StorableFont.swift deleted file mode 100644 index 337ad621..00000000 --- a/Tool/Sources/Preferences/Types/StorableFont.swift +++ /dev/null @@ -1,48 +0,0 @@ -import AppKit -import Foundation - -public struct StorableFont: Codable, Equatable { - public var nsFont: NSFont - - public init(nsFont: NSFont) { - self.nsFont = nsFont - } - - public init(name: String, size: Double) { - if let font = NSFont(name: name, size: size) { - self.nsFont = font - } else { - self.nsFont = .monospacedSystemFont(ofSize: size, weight: .regular) - } - } - - public enum CodingKeys: String, CodingKey { - case nsFont - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let fontData = try container.decode(Data.self, forKey: .nsFont) - guard let nsFont = try NSKeyedUnarchiver.unarchivedObject( - ofClass: NSFont.self, - from: fontData - ) else { - throw DecodingError.dataCorruptedError( - forKey: .nsFont, - in: container, - debugDescription: "Failed to decode NSFont" - ) - } - self.nsFont = nsFont - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - let fontData = try NSKeyedArchiver.archivedData( - withRootObject: nsFont, - requiringSecureCoding: false - ) - try container.encode(fontData, forKey: .nsFont) - } -} - diff --git a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift deleted file mode 100644 index 90674051..00000000 --- a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -public enum BuiltInSuggestionFeatureProvider: Int, CaseIterable, Codable { - case gitHubCopilot -} - -public enum SuggestionFeatureProvider: RawRepresentable, Hashable { - case builtIn(BuiltInSuggestionFeatureProvider) - case `extension`(name: String, bundleIdentifier: String) - - enum Storage: Codable { - case builtIn(BuiltInSuggestionFeatureProvider) - case `extension`(name: String, bundleIdentifier: String) - } - - public init?(rawValue: String) { - guard let data = rawValue.data(using: .utf8), - let value = try? JSONDecoder().decode(Storage.self, from: data) - else { return nil } - - switch value { - case let .builtIn(provider): - self = .builtIn(provider) - case let .extension(name, bundleIdentifier): - self = .extension(name: name, bundleIdentifier: bundleIdentifier) - } - } - - public var rawValue: String { - let storage: Storage = switch self { - case let .builtIn(provider): .builtIn(provider) - case let .extension(name, bundleIdentifier): - .extension(name: name, bundleIdentifier: bundleIdentifier) - } - if let data = try? JSONEncoder().encode(storage) { - return String(data: data, encoding: .utf8) ?? "" - } - return "" - } -} - diff --git a/Tool/Sources/Preferences/Types/SuggestionWidgetPositionMode.swift b/Tool/Sources/Preferences/Types/SuggestionWidgetPositionMode.swift deleted file mode 100644 index 8fd31438..00000000 --- a/Tool/Sources/Preferences/Types/SuggestionWidgetPositionMode.swift +++ /dev/null @@ -1,4 +0,0 @@ -public enum SuggestionWidgetPositionMode: Int, CaseIterable { - case fixedToBottom = 0 - case alignToTextCursor = 1 -} diff --git a/Tool/Sources/Preferences/Types/WidgetColorScheme.swift b/Tool/Sources/Preferences/Types/WidgetColorScheme.swift deleted file mode 100644 index aa459c42..00000000 --- a/Tool/Sources/Preferences/Types/WidgetColorScheme.swift +++ /dev/null @@ -1,5 +0,0 @@ -public enum WidgetColorScheme: Int, CaseIterable { - case system = 0 - case light = 1 - case dark = 2 -} diff --git a/Tool/Sources/Preferences/Types/XcodeColorScheme.swift b/Tool/Sources/Preferences/Types/XcodeColorScheme.swift deleted file mode 100644 index 30c46617..00000000 --- a/Tool/Sources/Preferences/Types/XcodeColorScheme.swift +++ /dev/null @@ -1,7 +0,0 @@ -import SwiftUI - -public enum XcodeColorScheme: Int, CaseIterable { - case system = 0 - case light = 1 - case dark = 2 -} diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift deleted file mode 100644 index dfaa5b67..00000000 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ /dev/null @@ -1,306 +0,0 @@ -import AppKit -import Configs -import Foundation - -public protocol UserDefaultsType { - func value(forKey: String) -> Any? - func set(_ value: Any?, forKey: String) -} - -public extension UserDefaults { - static var shared = UserDefaults(suiteName: userDefaultSuiteName)! - - static func setupDefaultSettings() { - shared.setupDefaultValue(for: \.quitXPCServiceOnXcodeAndAppQuit) - shared.setupDefaultValue(for: \.realtimeSuggestionToggle) - shared.setupDefaultValue(for: \.realtimeSuggestionDebounce) - shared.setupDefaultValue(for: \.suggestionPresentationMode) - shared.setupDefaultValue(for: \.autoAttachChatToXcode) - shared.setupDefaultValue(for: \.widgetColorScheme) - shared.setupDefaultValue(for: \.customCommands) - shared.setupDefaultValue( - for: \.suggestionFeatureProvider, - defaultValue: .builtIn(shared.deprecatedValue(for: \.oldSuggestionFeatureProvider)) - ) - shared.setupDefaultValue( - for: \.promptToCodeCodeFontSize, - defaultValue: shared.value(for: \.suggestionCodeFontSize) - ) - shared.setupDefaultValue( - for: \.suggestionCodeFont, - defaultValue: .init(.init(nsFont: .monospacedSystemFont( - ofSize: shared.value(for: \.suggestionCodeFontSize), - weight: .regular - ))) - ) - shared.setupDefaultValue( - for: \.codeFontLight, - defaultValue: .init(.init(nsFont: .monospacedSystemFont( - ofSize: 12, - weight: .regular - ))) - ) - shared.setupDefaultValue( - for: \.codeFontDark, - defaultValue: .init(.init(nsFont: .monospacedSystemFont( - ofSize: 12, - weight: .regular - ))) - ) - shared.setupDefaultValue( - for: \.promptToCodeCodeFont, - defaultValue: .init(.init(nsFont: .monospacedSystemFont( - ofSize: shared.value(for: \.promptToCodeCodeFontSize), - weight: .regular - ))) - ) - shared.setupDefaultValue( - for: \.chatCodeFont, - defaultValue: .init(.init(nsFont: .monospacedSystemFont( - ofSize: shared.value(for: \.chatCodeFontSize), - weight: .regular - ))) - ) - } -} - -extension UserDefaults: UserDefaultsType {} - -public protocol UserDefaultsStorable {} - -extension Int: UserDefaultsStorable {} -extension Double: UserDefaultsStorable {} -extension Bool: UserDefaultsStorable {} -extension String: UserDefaultsStorable {} -extension Data: UserDefaultsStorable {} -extension URL: UserDefaultsStorable {} - -extension Array: RawRepresentable where Element: Codable { - public init?(rawValue: String) { - guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode([Element].self, from: data) - else { - return nil - } - self = result - } - - public var rawValue: String { - guard let data = try? JSONEncoder().encode(self), - let result = String(data: data, encoding: .utf8) - else { - return "[]" - } - return result - } -} - -public struct UserDefaultsStorageBox: RawRepresentable { - public let value: Element - - public init(_ value: Element) { - self.value = value - } - - public init?(rawValue: String) { - guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode(Element.self, from: data) - else { - return nil - } - value = result - } - - public var rawValue: String { - guard let data = try? JSONEncoder().encode(value), - let result = String(data: data, encoding: .utf8) - else { - return "" - } - return result - } -} - -extension UserDefaultsStorageBox: Equatable where Element: Equatable {} - -public extension UserDefaultsType { - // MARK: Normal Types - - func value( - for keyPath: KeyPath - ) -> K.Value where K.Value: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - return (value(forKey: key.key) as? K.Value) ?? key.defaultValue - } - - func set( - _ value: K.Value, - for keyPath: KeyPath - ) where K.Value: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - set(value, forKey: key.key) - } - - func setupDefaultValue( - for keyPath: KeyPath - ) where K.Value: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - if value(forKey: key.key) == nil { - set(key.defaultValue, forKey: key.key) - } - } - - func setupDefaultValue( - for keyPath: KeyPath, - defaultValue: K.Value - ) where K.Value: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - if value(forKey: key.key) == nil { - set(defaultValue, forKey: key.key) - } - } - - // MARK: Raw Representable - - func value( - for keyPath: KeyPath - ) -> K.Value where K.Value: RawRepresentable, K.Value.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? String else { - return key.defaultValue - } - return K.Value(rawValue: rawValue) ?? key.defaultValue - } - - func value( - for keyPath: KeyPath - ) -> K.Value where K.Value: RawRepresentable, K.Value.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? Int else { - return key.defaultValue - } - return K.Value(rawValue: rawValue) ?? key.defaultValue - } - - func value( - for keyPath: KeyPath - ) -> V where K.Value == UserDefaultsStorageBox { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? String else { - return key.defaultValue.value - } - return (K.Value(rawValue: rawValue) ?? key.defaultValue).value - } - - func set( - _ value: K.Value, - for keyPath: KeyPath - ) where K.Value: RawRepresentable, K.Value.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - set(value.rawValue, forKey: key.key) - } - - func set( - _ value: K.Value, - for keyPath: KeyPath - ) where K.Value: RawRepresentable, K.Value.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - set(value.rawValue, forKey: key.key) - } - - func set( - _ value: V, - for keyPath: KeyPath - ) where K.Value == UserDefaultsStorageBox { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - set(UserDefaultsStorageBox(value).rawValue, forKey: key.key) - } - - func setupDefaultValue( - for keyPath: KeyPath, - defaultValue: K.Value? = nil - ) where K.Value: RawRepresentable, K.Value.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - if value(forKey: key.key) == nil { - set(defaultValue?.rawValue ?? key.defaultValue.rawValue, forKey: key.key) - } - } - - func setupDefaultValue( - for keyPath: KeyPath, - defaultValue: K.Value? = nil - ) where K.Value: RawRepresentable, K.Value.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - if value(forKey: key.key) == nil { - set(defaultValue?.rawValue ?? key.defaultValue.rawValue, forKey: key.key) - } - } -} - -// MARK: - Deprecated Key Accessor - -public extension UserDefaultsType { - // MARK: Normal Types - - func deprecatedValue( - for keyPath: KeyPath> - ) -> K where K: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - return (value(forKey: key.key) as? K) ?? key.defaultValue - } - - // MARK: Raw Representable - - func deprecatedValue( - for keyPath: KeyPath> - ) -> K where K: RawRepresentable, K.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? String else { - return key.defaultValue - } - return K(rawValue: rawValue) ?? key.defaultValue - } - - func deprecatedValue( - for keyPath: KeyPath> - ) -> K where K: RawRepresentable, K.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? Int else { - return key.defaultValue - } - return K(rawValue: rawValue) ?? key.defaultValue - } -} - -public extension UserDefaultsType { - @available(*, deprecated, message: "This preference key is deprecated.") - func value( - for keyPath: KeyPath> - ) -> K where K: UserDefaultsStorable { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - return (value(forKey: key.key) as? K) ?? key.defaultValue - } - - @available(*, deprecated, message: "This preference key is deprecated.") - func value( - for keyPath: KeyPath> - ) -> K where K: RawRepresentable, K.RawValue == String { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? String else { - return key.defaultValue - } - return K(rawValue: rawValue) ?? key.defaultValue - } - - @available(*, deprecated, message: "This preference key is deprecated.") - func value( - for keyPath: KeyPath> - ) -> K where K: RawRepresentable, K.RawValue == Int { - let key = UserDefaultPreferenceKeys()[keyPath: keyPath] - guard let rawValue = value(forKey: key.key) as? Int else { - return key.defaultValue - } - return K(rawValue: rawValue) ?? key.defaultValue - } -} - diff --git a/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift b/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift deleted file mode 100644 index 250f198a..00000000 --- a/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift +++ /dev/null @@ -1,270 +0,0 @@ -import DebounceFunction -import Foundation -import Perception -import SwiftUI - -public struct AsyncCodeBlock: View { - private struct Constants { - static let paddingLeading = 5.0 - static let paddingBottom = 10.0 - static let paddingTrailing = 10.0 - } - - @Environment(\.colorScheme) var colorScheme - @Binding var isExpanded: Bool - @State private var isHovering: Bool = false - @AppStorage(\.completionHintShown) var completionHintShown - - let code: String - let language: String - let startLineIndex: Int - let scenario: String - let firstLineIndent: Double - let lineHeight: Double - let font: NSFont - let proposedForegroundColor: Color? - let proposedBackgroundColor: Color? - let currentLineBackgroundColor: Color? - let dimmedCharacterCount: Int - let droppingLeadingSpaces: Bool - let isPanelDisplayed: Bool - - public init( - code: String, - language: String, - startLineIndex: Int, - scenario: String, - firstLineIndent: Double, - lineHeight: Double, - font: NSFont, - droppingLeadingSpaces: Bool, - proposedForegroundColor: Color?, - proposedBackgroundColor: Color?, - currentLineBackgroundColor: Color?, - dimmedCharacterCount: Int, - isExpanded: Binding, - isPanelDisplayed: Bool - ) { - self.code = code - self.startLineIndex = startLineIndex - self.language = language - self.scenario = scenario - self.firstLineIndent = firstLineIndent - self.lineHeight = lineHeight - self.font = font - self.proposedForegroundColor = proposedForegroundColor - self.proposedBackgroundColor = proposedBackgroundColor - self.currentLineBackgroundColor = currentLineBackgroundColor - self.dimmedCharacterCount = dimmedCharacterCount - self.droppingLeadingSpaces = droppingLeadingSpaces - self._isExpanded = isExpanded - self.isPanelDisplayed = isPanelDisplayed - } - - var foregroundColor: Color { - if let proposedForegroundColor = proposedForegroundColor { - return proposedForegroundColor - } - return colorScheme == .light ? .black.opacity(0.85) : .white.opacity(0.85) - } - - var foregroundTextColor: Color { - return foregroundColor.opacity(0.6) - } - - var backgroundColor: Color { - if let proposedBackgroundColor = proposedBackgroundColor { - return proposedBackgroundColor - } - return colorScheme == .dark ? Color(red: 0.1216, green: 0.1216, blue: 0.1412) : .white - } - - var fontHeight: Double { - (font.ascender + abs(font.descender)).rounded(.down) - } - - var lineSpacing: Double { - lineHeight - fontHeight - } - - var expandedIndent: Double { - let lines = code.splitByNewLine() - guard let firstLine = lines.first else { return 0 } - let existing = String(firstLine.prefix(dimmedCharacterCount)) - let attr = NSAttributedString(string: existing, attributes: [.font: font]) - return firstLineIndent - attr.size().width - } - - var hintText: String { - if isExpanded { - return "Press ⌥⇥ to accept full suggestion" - } - return "Hold ⌥ for full suggestion" - } - - - @ScaledMetric var keyPadding: Double = 3.0 - - @ViewBuilder - func keyBackground(content: () -> some View) -> some View { - content() - .padding(.horizontal, keyPadding) - .background( - RoundedRectangle(cornerRadius: 2) - .stroke(foregroundColor, lineWidth: 1) - .foregroundColor(.clear) - .frame( - minWidth: fontHeight, - minHeight: fontHeight, - maxHeight: fontHeight - ) - ) - } - - @ViewBuilder - var optionKey: some View { - keyBackground { - Image(systemName: "option") - .resizable() - .renderingMode(.template) - .scaledToFit() - .frame(height: font.capHeight) - } - } - - @ViewBuilder - var popoverContent: some View { - HStack { - if isExpanded { - Text("Press") - optionKey - keyBackground { - Text("tab") - .font(.init(font)) - } - Text("to accept full suggestion") - } else { - Text("Hold") - optionKey - Text("for full suggestion") - } - } - .padding(8) - .font(.body) - .fixedSize() - } - - @ViewBuilder - func lineBackgroundShape(_ multiLine: Bool) -> some View { - let color = currentLineBackgroundColor ?? backgroundColor - switch multiLine { - case true: HalfCapsule().fill(color) - case false: Rectangle().fill(color) - } - } - - @ScaledMetric var iconPadding: CGFloat = 9.0 - @ScaledMetric var iconSpacing: CGFloat = 6.0 - @ScaledMetric var optionPadding: CGFloat = 0.5 - - @ViewBuilder - var contentView: some View { - let lines = code.splitByNewLine() - if let firstLine = lines.first { - let firstLineTrimmed = firstLine - .dropFirst(dimmedCharacterCount) - HStack() { - HStack(alignment: .center, spacing: 10) { - Text(firstLineTrimmed) - .foregroundColor(foregroundTextColor) - .lineSpacing(lineSpacing) // This only has effect if a line wraps - if lines.count > 1 { - HStack(spacing: iconSpacing) { - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFit() - Image(systemName: "option") - .resizable() - .renderingMode(.template) - .scaledToFit() - .padding(.vertical, optionPadding) - } - .frame(height: lineHeight * 0.7) - .padding(.horizontal, iconPadding) - .background( - Capsule() - .fill(foregroundColor.opacity(isExpanded ? 0.1 : 0.2)) - .frame(height: lineHeight) - ) - .frame(height: lineHeight) // Moves popover attachment - .popover(isPresented: $isHovering) { - popoverContent - } - .task { - isHovering = !completionHintShown - completionHintShown = true - } - } - } - .frame(height: lineHeight) - .background(lineBackgroundShape(lines.count > 1)) - .padding(.leading, firstLineIndent) - .onHover { hovering in - guard hovering != isHovering else { return } - withAnimation { - isHovering = hovering - } - } - Spacer() - } - } - - if isExpanded && lines.count > 1 { - HStack() { - CustomScrollView { - VStack(alignment: .leading, spacing: 0) { - ForEach(Array(lines.dropFirst()), id: \.self) { line in - HStack(alignment: .firstTextBaseline, spacing: 4) { - Text(line) - .foregroundColor(foregroundTextColor) - .lineSpacing(lineSpacing) - } - .frame(minHeight: lineHeight) - } - } - } - .padding(EdgeInsets( - top: 0, - leading: Constants.paddingLeading, - bottom: Constants.paddingBottom, - trailing: Constants.paddingTrailing - )) - .background(backgroundColor) - .cornerRadius(10) - .overlay(RoundedRectangle(cornerRadius: 10).stroke(foregroundColor.opacity(0.2), lineWidth: 1)) // border - .shadow(color: Color.black.opacity(0.2), radius: 8.0, x: 1, y: 1) - .onHover { hovering in - guard hovering != isHovering else { return } - withAnimation { - isHovering = hovering - } - } - Spacer() - } - .padding(.leading, expandedIndent - Constants.paddingLeading) - } - } - - public var body: some View { - if isPanelDisplayed { - WithPerceptionTracking { - VStack(spacing: 0) { - contentView - } - .font(.init(font)) - .background(Color.clear) - } - } - } -} diff --git a/Tool/Sources/SharedUIComponents/Base/Colors.swift b/Tool/Sources/SharedUIComponents/Base/Colors.swift deleted file mode 100644 index 2015102a..00000000 --- a/Tool/Sources/SharedUIComponents/Base/Colors.swift +++ /dev/null @@ -1,5 +0,0 @@ -import SwiftUI - -public extension Color { - static var hoverColor: Color { .gray.opacity(0.1) } -} diff --git a/Tool/Sources/SharedUIComponents/Base/FileIcon.swift b/Tool/Sources/SharedUIComponents/Base/FileIcon.swift deleted file mode 100644 index 039a4925..00000000 --- a/Tool/Sources/SharedUIComponents/Base/FileIcon.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import SwiftUI - - -public func drawFileIcon(_ file: URL?) -> Image { - let defaultImage = Image(systemName: "doc.text") - - guard let file = file else { return defaultImage } - - let fileExtension = file.pathExtension.lowercased() - if fileExtension == "swift" { - if let nsImage = NSImage(named: "SwiftIcon") { - return Image(nsImage: nsImage) - } - } - - return defaultImage -} diff --git a/Tool/Sources/SharedUIComponents/Base/HoverButtunStyle.swift b/Tool/Sources/SharedUIComponents/Base/HoverButtunStyle.swift deleted file mode 100644 index e58b5b56..00000000 --- a/Tool/Sources/SharedUIComponents/Base/HoverButtunStyle.swift +++ /dev/null @@ -1,30 +0,0 @@ -import SwiftUI - -// This is a custom button style that changes its background color when hovered -public struct HoverButtonStyle: ButtonStyle { - @State private var isHovered: Bool - private var padding: CGFloat - private var hoverColor: Color - - public init(isHovered: Bool = false, padding: CGFloat = 4, hoverColor: Color = .hoverColor) { - self.isHovered = isHovered - self.padding = padding - self.hoverColor = hoverColor - } - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(padding) - .background( - configuration.isPressed - ? Color.gray.opacity(0.2) - : isHovered - ? hoverColor - : Color.clear - ) - .cornerRadius(4) - .onHover { hover in - isHovered = hover - } - } -} diff --git a/Tool/Sources/SharedUIComponents/Base/HoverScrollView.swift b/Tool/Sources/SharedUIComponents/Base/HoverScrollView.swift deleted file mode 100644 index ec9ec307..00000000 --- a/Tool/Sources/SharedUIComponents/Base/HoverScrollView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -public struct HoverScrollView: View { - let content: Content - @State private var isHovered = false - - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - public var body: some View { - ScrollView(showsIndicators: isHovered) { - content - } - .onHover { hovering in - isHovered = hovering - } - } -} diff --git a/Tool/Sources/SharedUIComponents/CodeBlock.swift b/Tool/Sources/SharedUIComponents/CodeBlock.swift deleted file mode 100644 index 6fd852ed..00000000 --- a/Tool/Sources/SharedUIComponents/CodeBlock.swift +++ /dev/null @@ -1,129 +0,0 @@ -import Preferences -import SwiftUI - -public struct CodeBlock: View { - public let code: String - public let language: String - public let startLineIndex: Int - public let scenario: String - public let colorScheme: ColorScheme - public let commonPrecedingSpaceCount: Int - public let highlightedCode: [NSAttributedString] - public let firstLinePrecedingSpaceCount: Int - public let font: NSFont - public let droppingLeadingSpaces: Bool - public let proposedForegroundColor: Color? - public let wrapCode: Bool - - public init( - code: String, - language: String, - startLineIndex: Int, - scenario: String, - colorScheme: ColorScheme, - firstLinePrecedingSpaceCount: Int = 0, - font: NSFont, - droppingLeadingSpaces: Bool, - proposedForegroundColor: Color?, - wrapCode: Bool = true - ) { - self.code = code - self.language = language - self.startLineIndex = startLineIndex - self.scenario = scenario - self.colorScheme = colorScheme - self.droppingLeadingSpaces = droppingLeadingSpaces - self.firstLinePrecedingSpaceCount = firstLinePrecedingSpaceCount - self.font = font - self.proposedForegroundColor = proposedForegroundColor - self.wrapCode = wrapCode - let padding = firstLinePrecedingSpaceCount > 0 - ? String(repeating: " ", count: firstLinePrecedingSpaceCount) - : "" - let result = Self.highlight( - code: padding + code, - language: language, - scenario: scenario, - colorScheme: colorScheme, - font: font, - droppingLeadingSpaces: droppingLeadingSpaces - ) - commonPrecedingSpaceCount = result.commonLeadingSpaceCount - highlightedCode = result.code - } - - var foregroundColor: Color { - proposedForegroundColor ?? (colorScheme == .dark ? .white : .black) - } - - public var body: some View { - VStack(spacing: 2) { - ForEach(0.. 0 { - Text("\(commonPrecedingSpaceCount + 1)") - .padding(.top, -12) - .font(.footnote) - .foregroundStyle(foregroundColor) - .opacity(0.3) - } - } - } - } - } - .foregroundColor(.white) - .font(.init(font)) - .padding(.leading, 4) - .padding([.trailing, .top, .bottom]) - } - - static func highlight( - code: String, - language: String, - scenario: String, - colorScheme: ColorScheme, - font: NSFont, - droppingLeadingSpaces: Bool - ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { - return CodeHighlighting.highlighted( - code: code, - language: language, - scenario: scenario, - brightMode: colorScheme != .dark, - droppingLeadingSpaces: droppingLeadingSpaces, - font: font - ) - } -} - -// MARK: - Preview - -struct CodeBlock_Previews: PreviewProvider { - static var previews: some View { - CodeBlock( - code: """ - let foo = Foo() - let bar = Bar() - """, - language: "swift", - startLineIndex: 0, - scenario: "", - colorScheme: .dark, - firstLinePrecedingSpaceCount: 0, - font: .monospacedSystemFont(ofSize: 12, weight: .regular), - droppingLeadingSpaces: true, - proposedForegroundColor: nil - ) - } -} - diff --git a/Tool/Sources/SharedUIComponents/ConditionalFontWeight.swift b/Tool/Sources/SharedUIComponents/ConditionalFontWeight.swift deleted file mode 100644 index 55cc15c7..00000000 --- a/Tool/Sources/SharedUIComponents/ConditionalFontWeight.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -public struct ConditionalFontWeight: ViewModifier { - let weight: Font.Weight? - - public init(weight: Font.Weight?) { - self.weight = weight - } - - public func body(content: Content) -> some View { - if #available(macOS 13.0, *), weight != nil { - content.fontWeight(weight) - } else { - content - } - } -} - -public extension View { - func conditionalFontWeight(_ weight: Font.Weight?) -> some View { - self.modifier(ConditionalFontWeight(weight: weight)) - } -} diff --git a/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift b/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift deleted file mode 100644 index a077a320..00000000 --- a/Tool/Sources/SharedUIComponents/CopilotIntroSheet.swift +++ /dev/null @@ -1,141 +0,0 @@ -import SwiftUI -import AppKit - -struct CopilotIntroItem: View { - let heading: String - let text: String - let image: Image - - public init(imageName: String, heading: String, text: String) { - self.init(imageObject: Image(imageName), heading: heading, text: text) - } - - public init(systemImage: String, heading: String, text: String) { - self.init(imageObject: Image(systemName: systemImage), heading: heading, text: text) - } - - public init(imageObject: Image, heading: String, text: String) { - self.heading = heading - self.text = text - self.image = imageObject - } - - var body: some View { - HStack(spacing: 16) { - image - .resizable() - .renderingMode(.template) - .foregroundColor(.blue) - .scaledToFit() - .frame(width: 28, height: 28) - VStack(alignment: .leading, spacing: 5) { - Text(heading) - .font(.system(size: 11, weight: .bold)) - Text(text) - .font(.system(size: 11)) - .lineSpacing(3) - } - } - } -} - -struct CopilotIntroContent: View { - let hideIntro: Binding - let continueAction: () -> Void - - var body: some View { - VStack { - let appImage = if let nsImage = NSImage(named: "AppIcon") { - Image(nsImage: nsImage) - } else { - Image(systemName: "app") - } - appImage - .resizable() - .scaledToFit() - .frame(width: 64, height: 64) - .padding(.bottom, 24) - Text("Welcome to Copilot for Xcode!") - .font(.title.bold()) - .padding(.bottom, 38) - - VStack(alignment: .leading, spacing: 20) { - CopilotIntroItem( - imageName: "CopilotLogo", - heading: "In-line Code Suggestions", - text: "Receive context-aware code suggestions and text completion in your Xcode editor. Just press Tab ⇥ to accept a suggestion." - ) - - CopilotIntroItem( - systemImage: "option", - heading: "Full Suggestions", - text: "Press Option ⌥ for full multi-line suggestions. Only the first line is shown inline. Use Copilot Chat to refine, explain, or improve them." - ) - - CopilotIntroItem( - imageName: "ChatIcon", - heading: "Chat", - text: "Get real-time coding assistance, debug issues, and generate code snippets directly within Xcode." - ) - - CopilotIntroItem( - imageName: "GitHubMark", - heading: "GitHub Context", - text: "Copilot gives smarter code suggestions using your GitHub and project context. Use chat to discuss your code, debug issues, or get explanations." - ) - } - .padding(.bottom, 64) - - VStack(spacing: 8) { - Button(action: continueAction) { - Text("Continue") - .padding(.horizontal, 80) - .padding(.vertical, 6) - } - .buttonStyle(.borderedProminent) - - Toggle("Don't show again", isOn: hideIntro) - .toggleStyle(.checkbox) - } - } - .padding(.horizontal, 56) - .padding(.top, 48) - .padding(.bottom, 16) - .frame(width: 560) - } -} - -public struct CopilotIntroSheet: View { - let content: Content - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - @AppStorage(\.hideIntro) var hideIntro - @AppStorage(\.introLastShownVersion) var introLastShownVersion - @State var isPresented = false - - public var body: some View { - content.sheet(isPresented: $isPresented) { - CopilotIntroContent(hideIntro: $hideIntro) { - isPresented = false - } - } - .task { - if hideIntro == false { - isPresented = true - introLastShownVersion = appVersion - } - } - } -} - -public extension View { - func copilotIntroSheet() -> some View { - CopilotIntroSheet(content: self) - } -} - - -// MARK: - Preview -@available(macOS 14.0, *) -#Preview(traits: .sizeThatFitsLayout) { - CopilotIntroContent(hideIntro: .constant(false)) { } -} diff --git a/Tool/Sources/SharedUIComponents/CopilotMessageHeader.swift b/Tool/Sources/SharedUIComponents/CopilotMessageHeader.swift deleted file mode 100644 index 922ed55f..00000000 --- a/Tool/Sources/SharedUIComponents/CopilotMessageHeader.swift +++ /dev/null @@ -1,32 +0,0 @@ -import SwiftUI - -public struct CopilotMessageHeader: View { - let spacing: CGFloat - - public init(spacing: CGFloat = 4) { - self.spacing = spacing - } - - public var body: some View { - HStack(spacing: spacing) { - ZStack { - Circle() - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - .frame(width: 24, height: 24) - - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFit() - .frame(width: 12, height: 12) - } - - Text("GitHub Copilot") - .font(.system(size: 13)) - .fontWeight(.semibold) - .padding(.leading, 4) - - Spacer() - } - } -} diff --git a/Tool/Sources/SharedUIComponents/CopyButton.swift b/Tool/Sources/SharedUIComponents/CopyButton.swift deleted file mode 100644 index 0e79a0b4..00000000 --- a/Tool/Sources/SharedUIComponents/CopyButton.swift +++ /dev/null @@ -1,40 +0,0 @@ -import AppKit -import SwiftUI - -public struct CopyButton: View { - public var copy: () -> Void - @State var isCopied = false - private var foregroundColor: Color? - private var fontWeight: Font.Weight? - - public init(copy: @escaping () -> Void, foregroundColor: Color? = nil, fontWeight: Font.Weight? = nil) { - self.copy = copy - self.foregroundColor = foregroundColor - self.fontWeight = fontWeight - } - - public var body: some View { - Button(action: { - withAnimation(.linear(duration: 0.1)) { - isCopied = true - } - copy() - Task { - try await Task.sleep(nanoseconds: 1_000_000_000) - withAnimation(.linear(duration: 0.1)) { - isCopied = false - } - } - }) { - Image(systemName: isCopied ? "checkmark.circle" : "doc.on.doc") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - .foregroundColor(foregroundColor ?? .secondary) - .conditionalFontWeight(fontWeight) - .padding(4) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .help("Copy") - } -} diff --git a/Tool/Sources/SharedUIComponents/CustomScrollView.swift b/Tool/Sources/SharedUIComponents/CustomScrollView.swift deleted file mode 100644 index 0eb486f0..00000000 --- a/Tool/Sources/SharedUIComponents/CustomScrollView.swift +++ /dev/null @@ -1,66 +0,0 @@ -import AppKit -import Combine -import Preferences -import SwiftUI - -public struct CustomScrollViewHeightPreferenceKey: SwiftUI.PreferenceKey { - public static var defaultValue: Double = 0 - public static func reduce(value: inout Double, nextValue: () -> Double) { - value = nextValue() + value - } -} - -public struct CustomScrollViewUpdateHeightModifier: ViewModifier { - public func body(content: Content) -> some View { - content - .background { - GeometryReader { proxy in - Color.clear - .preference( - key: CustomScrollViewHeightPreferenceKey.self, - value: proxy.size.height - ) - } - } - } -} - -/// Used to workaround a SwiftUI bug. https://github.com/intitni/CopilotForXcode/issues/122 -public struct CustomScrollView: View { - @ViewBuilder var content: () -> Content - @State var height: Double = 10 - @AppStorage(\.useCustomScrollViewWorkaround) var useNSScrollViewWrapper - - public init(content: @escaping () -> Content) { - self.content = content - } - - public var body: some View { - if useNSScrollViewWrapper { - List { - content() - .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) - .modifier(CustomScrollViewUpdateHeightModifier()) - } - .listStyle(.plain) - .modify { view in - if #available(macOS 13.0, *) { - view.listRowSeparator(.hidden).listSectionSeparator(.hidden) - } else { - view - } - } - .frame(idealHeight: max(10, height)) - .onPreferenceChange(CustomScrollViewHeightPreferenceKey.self) { newHeight in - Task { @MainActor in - height = newHeight - } - } - } else { - ScrollView { - content() - } - } - } -} - diff --git a/Tool/Sources/SharedUIComponents/CustomTextEditor.swift b/Tool/Sources/SharedUIComponents/CustomTextEditor.swift deleted file mode 100644 index e1ba7578..00000000 --- a/Tool/Sources/SharedUIComponents/CustomTextEditor.swift +++ /dev/null @@ -1,196 +0,0 @@ -import SwiftUI - -public struct AutoresizingCustomTextEditor: View { - @Binding public var text: String - public let font: NSFont - public let isEditable: Bool - public let maxHeight: Double - public let minHeight: Double - public let onSubmit: () -> Void - - @State private var textEditorHeight: CGFloat - - public init( - text: Binding, - font: NSFont, - isEditable: Bool, - maxHeight: Double, - onSubmit: @escaping () -> Void - ) { - _text = text - self.font = font - self.isEditable = isEditable - self.maxHeight = maxHeight - self.minHeight = Double(font.ascender + abs(font.descender) + font.leading) // Following the original padding: .top(1), .bottom(2) - self.onSubmit = onSubmit - - // Initialize with font height + 3 as in the original logic - _textEditorHeight = State(initialValue: self.minHeight) - } - - public var body: some View { - CustomTextEditor( - text: $text, - font: font, - isEditable: isEditable, - maxHeight: maxHeight, - minHeight: minHeight, - onSubmit: onSubmit, - heightDidChange: { height in - self.textEditorHeight = min(height, maxHeight) - } - ) - .frame(height: textEditorHeight) - .padding(.top, 1) - .padding(.bottom, -1) - } -} - -public struct CustomTextEditor: NSViewRepresentable { - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - @Binding public var text: String - public let font: NSFont - public let maxHeight: Double - public let minHeight: Double - public let isEditable: Bool - public let onSubmit: () -> Void - public let heightDidChange: (CGFloat) -> Void - - public init( - text: Binding, - font: NSFont, - isEditable: Bool = true, - maxHeight: Double, - minHeight: Double, - onSubmit: @escaping () -> Void, - heightDidChange: @escaping (CGFloat) -> Void - ) { - _text = text - self.font = font - self.isEditable = isEditable - self.maxHeight = maxHeight - self.minHeight = minHeight - self.onSubmit = onSubmit - self.heightDidChange = heightDidChange - } - - public func makeNSView(context: Context) -> NSScrollView { - let textView = (context.coordinator.theTextView.documentView as! NSTextView) - textView.delegate = context.coordinator - textView.string = text - textView.font = font - textView.allowsUndo = true - textView.drawsBackground = false - textView.isAutomaticQuoteSubstitutionEnabled = false - textView.isAutomaticDashSubstitutionEnabled = false - textView.isAutomaticTextReplacementEnabled = false - textView.setAccessibilityLabel("Chat Input, Ask Copilot. Type to ask questions or type / for topics, press enter to send out the request. Use the Chat Accessibility Help command for more information.") - - // Set up text container for dynamic height - textView.isVerticallyResizable = true - textView.isHorizontallyResizable = false - textView.textContainer?.containerSize = NSSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude) - textView.textContainer?.widthTracksTextView = true - - // Configure scroll view - let scrollView = context.coordinator.theTextView - scrollView.hasHorizontalScroller = false - scrollView.hasVerticalScroller = false // We'll manage the scrolling ourselves - - // Initialize height calculation - context.coordinator.view = self - context.coordinator.calculateAndUpdateHeight(textView: textView) - - return scrollView - } - - public func updateNSView(_ nsView: NSScrollView, context: Context) { - let textView = (context.coordinator.theTextView.documentView as! NSTextView) - textView.isEditable = isEditable - guard textView.string != text else { return } - textView.string = text - textView.undoManager?.removeAllActions() - - // Update height calculation when text changes - context.coordinator.calculateAndUpdateHeight(textView: textView) - } -} - -public extension CustomTextEditor { - class Coordinator: NSObject, NSTextViewDelegate { - var view: CustomTextEditor - var theTextView = NSTextView.scrollableTextView() - var affectedCharRange: NSRange? - - init(_ view: CustomTextEditor) { - self.view = view - } - - func calculateAndUpdateHeight(textView: NSTextView) { - guard let layoutManager = textView.layoutManager, - let textContainer = textView.textContainer else { - return - } - - let usedRect = layoutManager.usedRect(for: textContainer) - - // Add padding for text insets if needed - let textInsets = textView.textContainerInset - let newHeight = max(view.minHeight, usedRect.height + textInsets.height * 2) - - // Update scroll behavior based on height vs maxHeight - theTextView.hasVerticalScroller = newHeight >= view.maxHeight - - // Only report the height that will be used for display - let heightToReport = min(newHeight, view.maxHeight) - - // Inform the SwiftUI view of the height - DispatchQueue.main.async { - self.view.heightDidChange(heightToReport) - } - } - - public func textDidChange(_ notification: Notification) { - guard let textView = notification.object as? NSTextView else { - return - } - - // Defer updating the binding for large text changes - DispatchQueue.main.async { - self.view.text = textView.string - } - - // Update height after text changes - calculateAndUpdateHeight(textView: textView) - } - - public func textView( - _ textView: NSTextView, - doCommandBy commandSelector: Selector - ) -> Bool { - if commandSelector == #selector(NSTextView.insertNewline(_:)) { - if let event = NSApplication.shared.currentEvent, - !event.modifierFlags.contains(.shift), - event.keyCode == 36 // enter - { - view.onSubmit() - return true - } - } - - return false - } - - public func textView( - _ textView: NSTextView, - shouldChangeTextIn affectedCharRange: NSRange, - replacementString: String? - ) -> Bool { - return true - } - } -} - diff --git a/Tool/Sources/SharedUIComponents/DownvoteButton.swift b/Tool/Sources/SharedUIComponents/DownvoteButton.swift deleted file mode 100644 index 952aadbc..00000000 --- a/Tool/Sources/SharedUIComponents/DownvoteButton.swift +++ /dev/null @@ -1,33 +0,0 @@ -import AppKit -import SwiftUI -import ConversationServiceProvider - -public struct DownvoteButton: View { - public var downvote: (ConversationRating) -> Void - @State var isSelected = false - - public init(downvote: @escaping (ConversationRating) -> Void) { - self.downvote = downvote - } - - public var body: some View { - Button(action: { - isSelected = !isSelected - isSelected ? downvote(.unhelpful) : downvote(.unrated) - }) { - Image(systemName: isSelected ? "hand.thumbsdown.fill" : "hand.thumbsdown") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) -// .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.secondary) -// .background( -// .regularMaterial, -// in: RoundedRectangle(cornerRadius: 4, style: .circular) -// ) - .padding(4) - .help("Unhelpful") - } - .buttonStyle(HoverButtonStyle(padding: 0)) - } -} diff --git a/Tool/Sources/SharedUIComponents/DynamicHeightTextInFormWorkaround.swift b/Tool/Sources/SharedUIComponents/DynamicHeightTextInFormWorkaround.swift deleted file mode 100644 index 4a5efd31..00000000 --- a/Tool/Sources/SharedUIComponents/DynamicHeightTextInFormWorkaround.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -struct DynamicHeightTextInFormWorkaroundModifier: ViewModifier { - func body(content: Content) -> some View { - HStack(spacing: 0) { - content - Spacer() - } - .fixedSize(horizontal: false, vertical: true) - } -} - -public extension View { - func dynamicHeightTextInFormWorkaround() -> some View { - modifier(DynamicHeightTextInFormWorkaroundModifier()) - } -} diff --git a/Tool/Sources/SharedUIComponents/FontPicker.swift b/Tool/Sources/SharedUIComponents/FontPicker.swift deleted file mode 100644 index 2f91c9d0..00000000 --- a/Tool/Sources/SharedUIComponents/FontPicker.swift +++ /dev/null @@ -1,89 +0,0 @@ -import AppKit -import Foundation -import Preferences -import SwiftUI - -public struct FontPicker: View { - @State var fontManagerDelegate: FontManagerDelegate? - @Binding var font: NSFont - let label: Label - - public init(font: Binding, @ViewBuilder label: () -> Label) { - _font = font - self.label = label() - } - - public var body: some View { - if #available(macOS 13.0, *) { - LabeledContent { - button - } label: { - label - } - } else { - HStack { - label - button - } - } - } - - var button: some View { - Button { - if NSFontPanel.shared.isVisible { - NSFontPanel.shared.orderOut(nil) - } - - self.fontManagerDelegate = FontManagerDelegate(font: font) { - self.font = $0 - } - NSFontManager.shared.target = self.fontManagerDelegate - NSFontPanel.shared.setPanelFont(self.font, isMultiple: false) - NSFontPanel.shared.orderBack(nil) - } label: { - HStack { - Text(font.fontName) - + Text(" - ") - + Text(font.pointSize, format: .number.precision(.fractionLength(1))) - + Text("pt") - - Spacer().frame(width: 30) - - Image(systemName: "textformat") - .frame(width: 13) - .scaledToFit() - } - } - } - - final class FontManagerDelegate: NSObject { - let font: NSFont - let onSelection: (NSFont) -> Void - init(font: NSFont, onSelection: @escaping (NSFont) -> Void) { - self.font = font - self.onSelection = onSelection - } - - @objc func changeFont(_ sender: NSFontManager) { - onSelection(sender.convert(font)) - } - } -} - -public extension FontPicker { - init(font: Binding>, @ViewBuilder label: () -> Label) { - _font = Binding( - get: { font.wrappedValue.value.nsFont }, - set: { font.wrappedValue = .init(StorableFont(nsFont: $0)) } - ) - self.label = label() - } -} - -#Preview { - FontPicker(font: .constant(.systemFont(ofSize: 15))) { - Text("Font") - } - .padding() -} - diff --git a/Tool/Sources/SharedUIComponents/HalfCapsule.swift b/Tool/Sources/SharedUIComponents/HalfCapsule.swift deleted file mode 100644 index 68e9d0d5..00000000 --- a/Tool/Sources/SharedUIComponents/HalfCapsule.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -public struct HalfCapsule: Shape { - public func path(in rect: CGRect) -> Path { - Path { path in - path.move(to: .init(x:0, y: 0)) - path.addLine(to: .init(x:rect.width, y:0)) - path.addArc( - center: .init(x: rect.width - rect.height/2, y: rect.height/2), - radius: rect.height/2, - startAngle: .degrees(270), - endAngle: .degrees(90), - clockwise: false - ) - path.addLine(to: CGPoint(x:0, y:rect.height)) - path.addLine(to: CGPoint(x:0, y:rect.height)) - } - } -} diff --git a/Tool/Sources/SharedUIComponents/InsertButton.swift b/Tool/Sources/SharedUIComponents/InsertButton.swift deleted file mode 100644 index 355d8982..00000000 --- a/Tool/Sources/SharedUIComponents/InsertButton.swift +++ /dev/null @@ -1,35 +0,0 @@ -import SwiftUI - -public struct InsertButton: View { - public var insert: () -> Void - - @Environment(\.colorScheme) var colorScheme - - private var icon: Image { - return Image("CodeBlockInsertIcon") - } - - public init(insert: @escaping () -> Void) { - self.insert = insert - } - - public var body: some View { - Button(action: { - insert() - }) { - self.icon - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) -// .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.secondary) -// .background( -// .regularMaterial, -// in: RoundedRectangle(cornerRadius: 4, style: .circular) -// ) - .padding(4) - } - .buttonStyle(HoverButtonStyle(padding: 0)) - .help("Insert at Cursor") - } -} diff --git a/Tool/Sources/SharedUIComponents/InstructionView.swift b/Tool/Sources/SharedUIComponents/InstructionView.swift deleted file mode 100644 index 774ea7c8..00000000 --- a/Tool/Sources/SharedUIComponents/InstructionView.swift +++ /dev/null @@ -1,64 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -public struct Instruction: View { - @Binding var isAgentMode: Bool - - public init(isAgentMode: Binding) { - self._isAgentMode = isAgentMode - } - - public var body: some View { - WithPerceptionTracking { - VStack { - VStack(spacing: 24) { - - VStack(spacing: 16) { - Image("CopilotLogo") - .resizable() - .renderingMode(.template) - .scaledToFill() - .frame(width: 60.0, height: 60.0) - .foregroundColor(.secondary) - - if isAgentMode { - Text("Copilot Agent Mode") - .font(.title) - .foregroundColor(.primary) - - Text("Ask Copilot to edit your files in agent mode.\nIt will automatically use multiple requests to \nedit files, run terminal commands, and fix errors.") - .font(.system(size: 14, weight: .light)) - .multilineTextAlignment(.center) - .lineSpacing(4) - } - - Text("Copilot is powered by AI, so mistakes are possible. Review output carefully before use.") - .font(.system(size: 14, weight: .light)) - .multilineTextAlignment(.center) - .lineSpacing(4) - } - - VStack(alignment: .leading, spacing: 8) { - if isAgentMode { - Label("to configure MCP server", systemImage: "wrench.and.screwdriver") - .foregroundColor(Color("DescriptionForegroundColor")) - .font(.system(size: 14)) - } - Label("to reference context", systemImage: "paperclip") - .foregroundColor(Color("DescriptionForegroundColor")) - .font(.system(size: 14)) - if !isAgentMode { - Text("@ to chat with extensions") - .foregroundColor(Color("DescriptionForegroundColor")) - .font(.system(size: 14)) - Text("Type / to use commands") - .foregroundColor(Color("DescriptionForegroundColor")) - .font(.system(size: 14)) - } - } - } - }.frame(maxWidth: 350) - } - } -} - diff --git a/Tool/Sources/SharedUIComponents/SettingsDivider.swift b/Tool/Sources/SharedUIComponents/SettingsDivider.swift deleted file mode 100644 index 15db820d..00000000 --- a/Tool/Sources/SharedUIComponents/SettingsDivider.swift +++ /dev/null @@ -1,42 +0,0 @@ -import SwiftUI - -public struct SettingsDivider: View { - let title: Title? - - public init(_ title: Title) { - self.title = title - } - - public var body: some View { - if let title { - HStack { - VStack { - Divider() - } - title - .foregroundStyle(.secondary) - .font(.subheadline) - .zIndex(2) - VStack { - Divider() - } - } - .padding(.vertical, 8) - } else { - Divider() - .padding(.vertical, 8) - } - } -} - -extension SettingsDivider where Title == Text { - public init(_ title: String) { - self.title = Text(title) - } -} - -extension SettingsDivider where Title == EmptyView { - public init() { - self.title = nil - } -} diff --git a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift deleted file mode 100644 index 1de8b686..00000000 --- a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift +++ /dev/null @@ -1,160 +0,0 @@ -import AppKit -import Foundation -import Highlightr -import SuggestionBasic -import SwiftUI - -public enum CodeHighlighting { - public static func highlightedCodeBlock( - code: String, - language: String, - scenario: String, - brightMode: Bool, - font: NSFont - ) -> NSAttributedString { - var language = language - // Workaround: Highlightr uses a different identifier for Objective-C. - if language.lowercased().hasPrefix("objective"), language.lowercased().hasSuffix("c") { - language = "objectivec" - } - func unhighlightedCode() -> NSAttributedString { - return NSAttributedString( - string: code, - attributes: [ - .foregroundColor: brightMode ? NSColor.black : NSColor.white, - .font: font, - ] - ) - } - guard let highlighter = Highlightr() else { - return unhighlightedCode() - } - highlighter.setTheme(to: { - let mode = brightMode ? "light" : "dark" - if scenario.isEmpty { - return mode - } - return "\(scenario)-\(mode)" - }()) - highlighter.theme.setCodeFont(font) - guard let formatted = highlighter.highlight(code, as: language) else { - return unhighlightedCode() - } - if formatted.string == "undefined" { - return unhighlightedCode() - } - return formatted - } - - public static func highlighted( - code: String, - language: String, - scenario: String, - brightMode: Bool, - droppingLeadingSpaces: Bool, - font: NSFont, - replaceSpacesWithMiddleDots: Bool = false - ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { - let formatted = highlightedCodeBlock( - code: code, - language: language, - scenario: scenario, - brightMode: brightMode, - font: font - ) - let middleDotColor = brightMode - ? NSColor.black.withAlphaComponent(0.1) - : NSColor.white.withAlphaComponent(0.1) - return convertToCodeLines( - formatted, - middleDotColor: middleDotColor, - droppingLeadingSpaces: droppingLeadingSpaces, - replaceSpacesWithMiddleDots: replaceSpacesWithMiddleDots - ) - } - - public static func convertToCodeLines( - _ formattedCode: NSAttributedString, - middleDotColor: NSColor, - droppingLeadingSpaces: Bool, - replaceSpacesWithMiddleDots: Bool = false - ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { - let input = formattedCode.string - func isEmptyLine(_ line: String) -> Bool { - if line.isEmpty { return true } - guard let regex = try? NSRegularExpression(pattern: #"^\s*\n?$"#) else { return false } - let ns = NSString(string: line) - if regex.firstMatch( - in: line, - options: [], - range: NSMakeRange(0, ns.length) - ) != nil { - return true - } - return false - } - - let separatedInput = input.splitByNewLine(omittingEmptySubsequences: false) - .map { String($0) } - let commonLeadingSpaceCount = { - if !droppingLeadingSpaces { return 0 } - let split = separatedInput - var result = 0 - outerLoop: for i in stride(from: 40, through: 4, by: -4) { - for line in split { - if isEmptyLine(line) { continue } - if i >= line.count { continue outerLoop } - if !line.hasPrefix(.init(repeating: " ", count: i)) { continue outerLoop } - } - result = i - break - } - return result - }() - var output = [NSAttributedString]() - var start = 0 - for sub in separatedInput { - let range = NSMakeRange(start, sub.utf16.count) - let attributedString = formattedCode.attributedSubstring(from: range) - let mutable = NSMutableAttributedString(attributedString: attributedString) - - // remove leading spaces - if commonLeadingSpaceCount > 0 { - let leadingSpaces = String(repeating: " ", count: commonLeadingSpaceCount) - if mutable.string.hasPrefix(leadingSpaces) { - mutable.replaceCharacters( - in: NSRange(location: 0, length: commonLeadingSpaceCount), - with: "" - ) - } else if isEmptyLine(mutable.string) { - mutable.mutableString.setString("") - } - } - - if replaceSpacesWithMiddleDots { - // use regex to replace all spaces to a middle dot - do { - let regex = try NSRegularExpression(pattern: "[ ]*", options: []) - let result = regex.matches( - in: mutable.string, - range: NSRange(location: 0, length: mutable.mutableString.length) - ) - for r in result { - let range = r.range - mutable.replaceCharacters( - in: range, - with: String(repeating: "·", count: range.length) - ) - mutable.addAttributes([ - .foregroundColor: middleDotColor, - ], range: range) - } - } catch {} - } - output.append(mutable) - start += range.length + 1 - } - return (output, commonLeadingSpaceCount) - } -} - diff --git a/Tool/Sources/SharedUIComponents/UpvoteButton.swift b/Tool/Sources/SharedUIComponents/UpvoteButton.swift deleted file mode 100644 index b4e13e2a..00000000 --- a/Tool/Sources/SharedUIComponents/UpvoteButton.swift +++ /dev/null @@ -1,33 +0,0 @@ -import AppKit -import SwiftUI -import ConversationServiceProvider - -public struct UpvoteButton: View { - public var upvote: (ConversationRating) -> Void - @State var isSelected = false - - public init(upvote: @escaping (ConversationRating) -> Void) { - self.upvote = upvote - } - - public var body: some View { - Button(action: { - isSelected = !isSelected - isSelected ? upvote(.helpful) : upvote(.unrated) - }) { - Image(systemName: isSelected ? "hand.thumbsup.fill" : "hand.thumbsup") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) -// .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.secondary) -// .background( -// .regularMaterial, -// in: RoundedRectangle(cornerRadius: 4, style: .circular) -// ) - .padding(4) - .help("Helpful") - } - .buttonStyle(HoverButtonStyle(padding: 0)) - } -} diff --git a/Tool/Sources/SharedUIComponents/View+Modify.swift b/Tool/Sources/SharedUIComponents/View+Modify.swift deleted file mode 100644 index 59820772..00000000 --- a/Tool/Sources/SharedUIComponents/View+Modify.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -public extension View { - @ViewBuilder func modify(@ViewBuilder transform: (Self) -> Content) - -> some View - { - transform(self) - } -} - diff --git a/Tool/Sources/Status/Status.swift b/Tool/Sources/Status/Status.swift deleted file mode 100644 index 62176c94..00000000 --- a/Tool/Sources/Status/Status.swift +++ /dev/null @@ -1,212 +0,0 @@ -import AppKit -import Foundation - -@objc public enum ExtensionPermissionStatus: Int { - case unknown = -1, notGranted = 0, disabled = 1, granted = 2 -} - -@objc public enum ObservedAXStatus: Int { - case unknown = -1, granted = 1, notGranted = 0 -} - -private struct AuthStatusInfo { - let authIcon: StatusResponse.Icon? - let authStatus: AuthStatus.Status - let userName: String? -} - -private struct CLSStatusInfo { - let icon: StatusResponse.Icon? - let message: String -} - -private struct AccessibilityStatusInfo { - let icon: StatusResponse.Icon? - let message: String? - let url: String? -} - -public extension Notification.Name { - static let authStatusDidChange = Notification.Name("com.github.CopilotForXcode.authStatusDidChange") - static let serviceStatusDidChange = Notification.Name("com.github.CopilotForXcode.serviceStatusDidChange") -} - -private var currentUserName: String? = nil -private var currentUserCopilotPlan: String? = nil - -public final actor Status { - public static let shared = Status() - - private var extensionStatus: ExtensionPermissionStatus = .unknown - private var axStatus: ObservedAXStatus = .unknown - private var clsStatus = CLSStatus(status: .unknown, busy: false, message: "") - private var authStatus = AuthStatus(status: .unknown, username: nil, message: nil) - - private var currentUserQuotaInfo: GitHubCopilotQuotaInfo? = nil - - private let okIcon = StatusResponse.Icon(name: "MenuBarIcon") - private let errorIcon = StatusResponse.Icon(name: "MenuBarErrorIcon") - private let warningIcon = StatusResponse.Icon(name: "MenuBarWarningIcon") - private let inactiveIcon = StatusResponse.Icon(name: "MenuBarInactiveIcon") - - private init() {} - - public static func currentUser() -> String? { - return currentUserName - } - - public func currentUserPlan() -> String? { - return currentUserCopilotPlan - } - - public func updateQuotaInfo(_ quotaInfo: GitHubCopilotQuotaInfo?) { - guard quotaInfo != currentUserQuotaInfo else { return } - currentUserQuotaInfo = quotaInfo - currentUserCopilotPlan = quotaInfo?.copilotPlan - broadcast() - } - - public func updateExtensionStatus(_ status: ExtensionPermissionStatus) { - guard status != extensionStatus else { return } - extensionStatus = status - broadcast() - } - - public func updateAXStatus(_ status: ObservedAXStatus) { - guard status != axStatus else { return } - axStatus = status - broadcast() - } - - public func updateCLSStatus(_ status: CLSStatus.Status, busy: Bool, message: String) { - let newStatus = CLSStatus(status: status, busy: busy, message: message) - guard newStatus != clsStatus else { return } - clsStatus = newStatus - broadcast() - } - - public func updateAuthStatus(_ status: AuthStatus.Status, username: String? = nil, message: String? = nil) { - currentUserName = username - let newStatus = AuthStatus(status: status, username: username, message: message) - guard newStatus != authStatus else { return } - authStatus = newStatus - broadcast() - } - - public func getExtensionStatus() -> ExtensionPermissionStatus { - extensionStatus - } - - public func getAXStatus() -> ObservedAXStatus { - if isXcodeRunning() { - return axStatus - } else if AXIsProcessTrusted() { - return .granted - } else { - return axStatus - } - } - - private func isXcodeRunning() -> Bool { - !NSRunningApplication.runningApplications( - withBundleIdentifier: "com.apple.dt.Xcode" - ).isEmpty - } - - public func getAuthStatus() -> AuthStatus { - authStatus - } - - public func getCLSStatus() -> CLSStatus { - clsStatus - } - - public func getQuotaInfo() -> GitHubCopilotQuotaInfo? { - currentUserQuotaInfo - } - - public func getStatus() -> StatusResponse { - let authStatusInfo: AuthStatusInfo = getAuthStatusInfo() - let clsStatusInfo: CLSStatusInfo = getCLSStatusInfo() - let extensionStatusIcon = ( - extensionStatus == ExtensionPermissionStatus.disabled || extensionStatus == ExtensionPermissionStatus.notGranted - ) ? errorIcon : nil - let accessibilityStatusInfo: AccessibilityStatusInfo = getAccessibilityStatusInfo() - return .init( - icon: authStatusInfo.authIcon ?? clsStatusInfo.icon ?? extensionStatusIcon ?? accessibilityStatusInfo.icon ?? okIcon, - inProgress: clsStatus.busy, - clsMessage: clsStatus.message, - message: accessibilityStatusInfo.message, - extensionStatus: extensionStatus, - url: accessibilityStatusInfo.url, - authStatus: authStatusInfo.authStatus, - userName: authStatusInfo.userName, - quotaInfo: currentUserQuotaInfo - ) - } - - private func getAuthStatusInfo() -> AuthStatusInfo { - switch authStatus.status { - case .unknown, .loggedIn: - return AuthStatusInfo( - authIcon: nil, - authStatus: authStatus.status, - userName: authStatus.username - ) - case .notLoggedIn: - return AuthStatusInfo( - authIcon: errorIcon, - authStatus: authStatus.status, - userName: nil - ) - case .notAuthorized: - return AuthStatusInfo( - authIcon: inactiveIcon, - authStatus: authStatus.status, - userName: authStatus.username - ) - } - } - - private func getCLSStatusInfo() -> CLSStatusInfo { - if clsStatus.isInactiveStatus { - return CLSStatusInfo(icon: inactiveIcon, message: clsStatus.message) - } - if clsStatus.isWarningStatus { - return CLSStatusInfo(icon: warningIcon, message: clsStatus.message) - } - if clsStatus.isErrorStatus { - return CLSStatusInfo(icon: errorIcon, message: clsStatus.message) - } - return CLSStatusInfo(icon: nil, message: "") - } - - private func getAccessibilityStatusInfo() -> AccessibilityStatusInfo { - switch getAXStatus() { - case .granted: - return AccessibilityStatusInfo(icon: nil, message: nil, url: nil) - case .notGranted: - return AccessibilityStatusInfo( - icon: errorIcon, - message: """ - Enable accessibility in system preferences - """, - url: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - ) - case .unknown: - return AccessibilityStatusInfo( - icon: errorIcon, - message: """ - Enable accessibility or restart Copilot - """, - url: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" - ) - } - } - - private func broadcast() { - NotificationCenter.default.post(name: .serviceStatusDidChange, object: nil) - // Can remove DistributedNotificationCenter if the settings UI moves in-process - DistributedNotificationCenter.default().post(name: .serviceStatusDidChange, object: nil) - } -} diff --git a/Tool/Sources/Status/StatusObserver.swift b/Tool/Sources/Status/StatusObserver.swift deleted file mode 100644 index e19e3f70..00000000 --- a/Tool/Sources/Status/StatusObserver.swift +++ /dev/null @@ -1,134 +0,0 @@ -import SwiftUI -import Cache - -@MainActor -public class StatusObserver: ObservableObject { - @Published public private(set) var authStatus = AuthStatus(status: .unknown, username: nil, message: nil) - @Published public private(set) var clsStatus = CLSStatus(status: .unknown, busy: false, message: "") - @Published public private(set) var observedAXStatus = ObservedAXStatus.unknown - @Published public private(set) var quotaInfo: GitHubCopilotQuotaInfo? = nil - - public static let shared = StatusObserver() - - private init() { - Task { @MainActor in - await observeAuthStatus() - await observeCLSStatus() - await observeAXStatus() - await observeQuotaInfo() - } - } - - private func observeAuthStatus() async { - await updateAuthStatus() - setupAuthStatusNotificationObserver() - } - - private func observeCLSStatus() async { - await updateCLSStatus() - setupCLSStatusNotificationObserver() - } - - private func observeAXStatus() async { - await updateAXStatus() - setupAXStatusNotificationObserver() - } - - private func observeQuotaInfo() async { - await updateQuotaInfo() - setupQuotaInfoNotificationObserver() - } - - private func updateAuthStatus() async { - let authStatus = await Status.shared.getAuthStatus() - let statusInfo = await Status.shared.getStatus() - - if authStatus.status == .notLoggedIn { - await Status.shared.updateQuotaInfo(nil) - } - - self.authStatus = AuthStatus( - status: authStatus.status, - username: statusInfo.userName, - message: nil - ) - - // load avatar when auth status changed - AvatarViewModel.shared.loadAvatar(forUser: self.authStatus.username) - } - - private func updateCLSStatus() async { - self.clsStatus = await Status.shared.getCLSStatus() - } - - private func updateAXStatus() async { - self.observedAXStatus = await Status.shared.getAXStatus() - } - - private func updateQuotaInfo() async { - self.quotaInfo = await Status.shared.getQuotaInfo() - } - - private func setupAuthStatusNotificationObserver() { - NotificationCenter.default.addObserver( - forName: .serviceStatusDidChange, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor [self] in - await self.updateAuthStatus() - } - } - - DistributedNotificationCenter.default().addObserver( - forName: .authStatusDidChange, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor [self] in - await self.updateAuthStatus() - } - } - } - - private func setupCLSStatusNotificationObserver() { - NotificationCenter.default.addObserver( - forName: .serviceStatusDidChange, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor [self] in - await self.updateCLSStatus() - } - } - } - - private func setupAXStatusNotificationObserver() { - NotificationCenter.default.addObserver( - forName: .serviceStatusDidChange, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor [self] in - await self.updateAXStatus() - } - } - } - - private func setupQuotaInfoNotificationObserver() { - NotificationCenter.default.addObserver( - forName: .serviceStatusDidChange, - object: nil, - queue: .main - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor [self] in - await self.updateQuotaInfo() - } - } - } -} diff --git a/Tool/Sources/Status/Types/AuthStatus.swift b/Tool/Sources/Status/Types/AuthStatus.swift deleted file mode 100644 index 668b4a11..00000000 --- a/Tool/Sources/Status/Types/AuthStatus.swift +++ /dev/null @@ -1,17 +0,0 @@ -public struct AuthStatus: Codable, Equatable, Hashable { - public enum Status: Codable, Equatable, Hashable { - case unknown - case loggedIn - case notLoggedIn - case notAuthorized - } - public let status: Status - public let username: String? - public let message: String? - - public init(status: Status, username: String? = nil, message: String? = nil) { - self.status = status - self.username = username - self.message = message - } -} diff --git a/Tool/Sources/Status/Types/CLSStatus.swift b/Tool/Sources/Status/Types/CLSStatus.swift deleted file mode 100644 index 07b5d765..00000000 --- a/Tool/Sources/Status/Types/CLSStatus.swift +++ /dev/null @@ -1,10 +0,0 @@ -public struct CLSStatus: Equatable { - public enum Status { case unknown, normal, error, warning, inactive } - public let status: Status - public let busy: Bool - public let message: String - - public var isInactiveStatus: Bool { status == .inactive && !message.isEmpty } - public var isErrorStatus: Bool { status == .error && !message.isEmpty } - public var isWarningStatus: Bool { status == .warning && !message.isEmpty } -} diff --git a/Tool/Sources/Status/Types/GitHubCopilotQuotaInfo.swift b/Tool/Sources/Status/Types/GitHubCopilotQuotaInfo.swift deleted file mode 100644 index 50ffc4f3..00000000 --- a/Tool/Sources/Status/Types/GitHubCopilotQuotaInfo.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -public struct QuotaSnapshot: Codable, Equatable, Hashable { - public var percentRemaining: Float - public var unlimited: Bool - public var overagePermitted: Bool -} - -public struct GitHubCopilotQuotaInfo: Codable, Equatable, Hashable { - public var chat: QuotaSnapshot - public var completions: QuotaSnapshot - public var premiumInteractions: QuotaSnapshot - public var resetDate: String - public var copilotPlan: String - - public var isFreeUser: Bool { copilotPlan == "free" } -} diff --git a/Tool/Sources/Status/Types/StatusResponse.swift b/Tool/Sources/Status/Types/StatusResponse.swift deleted file mode 100644 index 3842c088..00000000 --- a/Tool/Sources/Status/Types/StatusResponse.swift +++ /dev/null @@ -1,35 +0,0 @@ -import AppKit - -public struct StatusResponse { - public struct Icon { - /// Name of the icon resource - public let name: String - - public init(name: String) { - self.name = name - } - - public var nsImage: NSImage? { - return NSImage(named: name) - } - } - - /// The icon to display in the menu bar - public let icon: Icon - /// Indicates if an operation is in progress - public let inProgress: Bool - /// Message from the CLS (Copilot Language Server) status - public let clsMessage: String - /// Additional message (for accessibility or extension status) - public let message: String? - /// Extension status - public let extensionStatus: ExtensionPermissionStatus - /// URL for system preferences or other actions - public let url: String? - /// Current authentication status - public let authStatus: AuthStatus.Status - /// GitHub username of the authenticated user - public let userName: String? - /// Quota information for GitHub Copilot - public let quotaInfo: GitHubCopilotQuotaInfo? -} diff --git a/Tool/Sources/StatusBarItemView/AccountItemView.swift b/Tool/Sources/StatusBarItemView/AccountItemView.swift deleted file mode 100644 index 3eff1406..00000000 --- a/Tool/Sources/StatusBarItemView/AccountItemView.swift +++ /dev/null @@ -1,204 +0,0 @@ -import SwiftUI -import Cache - -public class AccountItemView: NSView { - private var target: AnyObject? - private var action: Selector? - private var isHovered = false - private var visualEffect: NSVisualEffectView - private let menuItemPadding: CGFloat = 6 - private let topInset: CGFloat = 4 // Customize this value - private let bottomInset: CGFloat = 0 - - private var userName: String - private var nameLabel: NSTextField! - let avatarSize = 28.0 - let horizontalPadding = 14.0 - let verticalPadding = 8.0 - - public override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - updateVisualEffectFrame() - } - - public init( - target: AnyObject? = nil, - action: Selector? = nil, - userName: String = "" - ) { - self.target = target - self.action = action - self.userName = userName - - // Initialize visualEffect with zero frame - it will be updated in layout - self.visualEffect = NSVisualEffectView(frame: .zero) - self.visualEffect.material = .selection - self.visualEffect.state = .active - self.visualEffect.blendingMode = .withinWindow - self.visualEffect.isHidden = true - self.visualEffect.wantsLayer = true - self.visualEffect.layer?.cornerRadius = 4 - self.visualEffect.layer?.backgroundColor = NSColor.controlAccentColor.cgColor - self.visualEffect.isEmphasized = true - - // Initialize with a reasonable starting size - super.init( - frame: NSRect( - x: 0, - y: 0, - width: 240, - height: avatarSize+verticalPadding+topInset - ) - ) - - // Set up autoresizing mask to allow the view to resize with its superview - self.autoresizingMask = [.width] - self.visualEffect.autoresizingMask = [.width, .height] - - wantsLayer = true - addSubview(visualEffect) - - // Create and configure subviews - setupSubviews() - } - - private func setupSubviews() { - // Create avatar view with hover state - let avatarView = NSHostingView(rootView: AvatarView(userName: userName, isHovered: isHovered)) - avatarView.frame = NSRect( - x: horizontalPadding, - y: 4, - width: avatarSize, - height: avatarSize - ) - addSubview(avatarView) - - // Store nameLabel as property and configure it - nameLabel = NSTextField( - labelWithString: userName.isEmpty ? "Sign In to GitHub Account" : userName - ) - nameLabel.font = - .systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) - nameLabel.frame = NSRect( - x: horizontalPadding*1.5 + avatarSize, - y: 0, - width: 180, - height: avatarSize - ) - nameLabel.cell?.truncatesLastVisibleLine = true - nameLabel.cell?.lineBreakMode = .byTruncatingTail - nameLabel.textColor = .labelColor - addSubview(nameLabel) - - // Make sure nameLabel resizes with the view - nameLabel.autoresizingMask = [.width] - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func mouseUp(with event: NSEvent) { - if let target = target, let action = action { - NSApp.sendAction(action, to: target, from: self) - } - } - - public override func updateTrackingAreas() { - super.updateTrackingAreas() - trackingAreas.forEach { removeTrackingArea($0) } - let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] - let trackingArea = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil) - addTrackingArea(trackingArea) - } - - public override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - isHovered = true - visualEffect.isHidden = false - nameLabel.textColor = .white - if let avatarView = subviews.first(where: { $0 is NSHostingView }) as? NSHostingView { - avatarView.rootView = AvatarView(userName: userName, isHovered: true) - } - } - - public override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - isHovered = false - visualEffect.isHidden = true - nameLabel.textColor = .labelColor - if let avatarView = subviews.first(where: { $0 is NSHostingView }) as? NSHostingView { - avatarView.rootView = AvatarView(userName: userName, isHovered: false) - } - } - - public override func resetCursorRects() { - addCursorRect(bounds, cursor: .pointingHand) - } - - public override func layout() { - super.layout() - updateVisualEffectFrame() - } - - private func updateVisualEffectFrame() { - var paddedFrame = bounds - paddedFrame.origin.x += menuItemPadding - paddedFrame.origin.y += bottomInset - paddedFrame.size.width -= menuItemPadding*2 - paddedFrame.size.height -= (topInset + bottomInset) - visualEffect.frame = paddedFrame - } -} - -struct AvatarView: View { - let userName: String - let isHovered: Bool - @ObservedObject private var viewModel = AvatarViewModel.shared - - init(userName: String, isHovered: Bool = false) { - self.userName = userName - self.isHovered = isHovered - } - - var body: some View { - Group { - if let avatarImage = viewModel.avatarImage { - avatarImage - .resizable() - .scaledToFit() - .clipShape(Circle()) - } else if userName.isEmpty { - Image(systemName: "person.crop.circle") - .resizable() - .scaledToFit() - .foregroundStyle(isHovered ? .white : .primary) - } else { - ProgressView() - .clipShape(Circle()) - } - } - } -} - -struct NSViewPreview: NSViewRepresentable { - var userName: String = "" - - func makeNSView(context: Context) -> NSView { - let NSView = AccountItemView( - userName: userName - ) - return NSView - } - - func updateNSView(_ nsView: NSView, context: Context) { - // Update as needed... - } -} - -#Preview("Not Signed In") { - NSViewPreview().frame(width: 245, height: 52) -} -#Preview("Signed In, Active") { - NSViewPreview(userName: "xcode-test").frame(width: 245, height: 52) -} diff --git a/Tool/Sources/StatusBarItemView/ErrorMessageView.swift b/Tool/Sources/StatusBarItemView/ErrorMessageView.swift deleted file mode 100644 index 8229c841..00000000 --- a/Tool/Sources/StatusBarItemView/ErrorMessageView.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -public class ErrorMessageView: NSView { - public init(errorMessage: String) { - // Create a custom view for the menu item - let maxWidth: CGFloat = 240 - let padding = NSEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) - - // Initialize with temporary frame, will be adjusted - super.init(frame: NSRect(x: 0, y: 0, width: maxWidth, height: 0)) - - let textField = NSTextField(frame: .zero) - textField.stringValue = errorMessage - textField.isEditable = false - textField.isBordered = false - textField.drawsBackground = false - textField.lineBreakMode = .byWordWrapping - textField.usesSingleLineMode = false - textField.cell?.wraps = true - textField.cell?.isScrollable = false - textField.textColor = .secondaryLabelColor - - // Calculate the required height - let fittingSize = textField.sizeThatFits( - NSSize(width: maxWidth - padding.left - padding.right, - height: CGFloat.greatestFiniteMagnitude) - ) - - // Set the final frames - self.frame = NSRect( - x: 0, y: 0, - width: maxWidth, - height: fittingSize.height + padding.top + padding.bottom - ) - - textField.frame = NSRect( - x: padding.left, - y: padding.bottom, - width: maxWidth - padding.left - padding.right, - height: fittingSize.height - ) - - addSubview(textField) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/Tool/Sources/StatusBarItemView/HoverButton.swift b/Tool/Sources/StatusBarItemView/HoverButton.swift deleted file mode 100644 index 66b58bb8..00000000 --- a/Tool/Sources/StatusBarItemView/HoverButton.swift +++ /dev/null @@ -1,145 +0,0 @@ -import AppKit - -class HoverButton: NSButton { - private var isLinkMode = false - - override func awakeFromNib() { - super.awakeFromNib() - setupButton() - } - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setupButton() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupButton() - } - - private func setupButton() { - self.wantsLayer = true - self.layer?.backgroundColor = NSColor.clear.cgColor - self.layer?.cornerRadius = 3 - } - - private func resetToDefaultState() { - self.layer?.backgroundColor = NSColor.clear.cgColor - if isLinkMode { - updateLinkAppearance(isHovered: false) - } - } - - override func viewDidMoveToSuperview() { - super.viewDidMoveToSuperview() - DispatchQueue.main.async { - self.updateTrackingAreas() - } - } - - override func layout() { - super.layout() - updateTrackingAreas() - } - - func configureLinkMode() { - isLinkMode = true - self.isBordered = false - self.setButtonType(.momentaryChange) - self.layer?.backgroundColor = NSColor.clear.cgColor - } - - func setLinkStyle(title: String, fontSize: CGFloat) { - configureLinkMode() - updateLinkAppearance(title: title, fontSize: fontSize, isHovered: false) - } - - override func mouseEntered(with event: NSEvent) { - if isLinkMode { - updateLinkAppearance(isHovered: true) - } else { - self.layer?.backgroundColor = NSColor.labelColor.withAlphaComponent(0.15).cgColor - super.mouseEntered(with: event) - } - } - - override func mouseExited(with event: NSEvent) { - if isLinkMode { - updateLinkAppearance(isHovered: false) - } else { - super.mouseExited(with: event) - resetToDefaultState() - } - } - - private func updateLinkAppearance(title: String? = nil, fontSize: CGFloat? = nil, isHovered: Bool = false) { - let buttonTitle = title ?? self.title - let font = fontSize != nil ? NSFont.systemFont(ofSize: fontSize!, weight: .regular) : NSFont.systemFont(ofSize: 11) - - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: NSColor.controlAccentColor, - .font: font, - .underlineStyle: isHovered ? NSUnderlineStyle.single.rawValue : 0 - ] - - let attributedTitle = NSAttributedString(string: buttonTitle, attributes: attributes) - self.attributedTitle = attributedTitle - } - - override func mouseDown(with event: NSEvent) { - super.mouseDown(with: event) - // Reset state immediately after click - DispatchQueue.main.async { - self.resetToDefaultState() - } - } - - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - // Ensure state is reset - DispatchQueue.main.async { - self.resetToDefaultState() - } - } - - override func viewDidHide() { - super.viewDidHide() - // Reset state when view is hidden (like when menu closes) - resetToDefaultState() - } - - override func viewDidUnhide() { - super.viewDidUnhide() - // Ensure clean state when view reappears - resetToDefaultState() - } - - override func removeFromSuperview() { - super.removeFromSuperview() - // Reset state when removed from superview - resetToDefaultState() - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - for trackingArea in self.trackingAreas { - self.removeTrackingArea(trackingArea) - } - - guard self.bounds.width > 0 && self.bounds.height > 0 else { return } - - let trackingArea = NSTrackingArea( - rect: self.bounds, - options: [ - .mouseEnteredAndExited, - .activeAlways, - .inVisibleRect - ], - owner: self, - userInfo: nil - ) - self.addTrackingArea(trackingArea) - } -} diff --git a/Tool/Sources/StatusBarItemView/QuotaView.swift b/Tool/Sources/StatusBarItemView/QuotaView.swift deleted file mode 100644 index f1b2d1d3..00000000 --- a/Tool/Sources/StatusBarItemView/QuotaView.swift +++ /dev/null @@ -1,617 +0,0 @@ -import SwiftUI -import Foundation - -// MARK: - QuotaSnapshot Model -public struct QuotaSnapshot { - public var percentRemaining: Float - public var unlimited: Bool - public var overagePermitted: Bool - - public init(percentRemaining: Float, unlimited: Bool, overagePermitted: Bool) { - self.percentRemaining = percentRemaining - self.unlimited = unlimited - self.overagePermitted = overagePermitted - } -} - -// MARK: - QuotaView Main Class -public class QuotaView: NSView { - - // MARK: - Properties - private let chat: QuotaSnapshot - private let completions: QuotaSnapshot - private let premiumInteractions: QuotaSnapshot - private let resetDate: String - private let copilotPlan: String - - private var isFreeUser: Bool { - return copilotPlan == "free" - } - - private var isOrgUser: Bool { - return copilotPlan == "business" || copilotPlan == "enterprise" - } - - private var isFreeQuotaUsedUp: Bool { - return chat.percentRemaining == 0 && completions.percentRemaining == 0 - } - - private var isFreeQuotaRemaining: Bool { - return chat.percentRemaining > 25 && completions.percentRemaining > 25 - } - - // MARK: - Initialization - public init( - chat: QuotaSnapshot, - completions: QuotaSnapshot, - premiumInteractions: QuotaSnapshot, - resetDate: String, - copilotPlan: String - ) { - self.chat = chat - self.completions = completions - self.premiumInteractions = premiumInteractions - self.resetDate = resetDate - self.copilotPlan = copilotPlan - - super.init(frame: NSRect(x: 0, y: 0, width: Layout.viewWidth, height: 0)) - - configureView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Configuration - private func configureView() { - autoresizingMask = [.width] - setupView() - - layoutSubtreeIfNeeded() - let calculatedHeight = fittingSize.height - frame = NSRect(x: 0, y: 0, width: Layout.viewWidth, height: calculatedHeight) - } - - private func setupView() { - let components = createViewComponents() - addSubviewsToHierarchy(components) - setupLayoutConstraints(components) - } - - // MARK: - Component Creation - private func createViewComponents() -> ViewComponents { - return ViewComponents( - titleContainer: createTitleContainer(), - progressViews: createProgressViews(), - statusMessageLabel: createStatusMessageLabel(), - resetTextLabel: createResetTextLabel(), - upsellLabel: createUpsellLabel() - ) - } - - private func addSubviewsToHierarchy(_ components: ViewComponents) { - addSubview(components.titleContainer) - components.progressViews.forEach { addSubview($0) } - if !isFreeUser { - addSubview(components.statusMessageLabel) - } - addSubview(components.resetTextLabel) - if !(isOrgUser || (isFreeUser && isFreeQuotaRemaining)) { - addSubview(components.upsellLabel) - } - } -} - -// MARK: - Title Section -extension QuotaView { - private func createTitleContainer() -> NSView { - let container = NSView() - container.translatesAutoresizingMaskIntoConstraints = false - - let titleLabel = createTitleLabel() - let settingsButton = createSettingsButton() - - container.addSubview(titleLabel) - container.addSubview(settingsButton) - - setupTitleConstraints(container: container, titleLabel: titleLabel, settingsButton: settingsButton) - - return container - } - - private func createTitleLabel() -> NSTextField { - let label = NSTextField(labelWithString: "Copilot Usage") - label.font = NSFont.systemFont(ofSize: Style.titleFontSize, weight: .medium) - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .systemGray - return label - } - - private func createSettingsButton() -> HoverButton { - let button = HoverButton() - - if let image = NSImage(systemSymbolName: "slider.horizontal.3", accessibilityDescription: "Manage Copilot") { - image.isTemplate = true - button.image = image - } - - button.imagePosition = .imageOnly - button.alphaValue = Style.buttonAlphaValue - button.toolTip = "Manage Copilot" - button.setButtonType(.momentaryChange) - button.isBordered = false - button.translatesAutoresizingMaskIntoConstraints = false - button.target = self - button.action = #selector(openCopilotSettings) - - return button - } - - private func setupTitleConstraints(container: NSView, titleLabel: NSTextField, settingsButton: HoverButton) { - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor), - titleLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor), - - settingsButton.trailingAnchor.constraint(equalTo: container.trailingAnchor), - settingsButton.centerYAnchor.constraint(equalTo: container.centerYAnchor), - settingsButton.widthAnchor.constraint(equalToConstant: Layout.settingsButtonSize), - settingsButton.heightAnchor.constraint(equalToConstant: Layout.settingsButtonHoverSize), - - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: settingsButton.leadingAnchor, constant: -Layout.settingsButtonSpacing) - ]) - } -} - -// MARK: - Progress Bars Section -extension QuotaView { - private func createProgressViews() -> [NSView] { - let completionsView = createProgressBarSection( - title: "Code Completions", - snapshot: completions - ) - - let chatView = createProgressBarSection( - title: "Chat Messages", - snapshot: chat - ) - - if isFreeUser { - return [completionsView, chatView] - } - - let premiumView = createProgressBarSection( - title: "Premium Requests", - snapshot: premiumInteractions - ) - - return [completionsView, chatView, premiumView] - } - - private func createProgressBarSection(title: String, snapshot: QuotaSnapshot) -> NSView { - let container = NSView() - container.translatesAutoresizingMaskIntoConstraints = false - - let titleLabel = createProgressTitleLabel(title: title) - let percentageLabel = createPercentageLabel(snapshot: snapshot) - - container.addSubview(titleLabel) - container.addSubview(percentageLabel) - - if !snapshot.unlimited { - addProgressBar(to: container, snapshot: snapshot, titleLabel: titleLabel, percentageLabel: percentageLabel) - } else { - setupUnlimitedLayout(container: container, titleLabel: titleLabel, percentageLabel: percentageLabel) - } - - return container - } - - private func createProgressTitleLabel(title: String) -> NSTextField { - let label = NSTextField(labelWithString: title) - label.font = NSFont.systemFont(ofSize: Style.progressFontSize, weight: .regular) - label.textColor = .labelColor - label.translatesAutoresizingMaskIntoConstraints = false - return label - } - - private func createPercentageLabel(snapshot: QuotaSnapshot) -> NSTextField { - let usedPercentage = (100.0 - snapshot.percentRemaining) - let numberPart = usedPercentage.truncatingRemainder(dividingBy: 1) == 0 - ? String(format: "%.0f", usedPercentage) - : String(format: "%.1f", usedPercentage) - let text = snapshot.unlimited ? "Included" : "\(numberPart)%" - - let label = NSTextField(labelWithString: text) - label.font = NSFont.systemFont(ofSize: Style.percentageFontSize, weight: .regular) - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .secondaryLabelColor - label.alignment = .right - - return label - } - - private func addProgressBar(to container: NSView, snapshot: QuotaSnapshot, titleLabel: NSTextField, percentageLabel: NSTextField) { - let usedPercentage = 100.0 - snapshot.percentRemaining - let color = getProgressBarColor(for: usedPercentage) - - let progressBackground = createProgressBackground(color: color) - let progressFill = createProgressFill(color: color, usedPercentage: usedPercentage) - - progressBackground.addSubview(progressFill) - container.addSubview(progressBackground) - - setupProgressBarConstraints( - container: container, - titleLabel: titleLabel, - percentageLabel: percentageLabel, - progressBackground: progressBackground, - progressFill: progressFill, - usedPercentage: usedPercentage - ) - } - - private func createProgressBackground(color: NSColor) -> NSView { - let background = NSView() - background.wantsLayer = true - background.layer?.backgroundColor = color.cgColor.copy(alpha: Style.progressBarBackgroundAlpha) - background.layer?.cornerRadius = Layout.progressBarCornerRadius - background.translatesAutoresizingMaskIntoConstraints = false - return background - } - - private func createProgressFill(color: NSColor, usedPercentage: Float) -> NSView { - let fill = NSView() - fill.wantsLayer = true - fill.translatesAutoresizingMaskIntoConstraints = false - fill.layer?.backgroundColor = color.cgColor - fill.layer?.cornerRadius = Layout.progressBarCornerRadius - return fill - } - - private func setupProgressBarConstraints( - container: NSView, - titleLabel: NSTextField, - percentageLabel: NSTextField, - progressBackground: NSView, - progressFill: NSView, - usedPercentage: Float - ) { - NSLayoutConstraint.activate([ - // Title and percentage on the same line - titleLabel.topAnchor.constraint(equalTo: container.topAnchor), - titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: percentageLabel.leadingAnchor, constant: -Layout.percentageLabelSpacing), - - percentageLabel.topAnchor.constraint(equalTo: container.topAnchor), - percentageLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor), - percentageLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: Layout.percentageLabelMinWidth), - - // Progress bar background - progressBackground.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: Layout.progressBarVerticalOffset), - progressBackground.leadingAnchor.constraint(equalTo: container.leadingAnchor), - progressBackground.trailingAnchor.constraint(equalTo: container.trailingAnchor), - progressBackground.bottomAnchor.constraint(equalTo: container.bottomAnchor), - progressBackground.heightAnchor.constraint(equalToConstant: Layout.progressBarThickness), - - // Progress bar fill - progressFill.topAnchor.constraint(equalTo: progressBackground.topAnchor), - progressFill.leadingAnchor.constraint(equalTo: progressBackground.leadingAnchor), - progressFill.bottomAnchor.constraint(equalTo: progressBackground.bottomAnchor), - progressFill.widthAnchor.constraint(equalTo: progressBackground.widthAnchor, multiplier: CGFloat(usedPercentage / 100.0)) - ]) - } - - private func setupUnlimitedLayout(container: NSView, titleLabel: NSTextField, percentageLabel: NSTextField) { - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: container.topAnchor), - titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: percentageLabel.leadingAnchor, constant: -Layout.percentageLabelSpacing), - titleLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor), - - percentageLabel.topAnchor.constraint(equalTo: container.topAnchor), - percentageLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor), - percentageLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: Layout.percentageLabelMinWidth), - percentageLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor) - ]) - } - - private func getProgressBarColor(for usedPercentage: Float) -> NSColor { - switch usedPercentage { - case 90...: - return .systemRed - case 75..<90: - return .systemYellow - default: - return .systemBlue - } - } -} - -// MARK: - Footer Section -extension QuotaView { - private func createStatusMessageLabel() -> NSTextField { - let message = premiumInteractions.overagePermitted ? - "Additional paid premium requests enabled." : - "Additional paid premium requests disabled." - - let label = NSTextField(labelWithString: isFreeUser ? "" : message) - label.font = NSFont.systemFont(ofSize: Style.footerFontSize, weight: .regular) - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .secondaryLabelColor - label.alignment = .left - return label - } - - private func createResetTextLabel() -> NSTextField { - - // Format reset date - let formatter = DateFormatter() - formatter.dateFormat = "yyyy.MM.dd" - - var resetText = "Allowance resets \(resetDate)." - - if let date = formatter.date(from: resetDate) { - let outputFormatter = DateFormatter() - outputFormatter.dateFormat = "MMMM d, yyyy" - let formattedDate = outputFormatter.string(from: date) - resetText = "Allowance resets \(formattedDate)." - } - - let label = NSTextField(labelWithString: resetText) - label.font = NSFont.systemFont(ofSize: Style.footerFontSize, weight: .regular) - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .secondaryLabelColor - label.alignment = .left - return label - } - - private func createUpsellLabel() -> NSButton { - if isFreeUser { - let button = NSButton() - let upgradeTitle = "Upgrade to Copilot Pro" - - button.translatesAutoresizingMaskIntoConstraints = false - button.bezelStyle = .push - if isFreeQuotaUsedUp { - button.attributedTitle = NSAttributedString( - string: upgradeTitle, - attributes: [.foregroundColor: NSColor.white] - ) - button.bezelColor = .controlAccentColor - } else { - button.title = upgradeTitle - } - button.controlSize = .large - button.target = self - button.action = #selector(openCopilotUpgradePlan) - - return button - } else { - let button = HoverButton() - let title = "Manage paid premium requests" - - button.setLinkStyle(title: title, fontSize: Style.footerFontSize) - button.translatesAutoresizingMaskIntoConstraints = false - button.alphaValue = Style.labelAlphaValue - button.alignment = .left - button.target = self - button.action = #selector(openCopilotManageOverage) - - return button - } - } -} - -// MARK: - Layout Constraints -extension QuotaView { - private func setupLayoutConstraints(_ components: ViewComponents) { - let constraints = buildConstraints(components) - NSLayoutConstraint.activate(constraints) - } - - private func buildConstraints(_ components: ViewComponents) -> [NSLayoutConstraint] { - var constraints: [NSLayoutConstraint] = [] - - // Title constraints - constraints.append(contentsOf: buildTitleConstraints(components.titleContainer)) - - // Progress view constraints - constraints.append(contentsOf: buildProgressViewConstraints(components)) - - // Footer constraints - constraints.append(contentsOf: buildFooterConstraints(components)) - - return constraints - } - - private func buildTitleConstraints(_ titleContainer: NSView) -> [NSLayoutConstraint] { - return [ - titleContainer.topAnchor.constraint(equalTo: topAnchor, constant: 0), - titleContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - titleContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - titleContainer.heightAnchor.constraint(equalToConstant: Layout.titleHeight) - ] - } - - private func buildProgressViewConstraints(_ components: ViewComponents) -> [NSLayoutConstraint] { - let completionsView = components.progressViews[0] - let chatView = components.progressViews[1] - - var constraints: [NSLayoutConstraint] = [] - - if !isFreeUser { - let premiumView = components.progressViews[2] - constraints.append(contentsOf: buildPremiumProgressConstraints(premiumView, titleContainer: components.titleContainer)) - constraints.append(contentsOf: buildCompletionsProgressConstraints(completionsView, topView: premiumView, isPremiumUnlimited: premiumInteractions.unlimited)) - } else { - constraints.append(contentsOf: buildCompletionsProgressConstraints(completionsView, topView: components.titleContainer, isPremiumUnlimited: false)) - } - - constraints.append(contentsOf: buildChatProgressConstraints(chatView, topView: completionsView)) - - return constraints - } - - private func buildPremiumProgressConstraints(_ premiumView: NSView, titleContainer: NSView) -> [NSLayoutConstraint] { - return [ - premiumView.topAnchor.constraint(equalTo: titleContainer.bottomAnchor, constant: Layout.verticalSpacing), - premiumView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - premiumView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - premiumView.heightAnchor.constraint( - equalToConstant: premiumInteractions.unlimited ? Layout.unlimitedProgressBarHeight : Layout.progressBarHeight - ) - ] - } - - private func buildCompletionsProgressConstraints(_ completionsView: NSView, topView: NSView, isPremiumUnlimited: Bool) -> [NSLayoutConstraint] { - let topSpacing = isPremiumUnlimited ? Layout.unlimitedVerticalSpacing : Layout.verticalSpacing - - return [ - completionsView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: topSpacing), - completionsView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - completionsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - completionsView.heightAnchor.constraint( - equalToConstant: completions.unlimited ? Layout.unlimitedProgressBarHeight : Layout.progressBarHeight - ) - ] - } - - private func buildChatProgressConstraints(_ chatView: NSView, topView: NSView) -> [NSLayoutConstraint] { - let topSpacing = completions.unlimited ? Layout.unlimitedVerticalSpacing : Layout.verticalSpacing - - return [ - chatView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: topSpacing), - chatView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - chatView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - chatView.heightAnchor.constraint( - equalToConstant: chat.unlimited ? Layout.unlimitedProgressBarHeight : Layout.progressBarHeight - ) - ] - } - - private func buildFooterConstraints(_ components: ViewComponents) -> [NSLayoutConstraint] { - let chatView = components.progressViews[1] - let topSpacing = chat.unlimited ? Layout.unlimitedVerticalSpacing : Layout.verticalSpacing - - var constraints = [NSLayoutConstraint]() - - if !isFreeUser { - // Add status message label constraints - constraints.append(contentsOf: [ - components.statusMessageLabel.topAnchor.constraint(equalTo: chatView.bottomAnchor, constant: topSpacing), - components.statusMessageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - components.statusMessageLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - components.statusMessageLabel.heightAnchor.constraint(equalToConstant: Layout.footerTextHeight) - ]) - - // Add reset text label constraints with status message label as the top anchor - constraints.append(contentsOf: [ - components.resetTextLabel.topAnchor.constraint(equalTo: components.statusMessageLabel.bottomAnchor), - components.resetTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - components.resetTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - components.resetTextLabel.heightAnchor.constraint(equalToConstant: Layout.footerTextHeight) - ]) - } else { - // For free users, only show reset text label - constraints.append(contentsOf: [ - components.resetTextLabel.topAnchor.constraint(equalTo: chatView.bottomAnchor, constant: topSpacing), - components.resetTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - components.resetTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - components.resetTextLabel.heightAnchor.constraint(equalToConstant: Layout.footerTextHeight) - ]) - } - - if isOrgUser || (isFreeUser && isFreeQuotaRemaining) { - // Do not show link label for business or enterprise users - constraints.append(components.resetTextLabel.bottomAnchor.constraint(equalTo: bottomAnchor)) - return constraints - } - - // Add link label constraints - constraints.append(contentsOf: [ - components.upsellLabel.topAnchor.constraint(equalTo: components.resetTextLabel.bottomAnchor), - components.upsellLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalMargin), - components.upsellLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalMargin), - components.upsellLabel.heightAnchor.constraint(equalToConstant: isFreeUser ? Layout.upgradeButtonHeight : Layout.linkLabelHeight), - - components.upsellLabel.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - - return constraints - } -} - -// MARK: - Actions -extension QuotaView { - @objc private func openCopilotSettings() { - Task { - if let url = URL(string: "https://aka.ms/github-copilot-settings") { - NSWorkspace.shared.open(url) - } - } - } - - @objc private func openCopilotManageOverage() { - Task { - if let url = URL(string: "https://aka.ms/github-copilot-manage-overage") { - NSWorkspace.shared.open(url) - } - } - } - - @objc private func openCopilotUpgradePlan() { - Task { - if let url = URL(string: "https://aka.ms/github-copilot-upgrade-plan") { - NSWorkspace.shared.open(url) - } - } - } -} - -// MARK: - Helper Types -private struct ViewComponents { - let titleContainer: NSView - let progressViews: [NSView] - let statusMessageLabel: NSTextField - let resetTextLabel: NSTextField - let upsellLabel: NSButton -} - -// MARK: - Layout Constants -private struct Layout { - static let viewWidth: CGFloat = 256 - static let horizontalMargin: CGFloat = 14 - static let verticalSpacing: CGFloat = 8 - static let unlimitedVerticalSpacing: CGFloat = 6 - static let smallVerticalSpacing: CGFloat = 4 - - static let titleHeight: CGFloat = 20 - static let progressBarHeight: CGFloat = 22 - static let unlimitedProgressBarHeight: CGFloat = 16 - static let footerTextHeight: CGFloat = 16 - static let linkLabelHeight: CGFloat = 16 - static let upgradeButtonHeight: CGFloat = 40 - - static let settingsButtonSize: CGFloat = 20 - static let settingsButtonHoverSize: CGFloat = 14 - static let settingsButtonSpacing: CGFloat = 8 - - static let progressBarThickness: CGFloat = 3 - static let progressBarCornerRadius: CGFloat = 1.5 - static let progressBarVerticalOffset: CGFloat = -10 - static let percentageLabelMinWidth: CGFloat = 35 - static let percentageLabelSpacing: CGFloat = 8 -} - -// MARK: - Style Constants -private struct Style { - static let labelAlphaValue: CGFloat = 0.85 - static let progressBarBackgroundAlpha: CGFloat = 0.3 - static let buttonAlphaValue: CGFloat = 0.85 - - static let titleFontSize: CGFloat = 11 - static let progressFontSize: CGFloat = 13 - static let percentageFontSize: CGFloat = 11 - static let footerFontSize: CGFloat = 11 -} diff --git a/Tool/Sources/SuggestionBasic/CodeSuggestion.swift b/Tool/Sources/SuggestionBasic/CodeSuggestion.swift deleted file mode 100644 index bd124fc1..00000000 --- a/Tool/Sources/SuggestionBasic/CodeSuggestion.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import CodableWrappers - -public struct CodeSuggestion: Codable, Equatable { - public init( - id: String, - text: String, - position: CursorPosition, - range: CursorRange - ) { - self.text = text - self.position = position - self.id = id - self.range = range - middlewareComments = [] - } - - public static func == (lhs: CodeSuggestion, rhs: CodeSuggestion) -> Bool { - return lhs.text == rhs.text - && lhs.position == rhs.position - && lhs.id == rhs.id - && lhs.range == rhs.range - } - - /// The new code to be inserted and the original code on the first line. - public var text: String - /// The position of the cursor before generating the completion. - public var position: CursorPosition - /// An id. - public var id: String - /// The range of the original code that should be replaced. - public var range: CursorRange - /// A place to store comments inserted by middleware for debugging use. - @FallbackDecoding public var middlewareComments: [String] -} - diff --git a/Tool/Sources/SuggestionBasic/EditorInformation.swift b/Tool/Sources/SuggestionBasic/EditorInformation.swift deleted file mode 100644 index 8518b8b0..00000000 --- a/Tool/Sources/SuggestionBasic/EditorInformation.swift +++ /dev/null @@ -1,167 +0,0 @@ -import Foundation -import Parsing - -public struct EditorInformation { - public struct LineAnnotation { - public var type: String - public var line: Int - public var message: String - } - - public struct SourceEditorContent { - /// The content of the source editor. - public var content: String - /// The content of the source editor in lines. Every line should ends with `\n`. - public var lines: [String] - /// The selection ranges of the source editor. - public var selections: [CursorRange] - /// The cursor position of the source editor. - public var cursorPosition: CursorPosition - /// The cursor position as offset. - public var cursorOffset: Int - /// Line annotations of the source editor. - public var lineAnnotations: [LineAnnotation] - - public var selectedContent: String { - if let range = selections.first { - let startIndex = min( - max(0, range.start.line), - lines.endIndex - 1 - ) - let endIndex = min( - max(startIndex, range.end.line), - lines.endIndex - 1 - ) - let selectedContent = lines[startIndex...endIndex] - return selectedContent.joined() - } - return "" - } - - public init( - content: String, - lines: [String], - selections: [CursorRange], - cursorPosition: CursorPosition, - cursorOffset: Int, - lineAnnotations: [String] - ) { - self.content = content - self.lines = lines - self.selections = selections - self.cursorPosition = cursorPosition - self.cursorOffset = cursorOffset - self.lineAnnotations = lineAnnotations.map(EditorInformation.parseLineAnnotation) - } - } - - public let editorContent: SourceEditorContent? - public let selectedContent: String - public let selectedLines: [String] - public let documentURL: URL - public let workspaceURL: URL - public let projectRootURL: URL - public let relativePath: String - public let language: CodeLanguage - - public init( - editorContent: SourceEditorContent?, - selectedContent: String, - selectedLines: [String], - documentURL: URL, - workspaceURL: URL, - projectRootURL: URL, - relativePath: String, - language: CodeLanguage - ) { - self.editorContent = editorContent - self.selectedContent = selectedContent - self.selectedLines = selectedLines - self.documentURL = documentURL - self.workspaceURL = workspaceURL - self.projectRootURL = projectRootURL - self.relativePath = relativePath - self.language = language - } - - public func code(in range: CursorRange) -> String { - return EditorInformation.code(in: editorContent?.lines ?? [], inside: range).code - } - - public static func lines(in code: [String], containing range: CursorRange) -> [String] { - guard !code.isEmpty else { return [] } - guard range.start.line <= range.end.line else { return [] } - let startIndex = min(max(0, range.start.line), code.endIndex - 1) - let endIndex = min(max(startIndex, range.end.line), code.endIndex - 1) - guard startIndex <= endIndex else { return [] } - let selectedLines = code[startIndex...endIndex] - return Array(selectedLines) - } - - public static func code( - in code: [String], - inside range: CursorRange, - ignoreColumns: Bool = false - ) -> (code: String, lines: [String]) { - guard range.start <= range.end else { return ("", []) } - - let rangeLines = lines(in: code, containing: range) - if ignoreColumns { - return (rangeLines.joined(), rangeLines) - } - var content = rangeLines - if !content.isEmpty { - let lastLine = content[content.endIndex - 1] - let droppedEndIndex = lastLine.utf16.index( - lastLine.utf16.startIndex, - offsetBy: range.end.character, - limitedBy: lastLine.utf16.endIndex - ) ?? lastLine.utf16.endIndex - content[content.endIndex - 1] = if droppedEndIndex > lastLine.utf16.startIndex { - String(lastLine[.. LineAnnotation { - let lineAnnotationParser = Parse(input: Substring.self) { - PrefixUpTo(":") - ":" - PrefixUpTo(":") - ":" - Int.parser() - Prefix(while: { _ in true }) - }.map { (prefix: Substring, _: Substring, line: Int, message: Substring) in - let type = String(prefix.split(separator: " ").first ?? prefix) - return LineAnnotation( - type: type.trimmingCharacters(in: .whitespacesAndNewlines), - line: line, - message: message.trimmingCharacters(in: .whitespacesAndNewlines) - ) - } - - do { - return try lineAnnotationParser.parse(annotation[...]) - } catch { - return .init(type: "", line: 0, message: annotation) - } - } -} - diff --git a/Tool/Sources/SuggestionBasic/ExportedFromLSP.swift b/Tool/Sources/SuggestionBasic/ExportedFromLSP.swift deleted file mode 100644 index 0a008da7..00000000 --- a/Tool/Sources/SuggestionBasic/ExportedFromLSP.swift +++ /dev/null @@ -1,88 +0,0 @@ -import LanguageServerProtocol - -/// Line starts at 0. -public typealias CursorPosition = LanguageServerProtocol.Position - -public extension CursorPosition { - static let zero = CursorPosition(line: 0, character: 0) - static var outOfScope: CursorPosition { .init(line: -1, character: -1) } - - var readableText: String { - return "[\(line + 1), \(character)]" - } -} - -public struct CursorRange: Codable, Hashable, Sendable, Equatable, CustomStringConvertible { - public static let zero = CursorRange(start: .zero, end: .zero) - - public var start: CursorPosition - public var end: CursorPosition - - public init(start: Position, end: Position) { - self.start = start - self.end = end - } - - public init(startPair: (Int, Int), endPair: (Int, Int)) { - start = CursorPosition(startPair) - end = CursorPosition(endPair) - } - - public func contains(_ position: CursorPosition) -> Bool { - return position >= start && position <= end - } - - public func contains(_ range: CursorRange) -> Bool { - return range.start >= start && range.end <= end - } - - public func strictlyContains(_ range: CursorRange) -> Bool { - return range.start > start && range.end < end - } - - public func intersects(_ other: LSPRange) -> Bool { - return contains(other.start) || contains(other.end) - } - - public var isEmpty: Bool { - return start == end - } - - public var isOneLine: Bool { - return start.line == end.line - } - - /// The number of lines in the range. - public var lineCount: Int { - return end.line - start.line + 1 - } - - public static func == (lhs: CursorRange, rhs: CursorRange) -> Bool { - return lhs.start == rhs.start && lhs.end == rhs.end - } - - public var description: String { - return "\(start.readableText) - \(end.readableText)" - } - - public var isValid: Bool { - let startLine = start.line - let startCharacter = start.character - let endLine = end.line - let endCharacter = end.character - - guard startLine >= 0 && startCharacter >= 0 && endLine >= 0 && endCharacter >= 0 else {return false} - - guard startLine < endLine || (startLine == endLine && startCharacter <= endCharacter) else {return false} - - return true - } -} - -public extension CursorRange { - static var outOfScope: CursorRange { .init(start: .outOfScope, end: .outOfScope) } - static func cursor(_ position: CursorPosition) -> CursorRange { - return .init(start: position, end: position) - } -} - diff --git a/Tool/Sources/SuggestionBasic/LanguageIdentifierFromFilePath.swift b/Tool/Sources/SuggestionBasic/LanguageIdentifierFromFilePath.swift deleted file mode 100644 index a0478833..00000000 --- a/Tool/Sources/SuggestionBasic/LanguageIdentifierFromFilePath.swift +++ /dev/null @@ -1,274 +0,0 @@ -import Foundation -import LanguageServerProtocol - -public enum CodeLanguage: RawRepresentable, Codable, CaseIterable, Hashable { - case builtIn(LanguageIdentifier) - case plaintext - case other(String) - - public var rawValue: String { - switch self { - case let .builtIn(language): - return language.rawValue - case .plaintext: - return "plaintext" - case let .other(language): - return language - } - } - - public var hashValue: Int { - rawValue.hashValue - } - - public init?(rawValue: String) { - if let language = LanguageIdentifier(rawValue: rawValue) { - self = .builtIn(language) - } else if rawValue == "txt" || rawValue.isEmpty { - self = .plaintext - } else { - self = .other(rawValue) - } - } - - public init(fileURL: URL) { - self = languageIdentifierFromFileURL(fileURL) - } - - public init(filePath: String) { - self = languageIdentifierFromFileURL(URL(fileURLWithPath: filePath)) - } - - public static var allCases: [CodeLanguage] { - var all = LanguageIdentifier.allCases.map(CodeLanguage.builtIn) - all.append(.plaintext) - return all - } -} - -public extension LanguageIdentifier { - /// Copied from https://github.com/github/linguist/blob/master/lib/linguist/languages.yml [MIT] - var fileExtensions: [String] { - switch self { - case .abap: - return ["abap"] - case .windowsbat: - return ["bat", "cmd"] - case .bibtex: - return ["bib", "bibtex"] - case .clojure: - return ["clj", "boot", "cl2", "cljc", "cljs", "cljs.hl", "cljscm", "cljx", "hic"] - case .coffeescript: - return ["coffee", "_coffee", "cjsx", "cson", "iced"] - case .c: - return ["c", "cats", "idc"] - case .cpp: - return ["cpp", "c++", "cc", "cp", "cxx", "h++", "hh", "hpp", "hxx", "inl", "ino", "ipp", - "ixx", "re", "tcc", "tpp"] - case .csharp: - return ["cs", "cake", "csx", "linq"] - case .css: - return ["css"] - case .diff: - return ["diff", "patch"] - case .dart: - return ["dart"] - case .dockerfile: - return ["dockerfile"] - case .elixir: - return ["ex", "exs"] - case .erlang: - return ["erl", "es", "escript", "hrl"] - case .fsharp: - return ["fs", "fsi", "fsx"] - case .gitcommit: - return [] - case .gitrebase: - return [] - case .go: - return ["go"] - case .groovy: - return ["groovy", "grt", "gtpl", "gvy"] - case .handlebars: - return ["handlebars", "hbs"] - case .html: - return ["html", "hta", "htm", "inc", "xht", "xhtml"] - case .ini: - return ["ini", "cfg", "dof", "lektorproject", "prefs", "pro", "properties", "url"] - case .java: - return ["java"] - case .javascript: - return ["js", "_js", "bones", "es6", "frag", "gs", "jake", "jsb", "jsfl", "jsm", "jss", - "njs", "pac", "sjs", "ssjs", "xsjs", "xsjslib"] - case .javascriptreact: - return ["jsx"] - case .json: - return ["json"] - case .latex: - return ["tex"] - case .less: - return ["less"] - case .lua: - return ["lua"] - case .makefile: - return ["mak", "d", "mk"] - case .markdown: - return ["md", "livemd", "markdown", "mkd", "mkdn", "mkdown", "ronn", "scd", "workbook"] - case .objc: - return ["m", "h"] - case .objcpp: - return ["mm"] - case .perl: - return ["pl", "perl", "ph", "plx", /* "pm", */ "pod", "psgi" /* "t" */ ] - case .perl6: - return ["6pl", "6pm", "nqp", "p6", "p6l", "p6m", /* "pl", */ "pl6", "pm", "pm6", "t"] - case .php: - return ["php", "aw", "ctp", "php3", "php4", "php5", "phpt"] - case .powershell: - return ["ps1", "psd1", "psm1"] - case .pug: - return ["jade", "pug"] - case .python: - return ["py", "cgi", "gyp", "lmi", "pyde", "pyp", "pyt", "pyw", "tac", "wsgi", "xpy"] - case .r: - return ["r", "rd", "rsx"] - case .razor: - return ["cshtml", "razor"] - case .ruby: - return ["rb", "builder", "gemspec", "god", "irbrc", "jbuilder", "mspec", "pluginspec", - "podspec", "rabl", "rake", "rbuild", "rbw", "rbx", "ru", "ruby", "thor", - "watchr"] - case .rust: - return ["rs"] - case .scss: - return ["scss"] - case .sass: - return ["sass"] - case .scala: - return ["scala", "sbt", "sc"] - case .shaderlab: - return ["shader"] - case .shellscript: - return ["sh"] - case .sql: - return ["sql", "cql", "ddl", "prc", "tab", "udf", "viw"] - case .swift: - return ["swift", "xcplayground", "xcplaygroundpage", "playground"] - case .typescript: - return ["ts"] - case .typescriptreact: - return ["tsx"] - case .tex: - return [ /* "tex", */ "aux", "bbx", "cbx", "cls", "dtx", "ins", "lbx", "ltx", "mkii", - "mkiv", "mkvi", "sty", "toc"] - case .vb: - return [ - "vb", - "bas", -// "cls", - "frm", - "frx", - "vba", - "vbhtml", - "vbs", - ] - case .xml: - return [ - "xml", - "ant", - "axml", - "ccxml", - "clixml", - "cproject", - "csproj", - "ct", - "dita", - "ditamap", - "ditaval", - "dll.config", - "filters", - "fsproj", - "fxml", - "glade", - "grxml", - "ivy", - "jelly", - "kml", - "launch", - "mxml", - "nproj", - "nuspec", - "odd", - "osm", - "plist", -// "pluginspec", - "ps1xml", - "psc1", - "pt", - "rdf", - "rss", - "scxml", - "srdf", - "storyboard", - "stTheme", - "sublime-snippet", - "targets", - "tmCommand", - "tml", - "tmLanguage", - "tmPreferences", - "tmSnippet", - "tmTheme", - "ui", - "urdf", - "vbproj", - "vcxproj", - "vxml", - "wsdl", - "wsf", - "wxi", - "wxl", - "wxs", - "x3d", - "xacro", - "xaml", - "xib", - "xlf", - "xliff", - "xmi", - "xml.dist", - "xsd", - "xul", - "zcml", - ] - case .xsl: - return ["xsl"] - case .yaml: - return [ - "yml", - "reek", - "rviz", - "yaml", - ] - } - } -} - -let fileExtensionToLanguageId = { - var dict = [String: LanguageIdentifier]() - for languageId in LanguageIdentifier.allCases { - for e in languageId.fileExtensions { - dict[e] = languageId - } - } - return dict -}() - -public func languageIdentifierFromFileURL(_ fileURL: URL) -> CodeLanguage { - let fileExtension = fileURL.pathExtension - if let builtIn = fileExtensionToLanguageId[fileExtension] { - return .builtIn(builtIn) - } - return .init(rawValue: fileExtension) ?? .plaintext -} - diff --git a/Tool/Sources/SuggestionBasic/Modification.swift b/Tool/Sources/SuggestionBasic/Modification.swift deleted file mode 100644 index 5e35c96e..00000000 --- a/Tool/Sources/SuggestionBasic/Modification.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation - -public enum Modification: Codable, Equatable { - case deleted(ClosedRange) - case inserted(Int, [String]) - case deletedSelection(CursorRange) -} - -public extension [String] { - mutating func apply(_ modifications: [Modification]) { - for modification in modifications { - switch modification { - case let .deleted(range): - if isEmpty { break } - let removingRange = range.lowerBound..<(range.upperBound + 1) - removeSubrange(removingRange.clamped(to: 0.. Array { - var newArray = self - newArray.apply(modifications) - return newArray - } -} - -public extension NSMutableArray { - func apply(_ modifications: [Modification]) { - for modification in modifications { - switch modification { - case let .deleted(range): - if count == 0 { break } - let newRange = range.clamped(to: 0...(count - 1)) - removeObjects(in: NSRange(newRange)) - case let .inserted(index, strings): - for string in strings.reversed() { - insert(string, at: Swift.min(count, index)) - } - case let .deletedSelection(cursorRange): - if count == 0 { break } - let startLine = cursorRange.start.line - let startCharacter = cursorRange.start.character - let endLine = cursorRange.end.line - let endCharacter = cursorRange.end.character - - guard startLine < count && endLine < count else { break } - - if startLine == endLine { - if let line = self[startLine] as? String { - let startIndex = line.index(line.startIndex, offsetBy: startCharacter) - let endIndex = line.index(line.startIndex, offsetBy: endCharacter) - let newLine = line.replacingCharacters(in: startIndex.. [Substring] { - if fast { - let lineEndingInText = lineEnding - return split( - separator: lineEndingInText, - omittingEmptySubsequences: omittingEmptySubsequences - ) - } - return split( - omittingEmptySubsequences: omittingEmptySubsequences, - whereSeparator: \.isNewline - ) - } - - /// Break a string into lines. - func breakLines( - proposedLineEnding: String? = nil, - appendLineBreakToLastLine: Bool = false - ) -> [String] { - let lineEndingInText = lineEnding - let lineEnding = proposedLineEnding ?? String(lineEndingInText) - // Split on character for better performance. - let lines = split(separator: lineEndingInText, omittingEmptySubsequences: false) - var all = [String]() - for (index, line) in lines.enumerated() { - if !appendLineBreakToLastLine, index == lines.endIndex - 1 { - all.append(String(line)) - } else { - all.append(String(line) + lineEnding) - } - } - return all - } -} - diff --git a/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift deleted file mode 100644 index e69e29d2..00000000 --- a/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Foundation -import SuggestionBasic - -public struct PostProcessingSuggestionServiceMiddleware: SuggestionServiceMiddleware { - public init() {} - - public func getSuggestion( - _ request: SuggestionRequest, - configuration: SuggestionServiceConfiguration, - next: Next - ) async throws -> [CodeSuggestion] { - let suggestions = try await next(request) - - return suggestions.compactMap { - var suggestion = $0 - if suggestion.text.allSatisfy({ $0.isWhitespace || $0.isNewline }) { return nil } - Self.removeTrailingWhitespacesAndNewlines(&suggestion) - if !Self.checkIfSuggestionHasNoEffect(suggestion, request: request) { return nil } - return suggestion - } - } - - static func removeTrailingWhitespacesAndNewlines(_ suggestion: inout CodeSuggestion) { - var text = suggestion.text[...] - while let last = text.last, last.isNewline || last.isWhitespace { - text = text.dropLast(1) - } - suggestion.text = String(text) - } - - static func checkIfSuggestionHasNoEffect( - _ suggestion: CodeSuggestion, - request: SuggestionRequest - ) -> Bool { - // We only check suggestions that are on a single line. - if suggestion.range.isOneLine { - let line = suggestion.range.start.line - if line >= 0, line < request.lines.count { - let replacingText = request.lines[line] - - let start = suggestion.range.start.character - let end = suggestion.range.end.character - if let endIndex = replacingText.utf16.index( - replacingText.startIndex, - offsetBy: end, - limitedBy: replacingText.endIndex - ), - let startIndex = replacingText.utf16.index( - replacingText.startIndex, - offsetBy: start, - limitedBy: endIndex - ), - startIndex < endIndex - { - let replacingRange = startIndex.. [CodeSuggestion] - - func getSuggestion( - _ request: SuggestionRequest, - configuration: SuggestionServiceConfiguration, - next: Next - ) async throws -> [CodeSuggestion] -} - -public enum SuggestionServiceMiddlewareContainer { - static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ - DisabledLanguageSuggestionServiceMiddleware(), - PostProcessingSuggestionServiceMiddleware() - ] - - static var customMiddlewares: [SuggestionServiceMiddleware] = [] - - public static var middlewares: [SuggestionServiceMiddleware] { - builtInMiddlewares + customMiddlewares - } - - public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { - customMiddlewares.append(middleware) - } -} - -public struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware { - public init() {} - - public func getSuggestion( - _ request: SuggestionRequest, - configuration: SuggestionServiceConfiguration, - next: Next - ) async throws -> [CodeSuggestion] { - let language = languageIdentifierFromFileURL(request.fileURL) - if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) - .contains(where: { $0 == language.rawValue }) - { - #if DEBUG - Logger.service.info("Suggestion service is disabled for \(language).") - #endif - return [] - } - - return try await next(request) - } -} - -public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { - public init() {} - - public func getSuggestion( - _ request: SuggestionRequest, - configuration: SuggestionServiceConfiguration, - next: Next - ) async throws -> [CodeSuggestion] { - Logger.service.info(""" - Get suggestion for \(request.fileURL) at \(request.cursorPosition) - """) - do { - let suggestions = try await next(request) - Logger.service.info(""" - Receive \(suggestions.count) suggestions for \(request.fileURL) \ - at \(request.cursorPosition) - """) - return suggestions - } catch { - Logger.service.info(""" - Error: \(error.localizedDescription) - """) - throw error - } - } -} - diff --git a/Tool/Sources/SuggestionProvider/SuggestionServiceProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionServiceProvider.swift deleted file mode 100644 index 24265613..00000000 --- a/Tool/Sources/SuggestionProvider/SuggestionServiceProvider.swift +++ /dev/null @@ -1,79 +0,0 @@ -import AppKit -import struct CopilotForXcodeKit.SuggestionServiceConfiguration -import struct CopilotForXcodeKit.WorkspaceInfo -import Foundation -import Preferences -import SuggestionBasic -import UserDefaultsObserver - -public struct SuggestionRequest { - public var fileURL: URL - public var relativePath: String - public var content: String - public var originalContent: String - public var lines: [String] - public var cursorPosition: CursorPosition - public var cursorOffset: Int - public var tabSize: Int - public var indentSize: Int - public var usesTabsForIndentation: Bool - public var relevantCodeSnippets: [RelevantCodeSnippet] - - public init( - fileURL: URL, - relativePath: String, - content: String, - originalContent: String, - lines: [String], - cursorPosition: CursorPosition, - cursorOffset: Int, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - relevantCodeSnippets: [RelevantCodeSnippet] - ) { - self.fileURL = fileURL - self.relativePath = relativePath - self.content = content - self.originalContent = content - self.lines = lines - self.cursorPosition = cursorPosition - self.cursorOffset = cursorOffset - self.tabSize = tabSize - self.indentSize = indentSize - self.usesTabsForIndentation = usesTabsForIndentation - self.relevantCodeSnippets = relevantCodeSnippets - } -} - -public struct RelevantCodeSnippet: Codable { - public var content: String - public var priority: Int - public var filePath: String - - public init(content: String, priority: Int, filePath: String) { - self.content = content - self.priority = priority - self.filePath = filePath - } -} - -public protocol SuggestionServiceProvider { - func getSuggestions( - _ request: SuggestionRequest, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async throws -> [CodeSuggestion] - func notifyAccepted( - _ suggestion: CodeSuggestion, - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async - func notifyRejected( - _ suggestions: [CodeSuggestion], - workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async - func cancelRequest(workspaceInfo: CopilotForXcodeKit.WorkspaceInfo) async - - var configuration: SuggestionServiceConfiguration { get async } -} - -public typealias SuggestionServiceConfiguration = CopilotForXcodeKit.SuggestionServiceConfiguration diff --git a/Tool/Sources/SystemUtils/FileUtils.swift b/Tool/Sources/SystemUtils/FileUtils.swift deleted file mode 100644 index 0af7e34e..00000000 --- a/Tool/Sources/SystemUtils/FileUtils.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -public struct FileUtils{ - public typealias ReadabilityErrorMessageProvider = (ReadabilityStatus) -> String? - - public enum ReadabilityStatus { - case readable - case notFound - case permissionDenied - - public var isReadable: Bool { - switch self { - case .readable: true - case .notFound, .permissionDenied: false - } - } - - public func errorMessage(using provider: ReadabilityErrorMessageProvider? = nil) -> String? { - if let provider = provider { - return provider(self) - } - - // Default error messages - switch self { - case .readable: - return nil - case .notFound: - return "File may have been removed or is unavailable." - case .permissionDenied: - return "Permission Denied to access file." - } - } - } - - public static func checkFileReadability(at path: String) -> ReadabilityStatus { - let fileManager = FileManager.default - if fileManager.fileExists(atPath: path) { - if fileManager.isReadableFile(atPath: path) { - return .readable - } else { - return .permissionDenied - } - } else { - return .notFound - } - } -} diff --git a/Tool/Sources/SystemUtils/SystemUtils.swift b/Tool/Sources/SystemUtils/SystemUtils.swift deleted file mode 100644 index 43569b88..00000000 --- a/Tool/Sources/SystemUtils/SystemUtils.swift +++ /dev/null @@ -1,249 +0,0 @@ -import Foundation -import Logger -import IOKit -import CryptoKit - -public class SystemUtils { - public static let shared = SystemUtils() - - // Static properties for constant values - public static let machineId: String = { - return shared.computeMachineId() - }() - - public static let osVersion: String = { - return "\(ProcessInfo.processInfo.operatingSystemVersion.majorVersion).\(ProcessInfo.processInfo.operatingSystemVersion.minorVersion).\(ProcessInfo.processInfo.operatingSystemVersion.patchVersion)" - }() - - public static let xcodeVersion: String? = { - return shared.computeXcodeVersion() - }() - - public static let editorVersionString: String = { - return "Xcode/\(xcodeVersion ?? "0.0.0")" - }() - - public static let editorPluginVersion: String? = { - return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - }() - - public static let editorPluginVersionString: String = { - return "\(editorPluginVersion ?? "0.0.0")" - }() - - public static let build: String = { - return shared.isDeveloperMode() ? "dev" : "" - }() - - public static let buildType: String = { - return shared.isDeveloperMode() ? "true" : "false" - }() - - private init() {} - - // Renamed to computeMachineId since it's now an internal implementation detail - private func computeMachineId() -> String { - // Original getMachineId implementation - let matchingDict = IOServiceMatching("IOEthernetInterface") as NSMutableDictionary - var iterator: io_iterator_t = 0 - let result = IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) - - if result != KERN_SUCCESS { - return UUID().uuidString - } - - var macAddress: String = "" - var service = IOIteratorNext(iterator) - - while service != 0 { - var parentService: io_object_t = 0 - let kernResult = IORegistryEntryGetParentEntry(service, "IOService", &parentService) - - if kernResult == KERN_SUCCESS { - let propertyPtr = UnsafeMutablePointer?>.allocate(capacity: 1) - _ = IORegistryEntryCreateCFProperties( - parentService, - propertyPtr, - kCFAllocatorDefault, - 0 - ) - - if let properties = propertyPtr.pointee?.takeUnretainedValue() as? [String: Any], - let data = properties["IOMACAddress"] as? Data { - macAddress = data.map { String(format: "%02x", $0) }.joined() - IOObjectRelease(parentService) - break - } - - IOObjectRelease(parentService) - } - - IOObjectRelease(service) - service = IOIteratorNext(iterator) - } - - IOObjectRelease(iterator) - - // Hash the MAC address using SHA256 - if !macAddress.isEmpty, let macData = macAddress.data(using: .utf8) { - let hashedData = SHA256.hash(data: macData) - return hashedData.compactMap { String(format: "%02x", $0) }.joined() - } - - return "unknown" - } - - public func getXcodeBinaryPath() -> String { - var systemInfo = utsname() - uname(&systemInfo) - - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } - - let path: String - if identifier == "x86_64" { - path = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/copilot-language-server").path - } else if identifier == "arm64" { - path = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/copilot-language-server-arm64").path - } else { - fatalError("Unsupported architecture") - } - - return path - } - - private func computeXcodeVersion() -> String? { - let process = Process() - let pipe = Pipe() - - defer { - pipe.fileHandleForReading.closeFile() - if process.isRunning { - process.terminate() - } - } - - process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") - process.arguments = ["xcodebuild", "-version"] - process.standardOutput = pipe - - do { - try process.run() - } catch { - print("Error running xcrun xcodebuild: \(error)") - return nil - } - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let output = String(data: data, encoding: .utf8) else { - return nil - } - - let lines = output.split(separator: "\n") - return lines.first?.split(separator: " ").last.map(String.init) - } - - public func getEditorVersionString() -> String { - return "Xcode/\(computeXcodeVersion() ?? "0.0.0")" - } - - public func getEditorPluginVersion() -> String? { - return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - } - - public func getEditorPluginVersionString() -> String { - return "copilot-xcode/\(getEditorPluginVersion() ?? "0.0.0")" - } - - public func getBuild() -> String { - return isDeveloperMode() ? "dev" : "" - } - - public func getBuildType() -> String { - return isDeveloperMode() ? "true" : "false" - } - - func isDeveloperMode() -> Bool { - #if DEBUG - return true - #else - return false - #endif - } - - /// Returns the environment of a login shell (to get correct PATH and other variables) - public func getLoginShellEnvironment(shellPath: String = "/bin/zsh") -> [String: String]? { - do { - guard let output = try Self.executeCommand( - path: shellPath, - arguments: ["-i", "-l", "-c", "env"]) - else { return nil } - - var env: [String: String] = [:] - for line in output.split(separator: "\n") { - if let idx = line.firstIndex(of: "=") { - let key = String(line[.. String? { - let task = Process() - let pipe = Pipe() - - defer { - pipe.fileHandleForReading.closeFile() - if task.isRunning { - task.terminate() - } - } - - task.executableURL = URL(fileURLWithPath: path) - task.arguments = arguments - task.standardOutput = pipe - task.currentDirectoryURL = URL(fileURLWithPath: directory) - - try task.run() - task.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - return String(data: data, encoding: .utf8) - } - - public func appendCommonBinPaths(path: String) -> String { - let homeDirectory = NSHomeDirectory() - let commonPaths = [ - "/usr/local/bin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - homeDirectory + "/.local/bin", - "/opt/homebrew/bin", - "/opt/homebrew/sbin", - ] - - let paths = path.split(separator: ":").map { String($0) } - var newPath = path - for commonPath in commonPaths { - if FileManager.default.fileExists(atPath: commonPath) && !paths.contains(commonPath) { - newPath += (newPath.isEmpty ? "" : ":") + commonPath - } - } - - return newPath - } -} diff --git a/Tool/Sources/TelemetryService/GithubPanicErrorReporter.swift b/Tool/Sources/TelemetryService/GithubPanicErrorReporter.swift deleted file mode 100644 index a7bb0763..00000000 --- a/Tool/Sources/TelemetryService/GithubPanicErrorReporter.swift +++ /dev/null @@ -1,201 +0,0 @@ -import Foundation -import TelemetryServiceProvider -import UserDefaultsObserver -import Preferences - -public class GitHubPanicErrorReporter { - private static let panicEndpoint = URL(string: "https://copilot-telemetry.githubusercontent.com/telemetry")! - private static let sessionId = UUID().uuidString - private static let standardChannelKey = Bundle.main - .object(forInfoDictionaryKey: "STANDARD_TELEMETRY_CHANNEL_KEY") as! String - - private static let userDefaultsObserver = UserDefaultsObserver( - object: UserDefaults.shared, - forKeyPaths: [ - UserDefaultPreferenceKeys().gitHubCopilotProxyUrl.key, - UserDefaultPreferenceKeys().gitHubCopilotProxyUsername.key, - UserDefaultPreferenceKeys().gitHubCopilotProxyPassword.key, - UserDefaultPreferenceKeys().gitHubCopilotUseStrictSSL.key, - ], - context: nil - ) - - // Use static initializer to set up the observer - private static let _initializer: Void = { - userDefaultsObserver.onChange = { - urlSession = configuredURLSession() - } - }() - - private static var urlSession: URLSession = { - // Initialize urlSession after observer setup - _ = _initializer - return configuredURLSession() - }() - - // Helper: Format current time in ISO8601 style - private static func currentTime() -> String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSX" - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - return formatter.string(from: Date()) - } - - // Helper: Create failbot payload JSON string and update properties - private static func createFailbotPayload( - for request: TelemetryExceptionRequest, - properties: inout [String: Any] - ) -> String? { - let payload: [String: Any] = [ - "context": [:], - "app": "copilot-xcode", - "catalog_service": "CopilotXcode", - "release": "copilot-xcode@\(properties["common_extversion"] ?? "0.0.0")", - "rollup_id": "auto", - "platform": "macOS", - "exception_detail": request.exceptionDetail?.toDictionary() ?? [] - ] - guard let data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { - return nil - } - return String(data: data, encoding: .utf8) - } - - // Helper: Create payload with a channel input, but always using standard telemetry key. - private static func createPayload( - for request: TelemetryExceptionRequest, - properties: inout [String: Any] - ) -> [String: Any] { - // Build and add failbot payload to properties - if let payloadString = createFailbotPayload(for: request, properties: &properties) { - properties["failbot_payload"] = payloadString - } - properties["common_vscodesessionid"] = sessionId - properties["client_sessionid"] = sessionId - - let baseData: [String: Any] = [ - "ver": 2, - "severityLevel": "Error", - "name": "agent/error.exception", - "properties": properties, - "exceptions": [], - "measurements": [:] - ] - - return [ - "ver": 1, - "time": currentTime(), - "severityLevel": "Error", - "name": "Microsoft.ApplicationInsights.standard.Event", - "iKey": standardChannelKey, - "data": [ - "baseData": baseData, - "baseType": "ExceptionData" - ] - ] - } - - private static func configuredURLSession() -> URLSession { - let proxyURL = UserDefaults.shared.value(for: \.gitHubCopilotProxyUrl) - let strictSSL = UserDefaults.shared.value(for: \.gitHubCopilotUseStrictSSL) - - // If no proxy, use shared session - if proxyURL.isEmpty { - return .shared - } - - let configuration = URLSessionConfiguration.default - - if let url = URL(string: proxyURL) { - var proxyConfig: [String: Any] = [:] - let scheme = url.scheme?.lowercased() - - // Set proxy type based on URL scheme - switch scheme { - case "https": - proxyConfig[kCFProxyTypeKey as String] = kCFProxyTypeHTTPS - proxyConfig[kCFNetworkProxiesHTTPSEnable as String] = true - proxyConfig[kCFNetworkProxiesHTTPSProxy as String] = url.host - proxyConfig[kCFNetworkProxiesHTTPSPort as String] = url.port - case "socks", "socks5": - proxyConfig[kCFProxyTypeKey as String] = kCFProxyTypeSOCKS - proxyConfig[kCFNetworkProxiesSOCKSEnable as String] = true - proxyConfig[kCFNetworkProxiesSOCKSProxy as String] = url.host - proxyConfig[kCFNetworkProxiesSOCKSPort as String] = url.port - default: - proxyConfig[kCFProxyTypeKey as String] = kCFProxyTypeHTTP - proxyConfig[kCFProxyHostNameKey as String] = url.host - proxyConfig[kCFProxyPortNumberKey as String] = url.port - } - - // Add proxy authentication if configured - let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) - let password = UserDefaults.shared.value(for: \.gitHubCopilotProxyPassword) - if !username.isEmpty { - proxyConfig[kCFProxyUsernameKey as String] = username - proxyConfig[kCFProxyPasswordKey as String] = password - } - - configuration.connectionProxyDictionary = proxyConfig - } - - // Configure SSL verification - if strictSSL { - return URLSession(configuration: configuration) - } - - let sessionDelegate = CustomURLSessionDelegate() - - return URLSession( - configuration: configuration, - delegate: sessionDelegate, - delegateQueue: nil - ) - } - - private class CustomURLSessionDelegate: NSObject, URLSessionDelegate { - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - // Accept all certificates when strict SSL is disabled - guard let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - let credential = URLCredential(trust: serverTrust) - completionHandler(.useCredential, credential) - } - } - - public static func report(_ request: TelemetryExceptionRequest) async { - do { - var properties: [String : Any] = request.properties ?? [:] - let payload = createPayload( - for: request, - properties: &properties - ) - - let jsonData = try JSONSerialization.data(withJSONObject: [payload], options: []) - var httpRequest = URLRequest(url: panicEndpoint) - httpRequest.httpMethod = "POST" - httpRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") - httpRequest.httpBody = jsonData - - // Use the cached URLSession instead of creating a new one - let (_, response) = try await urlSession.data(for: httpRequest) - #if DEBUG - guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else { - throw URLError(.badServerResponse) - } - #endif - } catch { - #if DEBUG - print("Fails to send to Panic Endpoint: \(error)") - #endif - } - } -} diff --git a/Tool/Sources/TelemetryService/TelemetryCleaner.swift b/Tool/Sources/TelemetryService/TelemetryCleaner.swift deleted file mode 100644 index 069ad843..00000000 --- a/Tool/Sources/TelemetryService/TelemetryCleaner.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Foundation - -// reference the redact algorithm from https://github.com/microsoft/vscode/blame/main/src/vs/platform/telemetry/common/telemetryUtils.ts -public struct TelemetryCleaner { - private let cleanupPatterns: [NSRegularExpression] - - public init(cleanupPatterns: [NSRegularExpression]) { - self.cleanupPatterns = cleanupPatterns - } - - public func redactMap(_ data: [String: Any]?) -> [String: Any]? { - guard let data = data else { - return nil - } - return data.mapValues { value in - if let stringValue = value as? String { - return redact(stringValue) ?? "" - } - - return value - } - } - - public func redact(_ value: String?) -> String? { - guard let value = value else { - return nil - } - var cleanedValue = value.replacingOccurrences(of: "%20", with: " ") - cleanedValue = anonymizeFilePaths(cleanedValue) - cleanedValue = removeUserInfo(cleanedValue) - return cleanedValue - } - - private func anonymizeFilePaths(_ stack: String) -> String { - guard stack.contains("/") || stack.contains("\\") else { - return stack - } - - var updatedStack = stack - for pattern in cleanupPatterns { - updatedStack = pattern.stringByReplacingMatches( - in: updatedStack, - range: NSRange(updatedStack.startIndex..., in: updatedStack), - withTemplate: "" - ) - } - - // Replace file paths with redacted marker - let filePattern = try! NSRegularExpression( - pattern: "(file:\\/\\/)?([a-zA-Z]:(\\\\|\\/)|(\\\\\\\\/|\\\\|\\/))?([\\w-\\._]+(\\\\|\\/))+" - ) - updatedStack = filePattern.stringByReplacingMatches( - in: updatedStack, - range: NSRange(updatedStack.startIndex..., in: updatedStack), - withTemplate: "" - ) - - return updatedStack - } - - private func removeUserInfo(_ value: String) -> String { - let patterns: [(label: String, pattern: String)] = [ - ("Google API Key", "AIza[A-Za-z0-9_\\\\\\-]{35}"), - ("Slack Token", "xox[pbar]\\-[A-Za-z0-9]"), - ("GitHub Token", "(gh[psuro]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})"), - ("Generic Secret", "(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]"), - ("CLI Credentials", "((login|psexec|(certutil|psexec)\\.exe).{1,50}(\\s-u(ser(name)?)?\\s+.{3,100})?\\s-(admin|user|vm|root)?p(ass(word)?)?\\s+[\"']?[^$\\-\\/\\s]|(^|[\\s\\r\\n\\])net(\\.exe)?.{1,5}(user\\s+|share\\s+\\/user:| user -? secrets ? set) \\s + [^ $\\s \\/])"), - ("Microsoft Entra ID", "eyJ(?:0eXAiOiJKV1Qi|hbGci|[a-zA-Z0-9\\-_]+\\.[a-zA-Z0-9\\-_]+\\.)"), - ("Email", "@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+") - ] - - var cleanedValue = value - for (label, pattern) in patterns { - if let regex = try? NSRegularExpression(pattern: pattern) { - if regex.firstMatch( - in: cleanedValue, - range: NSRange(cleanedValue.startIndex..., in: cleanedValue) - ) != nil { - return "" - } - } - } - - return cleanedValue - } -} diff --git a/Tool/Sources/TelemetryService/TelemetryService.swift b/Tool/Sources/TelemetryService/TelemetryService.swift deleted file mode 100644 index 79cda709..00000000 --- a/Tool/Sources/TelemetryService/TelemetryService.swift +++ /dev/null @@ -1,337 +0,0 @@ -import Foundation -import SystemUtils -import TelemetryServiceProvider -import BuiltinExtension -import GitHubCopilotService - -public protocol WrappedTelemetryServiceType { - func sendError( - _ error: Error?, - transaction: String?, - additionalProperties: [String: String]?, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - from symbols: [String] - ) - - func sendError( - _ message: String, - transaction: String?, - additionalProperties: [String: String]?, - category: String, - file: StaticString, - line: UInt, - function: StaticString, - from symbols: [String] - ) -} - -public actor TelemetryService: WrappedTelemetryServiceType { - private let telemetryProvider: TelemetryServiceProvider? - private var commonProperties: [String: String] = [:] - private let telemetryCleaner: TelemetryCleaner = TelemetryCleaner(cleanupPatterns: []) - - public static var shared: TelemetryService = TelemetryService.service() - - init( - provider: any TelemetryServiceProvider - ) { - telemetryProvider = provider - self.commonProperties = [ - "common_extname": "copilot-xcode", - "common_extversion": SystemUtils.editorPluginVersionString, - "common_os": "darwin", - "common_platformversion": SystemUtils.osVersion, - "common_uikind": "desktop", - "common_vscodemachineid": SystemUtils.machineId, - "client_machineid": SystemUtils.machineId, - "editor_version": SystemUtils.editorVersionString, - "editor_plugin_version": "copilot-xcode/\(SystemUtils.editorPluginVersionString)", - "copilot_build": SystemUtils.build, - "copilot_buildType": SystemUtils.buildType - ] - } - - public static func service() -> TelemetryService { - let provider = BuiltinExtensionTelemetryServiceProvider( - extension: GitHubCopilotExtension.self - ) - return TelemetryService(provider: provider) - } - - enum TelemetryServiceError: Error { - case providerNotFound - } - - private enum ErrorSource { - case message(String) - case error(Error?) - } - - /// Sends an error with the given parameters - public nonisolated func sendError( - _ error: Error?, - transaction: String? = nil, - additionalProperties: [String: String]? = nil, - category: String = "", - file: StaticString, - line: UInt, - function: StaticString, - from symbols: [String] - ) { - Task.detached(priority: .background) { - await self.sendErrorInternal( - .error(error), - transaction: transaction, - additionalProperties: additionalProperties, - category: category, - file: file, - line: line, - function: function, - from: symbols - ) - } - } - - /// Sends an error message with the given parameters - public nonisolated func sendError( - _ message: String, - transaction: String? = nil, - additionalProperties: [String: String]? = nil, - category: String = "", - file: StaticString, - line: UInt, - function: StaticString, - from symbols: [String] - ) { - Task.detached(priority: .background) { - await self.sendErrorInternal( - .message(message), - transaction: transaction, - additionalProperties: additionalProperties, - category: category, - file: file, - line: line, - function: function, - from: symbols - ) - } - } - - /// Internal implementation for sending errors - private func sendErrorInternal( - _ source: ErrorSource, - transaction: String? = nil, - additionalProperties: [String: String]? = nil, - category: String = "", - file: StaticString, - line: UInt, - function: StaticString, - from symbols: [String] - ) async { - var props = commonProperties - additionalProperties?.forEach { props[$0.key] = $0.value } - let fileName: String = telemetryCleaner.redact(String(describing: file)) ?? "" - let request = createTelemetryExceptionRequest( - errorSource: source, - transaction: transaction, - additionalProperties: props, - category: category, - file: fileName, - line: line, - function: function, - symbols: symbols - ) - - do { - if let provider = telemetryProvider { - try await provider.sendError(request) - } else { - throw TelemetryServiceError.providerNotFound - } - } catch { - await GitHubPanicErrorReporter.report(request) - } - } - - /// Creates a telemetry exception request from the given parameters - private func createTelemetryExceptionRequest( - errorSource: ErrorSource, - transaction: String?, - additionalProperties: [String: String], - category: String, - file: String, - line: UInt, - function: StaticString, - symbols: [String] - ) -> TelemetryExceptionRequest { - let stacktrace: String? = switch errorSource { - case .message(let message): - message - case .error(let error): - error?.localizedDescription - } - - let exceptionDetails = convertErrorToExceptionDetails( - errorSource, - category: category, - file: file, - line: line, - function: function, - from: symbols - ) - - return TelemetryExceptionRequest( - transaction: transaction, - stacktrace: telemetryCleaner.redact(stacktrace), - properties: additionalProperties, - platform: "macOS", - exceptionDetail: exceptionDetails - ) - } - - /// Converts error source to exception details array - private func convertErrorToExceptionDetails( - _ errorSource: ErrorSource, - category: String, - file: String, - line: UInt, - function: StaticString, - from symbols: [String] - ) -> [ExceptionDetail] { - let (errorType, errorValue) = extractErrorInfo(from: errorSource, category: category) - let stackFrames = createStackFrames( - errorSource: errorSource, - file: file, - line: line, - function: function, - symbols: symbols - ) - - return [ - ExceptionDetail( - type: errorType, - value: telemetryCleaner.redact(errorValue), - stacktrace: stackFrames - ) - ] - } - - /// Extracts error type and value from error source - private func extractErrorInfo(from errorSource: ErrorSource, category: String) -> (type: String, value: String) { - switch errorSource { - case .message(let message): - let type = "ErrorMessage \(category)" - return (type, message) - - case .error(let error): - guard let error = error else { - let type = "UnknownError \(category)" - return (type, "Unknown error occurred") - } - - var typePrefix = String(describing: type(of: error)) - if typePrefix == "NSError" { - let nsError = error as NSError - typePrefix += ":\(nsError.domain):\(nsError.code)" - } - - let type = typePrefix + " \(category)" - return (type, error.localizedDescription) - } - } - - /// Creates stack trace frames from error information - private func createStackFrames( - errorSource: ErrorSource, - file: String, - line: UInt, - function: StaticString, - symbols: [String] - ) -> [StackTraceFrame] { - let callSiteFrame = StackTraceFrame( - filename: file, - lineno: .integer(Int(line)), - colno: nil, - function: String(describing: function), - inApp: true - ) - - switch errorSource { - case .message: - return [callSiteFrame] - - case .error: - var frames = parseStackFrames(from: symbols) - frames.insert(callSiteFrame, at: 0) - return frames - } - } - - /// Parses call stack symbols into stack trace frames - private func parseStackFrames(from symbols: [String]) -> [StackTraceFrame] { - symbols.map { symbol -> StackTraceFrame? in - let pattern = #"^(\d+)\s+(.+?)\s+(0x[0-9a-fA-F]+)\s+(.+?)\s+\+\s+(\d+)$"# - guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return nil } - guard let match = regex.firstMatch(in: symbol, range: NSRange(symbol.startIndex..., in: symbol)) else { return nil } - - let components = (1.. String in - if let range = Range(match.range(at: i), in: symbol) { - return String(symbol[range]) - } - return "" - } - - guard components.count == 5, - let offset = Int(components[4]) else { return nil } - - let module = components[1] - let parsedSymbol = parseDemangledSymbol(swift_demangle(components[3])) - - return StackTraceFrame( - filename: parsedSymbol?.module ?? module, - lineno: .integer(offset), - colno: nil, - function: parsedSymbol?.function ?? components[3], - inApp: module.contains("GitHub Copilot for Xcode Extension") - ) - }.compactMap { $0 } - } - - /// Demangles Swift symbol names using the Swift runtime - typealias Swift_Demangle = @convention(c) (_ mangledName: UnsafePointer?, - _ mangledNameLength: Int, - _ outputBuffer: UnsafeMutablePointer?, - _ outputBufferSize: UnsafeMutablePointer?, - _ flags: UInt32) -> UnsafeMutablePointer? - - func swift_demangle(_ mangled: String) -> String { - let RTLD_DEFAULT = dlopen(nil, RTLD_NOW) - if let sym = dlsym(RTLD_DEFAULT, "swift_demangle") { - let f = unsafeBitCast(sym, to: Swift_Demangle.self) - if let cString = f(mangled, mangled.count, nil, nil, 0) { - defer { cString.deallocate() } - return String(cString: cString) - } - } - return "" - } - - /// Parses demangled symbol into module and function components - func parseDemangledSymbol(_ demangled: String) -> (module: String, function: String)? { - let regex = try! NSRegularExpression( - pattern: #"^\((\d+)\)\s*(.*?)\s*for\s*([^\s]+(?: [^\s]+)*?)\s*((?:async)?)\s*((?:throws)?)\s*(?:->\s*(.*))?$"#, - options: [.anchorsMatchLines] - ) - guard let match = regex.firstMatch( - in: demangled, options: [], - range: NSRange(location: 0, length: demangled.utf16.count) - ) else { - return nil - } - let functionName = (demangled as NSString).substring(with: match.range(at: 3)) - return (module: functionName, function: demangled) - } -} diff --git a/Tool/Sources/TelemetryServiceProvider/TelemetryServiceProvider.swift b/Tool/Sources/TelemetryServiceProvider/TelemetryServiceProvider.swift deleted file mode 100644 index b82df33a..00000000 --- a/Tool/Sources/TelemetryServiceProvider/TelemetryServiceProvider.swift +++ /dev/null @@ -1,167 +0,0 @@ -import CopilotForXcodeKit -import Foundation -import CodableWrappers - -public protocol TelemetryServiceType { - func sendError( - _ request: TelemetryExceptionRequest, - workspace: WorkspaceInfo - ) async throws -} - -public protocol TelemetryServiceProvider { - func sendError(_ request: TelemetryExceptionRequest) async throws -} - -/// Represents a telemetry exception request, containing error details and additional properties. -public struct TelemetryExceptionRequest { - /// An identifier to group or track the transaction. - public let transaction: String? - /// The error stacktrace as a string. - public let stacktrace: String? - /// Additional telemetry properties as key-value pairs. - public let properties: [String: String]? - /// The target platform information (default to macOS). - public let platform: String? - /// A list of detailed exceptions, each with its own context. - public let exceptionDetail: [ExceptionDetail]? - - public init( - transaction: String? = nil, - stacktrace: String? = nil, - properties: [String: String]? = nil, - platform: String? = nil, - exceptionDetail: [ExceptionDetail]? = nil - ) { - self.transaction = transaction - self.stacktrace = stacktrace - self.properties = properties - self.platform = platform - self.exceptionDetail = exceptionDetail - } -} - -public struct ExceptionDetail: Codable { - public let type: String? - public let value: String? - public let stacktrace: [StackTraceFrame]? - - public init(type: String? = nil, value: String? = nil, stacktrace: [StackTraceFrame]? = nil) { - self.type = type - self.value = value - self.stacktrace = stacktrace - } - - func toDictionary() -> [String: Any] { - var dict: [String: Any] = [:] - if let type = type { - dict["type"] = type - } - if let value = value { - dict["value"] = value - } - if let stacktrace = stacktrace { - dict["stacktrace"] = stacktrace.map { $0.toDictionary() } - } - return dict - } -} - -public struct StackTraceFrame: Codable { - public let filename: String? - public let lineno: PositionNumberType? - public let colno: PositionNumberType? - public let function: String? - public let inApp: Bool? - - public init( - filename: String? = nil, - lineno: PositionNumberType? = nil, - colno: PositionNumberType? = nil, - function: String? = nil, - inApp: Bool? = nil - ) { - self.filename = filename - self.lineno = lineno - self.colno = colno - self.function = function - self.inApp = inApp - } - - enum CodingKeys: String, CodingKey { - case filename - case lineno - case colno - case function - case inApp = "in_app" - } - - func toDictionary() -> [String: Any] { - var dict: [String: Any] = [:] - if let filename = filename { - dict["filename"] = filename - } - if let lineno = lineno { - dict["lineno"] = lineno.toAny() - } - if let colno = colno { - dict["colno"] = colno.toAny() - } - if let function = function { - dict["function"] = function - } - if let inApp = inApp { - dict["in_app"] = inApp - } - return dict - } -} - -public enum PositionNumberType: Codable { - case string(String) - case integer(Int) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let stringValue = try? container.decode(String.self) { - self = .string(stringValue) - } else if let intValue = try? container.decode(Int.self) { - self = .integer(intValue) - } else { - self = .string("") - } - } - - public init(fromInt intValue: Int) { - self = .integer(intValue) - } - - public init(fromString stringValue: String) { - self = .string(stringValue) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .string(let value): - try container.encode(value) - case .integer(let value): - try container.encode(value) - } - } - - func toAny() -> Any { - switch self { - case .string(let value): - return value - case .integer(let value): - return value - } - } -} - -extension Array where Element == ExceptionDetail { - public func toDictionary() -> [[String: Any]] { - return self.map { $0.toDictionary() } - } -} diff --git a/Tool/Sources/Terminal/Terminal.swift b/Tool/Sources/Terminal/Terminal.swift deleted file mode 100644 index 89812c4b..00000000 --- a/Tool/Sources/Terminal/Terminal.swift +++ /dev/null @@ -1,200 +0,0 @@ -import AppKit -import Foundation - -public protocol TerminalType { - func streamCommand( - _ command: String, - arguments: [String], - currentDirectoryURL: URL?, - environment: [String: String] - ) -> AsyncThrowingStream - - func runCommand( - _ command: String, - arguments: [String], - currentDirectoryURL: URL?, - environment: [String: String] - ) async throws -> String - - func terminate() async - func writeInput(_ input: String) async - var isRunning: Bool { get } -} - -public final class Terminal: TerminalType, @unchecked Sendable { - var process: Process? - var outputPipe: Pipe? - var inputPipe: Pipe? - - public var isRunning: Bool { process?.isRunning ?? false } - - public struct TerminationError: Error { - public let reason: Process.TerminationReason - public let status: Int32 - } - - public init() {} - - func getEnvironmentVariables() -> [String: String] { - let env = ProcessInfo.processInfo.environment - .merging(["LANG": "en_US.UTF-8"], uniquingKeysWith: { $1 }) - return env - } - - public func streamCommand( - _ command: String = "/bin/bash", - arguments: [String], - currentDirectoryURL: URL? = nil, - environment: [String: String] - ) -> AsyncThrowingStream { - self.process?.terminate() - let process = Process() - self.process = process - - process.launchPath = command - process.currentDirectoryURL = currentDirectoryURL - process.arguments = arguments - process.environment = getEnvironmentVariables() - .merging(environment, uniquingKeysWith: { $1 }) - - let outputPipe = Pipe() - process.standardOutput = outputPipe - process.standardError = outputPipe - self.outputPipe = outputPipe - - let inputPipe = Pipe() - process.standardInput = inputPipe - self.inputPipe = inputPipe - - var continuation: AsyncThrowingStream.Continuation! - let contentStream = AsyncThrowingStream { cont in - continuation = cont - } - - Task { [continuation, self] in - let notificationCenter = NotificationCenter.default - let notifications = notificationCenter.notifications( - named: FileHandle.readCompletionNotification, - object: outputPipe.fileHandleForReading - ) - for await notification in notifications { - let userInfo = notification.userInfo - if let data = userInfo?[NSFileHandleNotificationDataItem] as? Data, - let content = String(data: data, encoding: .utf8), - !content.isEmpty - { - continuation?.yield(content) - } - if !(self.process?.isRunning ?? false) { - let reason = self.process?.terminationReason ?? .exit - let status = self.process?.terminationStatus ?? 1 - if let output = (self.process?.standardOutput as? Pipe)?.fileHandleForReading - .readDataToEndOfFile(), - let content = String(data: output, encoding: .utf8), - !content.isEmpty - { - continuation?.yield(content) - } - - if status == 0 { - continuation?.finish() - } else { - continuation?.finish(throwing: TerminationError( - reason: reason, - status: status - )) - } - break - } - Task { @MainActor in - outputPipe.fileHandleForReading.readInBackgroundAndNotify(forModes: [.common]) - } - } - } - - Task { @MainActor in - outputPipe.fileHandleForReading.readInBackgroundAndNotify(forModes: [.common]) - } - - do { - try process.run() - } catch { - continuation.finish(throwing: error) - } - - return contentStream - } - - public func runCommand( - _ command: String = "/bin/bash", - arguments: [String], - currentDirectoryURL: URL? = nil, - environment: [String: String] - ) async throws -> String { - let process = Process() - process.launchPath = command - process.currentDirectoryURL = currentDirectoryURL - process.arguments = arguments - process.environment = getEnvironmentVariables() - .merging(environment, uniquingKeysWith: { $1 }) - - let outputPipe = Pipe() - process.standardOutput = outputPipe - process.standardError = outputPipe - self.outputPipe = outputPipe - - let inputPipe = Pipe() - process.standardInput = inputPipe - self.inputPipe = inputPipe - - return try await withUnsafeThrowingContinuation { continuation in - do { - process.terminationHandler = { process in - do { - if let data = try outputPipe.fileHandleForReading.readToEnd(), - let content = String(data: data, encoding: .utf8) - { - if process.terminationStatus == 0 { - continuation.resume(returning: content) - } else { - struct LocalizedTerminationError: Error, LocalizedError { - let terminationError: TerminationError - let errorDescription: String? - } - continuation.resume(throwing: LocalizedTerminationError( - terminationError: .init( - reason: process.terminationReason, - status: process.terminationStatus - ), - errorDescription: content - )) - } - return - } - continuation.resume(returning: "") - } catch { - continuation.resume(throwing: error) - } - } - try process.run() - } catch { - continuation.resume(throwing: error) - } - } - } - - public func writeInput(_ input: String) { - guard let data = input.data(using: .utf8) else { - return - } - - inputPipe?.fileHandleForWriting.write(data) - inputPipe?.fileHandleForWriting.closeFile() - } - - public func terminate() async { - process?.terminate() - process = nil - } -} - diff --git a/Tool/Sources/Terminal/TerminalSession.swift b/Tool/Sources/Terminal/TerminalSession.swift deleted file mode 100644 index 6db53ef6..00000000 --- a/Tool/Sources/Terminal/TerminalSession.swift +++ /dev/null @@ -1,237 +0,0 @@ -import Foundation -import SystemUtils -import Logger -import Combine - -/** - * Manages shell processes for terminal emulation - */ -class ShellProcessManager { - private var process: Process? - private var outputPipe: Pipe? - private var inputPipe: Pipe? - private var isRunning = false - var onOutputReceived: ((String) -> Void)? - - private let shellIntegrationScript = """ - # Shell integration for tracking command execution and exit codes - __terminal_command_start() { - printf "\\033]133;C\\007" # Command started - } - - __terminal_command_finished() { - local EXIT="$?" - printf "\\033]133;D;%d\\007" "$EXIT" # Command finished with exit code - return $EXIT - } - - # Set up precmd and preexec hooks - autoload -Uz add-zsh-hook - add-zsh-hook precmd __terminal_command_finished - add-zsh-hook preexec __terminal_command_start - - # print the initial prompt to output - echo -n - """ - - /** - * Starts a shell process - */ - func startShell(inDirectory directory: String = NSHomeDirectory()) { - guard !isRunning else { return } - - process = Process() - outputPipe = Pipe() - inputPipe = Pipe() - - // Configure the process - process?.executableURL = URL(fileURLWithPath: "/bin/zsh") - process?.arguments = ["-i", "-l"] - - // Create temporary file for shell integration - let tempDir = FileManager.default.temporaryDirectory - let copilotZshPath = tempDir.appendingPathComponent("xcode-copilot-zsh") - - var zshdir = tempDir - if !FileManager.default.fileExists(atPath: copilotZshPath.path) { - do { - try FileManager.default.createDirectory(at: copilotZshPath, withIntermediateDirectories: true, attributes: nil) - zshdir = copilotZshPath - } catch { - Logger.client.info("Error creating zsh directory: \(error.localizedDescription)") - } - } else { - zshdir = copilotZshPath - } - - let integrationFile = zshdir.appendingPathComponent("shell_integration.zsh") - try? shellIntegrationScript.write(to: integrationFile, atomically: true, encoding: .utf8) - - var environment = ProcessInfo.processInfo.environment - // Fetch login shell environment to get correct PATH - if let shellEnv = SystemUtils.shared.getLoginShellEnvironment(shellPath: "/bin/zsh") { - for (key, value) in shellEnv { - environment[key] = value - } - } - // Append common bin paths to PATH - environment["PATH"] = SystemUtils.shared.appendCommonBinPaths(path: environment["PATH"] ?? "") - - let userZdotdir = environment["ZDOTDIR"] ?? NSHomeDirectory() - environment["ZDOTDIR"] = zshdir.path - environment["USER_ZDOTDIR"] = userZdotdir - environment["SHELL_INTEGRATION"] = integrationFile.path - process?.environment = environment - - // Source shell integration in zsh startup - let zshrcContent = "source \"$SHELL_INTEGRATION\"\n" - try? zshrcContent.write(to: zshdir.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8) - - process?.standardOutput = outputPipe - process?.standardError = outputPipe - process?.standardInput = inputPipe - process?.currentDirectoryURL = URL(fileURLWithPath: directory) - - // Handle output from the process - outputPipe?.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in - let data = fileHandle.availableData - if !data.isEmpty, let output = String(data: data, encoding: .utf8) { - DispatchQueue.main.async { - self?.onOutputReceived?(output) - } - } - } - - do { - try process?.run() - isRunning = true - } catch { - onOutputReceived?("Failed to start shell: \(error.localizedDescription)\r\n") - Logger.client.error("Failed to start shell: \(error.localizedDescription)") - } - } - - /** - * Sends a command to the shell process - * @param command The command to send - */ - func sendCommand(_ command: String) { - guard isRunning, let inputPipe = inputPipe else { return } - - if let data = (command).data(using: .utf8) { - try? inputPipe.fileHandleForWriting.write(contentsOf: data) - } - } - - func stopCommand() { - // Send SIGINT (Ctrl+C) to the running process - guard let process = process else { return } - process.interrupt() // Sends SIGINT to the process - } - - /** - * Terminates the shell process - */ - func terminateShell() { - guard isRunning else { return } - - outputPipe?.fileHandleForReading.readabilityHandler = nil - process?.terminate() - isRunning = false - } - - deinit { - terminateShell() - } -} - -public struct CommandExecutionResult { - public let success: Bool - public let output: String -} - -public class TerminalSession: ObservableObject { - @Published public var terminalOutput = "" - - private var shellManager = ShellProcessManager() - private var hasPendingCommand = false - private var pendingCommandResult = "" - // Add command completion handler - private var onCommandCompleted: ((CommandExecutionResult) -> Void)? - - init() { - // Set up the shell process manager to handle shell output - shellManager.onOutputReceived = { [weak self] output in - self?.handleShellOutput(output) - } - } - - public func executeCommand(currentDirectory: String, command: String, completion: @escaping (CommandExecutionResult) -> Void) { - onCommandCompleted = completion - pendingCommandResult = "" - - // Start shell in the requested directory - self.shellManager.startShell(inDirectory: currentDirectory.isEmpty ? NSHomeDirectory() : currentDirectory) - - // Wait for shell prompt to appear before sending command - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - self?.terminalOutput += "\(command)\n" - self?.shellManager.sendCommand(command + "\n") - self?.hasPendingCommand = true - } - } - - /** - * Handles input from the terminal view - * @param input Input received from terminal - */ - public func handleTerminalInput(_ input: String) { - DispatchQueue.main.async { [weak self] in - if input.contains("\u{03}") { // CTRL+C - let newInput = input.replacingOccurrences(of: "\u{03}", with: "\n") - self?.terminalOutput += newInput - self?.shellManager.stopCommand() - self?.shellManager.sendCommand("\n") - return - } - - // Echo the input to the terminal - self?.terminalOutput += input - self?.shellManager.sendCommand(input) - } - } - - public func getCommandOutput() -> String { - return self.pendingCommandResult - } - - /** - * Handles output from the shell process - * @param output Output from shell process - */ - private func handleShellOutput(_ output: String) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.terminalOutput += output - // Look for shell integration escape sequences - if output.contains("\u{1B}]133;D;0\u{07}") && self.hasPendingCommand { - // Command succeeded - self.onCommandCompleted?(CommandExecutionResult(success: true, output: self.pendingCommandResult)) - self.hasPendingCommand = false - } else if output.contains("\u{1B}]133;D;") && self.hasPendingCommand { - // Command failed - self.onCommandCompleted?(CommandExecutionResult(success: false, output: self.pendingCommandResult)) - self.hasPendingCommand = false - } else if output.contains("\u{1B}]133;C\u{07}") { - // Command start - } else if self.hasPendingCommand { - self.pendingCommandResult += output - } - } - } - - public func cleanup() { - shellManager.terminateShell() - } -} diff --git a/Tool/Sources/Terminal/TerminalSessionManager.swift b/Tool/Sources/Terminal/TerminalSessionManager.swift deleted file mode 100644 index 19fb9e6f..00000000 --- a/Tool/Sources/Terminal/TerminalSessionManager.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import Combine - -public class TerminalSessionManager { - public static let shared = TerminalSessionManager() - private var sessions: [String: TerminalSession] = [:] - - public func createSession(for terminalId: String) -> TerminalSession { - if let existingSession = sessions[terminalId] { - return existingSession - } else { - let newSession = TerminalSession() - sessions[terminalId] = newSession - return newSession - } - } - - public func getSession(for terminalId: String) -> TerminalSession? { - return sessions[terminalId] - } - - public func clearSession(for terminalId: String) { - sessions[terminalId]?.cleanup() - sessions.removeValue(forKey: terminalId) - } -} diff --git a/Tool/Sources/Toast/NotificationView.swift b/Tool/Sources/Toast/NotificationView.swift deleted file mode 100644 index f29c9a8e..00000000 --- a/Tool/Sources/Toast/NotificationView.swift +++ /dev/null @@ -1,85 +0,0 @@ -import SwiftUI - -struct AutoDismissMessage: View { - let message: ToastController.Message - - init(message: ToastController.Message) { - self.message = message - } - - var body: some View { - message.content - .foregroundColor(.white) - .padding(8) - .background( - message.level.color as Color, - in: RoundedRectangle(cornerRadius: 8) - ) - .frame(minWidth: 300) - } -} - -public struct NotificationView: View { - let message: ToastController.Message - let onDismiss: () -> Void - - public init( - message: ToastController.Message, - onDismiss: @escaping () -> Void = {} - ) { - self.message = message - self.onDismiss = onDismiss - } - - public var body: some View { - if let notificationTitle = message.title { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .center, spacing: 4) { - Image(systemName: message.level.icon) - .foregroundColor(message.level.color) - Text(notificationTitle) - - Spacer() - - Button(action: onDismiss) { - Image(systemName: "xmark") - .foregroundColor(Color("ToastDismissButtonColor")) - } - .buttonStyle(.plain) - } - - HStack(alignment: .bottom, spacing: 1) { - message.content - - Spacer() - - if let button = message.button { - Button(action: { - button.action() - onDismiss() - }) { - Text(button.title) - .padding(.horizontal, 7) - .padding(.vertical, 3) - .background(Color("ToastActionButtonColor")) - .cornerRadius(5) - } - .buttonStyle(.plain) - } - } - } - .padding(.horizontal, 12) - .padding(.vertical, 16) - .frame(width: 450, alignment: .topLeading) - .background(Color("ToastBackgroundColor")) - .cornerRadius(4) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(Color("ToastStrokeColor"), lineWidth: 1) - ) - } else { - AutoDismissMessage(message: message) - .frame(maxWidth: .infinity) - } - } -} diff --git a/Tool/Sources/Toast/Toast.swift b/Tool/Sources/Toast/Toast.swift deleted file mode 100644 index 704af7df..00000000 --- a/Tool/Sources/Toast/Toast.swift +++ /dev/null @@ -1,313 +0,0 @@ -import ComposableArchitecture -import Dependencies -import Foundation -import SwiftUI -import AppKitExtension - -public enum ToastLevel { - case info - case warning - case danger - case error - - var icon: String { - switch self { - case .warning: return "exclamationmark.circle.fill" - case .danger: return "exclamationmark.circle.fill" - case .error: return "xmark.circle.fill" - case .info: return "exclamationmark.triangle.fill" - } - } - - var color: Color { - switch self { - case .warning: return Color(nsColor: .systemOrange) - case .danger, .error: return Color(nsColor: .systemRed) - case .info: return Color.accentColor - } - } -} - -public struct ToastKey: EnvironmentKey { - public static var defaultValue: (String, ToastLevel) -> Void = { _, _ in } -} - -public extension EnvironmentValues { - var toast: (String, ToastLevel) -> Void { - get { self[ToastKey.self] } - set { self[ToastKey.self] = newValue } - } -} - -public struct ToastControllerDependencyKey: DependencyKey { - public static let liveValue = ToastController(messages: []) -} - -public extension DependencyValues { - var toastController: ToastController { - get { self[ToastControllerDependencyKey.self] } - set { self[ToastControllerDependencyKey.self] = newValue } - } - - var toast: (String, ToastLevel) -> Void { - return { content, level in - toastController.toast(content: content, level: level, namespace: nil) - } - } - - var namespacedToast: (String, ToastLevel, String) -> Void { - return { - content, level, namespace in - toastController.toast(content: content, level: level, namespace: namespace) - } - } - - var persistentToast: (String, String, ToastLevel) -> Void { - return { title, content, level in - toastController.toast(title: title, content: content, level: level, namespace: nil) - } - } -} - -public struct ToastButton: Equatable { - public let title: String - public let action: () -> Void - - public init(title: String, action: @escaping () -> Void) { - self.title = title - self.action = action - } - - public static func ==(lhs: ToastButton, rhs: ToastButton) -> Bool { - lhs.title == rhs.title - } -} - -public class ToastController: ObservableObject { - public struct Message: Identifiable, Equatable { - public var namespace: String? - public var title: String? - public var id: UUID - public var level: ToastLevel - public var content: Text - public var button: ToastButton? - - // Convenience initializer for auto-dismissing messages (no title, no button) - public init( - id: UUID = UUID(), - level: ToastLevel, - namespace: String? = nil, - content: Text - ) { - self.id = id - self.level = level - self.namespace = namespace - self.title = nil - self.content = content - self.button = nil - } - - // Convenience initializer for persistent messages (title is required) - public init( - id: UUID = UUID(), - level: ToastLevel, - namespace: String? = nil, - title: String, - content: Text, - button: ToastButton? = nil - ) { - self.id = id - self.level = level - self.namespace = namespace - self.title = title - self.content = content - self.button = button - } - } - - @Published public var messages: [Message] = [] - - public init(messages: [Message]) { - self.messages = messages - } - - @MainActor - private func removeMessageWithAnimation(withId id: UUID) { - withAnimation(.easeInOut(duration: 0.2)) { - messages.removeAll { $0.id == id } - } - } - - private func showMessage(_ message: Message, autoDismissDelay: UInt64?) { - Task { @MainActor in - withAnimation(.easeInOut(duration: 0.2)) { - messages.append(message) - messages = messages.suffix(3) - } - if let autoDismissDelay = autoDismissDelay { - try await Task.sleep(nanoseconds: autoDismissDelay) - removeMessageWithAnimation(withId: message.id) - } - } - } - - // Auto-dismissing toast (title and button are not allowed) - public func toast( - content: String, - level: ToastLevel, - namespace: String? = nil - ) { - let message = Message(level: level, namespace: namespace, content: Text(content)) - showMessage(message, autoDismissDelay: 4_000_000_000) - } - - // Persistent toast (title is required, button is optional) - public func toast( - title: String, - content: String, - level: ToastLevel, - namespace: String? = nil, - button: ToastButton? = nil - ) { - // Support markdown in persistent toasts - let contentText: Text - if let attributedString = try? AttributedString(markdown: content) { - contentText = Text(attributedString) - } else { - contentText = Text(content) - } - let message = Message( - level: level, - namespace: namespace, - title: title, - content: contentText, - button: button - ) - showMessage(message, autoDismissDelay: nil) - } - - public func dismissMessage(withId id: UUID) { - Task { @MainActor in - removeMessageWithAnimation(withId: id) - } - } -} - -@Reducer -public struct Toast { - public typealias Message = ToastController.Message - - @ObservableState - public struct State: Equatable { - var isObservingToastController = false - public var messages: [Message] = [] - - public init(messages: [Message] = []) { - self.messages = messages - } - } - - public enum Action: Equatable { - case start - case updateMessages([Message]) - case toast(String, ToastLevel, String?) - case toastPersistent(String, String, ToastLevel, String?, ToastButton?) - } - - @Dependency(\.toastController) var toastController - - struct CancelID: Hashable {} - - public init() {} - - public var body: some ReducerOf { - Reduce { state, action in - switch action { - case .start: - guard !state.isObservingToastController else { return .none } - state.isObservingToastController = true - return .run { send in - let stream = AsyncStream<[Message]> { continuation in - let cancellable = toastController.$messages.sink { newValue in - continuation.yield(newValue) - } - continuation.onTermination = { _ in - cancellable.cancel() - } - } - for await newValue in stream { - try Task.checkCancellation() - await send(.updateMessages(newValue), animation: .linear(duration: 0.2)) - } - }.cancellable(id: CancelID(), cancelInFlight: true) - case let .updateMessages(messages): - state.messages = messages - return .none - case let .toast(content, level, namespace): - toastController.toast(content: content, level: level, namespace: namespace) - return .none - case let .toastPersistent(title, content, level, namespace, button): - toastController - .toast( - title: title, - content: content, - level: level, - namespace: namespace, - button: button - ) - return .none - } - } - } -} - -public extension NSWorkspace { - /// Opens the System Preferences/Settings app at the Extensions pane - /// - Parameter extensionPointIdentifier: Optional identifier for specific extension type - static func openExtensionsPreferences(extensionPointIdentifier: String? = nil) { - if #available(macOS 13.0, *) { - var urlString = "x-apple.systempreferences:com.apple.ExtensionsPreferences" - if let extensionPointIdentifier = extensionPointIdentifier { - urlString += "?extensionPointIdentifier=\(extensionPointIdentifier)" - } - NSWorkspace.shared.open(URL(string: urlString)!) - } else { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/open") - process.arguments = [ - "-b", - "com.apple.systempreferences", - "/System/Library/PreferencePanes/Extensions.prefPane" - ] - - do { - try process.run() - } catch { - // Handle error silently - return - } - } - } - - /// Opens the Xcode Extensions preferences directly - static func openXcodeExtensionsPreferences() { - openExtensionsPreferences(extensionPointIdentifier: "com.apple.dt.Xcode.extension.source-editor") - } - - static func restartXcode() { - // Find current Xcode path before quitting - // Restart if we found a valid path - if let xcodeURL = getXcodeBundleURL() { - // Quit Xcode - let script = NSAppleScript(source: "tell application \"Xcode\" to quit") - script?.executeAndReturnError(nil) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - NSWorkspace.shared.openApplication( - at: xcodeURL, - configuration: NSWorkspace.OpenConfiguration() - ) - } - } - } -} diff --git a/Tool/Sources/UserDefaultsObserver/UserDefaultsObserver.swift b/Tool/Sources/UserDefaultsObserver/UserDefaultsObserver.swift deleted file mode 100644 index 62ecce3f..00000000 --- a/Tool/Sources/UserDefaultsObserver/UserDefaultsObserver.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -public final class UserDefaultsObserver: NSObject { - public var onChange: (() -> Void)? - private weak var object: NSObject? - private let keyPaths: [String] - - public init( - object: NSObject, - forKeyPaths keyPaths: [String], - context: UnsafeMutableRawPointer? - ) { - self.object = object - self.keyPaths = keyPaths - super.init() - for keyPath in keyPaths { - object.addObserver(self, forKeyPath: keyPath, options: .new, context: context) - } - } - - deinit { - for keyPath in keyPaths { - object?.removeObserver(self, forKeyPath: keyPath) - } - } - - public override func observeValue( - forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer? - ) { - onChange?() - } -} - diff --git a/Tool/Sources/WebContentExtractor/HTMLToMarkdownConverter.swift b/Tool/Sources/WebContentExtractor/HTMLToMarkdownConverter.swift deleted file mode 100644 index 56236a0d..00000000 --- a/Tool/Sources/WebContentExtractor/HTMLToMarkdownConverter.swift +++ /dev/null @@ -1,217 +0,0 @@ -import SwiftSoup -import WebKit - -class HTMLToMarkdownConverter { - - // MARK: - Configuration - private struct Config { - static let unwantedSelectors = "script, style, nav, header, footer, aside, noscript, iframe, .navigation, .sidebar, .ad, .advertisement, .cookie-banner, .popup, .social, .share, .social-share, .related, .comments, .menu, .breadcrumb" - static let mainContentSelectors = [ - "main", - "article", - "div.content", - "div#content", - "div.post-content", - "div.article-body", - "div.main-content", - "section.content", - ".content", - ".main", - ".main-content", - ".article", - ".article-content", - ".post-content", - "#content", - "#main", - ".container .row .col", - "[role='main']" - ] - } - - // MARK: - Main Conversion Method - func convertToMarkdown(from html: String) throws -> String { - let doc = try SwiftSoup.parse(html) - let rawMarkdown = try extractCleanContent(from: doc) - return cleanupExcessiveNewlines(rawMarkdown) - } - - // MARK: - Content Extraction - private func extractCleanContent(from doc: Document) throws -> String { - try removeUnwantedElements(from: doc) - - // Try to find main content areas - for selector in Config.mainContentSelectors { - if let mainElement = try findMainContent(in: doc, using: selector) { - return try convertElementToMarkdown(mainElement) - } - } - - // Fallback: clean body content - return try fallbackContentExtraction(from: doc) - } - - private func removeUnwantedElements(from doc: Document) throws { - try doc.select(Config.unwantedSelectors).remove() - } - - private func findMainContent(in doc: Document, using selector: String) throws -> Element? { - let elements = try doc.select(selector) - guard let mainElement = elements.first() else { return nil } - - // Clean nested unwanted elements - try mainElement.select("nav, aside, .related, .comments, .social-share, .advertisement").remove() - return mainElement - } - - private func fallbackContentExtraction(from doc: Document) throws -> String { - guard let body = doc.body() else { return "" } - try body.select(Config.unwantedSelectors).remove() - return try convertElementToMarkdown(body) - } - - // MARK: - Cleanup Method - private func cleanupExcessiveNewlines(_ markdown: String) -> String { - // Replace 3+ consecutive newlines with just 2 newlines - let cleaned = markdown.replacingOccurrences( - of: #"\n{3,}"#, - with: "\n\n", - options: .regularExpression - ) - return cleaned.trimmingCharacters(in: .whitespacesAndNewlines) - } - - // MARK: - Element Processing - private func convertElementToMarkdown(_ element: Element) throws -> String { - let markdown = try convertElement(element) - return markdown - } - - func convertElement(_ element: Element) throws -> String { - var result = "" - - for node in element.getChildNodes() { - if let textNode = node as? TextNode { - result += textNode.text() - } else if let childElement = node as? Element { - result += try convertSpecificElement(childElement) - } - } - - return result - } - - private func convertSpecificElement(_ element: Element) throws -> String { - let tagName = element.tagName().lowercased() - let text = try element.text() - - switch tagName { - case "h1": - return "\n# \(text)\n" - case "h2": - return "\n## \(text)\n" - case "h3": - return "\n### \(text)\n" - case "h4": - return "\n#### \(text)\n" - case "h5": - return "\n##### \(text)\n" - case "h6": - return "\n###### \(text)\n" - case "p": - return "\n\(try convertElement(element))\n" - case "br": - return "\n" - case "strong", "b": - return "**\(text)**" - case "em", "i": - return "*\(text)*" - case "code": - return "`\(text)`" - case "pre": - return "\n```\n\(text)\n```\n" - case "a": - let href = try element.attr("href") - let title = try element.attr("title") - if href.isEmpty { - return text - } - - // Skip non-http/https/file schemes - if let url = URL(string: href), - let scheme = url.scheme?.lowercased(), - !["http", "https", "file"].contains(scheme) { - return text - } - - let titlePart = title.isEmpty ? "" : " \"\(title.replacingOccurrences(of: "\"", with: "\\\""))\"" - return "[\(text)](\(href)\(titlePart))" - case "img": - let src = try element.attr("src") - let alt = try element.attr("alt") - let title = try element.attr("title") - - var finalSrc = src - // Remove data URIs - if src.hasPrefix("data:") { - finalSrc = src.components(separatedBy: ",").first ?? "" + "..." - } - - let titlePart = title.isEmpty ? "" : " \"\(title.replacingOccurrences(of: "\"", with: "\\\""))\"" - return "![\(alt)](\(finalSrc)\(titlePart))" - case "ul": - return try convertList(element, ordered: false) - case "ol": - return try convertList(element, ordered: true) - case "li": - return try convertElement(element) - case "table": - return try convertTable(element) - case "blockquote": - let content = try convertElement(element) - return content.components(separatedBy: .newlines) - .map { "> \($0)" } - .joined(separator: "\n") - default: - return try convertElement(element) - } - } - - private func convertList(_ element: Element, ordered: Bool) throws -> String { - var result = "\n" - let items = try element.select("li") - - for (index, item) in items.enumerated() { - let content = try convertElement(item).trimmingCharacters(in: .whitespacesAndNewlines) - if ordered { - result += "\(index + 1). \(content)\n" - } else { - result += "- \(content)\n" - } - } - - return result - } - - private func convertTable(_ element: Element) throws -> String { - var result = "\n" - let rows = try element.select("tr") - - guard !rows.isEmpty() else { return "" } - - var isFirstRow = true - for row in rows { - let cells = try row.select("td, th") - let cellContents = try cells.map { try $0.text() } - - result += "| " + cellContents.joined(separator: " | ") + " |\n" - - if isFirstRow { - let separator = Array(repeating: "---", count: cellContents.count).joined(separator: " | ") - result += "| \(separator) |\n" - isFirstRow = false - } - } - - return result - } -} diff --git a/Tool/Sources/WebContentExtractor/WebContentExtractor.swift b/Tool/Sources/WebContentExtractor/WebContentExtractor.swift deleted file mode 100644 index aee0d889..00000000 --- a/Tool/Sources/WebContentExtractor/WebContentExtractor.swift +++ /dev/null @@ -1,227 +0,0 @@ -import WebKit -import Logger -import Preferences - -public class WebContentFetcher: NSObject, WKNavigationDelegate { - private var webView: WKWebView? - private var loadingTimer: Timer? - private static let converter = HTMLToMarkdownConverter() - private var completion: ((Result) -> Void)? - - private struct Config { - static let timeout: TimeInterval = 30.0 - static let contentLoadDelay: TimeInterval = 2.0 - } - - public enum WebContentError: Error, LocalizedError { - case invalidURL(String) - case timeout - case noContent - case navigationFailed(Error) - case javascriptError(Error) - - public var errorDescription: String? { - switch self { - case .invalidURL(let url): "Invalid URL: \(url)" - case .timeout: "Request timed out" - case .noContent: "No content found" - case .navigationFailed(let error): "Navigation failed: \(error.localizedDescription)" - case .javascriptError(let error): "JavaScript execution error: \(error.localizedDescription)" - } - } - } - - // MARK: - Initialization - public override init() { - super.init() - setupWebView() - } - - deinit { - cleanup() - } - - // MARK: - Public Methods - public func fetchContent(from urlString: String, completion: @escaping (Result) -> Void) { - guard let url = URL(string: urlString) else { - completion(.failure(WebContentError.invalidURL(urlString))) - return - } - - DispatchQueue.main.async { [weak self] in - self?.completion = completion - self?.setupTimeout() - self?.loadContent(from: url) - } - } - - public static func fetchContentAsync(from urlString: String) async throws -> String { - try await withCheckedThrowingContinuation { continuation in - let fetcher = WebContentFetcher() - fetcher.fetchContent(from: urlString) { result in - withExtendedLifetime(fetcher) { - continuation.resume(with: result) - } - } - } - } - - public static func fetchMultipleContentAsync(from urls: [String]) async -> [String] { - var results: [String] = [] - - for url in urls { - do { - let content = try await fetchContentAsync(from: url) - results.append("Successfully fetched content from \(url): \(content)") - } catch { - Logger.client.error("Failed to fetch content from \(url): \(error.localizedDescription)") - results.append("Failed to fetch content from \(url) with error: \(error.localizedDescription)") - } - } - - return results - } - - // MARK: - Private Methods - private func setupWebView() { - let configuration = WKWebViewConfiguration() - let dataSource = WKWebsiteDataStore.nonPersistent() - - if #available(macOS 14.0, *) { - configureProxy(for: dataSource) - } - - configuration.websiteDataStore = dataSource - webView = WKWebView(frame: .zero, configuration: configuration) - webView?.navigationDelegate = self - } - - @available(macOS 14.0, *) - private func configureProxy(for dataSource: WKWebsiteDataStore) { - let proxyURL = UserDefaults.shared.value(for: \.gitHubCopilotProxyUrl) - guard let url = URL(string: proxyURL), - let host = url.host, - let port = url.port, - let proxyPort = NWEndpoint.Port(port.description) else { return } - - let tlsOptions = NWProtocolTLS.Options() - let useStrictSSL = UserDefaults.shared.value(for: \.gitHubCopilotUseStrictSSL) - - if !useStrictSSL { - let secOptions = tlsOptions.securityProtocolOptions - sec_protocol_options_set_verify_block(secOptions, { _, _, completion in - completion(true) - }, .main) - } - - let httpProxy = ProxyConfiguration( - httpCONNECTProxy: NWEndpoint.hostPort( - host: NWEndpoint.Host(host), - port: proxyPort - ), - tlsOptions: tlsOptions - ) - - httpProxy.applyCredential( - username: UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername), - password: UserDefaults.shared.value(for: \.gitHubCopilotProxyPassword) - ) - - dataSource.proxyConfigurations = [httpProxy] - } - - private func cleanup() { - loadingTimer?.invalidate() - loadingTimer = nil - webView?.navigationDelegate = nil - webView?.stopLoading() - webView = nil - } - - private func setupTimeout() { - loadingTimer?.invalidate() - loadingTimer = Timer.scheduledTimer(withTimeInterval: Config.timeout, repeats: false) { [weak self] _ in - DispatchQueue.main.async { - Logger.client.error("Request timed out") - self?.completeWithError(WebContentError.timeout) - } - } - } - - private func loadContent(from url: URL) { - if webView == nil { - setupWebView() - } - - guard let webView = webView else { - completeWithError(WebContentError.navigationFailed(NSError(domain: "WebView creation failed", code: -1))) - return - } - - let request = URLRequest( - url: url, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Config.timeout - ) - webView.load(request) - } - - private func processHTML(_ html: String) { - do { - let cleanedText = try Self.converter.convertToMarkdown(from: html) - completeWithSuccess(cleanedText) - } catch { - Logger.client.error("SwiftSoup parsing error: \(error.localizedDescription)") - completeWithError(error) - } - } - - private func completeWithSuccess(_ content: String) { - completion?(.success(content)) - completion = nil - } - - private func completeWithError(_ error: Error) { - completion?(.failure(error)) - completion = nil - } - - // MARK: - WKNavigationDelegate - public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - loadingTimer?.invalidate() - - DispatchQueue.main.asyncAfter(deadline: .now() + Config.contentLoadDelay) { - webView.evaluateJavaScript("document.body.innerHTML") { [weak self] result, error in - DispatchQueue.main.async { - if let error = error { - Logger.client.error("JavaScript execution error: \(error.localizedDescription)") - self?.completeWithError(WebContentError.javascriptError(error)) - return - } - - if let html = result as? String, !html.isEmpty { - self?.processHTML(html) - } else { - self?.completeWithError(WebContentError.noContent) - } - } - } - } - } - - public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - handleNavigationFailure(error) - } - - public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - handleNavigationFailure(error) - } - - private func handleNavigationFailure(_ error: Error) { - loadingTimer?.invalidate() - DispatchQueue.main.async { - Logger.client.error("Navigation failed: \(error.localizedDescription)") - self.completeWithError(WebContentError.navigationFailed(error)) - } - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/BatchingFileChangeWatcher.swift b/Tool/Sources/Workspace/FileChangeWatcher/BatchingFileChangeWatcher.swift deleted file mode 100644 index c63f0ad1..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/BatchingFileChangeWatcher.swift +++ /dev/null @@ -1,255 +0,0 @@ -import Foundation -import System -import Logger -import LanguageServerProtocol - -public final class BatchingFileChangeWatcher: DirectoryWatcherProtocol { - private var watchedPaths: [URL] - private let changePublisher: PublisherType - private let publishInterval: TimeInterval - - private var pendingEvents: [FileEvent] = [] - private var timer: Timer? - private let eventQueue: DispatchQueue - private let fsEventQueue: DispatchQueue - private var eventStream: FSEventStreamRef? - private(set) public var isWatching = false - - // Dependencies injected for testing - private let fsEventProvider: FSEventProvider - - /// TODO: set a proper value for stdio - public static let maxEventPublishSize = 100 - - init( - watchedPaths: [URL], - changePublisher: @escaping PublisherType, - publishInterval: TimeInterval = 3.0, - fsEventProvider: FSEventProvider = FileChangeWatcherFSEventProvider() - ) { - self.watchedPaths = watchedPaths - self.changePublisher = changePublisher - self.publishInterval = publishInterval - self.fsEventProvider = fsEventProvider - self.eventQueue = DispatchQueue(label: "com.github.copilot.filechangewatcher") - self.fsEventQueue = DispatchQueue(label: "com.github.copilot.filechangewatcherfseventstream", qos: .utility) - - self.start() - } - - private func updateWatchedPaths(_ paths: [URL]) { - guard isWatching, paths != watchedPaths else { return } - stopWatching() - watchedPaths = paths - _ = startWatching() - } - - public func addPaths(_ paths: [URL]) { - let newPaths = paths.filter { !watchedPaths.contains($0) } - if !newPaths.isEmpty { - let updatedPaths = watchedPaths + newPaths - updateWatchedPaths(updatedPaths) - } - } - - public func removePaths(_ paths: [URL]) { - let updatedPaths = watchedPaths.filter { !paths.contains($0) } - if updatedPaths.count != watchedPaths.count { - updateWatchedPaths(updatedPaths) - } - } - - public func paths() -> [URL] { - return watchedPaths - } - - internal func start() { - guard !isWatching else { return } - - guard self.startWatching() else { - Logger.client.info("Failed to start watching for: \(watchedPaths)") - return - } - self.startPublishTimer() - isWatching = true - } - - deinit { - stopWatching() - self.timer?.invalidate() - } - - internal func startPublishTimer() { - guard self.timer == nil else { return } - - Task { @MainActor [weak self] in - guard let self else { return } - self.timer = Timer.scheduledTimer(withTimeInterval: self.publishInterval, repeats: true) { [weak self] _ in - self?.publishChanges() - } - } - } - - internal func addEvent(file: URL, type: FileChangeType) { - eventQueue.async { - self.pendingEvents.append(FileEvent(uri: file.absoluteString, type: type)) - } - } - - public func onFileCreated(file: URL) { - addEvent(file: file, type: .created) - } - - public func onFileChanged(file: URL) { - addEvent(file: file, type: .changed) - } - - public func onFileDeleted(file: URL) { - addEvent(file: file, type: .deleted) - } - - private func publishChanges() { - eventQueue.async { - guard !self.pendingEvents.isEmpty else { return } - - var compressedEvent: [String: FileEvent] = [:] - for event in self.pendingEvents { - let existingEvent = compressedEvent[event.uri] - - guard existingEvent != nil else { - compressedEvent[event.uri] = event - continue - } - - if event.type == .deleted { /// file deleted. Cover created and changed event - compressedEvent[event.uri] = event - } else if event.type == .created { /// file created. Cover deleted and changed event - compressedEvent[event.uri] = event - } else if event.type == .changed { - if existingEvent?.type != .created { /// file changed. Won't cover created event - compressedEvent[event.uri] = event - } - } - } - - let compressedEventArray: [FileEvent] = Array(compressedEvent.values) - - let changes = Array(compressedEventArray.prefix(BatchingFileChangeWatcher.maxEventPublishSize)) - if compressedEventArray.count > BatchingFileChangeWatcher.maxEventPublishSize { - self.pendingEvents = Array(compressedEventArray[BatchingFileChangeWatcher.maxEventPublishSize.. Bool { - isWatching = true - var isEventStreamStarted = false - - var context = FSEventStreamContext() - context.info = Unmanaged.passUnretained(self).toOpaque() - - let paths = watchedPaths.map { $0.path } as CFArray - let flags = UInt32( - kFSEventStreamCreateFlagFileEvents | - kFSEventStreamCreateFlagNoDefer | - kFSEventStreamCreateFlagWatchRoot - ) - - eventStream = fsEventProvider.createEventStream( - paths: paths, - latency: 1, // 1 second latency, - flags: flags, - callback: { _, clientCallbackInfo, numEvents, eventPaths, eventFlags, _ in - guard let clientCallbackInfo = clientCallbackInfo else { return } - let watcher = Unmanaged.fromOpaque(clientCallbackInfo).takeUnretainedValue() - watcher.processEvent(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags) - }, - context: &context - ) - - if let eventStream = eventStream { - fsEventProvider.setDispatchQueue(eventStream, queue: fsEventQueue) - fsEventProvider.startStream(eventStream) - isEventStreamStarted = true - } - - return isEventStreamStarted - } - - /// Stops watching for file changes - public func stopWatching() { - guard isWatching, let eventStream = eventStream else { return } - - fsEventProvider.stopStream(eventStream) - fsEventProvider.invalidateStream(eventStream) - fsEventProvider.releaseStream(eventStream) - self.eventStream = nil - - isWatching = false - - Logger.client.info("Stoped watching for file changes in \(watchedPaths)") - } - - public func processEvent(numEvents: CFIndex, eventPaths: UnsafeRawPointer, eventFlags: UnsafePointer) { - let pathsPtr = eventPaths.bindMemory(to: UnsafeMutableRawPointer.self, capacity: numEvents) - - for i in 0.. Bool { - if let resourceValues = try? url.resourceValues(forKeys: [.isRegularFileKey, .isDirectoryKey]), - resourceValues.isDirectory == true { return true } - - if supportedFileExtensions.contains(url.pathExtension.lowercased()) == false { return true } - - if WorkspaceFile.isXCProject(url) || WorkspaceFile.isXCWorkspace(url) { return true } - - if WorkspaceFile.matchesPatterns(url, patterns: skipPatterns) { return true } - - // TODO: check if url is ignored by git / ide - - return false - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/DefaultFileWatcherFactory.swift b/Tool/Sources/Workspace/FileChangeWatcher/DefaultFileWatcherFactory.swift deleted file mode 100644 index eecbebbc..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/DefaultFileWatcherFactory.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -public class DefaultFileWatcherFactory: FileWatcherFactory { - public init() {} - - public func createFileWatcher(fileURL: URL, dispatchQueue: DispatchQueue?, - onFileModified: (() -> Void)? = nil, onFileDeleted: (() -> Void)? = nil, onFileRenamed: (() -> Void)? = nil) -> FileWatcherProtocol { - return SingleFileWatcher(fileURL: fileURL, - dispatchQueue: dispatchQueue, - onFileModified: onFileModified, - onFileDeleted: onFileDeleted, - onFileRenamed: onFileRenamed - ) - } - - public func createDirectoryWatcher(watchedPaths: [URL], changePublisher: @escaping PublisherType, - publishInterval: TimeInterval) -> DirectoryWatcherProtocol { - return BatchingFileChangeWatcher(watchedPaths: watchedPaths, - changePublisher: changePublisher, - publishInterval: publishInterval, - fsEventProvider: FileChangeWatcherFSEventProvider() - ) - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/FSEventProvider.swift b/Tool/Sources/Workspace/FileChangeWatcher/FSEventProvider.swift deleted file mode 100644 index 3a15c016..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/FSEventProvider.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -public protocol FSEventProvider { - func createEventStream( - paths: CFArray, - latency: CFTimeInterval, - flags: UInt32, - callback: @escaping FSEventStreamCallback, - context: UnsafeMutablePointer - ) -> FSEventStreamRef? - - func startStream(_ stream: FSEventStreamRef) - func stopStream(_ stream: FSEventStreamRef) - func invalidateStream(_ stream: FSEventStreamRef) - func releaseStream(_ stream: FSEventStreamRef) - func setDispatchQueue(_ stream: FSEventStreamRef, queue: DispatchQueue) -} - -class FileChangeWatcherFSEventProvider: FSEventProvider { - init() {} - - func createEventStream( - paths: CFArray, - latency: CFTimeInterval, - flags: UInt32, - callback: @escaping FSEventStreamCallback, - context: UnsafeMutablePointer - ) -> FSEventStreamRef? { - return FSEventStreamCreate( - kCFAllocatorDefault, - callback, - context, - paths, - FSEventStreamEventId(kFSEventStreamEventIdSinceNow), - latency, - flags - ) - } - - func startStream(_ stream: FSEventStreamRef) { - FSEventStreamStart(stream) - } - - func stopStream(_ stream: FSEventStreamRef) { - FSEventStreamStop(stream) - } - - func invalidateStream(_ stream: FSEventStreamRef) { - FSEventStreamInvalidate(stream) - } - - func releaseStream(_ stream: FSEventStreamRef) { - FSEventStreamRelease(stream) - } - - func setDispatchQueue(_ stream: FSEventStreamRef, queue: DispatchQueue) { - FSEventStreamSetDispatchQueue(stream, queue) - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/FileChangeWatcherService.swift b/Tool/Sources/Workspace/FileChangeWatcher/FileChangeWatcherService.swift deleted file mode 100644 index 2bd28eee..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/FileChangeWatcherService.swift +++ /dev/null @@ -1,206 +0,0 @@ -import Foundation -import System -import Logger -import CoreServices -import LanguageServerProtocol -import XcodeInspector - -public class FileChangeWatcherService { - internal var watcher: DirectoryWatcherProtocol? - - private(set) public var workspaceURL: URL - private(set) public var publisher: PublisherType - private(set) public var publishInterval: TimeInterval - - // Dependencies injected for testing - internal let workspaceFileProvider: WorkspaceFileProvider - internal let watcherFactory: FileWatcherFactory - - // Watching workspace metadata file - private var workspaceConfigFileWatcher: FileWatcherProtocol? - private var isMonitoringWorkspaceConfigFile = false - private let monitoringQueue = DispatchQueue(label: "com.github.copilot.workspaceMonitor", qos: .utility) - private let configFileEventQueue = DispatchQueue(label: "com.github.copilot.workspaceEventMonitor", qos: .utility) - - public init( - _ workspaceURL: URL, - publisher: @escaping PublisherType, - publishInterval: TimeInterval = 3.0, - workspaceFileProvider: WorkspaceFileProvider = FileChangeWatcherWorkspaceFileProvider(), - watcherFactory: FileWatcherFactory? = nil - ) { - self.workspaceURL = workspaceURL - self.publisher = publisher - self.publishInterval = publishInterval - self.workspaceFileProvider = workspaceFileProvider - self.watcherFactory = watcherFactory ?? DefaultFileWatcherFactory() - } - - deinit { - stopWorkspaceConfigFileMonitoring() - self.watcher = nil - } - - public func startWatching() { - guard workspaceURL.path != "/" else { return } - - guard watcher == nil else { return } - - let projects = workspaceFileProvider.getProjects(by: workspaceURL) - guard projects.count > 0 else { return } - - watcher = watcherFactory.createDirectoryWatcher(watchedPaths: projects, changePublisher: publisher, publishInterval: publishInterval) - Logger.client.info("Started watching for file changes in \(projects)") - - startWatchingProject() - } - - internal func startWatchingProject() { - if self.workspaceFileProvider.isXCWorkspace(self.workspaceURL) { - guard !isMonitoringWorkspaceConfigFile else { return } - isMonitoringWorkspaceConfigFile = true - recreateConfigFileMonitor() - } - } - - private func recreateConfigFileMonitor() { - let workspaceDataFile = workspaceURL.appendingPathComponent("contents.xcworkspacedata") - - // Clean up existing monitor first - cleanupCurrentMonitor() - - guard self.workspaceFileProvider.fileExists(atPath: workspaceDataFile.path) else { - Logger.client.info("[FileWatcher] contents.xcworkspacedata file not found at \(workspaceDataFile.path).") - return - } - - // Create SingleFileWatcher for the workspace file - workspaceConfigFileWatcher = self.watcherFactory.createFileWatcher( - fileURL: workspaceDataFile, - dispatchQueue: configFileEventQueue, - onFileModified: { [weak self] in - self?.handleWorkspaceConfigFileChange() - self?.scheduleMonitorRecreation(delay: 1.0) - }, - onFileDeleted: { [weak self] in - self?.handleWorkspaceConfigFileChange() - self?.scheduleMonitorRecreation(delay: 1.0) - }, - onFileRenamed: nil - ) - - let _ = workspaceConfigFileWatcher?.startWatching() - } - - private func handleWorkspaceConfigFileChange() { - guard let watcher = self.watcher else { - return - } - - let workspaceDataFile = workspaceURL.appendingPathComponent("contents.xcworkspacedata") - // Check if file still exists - let fileExists = self.workspaceFileProvider.fileExists(atPath: workspaceDataFile.path) - if fileExists { - // File was modified, check for project changes - let watchingProjects = Set(watcher.paths()) - let projects = Set(self.workspaceFileProvider.getProjects(by: self.workspaceURL)) - - /// find added projects - let addedProjects = projects.subtracting(watchingProjects) - if !addedProjects.isEmpty { - self.onProjectAdded(Array(addedProjects)) - } - - /// find removed projects - let removedProjects = watchingProjects.subtracting(projects) - if !removedProjects.isEmpty { - self.onProjectRemoved(Array(removedProjects)) - } - } else { - Logger.client.info("[FileWatcher] contents.xcworkspacedata file was deleted") - } - } - - private func scheduleMonitorRecreation(delay: TimeInterval) { - monitoringQueue.asyncAfter(deadline: .now() + delay) { [weak self] in - guard let self = self, self.isMonitoringWorkspaceConfigFile else { return } - self.recreateConfigFileMonitor() - } - } - - private func cleanupCurrentMonitor() { - workspaceConfigFileWatcher?.stopWatching() - workspaceConfigFileWatcher = nil - } - - private func stopWorkspaceConfigFileMonitoring() { - isMonitoringWorkspaceConfigFile = false - cleanupCurrentMonitor() - } - - internal func onProjectAdded(_ projectURLs: [URL]) { - guard let watcher = watcher, projectURLs.count > 0 else { return } - - watcher.addPaths(projectURLs) - - Logger.client.info("Started watching for file changes in \(projectURLs)") - - /// sync all the files as created in the project when added - for projectURL in projectURLs { - let files = workspaceFileProvider.getFilesInActiveWorkspace( - workspaceURL: projectURL, - workspaceRootURL: projectURL - ) - publisher(files.map { .init(uri: $0.url.absoluteString, type: .created) }) - } - } - - internal func onProjectRemoved(_ projectURLs: [URL]) { - guard let watcher = watcher, projectURLs.count > 0 else { return } - - watcher.removePaths(projectURLs) - - Logger.client.info("Stopped watching for file changes in \(projectURLs)") - - /// sync all the files as deleted in the project when removed - for projectURL in projectURLs { - let files = workspaceFileProvider.getFilesInActiveWorkspace(workspaceURL: projectURL, workspaceRootURL: projectURL) - publisher(files.map { .init(uri: $0.url.absoluteString, type: .deleted) }) - } - } -} - -@globalActor -public enum PoolActor: GlobalActor { - public actor Actor {} - public static let shared = Actor() -} - -public class FileChangeWatcherServicePool { - - public static let shared = FileChangeWatcherServicePool() - private var servicePool: [URL: FileChangeWatcherService] = [:] - - private init() {} - - @PoolActor - public func watch(for workspaceURL: URL, publisher: @escaping PublisherType) { - guard workspaceURL.path != "/" else { return } - - var validWorkspaceURL: URL? = nil - if WorkspaceFile.isXCWorkspace(workspaceURL) { - validWorkspaceURL = workspaceURL - } else if WorkspaceFile.isXCProject(workspaceURL) { - validWorkspaceURL = WorkspaceFile.getWorkspaceByProject(workspaceURL) - } - - guard let validWorkspaceURL else { return } - - guard servicePool[workspaceURL] == nil else { return } - - let watcherService = FileChangeWatcherService(validWorkspaceURL, publisher: publisher) - watcherService.startWatching() - - servicePool[workspaceURL] = watcherService - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/FileWatcherProtocol.swift b/Tool/Sources/Workspace/FileChangeWatcher/FileWatcherProtocol.swift deleted file mode 100644 index 7252d613..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/FileWatcherProtocol.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import LanguageServerProtocol - -public protocol FileWatcherProtocol { - func startWatching() -> Bool - func stopWatching() -} - -public typealias PublisherType = (([FileEvent]) -> Void) - -public protocol DirectoryWatcherProtocol: FileWatcherProtocol { - func addPaths(_ paths: [URL]) - func removePaths(_ paths: [URL]) - func paths() -> [URL] -} - -public protocol FileWatcherFactory { - func createFileWatcher( - fileURL: URL, - dispatchQueue: DispatchQueue?, - onFileModified: (() -> Void)?, - onFileDeleted: (() -> Void)?, - onFileRenamed: (() -> Void)? - ) -> FileWatcherProtocol - - func createDirectoryWatcher( - watchedPaths: [URL], - changePublisher: @escaping PublisherType, - publishInterval: TimeInterval - ) -> DirectoryWatcherProtocol -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/SingleFileWatcher.swift b/Tool/Sources/Workspace/FileChangeWatcher/SingleFileWatcher.swift deleted file mode 100644 index 612e402d..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/SingleFileWatcher.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation -import Logger - -class SingleFileWatcher: FileWatcherProtocol { - private var fileDescriptor: CInt = -1 - private var dispatchSource: DispatchSourceFileSystemObject? - private let fileURL: URL - private let dispatchQueue: DispatchQueue? - - // Callbacks for file events - private let onFileModified: (() -> Void)? - private let onFileDeleted: (() -> Void)? - private let onFileRenamed: (() -> Void)? - - init( - fileURL: URL, - dispatchQueue: DispatchQueue? = nil, - onFileModified: (() -> Void)? = nil, - onFileDeleted: (() -> Void)? = nil, - onFileRenamed: (() -> Void)? = nil - ) { - self.fileURL = fileURL - self.dispatchQueue = dispatchQueue - self.onFileModified = onFileModified - self.onFileDeleted = onFileDeleted - self.onFileRenamed = onFileRenamed - } - - func startWatching() -> Bool { - // Open the file for event-only monitoring - fileDescriptor = open(fileURL.path, O_EVTONLY) - guard fileDescriptor != -1 else { - Logger.client.info("[FileWatcher] Failed to open file \(fileURL.path).") - return false - } - - // Create DispatchSource to monitor the file descriptor - dispatchSource = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: fileDescriptor, - eventMask: [.write, .delete, .rename], - queue: self.dispatchQueue ?? DispatchQueue.global() - ) - - dispatchSource?.setEventHandler { [weak self] in - guard let self = self else { return } - - let flags = self.dispatchSource?.data ?? [] - - if flags.contains(.write) { - self.onFileModified?() - } - if flags.contains(.delete) { - self.onFileDeleted?() - self.stopWatching() - } - if flags.contains(.rename) { - self.onFileRenamed?() - self.stopWatching() - } - } - - dispatchSource?.setCancelHandler { [weak self] in - guard let self = self else { return } - close(self.fileDescriptor) - self.fileDescriptor = -1 - } - - dispatchSource?.resume() - Logger.client.info("[FileWatcher] Started watching file: \(fileURL.path)") - return true - } - - func stopWatching() { - dispatchSource?.cancel() - dispatchSource = nil - } - - deinit { - stopWatching() - } -} diff --git a/Tool/Sources/Workspace/FileChangeWatcher/WorkspaceFileProvider.swift b/Tool/Sources/Workspace/FileChangeWatcher/WorkspaceFileProvider.swift deleted file mode 100644 index 2a5d464a..00000000 --- a/Tool/Sources/Workspace/FileChangeWatcher/WorkspaceFileProvider.swift +++ /dev/null @@ -1,38 +0,0 @@ -import ConversationServiceProvider -import CopilotForXcodeKit -import Foundation - -public protocol WorkspaceFileProvider { - func getProjects(by workspaceURL: URL) -> [URL] - func getFilesInActiveWorkspace(workspaceURL: URL, workspaceRootURL: URL) -> [FileReference] - func isXCProject(_ url: URL) -> Bool - func isXCWorkspace(_ url: URL) -> Bool - func fileExists(atPath: String) -> Bool -} - -public class FileChangeWatcherWorkspaceFileProvider: WorkspaceFileProvider { - public init() {} - - public func getProjects(by workspaceURL: URL) -> [URL] { - guard let workspaceInfo = WorkspaceFile.getWorkspaceInfo(workspaceURL: workspaceURL) - else { return [] } - - return WorkspaceFile.getProjects(workspace: workspaceInfo).compactMap { URL(string: $0.uri) } - } - - public func getFilesInActiveWorkspace(workspaceURL: URL, workspaceRootURL: URL) -> [FileReference] { - return WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: workspaceURL, workspaceRootURL: workspaceRootURL) - } - - public func isXCProject(_ url: URL) -> Bool { - return WorkspaceFile.isXCProject(url) - } - - public func isXCWorkspace(_ url: URL) -> Bool { - return WorkspaceFile.isXCWorkspace(url) - } - - public func fileExists(atPath: String) -> Bool { - return FileManager.default.fileExists(atPath: atPath) - } -} diff --git a/Tool/Sources/Workspace/FileSaveWatcher.swift b/Tool/Sources/Workspace/FileSaveWatcher.swift deleted file mode 100644 index 97c01428..00000000 --- a/Tool/Sources/Workspace/FileSaveWatcher.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -final class FileSaveWatcher { - let url: URL - var fileHandle: FileHandle? - var source: DispatchSourceFileSystemObject? - var changeHandler: () -> Void = {} - - init(fileURL: URL) { - url = fileURL - startup() - } - - deinit { - source?.cancel() - } - - func startup() { - if let source = source { - source.cancel() - } - - fileHandle = try? FileHandle(forReadingFrom: url) - if let fileHandle = fileHandle { - source = DispatchSource.makeFileSystemObjectSource( - fileDescriptor: fileHandle.fileDescriptor, - eventMask: .link, - queue: .main - ) - - source?.setEventHandler { [weak self] in - self?.changeHandler() - self?.startup() - } - - source?.resume() - } - } -} diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift deleted file mode 100644 index 8da014a5..00000000 --- a/Tool/Sources/Workspace/Filespace.swift +++ /dev/null @@ -1,186 +0,0 @@ -import Dependencies -import Foundation -import SuggestionBasic - -public protocol FilespacePropertyKey { - associatedtype Value - static func createDefaultValue() -> Value -} - -public final class FilespacePropertyValues { - private var storage: [ObjectIdentifier: Any] = [:] - - @WorkspaceActor - public subscript(_ key: K.Type) -> K.Value { - get { - if let value = storage[ObjectIdentifier(key)] as? K.Value { - return value - } - let value = key.createDefaultValue() - storage[ObjectIdentifier(key)] = value - return value - } - set { - storage[ObjectIdentifier(key)] = newValue - } - } -} - -public struct FilespaceCodeMetadata: Equatable { - public var uti: String? - public var tabSize: Int? - public var indentSize: Int? - public var usesTabsForIndentation: Bool? - public var lineEnding: String = "\n" - - init( - uti: String? = nil, - tabSize: Int? = nil, - indentSize: Int? = nil, - usesTabsForIndentation: Bool? = nil, - lineEnding: String = "\n" - ) { - self.uti = uti - self.tabSize = tabSize - self.indentSize = indentSize - self.usesTabsForIndentation = usesTabsForIndentation - self.lineEnding = lineEnding - } - - public mutating func guessLineEnding(from text: String?) { - lineEnding = if let proposedEnding = text?.last { - if proposedEnding.isNewline { - String(proposedEnding) - } else { - "\n" - } - } else { - "\n" - } - } -} - -@dynamicMemberLookup -public final class Filespace { - - // MARK: Metadata - - public let fileURL: URL - public private(set) lazy var language: CodeLanguage = languageIdentifierFromFileURL(fileURL) - public var codeMetadata: FilespaceCodeMetadata = .init() - public var isTextReadable: Bool { - fileURL.pathExtension != "mlmodel" - } - - // MARK: Suggestions - - public private(set) var suggestionIndex: Int = 0 - public internal(set) var suggestions: [CodeSuggestion] = [] { - didSet { refreshUpdateTime() } - } - - public var presentingSuggestion: CodeSuggestion? { - guard suggestions.endIndex > suggestionIndex, suggestionIndex >= 0 else { return nil } - return suggestions[suggestionIndex] - } - - public private(set) var errorMessage: String = "" { - didSet { refreshUpdateTime() } - } - - // MARK: Life Cycle - - public var isExpired: Bool { - Environment.now().timeIntervalSince(lastUpdateTime) > 60 * 3 - } - - public private(set) var lastUpdateTime: Date = Environment.now() - private var additionalProperties = FilespacePropertyValues() - let fileSaveWatcher: FileSaveWatcher - let onClose: (URL) -> Void - - @WorkspaceActor - public private(set) var version: Int = 0 - - // MARK: Methods - - deinit { - onClose(fileURL) - } - - init( - fileURL: URL, - onSave: @escaping (Filespace) -> Void, - onClose: @escaping (URL) -> Void - ) { - self.fileURL = fileURL - self.onClose = onClose - fileSaveWatcher = .init(fileURL: fileURL) - fileSaveWatcher.changeHandler = { [weak self] in - guard let self else { return } - onSave(self) - } - } - - @WorkspaceActor - public subscript( - dynamicMember dynamicMember: WritableKeyPath - ) -> K { - get { additionalProperties[keyPath: dynamicMember] } - set { additionalProperties[keyPath: dynamicMember] = newValue } - } - - @WorkspaceActor - public func reset() { - suggestions = [] - suggestionIndex = 0 - } - - @WorkspaceActor - public func updateSuggestionsWithSameSelection(_ suggestions: [CodeSuggestion]) { - self.suggestions = suggestions - suggestionIndex = suggestionIndex < suggestions.count ? suggestionIndex : 0 - } - - public func refreshUpdateTime() { - lastUpdateTime = Environment.now() - } - - @WorkspaceActor - public func setSuggestions(_ suggestions: [CodeSuggestion]) { - self.suggestions = suggestions - suggestionIndex = 0 - } - - @WorkspaceActor - public func nextSuggestion() { - suggestionIndex += 1 - if suggestionIndex >= suggestions.endIndex { - suggestionIndex = 0 - } - } - - @WorkspaceActor - public func previousSuggestion() { - suggestionIndex -= 1 - if suggestionIndex < 0 { - suggestionIndex = suggestions.endIndex - 1 - } - } - - @WorkspaceActor - public func bumpVersion() { - version += 1 - } - - @WorkspaceActor - public func setError(_ message: String) { - errorMessage = message - } - - @WorkspaceActor - public func dismissError() { - errorMessage = "" - } -} - diff --git a/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift b/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift deleted file mode 100644 index 06833867..00000000 --- a/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import Preferences - -public final class OpenedFileRecoverableStorage { - let projectRootURL: URL - let userDefault = UserDefaults.shared - let key = "OpenedFileRecoverableStorage" - - init(projectRootURL: URL) { - self.projectRootURL = projectRootURL - } - - public func openFile(fileURL: URL) { - var dict = userDefault.dictionary(forKey: key) ?? [:] - var openedFiles = Set(dict[projectRootURL.path] as? [String] ?? []) - openedFiles.insert(fileURL.path) - dict[projectRootURL.path] = Array(openedFiles) - Task { @MainActor [dict] in - userDefault.set(dict, forKey: key) - } - } - - public func closeFile(fileURL: URL) { - var dict = userDefault.dictionary(forKey: key) ?? [:] - var openedFiles = dict[projectRootURL.path] as? [String] ?? [] - openedFiles.removeAll(where: { $0 == fileURL.path }) - dict[projectRootURL.path] = openedFiles - Task { @MainActor [dict] in - userDefault.set(dict, forKey: key) - } - } - - public var openedFiles: [URL] { - let dict = userDefault.dictionary(forKey: key) ?? [:] - let openedFiles = dict[projectRootURL.path] as? [String] ?? [] - return openedFiles.map { URL(fileURLWithPath: $0) } - } -} - diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift deleted file mode 100644 index 82248822..00000000 --- a/Tool/Sources/Workspace/Workspace.swift +++ /dev/null @@ -1,216 +0,0 @@ -import Foundation -import Preferences -import UserDefaultsObserver -import XcodeInspector -import Logger -import UniformTypeIdentifiers - -enum Environment { - static var now = { Date() } -} - -public protocol WorkspacePropertyKey { - associatedtype Value - static func createDefaultValue() -> Value -} - -public class WorkspacePropertyValues { - private var storage: [ObjectIdentifier: Any] = [:] - - @WorkspaceActor - public subscript(_ key: K.Type) -> K.Value { - get { - if let value = storage[ObjectIdentifier(key)] as? K.Value { - return value - } - let value = key.createDefaultValue() - storage[ObjectIdentifier(key)] = value - return value - } - set { - storage[ObjectIdentifier(key)] = newValue - } - } -} - -open class WorkspacePlugin { - public private(set) weak var workspace: Workspace? - public var projectRootURL: URL { workspace?.projectRootURL ?? URL(fileURLWithPath: "/") } - public var workspaceURL: URL { workspace?.workspaceURL ?? projectRootURL } - public var filespaces: [URL: Filespace] { workspace?.filespaces ?? [:] } - - public init(workspace: Workspace) { - self.workspace = workspace - } - - open func didOpenFilespace(_: Filespace) {} - open func didSaveFilespace(_: Filespace) {} - open func didUpdateFilespace(_: Filespace, content: String) {} - open func didCloseFilespace(_: URL) {} -} - -@dynamicMemberLookup -public final class Workspace { - public enum WorkspaceFileError: LocalizedError { - case unsupportedFile(extensionName: String) - case fileNotFound(fileURL: URL) - case invalidFileFormat(fileURL: URL) - - public var errorDescription: String? { - switch self { - case .unsupportedFile(let extensionName): - return "File type \(extensionName) unsupported." - case .fileNotFound(let fileURL): - return "File \(fileURL) not found." - case .invalidFileFormat(let fileURL): - return "The file \(fileURL.lastPathComponent) couldn't be opened because it isn't in the correct format." - } - } - } - - public struct CantFindWorkspaceError: Error, LocalizedError { - public var errorDescription: String? { - "Can't find workspace." - } - } - - private var additionalProperties = WorkspacePropertyValues() - public internal(set) var plugins = [ObjectIdentifier: WorkspacePlugin]() - public let workspaceURL: URL - public let projectRootURL: URL - public let openedFileRecoverableStorage: OpenedFileRecoverableStorage - public private(set) var lastLastUpdateTime = Environment.now() - public var isExpired: Bool { - Environment.now().timeIntervalSince(lastLastUpdateTime) > 60 * 60 * 1 - } - - public private(set) var filespaces = [URL: Filespace]() - - let userDefaultsObserver = UserDefaultsObserver( - object: UserDefaults.shared, forKeyPaths: [ - UserDefaultPreferenceKeys().suggestionFeatureEnabledProjectList.key, - UserDefaultPreferenceKeys().disableSuggestionFeatureGlobally.key, - ], context: nil - ) - - public subscript( - dynamicMember dynamicMember: WritableKeyPath - ) -> K { - get { additionalProperties[keyPath: dynamicMember] } - set { additionalProperties[keyPath: dynamicMember] = newValue } - } - - public func plugin(for type: P.Type) -> P? { - plugins[ObjectIdentifier(type)] as? P - } - - init(workspaceURL: URL) { - self.workspaceURL = workspaceURL - self.projectRootURL = WorkspaceXcodeWindowInspector.extractProjectURL( - workspaceURL: workspaceURL, - documentURL: nil - ) ?? workspaceURL - openedFileRecoverableStorage = .init(projectRootURL: projectRootURL) - let openedFiles = openedFileRecoverableStorage.openedFiles - Task { @WorkspaceActor in - for fileURL in openedFiles { - do { - _ = try createFilespaceIfNeeded(fileURL: fileURL) - } catch _ as WorkspaceFileError { - openedFileRecoverableStorage.closeFile(fileURL: fileURL) - } catch { - Logger.workspacePool.error(error) - } - } - } - } - - public func refreshUpdateTime() { - lastLastUpdateTime = Environment.now() - } - - @WorkspaceActor - public func createFilespaceIfNeeded(fileURL: URL) throws -> Filespace { - let extensionName = fileURL.pathExtension - - if ["xcworkspace", "xcodeproj"].contains( - extensionName - ) || FileManager.default - .fileIsDirectory(atPath: fileURL.path) { - throw WorkspaceFileError.unsupportedFile(extensionName: extensionName) - } - - guard FileManager.default.fileExists(atPath: fileURL.path) else { - throw WorkspaceFileError.fileNotFound(fileURL: fileURL) - } - - if let contentType = try fileURL.resourceValues(forKeys: [.contentTypeKey]).contentType, - !contentType.conforms(to: UTType.data) { - throw WorkspaceFileError.invalidFileFormat(fileURL: fileURL) - } - - let existedFilespace = filespaces[fileURL] - let filespace = existedFilespace ?? .init( - fileURL: fileURL, - onSave: { [weak self] filespace in - guard let self else { return } - self.didSaveFilespace(filespace) - }, - onClose: { [weak self] url in - guard let self else { return } - self.didCloseFilespace(url) - } - ) - if filespaces[fileURL] == nil { - filespaces[fileURL] = filespace - } - if existedFilespace == nil { - didOpenFilespace(filespace) - } else { - filespace.refreshUpdateTime() - } - return filespace - } - - @WorkspaceActor - public func closeFilespace(fileURL: URL) { - filespaces[fileURL] = nil - } - - @WorkspaceActor - public func didUpdateFilespace(fileURL: URL, content: String) { - refreshUpdateTime() - guard let filespace = filespaces[fileURL] else { return } - filespace.bumpVersion() - filespace.refreshUpdateTime() - for plugin in plugins.values { - plugin.didUpdateFilespace(filespace, content: content) - } - } - - @WorkspaceActor - func didOpenFilespace(_ filespace: Filespace) { - refreshUpdateTime() - openedFileRecoverableStorage.openFile(fileURL: filespace.fileURL) - for plugin in plugins.values { - plugin.didOpenFilespace(filespace) - } - } - - @WorkspaceActor - func didCloseFilespace(_ fileURL: URL) { - for plugin in self.plugins.values { - plugin.didCloseFilespace(fileURL) - } - } - - @WorkspaceActor - func didSaveFilespace(_ filespace: Filespace) { - refreshUpdateTime() - filespace.refreshUpdateTime() - for plugin in plugins.values { - plugin.didSaveFilespace(filespace) - } - } -} - diff --git a/Tool/Sources/Workspace/WorkspaceFile.swift b/Tool/Sources/Workspace/WorkspaceFile.swift deleted file mode 100644 index 449469cd..00000000 --- a/Tool/Sources/Workspace/WorkspaceFile.swift +++ /dev/null @@ -1,295 +0,0 @@ -import Foundation -import Logger -import ConversationServiceProvider -import CopilotForXcodeKit -import XcodeInspector - -public let supportedFileExtensions: Set = ["swift", "m", "mm", "h", "cpp", "c", "js", "ts", "py", "rb", "java", "applescript", "scpt", "plist", "entitlements", "md", "json", "xml", "txt", "yaml", "yml", "html", "css"] -public let skipPatterns: [String] = [ - ".git", - ".svn", - ".hg", - "CVS", - ".DS_Store", - "Thumbs.db", - "node_modules", - "bower_components" -] - -public struct ProjectInfo { - public let uri: String - public let name: String -} - -extension NSError { - var isPermissionDenied: Bool { - return (domain == NSCocoaErrorDomain && code == 257) || - (domain == NSPOSIXErrorDomain && code == 1) - } -} - -public struct WorkspaceFile { - private static let wellKnownBundleExtensions: Set = ["app", "xcarchive"] - - static func isXCWorkspace(_ url: URL) -> Bool { - return url.pathExtension == "xcworkspace" && FileManager.default.fileExists(atPath: url.appendingPathComponent("contents.xcworkspacedata").path) - } - - static func isXCProject(_ url: URL) -> Bool { - return url.pathExtension == "xcodeproj" && FileManager.default.fileExists(atPath: url.appendingPathComponent("project.pbxproj").path) - } - - static func isKnownPackageFolder(_ url: URL) -> Bool { - guard wellKnownBundleExtensions.contains(url.pathExtension) else { - return false - } - - let resourceValues = try? url.resourceValues(forKeys: [.isPackageKey]) - return resourceValues?.isPackage == true - } - - static func getWorkspaceByProject(_ url: URL) -> URL? { - guard isXCProject(url) else { return nil } - let workspaceURL = url.appendingPathComponent("project.xcworkspace") - - return isXCWorkspace(workspaceURL) ? workspaceURL : nil - } - - static func getSubprojectURLs(in workspaceURL: URL) -> [URL] { - let workspaceFile = workspaceURL.appendingPathComponent("contents.xcworkspacedata") - do { - let data = try Data(contentsOf: workspaceFile) - return getSubprojectURLs(workspaceURL: workspaceURL, data: data) - } catch let error as NSError { - if error.isPermissionDenied { - Logger.client.info("Permission denied for accessing file at \(workspaceFile.path)") - } else { - Logger.client.error("Failed to read workspace file at \(workspaceFile.path): \(error)") - } - return [] - } - } - - static func getSubprojectURLs(workspaceURL: URL, data: Data) -> [URL] { - do { - let xml = try XMLDocument(data: data) - let workspaceBaseURL = workspaceURL.deletingLastPathComponent() - // Process all FileRefs and Groups recursively - return processWorkspaceNodes(xml.rootElement()?.children ?? [], baseURL: workspaceBaseURL) - } catch { - Logger.client.error("Failed to parse workspace file: \(error)") - } - - return [] - } - - /// Recursively processes all nodes in a workspace file, collecting project URLs - private static func processWorkspaceNodes(_ nodes: [XMLNode], baseURL: URL, currentGroupPath: String = "") -> [URL] { - var results: [URL] = [] - - for node in nodes { - guard let element = node as? XMLElement else { continue } - - let location = element.attribute(forName: "location")?.stringValue ?? "" - if element.name == "FileRef" { - if let url = resolveProjectLocation(location: location, baseURL: baseURL, groupPath: currentGroupPath), - !results.contains(url) { - results.append(url) - } - } else if element.name == "Group" { - var groupPath = currentGroupPath - if !location.isEmpty, let path = extractPathFromLocation(location) { - groupPath = (groupPath as NSString).appendingPathComponent(path) - } - - // Process all children of this group, passing the updated group path - let childResults = processWorkspaceNodes(element.children ?? [], baseURL: baseURL, currentGroupPath: groupPath) - - for url in childResults { - if !results.contains(url) { - results.append(url) - } - } - } - } - - return results - } - - /// Extracts path component from a location string - private static func extractPathFromLocation(_ location: String) -> String? { - for prefix in ["group:", "container:", "self:"] { - if location.starts(with: prefix) { - return location.replacingOccurrences(of: prefix, with: "") - } - } - return nil - } - - static func resolveProjectLocation(location: String, baseURL: URL, groupPath: String = "") -> URL? { - var path = "" - - // Extract the path from the location string - if let extractedPath = extractPathFromLocation(location) { - path = extractedPath - } else { - // Unknown location format - return nil - } - - var url: URL = groupPath.isEmpty ? baseURL : baseURL.appendingPathComponent(groupPath) - url = path.isEmpty ? url : url.appendingPathComponent(path) - url = url.standardized // normalize “..” or “.” in the path - if isXCProject(url) { // return the containing directory of the .xcodeproj file - url.deleteLastPathComponent() - } - - return url - } - - static func matchesPatterns(_ url: URL, patterns: [String]) -> Bool { - let fileName = url.lastPathComponent - for pattern in patterns { - if fnmatch(pattern, fileName, 0) == 0 { - return true - } - } - return false - } - - public static func getWorkspaceInfo(workspaceURL: URL) -> WorkspaceInfo? { - guard let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL(workspaceURL: workspaceURL, documentURL: nil) else { - return nil - } - - let workspaceInfo = WorkspaceInfo(workspaceURL: workspaceURL, projectURL: projectURL) - return workspaceInfo - } - - public static func getProjects(workspace: WorkspaceInfo) -> [ProjectInfo] { - var subprojects: [ProjectInfo] = [] - if isXCWorkspace(workspace.workspaceURL) { - subprojects = getSubprojectURLs(in: workspace.workspaceURL).map( { projectURL in - ProjectInfo(uri: projectURL.absoluteString, name: getDisplayNameOfXcodeWorkspace(url: projectURL)) - }) - } else { - subprojects.append(ProjectInfo(uri: workspace.projectURL.absoluteString, name: getDisplayNameOfXcodeWorkspace(url: workspace.projectURL))) - } - return subprojects - } - - public static func getDisplayNameOfXcodeWorkspace(url: URL) -> String { - var name = url.lastPathComponent - let suffixes = [".xcworkspace", ".xcodeproj", ".playground"] - for suffix in suffixes { - if name.hasSuffix(suffix) { - name = String(name.dropLast(suffix.count)) - break - } - } - return name - } - - private static func shouldSkipFile(_ url: URL) -> Bool { - return matchesPatterns(url, patterns: skipPatterns) - || isXCWorkspace(url) - || isXCProject(url) - || isKnownPackageFolder(url) - || url.pathExtension == "xcassets" - } - - public static func isValidFile( - _ url: URL, - shouldExcludeFile: ((URL) -> Bool)? = nil - ) throws -> Bool { - if shouldSkipFile(url) { return false } - - let resourceValues = try url.resourceValues(forKeys: [.isRegularFileKey, .isDirectoryKey]) - - // Handle directories if needed - if resourceValues.isDirectory == true { return false } - - guard resourceValues.isRegularFile == true else { return false } - if supportedFileExtensions.contains(url.pathExtension.lowercased()) == false { - return false - } - - // Apply the custom file exclusion check if provided - if let shouldExcludeFile = shouldExcludeFile, - shouldExcludeFile(url) { return false } - - return true - } - - public static func getFilesInActiveWorkspace( - workspaceURL: URL, - workspaceRootURL: URL, - shouldExcludeFile: ((URL) -> Bool)? = nil - ) -> [FileReference] { - var files: [FileReference] = [] - do { - let fileManager = FileManager.default - var subprojects: [URL] = [] - if isXCWorkspace(workspaceURL) { - subprojects = getSubprojectURLs(in: workspaceURL) - } else { - subprojects.append(workspaceRootURL) - } - for subproject in subprojects { - guard FileManager.default.fileExists(atPath: subproject.path) else { - continue - } - - let enumerator = fileManager.enumerator( - at: subproject, - includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey], - options: [.skipsHiddenFiles] - ) - - while let fileURL = enumerator?.nextObject() as? URL { - // Skip items matching the specified pattern - if shouldSkipFile(fileURL) { - enumerator?.skipDescendants() - continue - } - - guard try isValidFile(fileURL, shouldExcludeFile: shouldExcludeFile) else { continue } - - let relativePath = fileURL.path.replacingOccurrences(of: workspaceRootURL.path, with: "") - let fileName = fileURL.lastPathComponent - - let file = FileReference(url: fileURL, relativePath: relativePath, fileName: fileName) - files.append(file) - } - } - } catch { - Logger.client.error("Failed to get files in workspace: \(error)") - } - - return files - } - - /* - used for `project-context` skill. Get filed for watching for syncing to CLS - */ - public static func getWatchedFiles( - workspaceURL: URL, - projectURL: URL, - excludeGitIgnoredFiles: Bool, - excludeIDEIgnoredFiles: Bool - ) -> [FileReference] { - // Directly return for invalid workspace - guard workspaceURL.path != "/" else { return [] } - - // TODO: implement - let shouldExcludeFile: ((URL) -> Bool)? = nil - - let files = getFilesInActiveWorkspace( - workspaceURL: workspaceURL, - workspaceRootURL: projectURL, - shouldExcludeFile: shouldExcludeFile - ) - - return files - } -} diff --git a/Tool/Sources/Workspace/WorkspaceFileIndex.swift b/Tool/Sources/Workspace/WorkspaceFileIndex.swift deleted file mode 100644 index f1e29819..00000000 --- a/Tool/Sources/Workspace/WorkspaceFileIndex.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation -import ConversationServiceProvider - -public class WorkspaceFileIndex { - public static let shared = WorkspaceFileIndex() - /// Maximum number of files allowed per workspace - public static let maxFilesPerWorkspace = 1_000_000 - - private var workspaceIndex: [URL: [FileReference]] = [:] - private let queue = DispatchQueue(label: "com.copilot.workspace-file-index") - - /// Reset files for a specific workspace URL - public func setFiles(_ files: [FileReference], for workspaceURL: URL) { - queue.sync { - // Enforce the file limit when setting files - if files.count > Self.maxFilesPerWorkspace { - self.workspaceIndex[workspaceURL] = Array(files.prefix(Self.maxFilesPerWorkspace)) - } else { - self.workspaceIndex[workspaceURL] = files - } - } - } - - /// Get all files for a specific workspace URL - public func getFiles(for workspaceURL: URL) -> [FileReference]? { - return workspaceIndex[workspaceURL] - } - - /// Add a file to the workspace index - /// - Returns: true if the file was added successfully, false if the workspace has reached the maximum file limit - @discardableResult - public func addFile(_ file: FileReference, to workspaceURL: URL) -> Bool { - return queue.sync { - if self.workspaceIndex[workspaceURL] == nil { - self.workspaceIndex[workspaceURL] = [] - } - - // Check if we've reached the maximum file limit - let currentFileCount = self.workspaceIndex[workspaceURL]!.count - if currentFileCount >= Self.maxFilesPerWorkspace { - return false - } - - // Avoid duplicates by checking if file already exists - if !self.workspaceIndex[workspaceURL]!.contains(file) { - self.workspaceIndex[workspaceURL]!.append(file) - return true - } - - return true // File already exists, so we consider this a successful "add" - } - } - - /// Remove a file from the workspace index - public func removeFile(_ file: FileReference, from workspaceURL: URL) { - queue.sync { - self.workspaceIndex[workspaceURL]?.removeAll { $0 == file } - } - } -} diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift deleted file mode 100644 index 9807702d..00000000 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ /dev/null @@ -1,173 +0,0 @@ -import Dependencies -import Foundation -import Logger -import XcodeInspector - -public struct WorkspacePoolDependencyKey: DependencyKey { - public static var liveValue: WorkspacePool = .init() -} - -public extension DependencyValues { - var workspacePool: WorkspacePool { - get { self[WorkspacePoolDependencyKey.self] } - set { self[WorkspacePoolDependencyKey.self] = newValue } - } -} - -@globalActor public enum WorkspaceActor { - public actor TheActor {} - public static let shared = TheActor() -} - -public class WorkspacePool { - public enum Error: Swift.Error, LocalizedError { - case invalidWorkspaceURL(URL) - - public var errorDescription: String? { - switch self { - case let .invalidWorkspaceURL(url): - return "Invalid workspace URL: \(url)" - } - } - } - - public internal(set) var workspaces: [URL: Workspace] = [:] - var plugins = [ObjectIdentifier: (Workspace) -> WorkspacePlugin]() - - public init( - workspaces: [URL: Workspace] = [:], - plugins: [ObjectIdentifier: (Workspace) -> WorkspacePlugin] = [:] - ) { - self.workspaces = workspaces - self.plugins = plugins - } - - public func registerPlugin(_ plugin: @escaping (Workspace) -> Plugin) { - let id = ObjectIdentifier(Plugin.self) - let erasedPlugin: (Workspace) -> WorkspacePlugin = { plugin($0) } - plugins[id] = erasedPlugin - - for workspace in workspaces.values { - addPlugin(erasedPlugin, id: id, to: workspace) - } - } - - public func unregisterPlugin(_: Plugin.Type) { - let id = ObjectIdentifier(Plugin.self) - plugins[id] = nil - - for workspace in workspaces.values { - removePlugin(id: id, from: workspace) - } - } - - public func fetchFilespaceIfExisted(fileURL: URL) -> Filespace? { - let filespaces = workspaces.values.compactMap { $0.filespaces[fileURL] } - if filespaces.isEmpty { return nil } - if filespaces.count == 1 { return filespaces.first } - Logger.workspacePool.info("Multiple workspaces found with file: \(fileURL)") - // If multiple workspaces are found, return the first with a suggestion - return filespaces.first { $0.presentingSuggestion != nil } - } - - @WorkspaceActor - public func fetchOrCreateWorkspace(workspaceURL: URL) async throws -> Workspace { - guard workspaceURL != URL(fileURLWithPath: "/") else { - throw Error.invalidWorkspaceURL(workspaceURL) - } - - if let existed = workspaces[workspaceURL] { - return existed - } - - let new = createNewWorkspace(workspaceURL: workspaceURL) - workspaces[workspaceURL] = new - return new - } - - @WorkspaceActor - public func fetchOrCreateWorkspaceAndFilespace(fileURL: URL) async throws - -> (workspace: Workspace, filespace: Filespace) - { - // If we can get the workspace URL directly. - if let currentWorkspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL { - if let existed = workspaces[currentWorkspaceURL] { - // Reuse the existed workspace. - let filespace = try existed.createFilespaceIfNeeded(fileURL: fileURL) - return (existed, filespace) - } - - let new = createNewWorkspace(workspaceURL: currentWorkspaceURL) - workspaces[currentWorkspaceURL] = new - let filespace = try new.createFilespaceIfNeeded(fileURL: fileURL) - return (new, filespace) - } - - // If not, we try to reuse a filespace if found. - // - // Sometimes, we can't get the project root path from Xcode window, for example, when the - // quick open window in displayed. - for workspace in workspaces.values { - if let filespace = workspace.filespaces[fileURL] { - return (workspace, filespace) - } - } - - // If we can't find the workspace URL, we will try to guess it. - // Most of the time we won't enter this branch, just incase. - - if let workspaceURL = WorkspaceXcodeWindowInspector.extractProjectURL( - workspaceURL: nil, - documentURL: fileURL - ) { - let workspace = { - if let existed = workspaces[workspaceURL] { - return existed - } - // Reuse existed workspace if possible - for (_, workspace) in workspaces { - if fileURL.path.hasPrefix(workspace.projectRootURL.path) { - return workspace - } - } - return createNewWorkspace(workspaceURL: workspaceURL) - }() - - let filespace = try workspace.createFilespaceIfNeeded(fileURL: fileURL) - workspaces[workspaceURL] = workspace - workspace.refreshUpdateTime() - return (workspace, filespace) - } - - throw Workspace.CantFindWorkspaceError() - } - - @WorkspaceActor - public func removeWorkspace(url: URL) { - workspaces[url] = nil - } -} - -extension WorkspacePool { - func addPlugin( - _ plugin: (Workspace) -> WorkspacePlugin, - id: ObjectIdentifier, - to workspace: Workspace - ) { - if workspace.plugins[id] != nil { return } - workspace.plugins[id] = plugin(workspace) - } - - func removePlugin(id: ObjectIdentifier, from workspace: Workspace) { - workspace.plugins[id] = nil - } - - func createNewWorkspace(workspaceURL: URL) -> Workspace { - let new = Workspace(workspaceURL: workspaceURL) - for (id, plugin) in plugins { - addPlugin(plugin, id: id, to: new) - } - return new - } -} - diff --git a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift deleted file mode 100644 index 47e1d9dc..00000000 --- a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Foundation -import SuggestionBasic -import Workspace -import XPCShared - -public struct FilespaceSuggestionSnapshot: Equatable { - public let linesHash: Int - public let prefixLinesHash: Int - public let suffixLinesHash: Int - public let cursorPosition: CursorPosition - public let currentLine: String - - public init(lines: [String], cursorPosition: CursorPosition) { - func safeIndex(_ index: Int) -> Int { - return max(min(index, lines.endIndex), lines.startIndex) - } - - self.linesHash = lines.hashValue - self.cursorPosition = cursorPosition - self.prefixLinesHash = lines[0..= lines.startIndex && cursorPosition.line < lines.endIndex ? lines[safeIndex(cursorPosition.line)] : "" - } - - public init(content: EditorContent) { - self.init(lines: content.lines, cursorPosition: content.cursorPosition) - } - - public func equalOrOnlyCurrentLineDiffers(comparedTo: FilespaceSuggestionSnapshot) -> Bool { - return prefixLinesHash == comparedTo.prefixLinesHash && - suffixLinesHash == comparedTo.suffixLinesHash && - cursorPosition.line == comparedTo.cursorPosition.line - } -} - -public struct FilespaceSuggestionSnapshotKey: FilespacePropertyKey { - public static func createDefaultValue() - -> FilespaceSuggestionSnapshot { .init(lines: [], cursorPosition: .outOfScope) } -} - -public extension FilespacePropertyValues { - @WorkspaceActor - var suggestionSourceSnapshot: FilespaceSuggestionSnapshot { - get { self[FilespaceSuggestionSnapshotKey.self] } - set { self[FilespaceSuggestionSnapshotKey.self] = newValue } - } -} - -public extension Filespace { - @WorkspaceActor - func resetSnapshot() { - // swiftformat:disable redundantSelf - self.suggestionSourceSnapshot = FilespaceSuggestionSnapshotKey.createDefaultValue() - // swiftformat:enable all - } - - /// Validate the suggestion is still valid. - /// - Parameters: - /// - lines: lines of the file - /// - cursorPosition: cursor position - /// - Returns: `true` if the suggestion is still valid - @WorkspaceActor - func validateSuggestions(lines: [String], cursorPosition: CursorPosition) -> Bool { - guard let presentingSuggestion else { return false } - - let updatedSnapshot = FilespaceSuggestionSnapshot(lines: lines, cursorPosition: cursorPosition) - - // document state is unchanged - if updatedSnapshot == self.suggestionSourceSnapshot { - return true - } - - // other parts of the document have changed - if !self.suggestionSourceSnapshot.equalOrOnlyCurrentLineDiffers(comparedTo: updatedSnapshot) { - reset() - resetSnapshot() - return false - } - - // the suggestion does not start on the current line - if presentingSuggestion.range.start.line != cursorPosition.line || - presentingSuggestion.range.start.character != 0 { - reset() - resetSnapshot() - return false - } - - // the cursor position is invalid - if cursorPosition.line >= lines.count { - reset() - resetSnapshot() - return false - } - - let edit = LineEdit( - snapshot: self.suggestionSourceSnapshot, - suggestion: presentingSuggestion, - lines: lines, - cursor: cursorPosition - ) - let suggestionLines = presentingSuggestion.text.split(whereSeparator: \.isNewline) - let suggestionFirstLine = suggestionLines.first ?? "" - - // there is user-entered text to the right of the cursor - if edit.userEntered.count > cursorPosition.character { - reset() - resetSnapshot() - return false - } - - // the replacement range can't be adjusted - if presentingSuggestion.range.end.line != cursorPosition.line { - reset() - resetSnapshot() - return false - } - - // typing into the completion - if edit.line.count < suggestionFirstLine.count && suggestionFirstLine.hasPrefix(edit.userEntered) { - updateSuggestionsWithSameSelection(edit.updateSuggestions(suggestions)) - return true - } - - reset() - resetSnapshot() - return false - } - -} - diff --git a/Tool/Sources/WorkspaceSuggestionService/LineEdit.swift b/Tool/Sources/WorkspaceSuggestionService/LineEdit.swift deleted file mode 100644 index 80254fad..00000000 --- a/Tool/Sources/WorkspaceSuggestionService/LineEdit.swift +++ /dev/null @@ -1,139 +0,0 @@ -import Foundation -import SuggestionBasic - -/// Represents an edit from a previous state of the document to the current -/// state when the modified portion of the document is constrained to the -/// current line (the line containing the cursor). -/// -/// This divides the current line into a `head` and `tail`. The `head` is -/// everything to the left of the cursor. -/// -/// The `tail` is all content to the right of the cursor which is permitted -/// when displaying a completion. That is, any content right of the cursor -/// which was present when the completion was first requested and any -/// characters which are permitted to the immediate right of the cursor for -/// middle-of-line completions (e.g. closing parens or braces). -/// -/// This also provides a `userEntered` property which contains everything to -/// the left of the cursor and any content to the right of the cursor which is -/// not permitted in a valid `tail`. When the `userEntered` portion extends to -/// the right of the cursor, it indicates an invalid middle-of-line position -/// for a completion (and any suggestions being shown must be invalidated). -/// -/// As an example, consider a file with this initial content (where `|` is the -/// cursor): -/// -/// ``` -/// let nestedTuple = (1, |) -/// ``` -/// -/// If the document is changed to (closing paren added automatically by the editor): -/// -/// ``` -/// let nestedTuple = (1, (2,|)) -/// ``` -/// -/// Here is how those properties would be set: -/// -/// ``` -/// let nestedTuple = (1, (2,|)) -/// ^ ^ = head -/// ^ ^ = userEntered -/// ^ ^ = tail -/// ``` -/// -/// An important responsibility of this type is determining how a `CodeSuggestion` -/// must be updated following the edit to remain vaild. This is handled by the -/// `updateSuggestions` method, which modifies the cursor position and selected -/// range of text to match the new document locations following the edit. -public struct LineEdit { - public let previousState: FilespaceSuggestionSnapshot - public let suggestion: CodeSuggestion - public let line: String.SubSequence - public let cursor: CursorPosition - public let headEnd: String.Index - public let tailStart: String.Index - - static let tailChars: Set = [")", ">", "}", "]", "\"", "'", "`"] - - /// The portion of the line to the left of the cursor. - public var head: String.SubSequence { - line[.. String.Index { - return onLine.index(onLine.startIndex, offsetBy: pos.character, limitedBy: onLine.endIndex) ?? onLine.endIndex - } - - func nextTailChar() -> Character { - return newLine[newLine.index(before: tailIdx)] - } - - let oldPos = previousState.cursorPosition - let oldLine = previousState.currentLine.dropLast(1) - let oldTail = oldLine[cursorIdx(oldPos, onLine: oldLine)...] - let newPos = cursorIdx(cursor, onLine: line) - let afterCursor = line[newPos...] - - // start with the same tail present when the completion was generated (if any) - if afterCursor.hasSuffix(oldTail) { - tailIdx = line.index(line.endIndex, offsetBy: -oldTail.count) - } - - // add any whitespace or valid middle of line characters from the old tail up to the cursor - while tailIdx > newPos && (LineEdit.tailChars.contains(nextTailChar()) || nextTailChar().isWhitespace) { - tailIdx = line.index(before: tailIdx) - } - - self.headEnd = newPos - self.tailStart = tailIdx - } - - /// Returns a new set of code suggestions containing the same suggestion - /// content, but updated with new cursor position and replacement ranges to - /// match this edit. - public func updateSuggestions(_ suggestions: [CodeSuggestion]) -> [CodeSuggestion] { - return suggestions.map({ - guard $0.position == suggestion.position else { return $0 } - - // if the tail includes everything right of the cursor, keep the - // range the same distance from the end of the line - let distance = previousState.currentLine.dropLast(1).count - $0.range.end.character - let rangeEnd = if headEnd == tailStart && $0.range.end.line == cursor.line { - CursorPosition(line: cursor.line, character: line.count - distance) - } else { - // otherwise (this is not expected), use the cursor position - cursor - } - - return CodeSuggestion( - id: $0.id, - text: $0.text, - position: cursor, - range: CursorRange(start: $0.range.start, end: rangeEnd) - ) - }) - } -} - diff --git a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift deleted file mode 100644 index 4b7403ad..00000000 --- a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift +++ /dev/null @@ -1,95 +0,0 @@ -import BuiltinExtension -import Foundation -import Preferences -import SuggestionBasic -import SuggestionProvider -import UserDefaultsObserver -import Workspace - -public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { - public typealias SuggestionServiceFactory = () -> any SuggestionServiceProvider - let suggestionServiceFactory: SuggestionServiceFactory - - let suggestionFeatureUsabilityObserver = UserDefaultsObserver( - object: UserDefaults.shared, forKeyPaths: [ - UserDefaultPreferenceKeys().suggestionFeatureEnabledProjectList.key, - UserDefaultPreferenceKeys().disableSuggestionFeatureGlobally.key, - ], context: nil - ) - - let providerChangeObserver = UserDefaultsObserver( - object: UserDefaults.shared, - forKeyPaths: [UserDefaultPreferenceKeys().suggestionFeatureProvider.key], - context: nil - ) - - public var isRealtimeSuggestionEnabled: Bool { - UserDefaults.shared.value(for: \.realtimeSuggestionToggle) - } - - private var _suggestionService: SuggestionServiceProvider? - - public var suggestionService: SuggestionServiceProvider? { - // Check if the workspace is disabled. - let isSuggestionDisabledGlobally = UserDefaults.shared - .value(for: \.disableSuggestionFeatureGlobally) - if isSuggestionDisabledGlobally { - let enabledList = UserDefaults.shared.value(for: \.suggestionFeatureEnabledProjectList) - if !enabledList.contains(where: { path in projectRootURL.path.hasPrefix(path) }) { - // If it's disable, remove the service - _suggestionService = nil - return nil - } - } - - if _suggestionService == nil { - _suggestionService = suggestionServiceFactory() - } - return _suggestionService - } - - public var isSuggestionFeatureEnabled: Bool { - let isSuggestionDisabledGlobally = UserDefaults.shared - .value(for: \.disableSuggestionFeatureGlobally) - if isSuggestionDisabledGlobally { - let enabledList = UserDefaults.shared.value(for: \.suggestionFeatureEnabledProjectList) - if !enabledList.contains(where: { path in projectRootURL.path.hasPrefix(path) }) { - return false - } - } - return true - } - - public init( - workspace: Workspace, - suggestionProviderFactory: @escaping SuggestionServiceFactory - ) { - suggestionServiceFactory = suggestionProviderFactory - super.init(workspace: workspace) - - suggestionFeatureUsabilityObserver.onChange = { [weak self] in - guard let self else { return } - _ = self.suggestionService - } - - providerChangeObserver.onChange = { [weak self] in - guard let self else { return } - self._suggestionService = nil - } - } - - func notifyAccepted(_ suggestion: CodeSuggestion) async { - await suggestionService?.notifyAccepted( - suggestion, - workspaceInfo: .init(workspaceURL: workspaceURL, projectURL: projectRootURL) - ) - } - - func notifyRejected(_ suggestions: [CodeSuggestion]) async { - await suggestionService?.notifyRejected( - suggestions, - workspaceInfo: .init(workspaceURL: workspaceURL, projectURL: projectRootURL) - ) - } -} - diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift deleted file mode 100644 index e0c3f0f1..00000000 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ /dev/null @@ -1,188 +0,0 @@ -import Foundation -import GitHubCopilotService -import SuggestionBasic -import SuggestionProvider -import Workspace -import Status -import XPCShared - -public extension Workspace { - var suggestionPlugin: SuggestionServiceWorkspacePlugin? { - plugin(for: SuggestionServiceWorkspacePlugin.self) - } - - var suggestionService: SuggestionServiceProvider? { - suggestionPlugin?.suggestionService - } - - var isSuggestionFeatureEnabled: Bool { - suggestionPlugin?.isSuggestionFeatureEnabled ?? false - } - - var gitHubCopilotPlugin: GitHubCopilotWorkspacePlugin? { - plugin(for: GitHubCopilotWorkspacePlugin.self) - } - - var gitHubCopilotService: GitHubCopilotService? { - gitHubCopilotPlugin?.gitHubCopilotService - } - - struct SuggestionFeatureDisabledError: Error, LocalizedError { - public var errorDescription: String? { - "Suggestion feature is disabled for this project." - } - } - - struct EditorCursorOutOfScopeError: Error, LocalizedError { - public var errorDescription: String? { - "Cursor position is out of scope." - } - } -} - -public extension Workspace { - @WorkspaceActor - @discardableResult - func generateSuggestions( - forFileAt fileURL: URL, - editor: EditorContent - ) async throws -> [CodeSuggestion] { - refreshUpdateTime() - - guard editor.cursorPosition != .outOfScope else { - throw EditorCursorOutOfScopeError() - } - - let filespace = try createFilespaceIfNeeded(fileURL: fileURL) - - if !editor.uti.isEmpty { - filespace.codeMetadata.uti = editor.uti - filespace.codeMetadata.tabSize = editor.tabSize - filespace.codeMetadata.indentSize = editor.indentSize - filespace.codeMetadata.usesTabsForIndentation = editor.usesTabsForIndentation - } - - filespace.codeMetadata.guessLineEnding(from: editor.lines.first) - - let snapshot = FilespaceSuggestionSnapshot(content: editor) - - filespace.suggestionSourceSnapshot = snapshot - - guard let suggestionService else { throw SuggestionFeatureDisabledError() } - let content = editor.lines.joined(separator: "") - let completions = try await suggestionService.getSuggestions( - .init( - fileURL: fileURL, - relativePath: fileURL.path.replacingOccurrences(of: projectRootURL.path, with: ""), - content: content, - originalContent: content, - lines: editor.lines, - cursorPosition: editor.cursorPosition, - cursorOffset: editor.cursorOffset, - tabSize: editor.tabSize, - indentSize: editor.indentSize, - usesTabsForIndentation: editor.usesTabsForIndentation, - relevantCodeSnippets: [] - ), - workspaceInfo: .init(workspaceURL: workspaceURL, projectURL: projectRootURL) - ) - - let clsStatus = await Status.shared.getCLSStatus() - if clsStatus.isErrorStatus && clsStatus.message.contains("Completions limit reached") { - filespace.setError(clsStatus.message) - } else { - filespace.setError("") - filespace.setSuggestions(completions) - } - - return completions - } - - @WorkspaceActor - func selectNextSuggestion(forFileAt fileURL: URL) { - refreshUpdateTime() - guard let filespace = filespaces[fileURL], - filespace.suggestions.count > 1 - else { return } - filespace.nextSuggestion() - } - - @WorkspaceActor - func selectPreviousSuggestion(forFileAt fileURL: URL) { - refreshUpdateTime() - guard let filespace = filespaces[fileURL], - filespace.suggestions.count > 1 - else { return } - filespace.previousSuggestion() - } - - @WorkspaceActor - func notifySuggestionShown(fileFileAt fileURL: URL) { - if let suggestion = filespaces[fileURL]?.presentingSuggestion { - Task { - await gitHubCopilotService?.notifyShown(suggestion) - } - } - } - - @WorkspaceActor - func rejectSuggestion(forFileAt fileURL: URL, editor: EditorContent?) { - refreshUpdateTime() - - if let editor, !editor.uti.isEmpty { - filespaces[fileURL]?.codeMetadata.uti = editor.uti - filespaces[fileURL]?.codeMetadata.tabSize = editor.tabSize - filespaces[fileURL]?.codeMetadata.indentSize = editor.indentSize - filespaces[fileURL]?.codeMetadata.usesTabsForIndentation = editor.usesTabsForIndentation - } - - Task { - await suggestionService?.notifyRejected( - filespaces[fileURL]?.suggestions ?? [], - workspaceInfo: .init( - workspaceURL: workspaceURL, - projectURL: projectRootURL - ) - ) - } - filespaces[fileURL]?.reset() - } - - @WorkspaceActor - func acceptSuggestion(forFileAt fileURL: URL, editor: EditorContent?, suggestionLineLimit: Int? = nil) -> CodeSuggestion? { - refreshUpdateTime() - guard let filespace = filespaces[fileURL], - !filespace.suggestions.isEmpty, - filespace.suggestionIndex >= 0, - filespace.suggestionIndex < filespace.suggestions.endIndex - else { return nil } - - if let editor, !editor.uti.isEmpty { - filespaces[fileURL]?.codeMetadata.uti = editor.uti - filespaces[fileURL]?.codeMetadata.tabSize = editor.tabSize - filespaces[fileURL]?.codeMetadata.indentSize = editor.indentSize - filespaces[fileURL]?.codeMetadata.usesTabsForIndentation = editor.usesTabsForIndentation - } - - var allSuggestions = filespace.suggestions - let suggestion = allSuggestions.remove(at: filespace.suggestionIndex) - - var length: Int? = nil - if let suggestionLineLimit { - let lines = suggestion.text.breakLines( - proposedLineEnding: filespaces[fileURL]?.codeMetadata.lineEnding - ) - length = lines.prefix(suggestionLineLimit).joined().count - } - - Task { - await gitHubCopilotService?.notifyAccepted(suggestion, acceptedLength: length) - } - - filespaces[fileURL]?.reset() - filespaces[fileURL]?.resetSnapshot() - - return suggestion - } -} - diff --git a/Tool/Sources/XPCShared/CommunicationBridgeXPCServiceProtocol.swift b/Tool/Sources/XPCShared/CommunicationBridgeXPCServiceProtocol.swift deleted file mode 100644 index aaf8c05b..00000000 --- a/Tool/Sources/XPCShared/CommunicationBridgeXPCServiceProtocol.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -@objc(CommunicationBridgeXPCServiceProtocol) -public protocol CommunicationBridgeXPCServiceProtocol { - func launchExtensionServiceIfNeeded(withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void) - func quit(withReply reply: @escaping () -> Void) - func updateServiceEndpoint( - endpoint: NSXPCListenerEndpoint, - withReply reply: @escaping () -> Void - ) -} - diff --git a/Tool/Sources/XPCShared/Models.swift b/Tool/Sources/XPCShared/Models.swift deleted file mode 100644 index 6cd6134a..00000000 --- a/Tool/Sources/XPCShared/Models.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation -import SuggestionBasic - -public struct EditorContent: Codable { - public struct Selection: Codable { - public var start: CursorPosition - public var end: CursorPosition - - public init(start: CursorPosition, end: CursorPosition) { - self.start = start - self.end = end - } - } - - public init( - content: String, - lines: [String], - uti: String, - cursorPosition: CursorPosition, - cursorOffset: Int, - selections: [Selection], - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - suggesionLineLimit: Int? = nil - ) { - self.content = content - self.lines = lines - self.uti = uti - self.cursorPosition = cursorPosition - self.cursorOffset = cursorOffset - self.selections = selections - self.tabSize = tabSize - self.indentSize = indentSize - self.usesTabsForIndentation = usesTabsForIndentation - self.suggesionLineLimit = suggesionLineLimit - } - - public var content: String - /// Every line has a trailing newline character. - public var lines: [String] - public var uti: String - public var cursorPosition: CursorPosition - public var cursorOffset: Int - public var selections: [Selection] - public var tabSize: Int - public var indentSize: Int - public var usesTabsForIndentation: Bool - public var suggesionLineLimit: Int? - - public func selectedCode(in selection: Selection) -> String { - return XPCShared.selectedCode(in: selection, for: lines) - } -} - -public struct UpdatedContent: Codable { - public init(content: String, newSelection: CursorRange? = nil, modifications: [Modification]) { - self.content = content - self.newSelection = newSelection - self.modifications = modifications - } - - public var content: String - public var newSelection: CursorRange? - public var modifications: [Modification] -} - -func selectedCode(in selection: EditorContent.Selection, for lines: [String]) -> String { - return EditorInformation.code( - in: lines, - inside: .init( - start: .init(line: selection.start.line, character: selection.start.character), - end: .init(line: selection.end.line, character: selection.end.character) - ), - ignoreColumns: false - ).code -} diff --git a/Tool/Sources/XPCShared/XPCCommunicationBridge.swift b/Tool/Sources/XPCShared/XPCCommunicationBridge.swift deleted file mode 100644 index 4b7d09cb..00000000 --- a/Tool/Sources/XPCShared/XPCCommunicationBridge.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import Logger -import AppKit - -public enum XPCCommunicationBridgeError: Swift.Error, LocalizedError { - case failedToCreateXPCConnection - case xpcServiceError(Error) - - public var errorDescription: String? { - switch self { - case .failedToCreateXPCConnection: - return "Failed to create XPC connection." - case let .xpcServiceError(error): - return "Connection to communication bridge error: \(error.localizedDescription)" - } - } -} - -public class XPCCommunicationBridge { - let service: XPCService - let logger: Logger - @XPCServiceActor - var serviceEndpoint: NSXPCListenerEndpoint? - - public init(logger: Logger) { - service = .init( - kind: .machService( - identifier: Bundle(for: XPCService.self) - .object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String + - ".CommunicationBridge" - ), - interface: NSXPCInterface(with: CommunicationBridgeXPCServiceProtocol.self), - logger: logger - ) - self.logger = logger - } - - public func setDelegate(_ delegate: XPCServiceDelegate?) { - service.delegate = delegate - } - - @discardableResult - public func launchExtensionServiceIfNeeded() async throws -> NSXPCListenerEndpoint? { - try await withXPCServiceConnected { service, continuation in - service.launchExtensionServiceIfNeeded { endpoint in - continuation.resume(endpoint) - } - } - } - - public func quit() async throws { - try await withXPCServiceConnected { service, continuation in - service.quit { - continuation.resume(()) - } - } - } - - public func updateServiceEndpoint(_ endpoint: NSXPCListenerEndpoint) async throws { - try await withXPCServiceConnected { service, continuation in - service.updateServiceEndpoint(endpoint: endpoint) { - continuation.resume(()) - } - } - } -} - -extension XPCCommunicationBridge { - @XPCServiceActor - func withXPCServiceConnected( - _ fn: @escaping (CommunicationBridgeXPCServiceProtocol, AutoFinishContinuation) -> Void - ) async throws -> T { - guard let connection = service.connection - else { throw XPCCommunicationBridgeError.failedToCreateXPCConnection } - do { - return try await XPCShared.withXPCServiceConnected(connection: connection, fn) - } catch { - throw XPCCommunicationBridgeError.xpcServiceError(error) - } - } -} - -@available(macOS 13.0, *) -public func showBackgroundPermissionAlert() { - let alert = NSAlert() - alert.messageText = "Background Permission Required" - alert.informativeText = "GitHub Copilot for Xcode needs permission to run in the background. Without this permission, features won't work correctly." - alert.alertStyle = .warning - - alert.addButton(withTitle: "Open Settings") - alert.addButton(withTitle: "Later") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension")!) - } -} diff --git a/Tool/Sources/XPCShared/XPCExtensionService.swift b/Tool/Sources/XPCShared/XPCExtensionService.swift deleted file mode 100644 index 5b1d7953..00000000 --- a/Tool/Sources/XPCShared/XPCExtensionService.swift +++ /dev/null @@ -1,441 +0,0 @@ -import Foundation -import GitHubCopilotService -import Logger -import Status - -public enum XPCExtensionServiceError: Swift.Error, LocalizedError { - case failedToGetServiceEndpoint - case failedToCreateXPCConnection - case xpcServiceError(Error) - - public var errorDescription: String? { - switch self { - case .failedToGetServiceEndpoint: - return "Waiting for service to connect to the communication bridge." - case .failedToCreateXPCConnection: - return "Failed to create XPC connection." - case let .xpcServiceError(error): - return "Connection to extension service error: \(error.localizedDescription)" - } - } -} - -public class XPCExtensionService { - @XPCServiceActor - var service: XPCService? - @XPCServiceActor - var connection: NSXPCConnection? { service?.connection } - let logger: Logger - let bridge: XPCCommunicationBridge - - public nonisolated - init(logger: Logger) { - self.logger = logger - bridge = XPCCommunicationBridge(logger: logger) - } - - /// Launches the extension service if it's not running, returns true if the service has finished - /// launching and the communication becomes available. - @XPCServiceActor - public func launchIfNeeded() async throws -> Bool { - try await bridge.launchExtensionServiceIfNeeded() != nil - } - - public func getXPCServiceVersion() async throws -> (version: String, build: String) { - try await withXPCServiceConnected { - service, continuation in - service.getXPCServiceVersion { version, build in - continuation.resume((version, build)) - } - } - } - - public func getXPCCLSVersion() async throws -> String? { - try await withXPCServiceConnected { - service, continuation in - service.getXPCCLSVersion { version in - continuation.resume(version) - } - } - } - - public func getXPCServiceAccessibilityPermission() async throws -> ObservedAXStatus { - try await withXPCServiceConnected { - service, continuation in - service.getXPCServiceAccessibilityPermission { isGranted in - continuation.resume(isGranted) - } - } - } - - public func getXPCServiceExtensionPermission() async throws -> ExtensionPermissionStatus { - try await withXPCServiceConnected { - service, continuation in - service.getXPCServiceExtensionPermission { isGranted in - continuation.resume(isGranted) - } - } - } - - public func getSuggestedCode(editorContent: EditorContent) async throws -> UpdatedContent? { - try await suggestionRequest( - editorContent, - { $0.getSuggestedCode } - ) - } - - public func getNextSuggestedCode(editorContent: EditorContent) async throws -> UpdatedContent? { - try await suggestionRequest( - editorContent, - { $0.getNextSuggestedCode } - ) - } - - public func getPreviousSuggestedCode(editorContent: EditorContent) async throws - -> UpdatedContent? - { - try await suggestionRequest( - editorContent, - { $0.getPreviousSuggestedCode } - ) - } - - public func getSuggestionAcceptedCode(editorContent: EditorContent) async throws - -> UpdatedContent? - { - try await suggestionRequest( - editorContent, - { $0.getSuggestionAcceptedCode } - ) - } - - public func getSuggestionRejectedCode(editorContent: EditorContent) async throws - -> UpdatedContent? - { - try await suggestionRequest( - editorContent, - { $0.getSuggestionRejectedCode } - ) - } - - public func getRealtimeSuggestedCode(editorContent: EditorContent) async throws - -> UpdatedContent? - { - try await suggestionRequest( - editorContent, - { $0.getRealtimeSuggestedCode } - ) - } - - public func getPromptToCodeAcceptedCode(editorContent: EditorContent) async throws - -> UpdatedContent? - { - try await suggestionRequest( - editorContent, - { $0.getPromptToCodeAcceptedCode } - ) - } - - public func toggleRealtimeSuggestion() async throws { - try await withXPCServiceConnected { - service, continuation in - service.toggleRealtimeSuggestion { error in - if let error { - continuation.reject(error) - return - } - continuation.resume(()) - } - } as Void - } - - public func prefetchRealtimeSuggestions(editorContent: EditorContent) async { - guard let data = try? JSONEncoder().encode(editorContent) else { return } - try? await withXPCServiceConnected { service, continuation in - service.prefetchRealtimeSuggestions(editorContent: data) { - continuation.resume(()) - } - } - } - - public func openChat() async throws { - try await withXPCServiceConnected { - service, continuation in - service.openChat { error in - if let error { - continuation.reject(error) - return - } - continuation.resume(()) - } - } as Void - } - - public func promptToCode(editorContent: EditorContent) async throws -> UpdatedContent? { - try await suggestionRequest( - editorContent, - { $0.promptToCode } - ) - } - - public func customCommand( - id: String, - editorContent: EditorContent - ) async throws -> UpdatedContent? { - try await suggestionRequest( - editorContent, - { service in { service.customCommand(id: id, editorContent: $0, withReply: $1) } } - ) - } - - public func quitService() async throws { - try await withXPCServiceConnectedWithoutLaunching { - service, continuation in - service.quit { - continuation.resume(()) - } - } - } - - public func postNotification(name: String) async throws { - try await withXPCServiceConnected { - service, continuation in - service.postNotification(name: name) { - continuation.resume(()) - } - } - } - - public func send( - requestBody: M - ) async throws -> M.ResponseBody { - try await withXPCServiceConnected { service, continuation in - do { - let requestBodyData = try JSONEncoder().encode(requestBody) - service.send(endpoint: M.endpoint, requestBody: requestBodyData) { data, error in - if let error { - continuation.reject(error) - } else { - do { - guard let data = data else { - continuation.reject(NoDataError()) - return - } - let responseBody = try JSONDecoder().decode( - M.ResponseBody.self, - from: data - ) - continuation.resume(responseBody) - } catch { - continuation.reject(error) - } - } - } - } catch { - continuation.reject(error) - } - } - } -} - -extension XPCExtensionService: XPCServiceDelegate { - public func connectionDidInterrupt() async { - Task { @XPCServiceActor in - service = nil - } - } - - public func connectionDidInvalidate() async { - Task { @XPCServiceActor in - service = nil - } - } -} - -extension XPCExtensionService { - @XPCServiceActor - private func updateEndpoint(_ endpoint: NSXPCListenerEndpoint) { - service = XPCService( - kind: .anonymous(endpoint: endpoint), - interface: NSXPCInterface(with: XPCServiceProtocol.self), - logger: logger, - delegate: self - ) - } - - @XPCServiceActor - private func withXPCServiceConnected( - _ fn: @escaping (XPCServiceProtocol, AutoFinishContinuation) -> Void - ) async throws -> T { - if let service, let connection = service.connection { - do { - return try await XPCShared.withXPCServiceConnected(connection: connection, fn) - } catch { - throw XPCExtensionServiceError.xpcServiceError(error) - } - } else { - guard let endpoint = try await bridge.launchExtensionServiceIfNeeded() - else { throw XPCExtensionServiceError.failedToGetServiceEndpoint } - updateEndpoint(endpoint) - - if let service, let connection = service.connection { - do { - return try await XPCShared.withXPCServiceConnected(connection: connection, fn) - } catch { - throw XPCExtensionServiceError.xpcServiceError(error) - } - } else { - throw XPCExtensionServiceError.failedToCreateXPCConnection - } - } - } - - @XPCServiceActor - private func withXPCServiceConnectedWithoutLaunching( - _ fn: @escaping (XPCServiceProtocol, AutoFinishContinuation) -> Void - ) async throws -> T { - if let service, let connection = service.connection { - do { - return try await XPCShared.withXPCServiceConnected(connection: connection, fn) - } catch { - throw XPCExtensionServiceError.xpcServiceError(error) - } - } - throw XPCExtensionServiceError.failedToCreateXPCConnection - } - - @XPCServiceActor - private func suggestionRequest( - _ editorContent: EditorContent, - _ fn: @escaping (any XPCServiceProtocol) -> (Data, @escaping (Data?, Error?) -> Void) - -> Void - ) async throws -> UpdatedContent? { - let data = try JSONEncoder().encode(editorContent) - return try await withXPCServiceConnected { - service, continuation in - fn(service)(data) { updatedData, error in - if let error { - continuation.reject(error) - return - } - do { - if let updatedData { - let updatedContent = try JSONDecoder() - .decode(UpdatedContent.self, from: updatedData) - continuation.resume(updatedContent) - } else { - continuation.resume(nil) - } - } catch { - continuation.reject(error) - } - } - } - } - - @XPCServiceActor - public func getXcodeInspectorData() async throws -> XcodeInspectorData { - return try await withXPCServiceConnected { - service, continuation in - service.getXcodeInspectorData { data, error in - if let error { - continuation.reject(error) - return - } - - guard let data else { - continuation.reject(NoDataError()) - return - } - - do { - let inspectorData = try JSONDecoder().decode(XcodeInspectorData.self, from: data) - continuation.resume(inspectorData) - } catch { - continuation.reject(error) - } - } - } - } - - @XPCServiceActor - public func getAvailableMCPServerToolsCollections() async throws -> [MCPServerToolsCollection]? { - return try await withXPCServiceConnected { - service, continuation in - service.getAvailableMCPServerToolsCollections { data in - guard let data else { - continuation.resume(nil) - return - } - - do { - let tools = try JSONDecoder().decode([MCPServerToolsCollection].self, from: data) - continuation.resume(tools) - } catch { - continuation.reject(error) - } - } - } - } - - @XPCServiceActor - public func updateMCPServerToolsStatus(_ update: [UpdateMCPToolsStatusServerCollection]) async throws { - return try await withXPCServiceConnected { - service, continuation in - do { - let data = try JSONEncoder().encode(update) - service.updateMCPServerToolsStatus(tools: data) - continuation.resume(()) - } catch { - continuation.reject(error) - } - } - } - - @XPCServiceActor - public func getCopilotFeatureFlags() async throws -> FeatureFlags? { - return try await withXPCServiceConnected { - service, continuation in - service.getCopilotFeatureFlags { data in - guard let data else { - continuation.resume(nil) - return - } - - do { - let tools = try JSONDecoder().decode(FeatureFlags.self, from: data) - continuation.resume(tools) - } catch { - continuation.reject(error) - } - } - } - } - - @XPCServiceActor - public func signOutAllGitHubCopilotService() async throws { - return try await withXPCServiceConnected { - service, _ in service.signOutAllGitHubCopilotService() - } - } - - @XPCServiceActor - public func getXPCServiceAuthStatus() async throws -> AuthStatus? { - return try await withXPCServiceConnected { - service, continuation in - service.getXPCServiceAuthStatus { data in - guard let data else { - continuation.resume(nil) - return - } - - do { - let authStatus = try JSONDecoder().decode(AuthStatus.self, from: data) - continuation.resume(authStatus) - } catch { - continuation.reject(error) - } - } - } - } -} diff --git a/Tool/Sources/XPCShared/XPCService.swift b/Tool/Sources/XPCShared/XPCService.swift deleted file mode 100644 index 218f9af3..00000000 --- a/Tool/Sources/XPCShared/XPCService.swift +++ /dev/null @@ -1,153 +0,0 @@ -import Foundation -import Logger - -@globalActor -public enum XPCServiceActor { - public actor TheActor {} - public static let shared = TheActor() -} - -class XPCService { - enum Kind { - case machService(identifier: String) - case anonymous(endpoint: NSXPCListenerEndpoint) - } - - let kind: Kind - let interface: NSXPCInterface - let logger: Logger - weak var delegate: XPCServiceDelegate? - - @XPCServiceActor - private var isInvalidated = false - - @XPCServiceActor - private lazy var _connection: InvalidatingConnection? = buildConnection() - - @XPCServiceActor - var connection: NSXPCConnection? { - if isInvalidated { _connection = nil } - if _connection == nil { rebuildConnection() } - return _connection?.connection - } - - init( - kind: Kind, - interface: NSXPCInterface, - logger: Logger, - delegate: XPCServiceDelegate? = nil - ) { - self.kind = kind - self.interface = interface - self.logger = logger - self.delegate = delegate - } - - @XPCServiceActor - private func buildConnection() -> InvalidatingConnection { - let connection = switch kind { - case let .machService(name): - NSXPCConnection(machServiceName: name) - case let .anonymous(endpoint): - NSXPCConnection(listenerEndpoint: endpoint) - } - connection.remoteObjectInterface = interface - connection.invalidationHandler = { [weak self] in - Task { [weak self] in - self?.markAsInvalidated() - await self?.delegate?.connectionDidInvalidate() - } - } - connection.interruptionHandler = { [weak self] in - self?.logger.info("XPCService interrupted") - Task { [weak self] in - await self?.delegate?.connectionDidInterrupt() - } - } - connection.resume() - return .init(connection) - } - - @XPCServiceActor - private func markAsInvalidated() { - isInvalidated = true - } - - @XPCServiceActor - private func rebuildConnection() { - _connection = buildConnection() - } -} - -public protocol XPCServiceDelegate: AnyObject { - func connectionDidInvalidate() async - func connectionDidInterrupt() async -} - -private class InvalidatingConnection { - let connection: NSXPCConnection - init(_ connection: NSXPCConnection) { - self.connection = connection - } - - deinit { - connection.invalidationHandler = {} - connection.interruptionHandler = {} - connection.invalidate() - } -} - -struct NoDataError: Error {} - -struct AutoFinishContinuation { - var continuation: AsyncThrowingStream.Continuation - - func resume(_ value: T) { - continuation.yield(value) - continuation.finish() - } - - func reject(_ error: Error) { - if (error as NSError).code == -100 { - continuation.finish(throwing: CancellationError()) - } else { - continuation.finish(throwing: error) - } - } -} - -@XPCServiceActor -func withXPCServiceConnected( - connection: NSXPCConnection, - _ fn: @escaping (P, AutoFinishContinuation) -> Void -) async throws -> T { - let stream: AsyncThrowingStream = AsyncThrowingStream { continuation in - let service = connection.remoteObjectProxyWithErrorHandler { - continuation.finish(throwing: $0) - } as! P - fn(service, .init(continuation: continuation)) - } - for try await result in stream { - return result - } - throw XPCExtensionServiceError.failedToCreateXPCConnection -} - -@XPCServiceActor -public func testXPCListenerEndpoint(_ endpoint: NSXPCListenerEndpoint) async -> Bool { - let connection = NSXPCConnection(listenerEndpoint: endpoint) - defer { connection.invalidate() } - let stream: AsyncThrowingStream = AsyncThrowingStream { continuation in - _ = connection.remoteObjectProxyWithErrorHandler { - continuation.finish(throwing: $0) - } - continuation.yield(()) - continuation.finish() - } - do { - try await stream.first(where: { _ in true })! - return true - } catch { - return false - } -} diff --git a/Tool/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift deleted file mode 100644 index 5552ea38..00000000 --- a/Tool/Sources/XPCShared/XPCServiceProtocol.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation -import Status -import SuggestionBasic - -@objc(XPCServiceProtocol) -public protocol XPCServiceProtocol { - func getSuggestedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func getNextSuggestedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func getPreviousSuggestedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func getSuggestionAcceptedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func getSuggestionRejectedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func getRealtimeSuggestedCode(editorContent: Data, withReply reply: @escaping (Data?, Error?) -> Void) - func getPromptToCodeAcceptedCode(editorContent: Data, withReply reply: @escaping (_ updatedContent: Data?, Error?) -> Void) - func openChat(withReply reply: @escaping (Error?) -> Void) - func promptToCode(editorContent: Data, withReply reply: @escaping (Data?, Error?) -> Void) - func customCommand(id: String, editorContent: Data, withReply reply: @escaping (Data?, Error?) -> Void) - func toggleRealtimeSuggestion(withReply reply: @escaping (Error?) -> Void) - func prefetchRealtimeSuggestions(editorContent: Data, withReply reply: @escaping () -> Void) - - func getXPCServiceVersion(withReply reply: @escaping (String, String) -> Void) - func getXPCCLSVersion(withReply reply: @escaping (String?) -> Void) - func getXPCServiceAccessibilityPermission(withReply reply: @escaping (ObservedAXStatus) -> Void) - func getXPCServiceExtensionPermission(withReply reply: @escaping (ExtensionPermissionStatus) -> Void) - func getXcodeInspectorData(withReply reply: @escaping (Data?, Error?) -> Void) - func getAvailableMCPServerToolsCollections(withReply reply: @escaping (Data?) -> Void) - func updateMCPServerToolsStatus(tools: Data) - - func getCopilotFeatureFlags(withReply reply: @escaping (Data?) -> Void) - - func signOutAllGitHubCopilotService() - func getXPCServiceAuthStatus(withReply reply: @escaping (Data?) -> Void) - - func postNotification(name: String, withReply reply: @escaping () -> Void) - func send(endpoint: String, requestBody: Data, reply: @escaping (Data?, Error?) -> Void) - func quit(reply: @escaping () -> Void) -} - -public struct NoResponse: Codable { - public static let none = NoResponse() -} - -public protocol ExtensionServiceRequestType: Codable { - associatedtype ResponseBody: Codable - static var endpoint: String { get } -} - -public enum ExtensionServiceRequests { - public struct OpenExtensionManager: ExtensionServiceRequestType { - public typealias ResponseBody = NoResponse - public static let endpoint = "OpenExtensionManager" - - public init() {} - } - - public struct GetExtensionSuggestionServices: ExtensionServiceRequestType { - public struct ServiceInfo: Codable { - public var bundleIdentifier: String - public var name: String - - public init(bundleIdentifier: String, name: String) { - self.bundleIdentifier = bundleIdentifier - self.name = name - } - } - - public typealias ResponseBody = [ServiceInfo] - public static let endpoint = "GetExtensionSuggestionServices" - - public init() {} - } -} - -public struct XPCRequestHandlerHitError: Error, LocalizedError { - public var errorDescription: String? { - "This is not an actual error, it just indicates a request handler was hit, and no more check is needed." - } - - public init() {} -} - -public struct XPCRequestNotHandledError: Error, LocalizedError { - public var errorDescription: String? { - "The request was not handled by the XPC server." - } - - public init() {} -} - -extension ExtensionServiceRequestType { - /// A helper method to handle requests. - static func _handle( - endpoint: String, - requestBody data: Data, - reply: @escaping (Data?, Error?) -> Void, - handler: @escaping (Request) async throws -> Response - ) throws { - guard endpoint == Self.endpoint else { - return - } - do { - let requestBody = try JSONDecoder().decode(Request.self, from: data) - Task { - do { - let responseBody = try await handler(requestBody) - let responseBodyData = try JSONEncoder().encode(responseBody) - reply(responseBodyData, nil) - } catch { - reply(nil, error) - } - } - } catch { - reply(nil, error) - } - throw XPCRequestHandlerHitError() - } - - public static func handle( - endpoint: String, - requestBody data: Data, - reply: @escaping (Data?, Error?) -> Void, - handler: @escaping (Self) async throws -> Self.ResponseBody - ) throws { - try _handle( - endpoint: endpoint, - requestBody: data, - reply: reply - ) { (request: Self) async throws -> Self.ResponseBody in - try await handler(request) - } - } -} diff --git a/Tool/Sources/XPCShared/XcodeInspectorData.swift b/Tool/Sources/XPCShared/XcodeInspectorData.swift deleted file mode 100644 index defe76b4..00000000 --- a/Tool/Sources/XPCShared/XcodeInspectorData.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -public struct XcodeInspectorData: Codable { - public let activeWorkspaceURL: String? - public let activeProjectRootURL: String? - public let realtimeActiveWorkspaceURL: String? - public let realtimeActiveProjectURL: String? - public let latestNonRootWorkspaceURL: String? - - public init( - activeWorkspaceURL: String?, - activeProjectRootURL: String?, - realtimeActiveWorkspaceURL: String?, - realtimeActiveProjectURL: String?, - latestNonRootWorkspaceURL: String? - ) { - self.activeWorkspaceURL = activeWorkspaceURL - self.activeProjectRootURL = activeProjectRootURL - self.realtimeActiveWorkspaceURL = realtimeActiveWorkspaceURL - self.realtimeActiveProjectURL = realtimeActiveProjectURL - self.latestNonRootWorkspaceURL = latestNonRootWorkspaceURL - } -} diff --git a/Tool/Sources/XcodeInspector/AppInstanceInspector.swift b/Tool/Sources/XcodeInspector/AppInstanceInspector.swift deleted file mode 100644 index 8c678aec..00000000 --- a/Tool/Sources/XcodeInspector/AppInstanceInspector.swift +++ /dev/null @@ -1,50 +0,0 @@ -import AppKit -import Foundation - -public class AppInstanceInspector: ObservableObject { - public let runningApplication: NSRunningApplication - public let processIdentifier: pid_t - public let bundleURL: URL? - public let bundleIdentifier: String? - - public var appElement: AXUIElement { - let app = AXUIElementCreateApplication(runningApplication.processIdentifier) - app.setMessagingTimeout(2) - return app - } - - public var isTerminated: Bool { - return runningApplication.isTerminated - } - - public var isActive: Bool { - guard !runningApplication.isTerminated else { return false } - return runningApplication.isActive - } - - public var isXcode: Bool { - guard !runningApplication.isTerminated else { return false } - return runningApplication.isXcode - } - - public var isExtensionService: Bool { - guard !runningApplication.isTerminated else { return false } - return runningApplication.isCopilotForXcodeExtensionService - } - - public func activate() -> Bool { - return runningApplication.activate() - } - - public func activate(options: NSApplication.ActivationOptions) -> Bool { - return runningApplication.activate(options: options) - } - - public init(runningApplication: NSRunningApplication) { - self.runningApplication = runningApplication - processIdentifier = runningApplication.processIdentifier - bundleURL = runningApplication.bundleURL - bundleIdentifier = runningApplication.bundleIdentifier - } -} - diff --git a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift deleted file mode 100644 index 54865f1d..00000000 --- a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift +++ /dev/null @@ -1,482 +0,0 @@ -import AppKit -import AsyncPassthroughSubject -import AXExtension -import AXNotificationStream -import Combine -import Foundation - -public final class XcodeAppInstanceInspector: AppInstanceInspector { - public struct AXNotification { - public var kind: AXNotificationKind - public var element: AXUIElement - } - - public enum AXNotificationKind { - case titleChanged - case applicationActivated - case applicationDeactivated - case moved - case resized - case mainWindowChanged - case focusedWindowChanged - case focusedUIElementChanged - case windowMoved - case windowResized - case windowMiniaturized - case windowDeminiaturized - case created - case uiElementDestroyed - case xcodeCompletionPanelChanged - - public init?(rawValue: String) { - switch rawValue { - case kAXTitleChangedNotification: - self = .titleChanged - case kAXApplicationActivatedNotification: - self = .applicationActivated - case kAXApplicationDeactivatedNotification: - self = .applicationDeactivated - case kAXMovedNotification: - self = .moved - case kAXResizedNotification: - self = .resized - case kAXMainWindowChangedNotification: - self = .mainWindowChanged - case kAXFocusedWindowChangedNotification: - self = .focusedWindowChanged - case kAXFocusedUIElementChangedNotification: - self = .focusedUIElementChanged - case kAXWindowMovedNotification: - self = .windowMoved - case kAXWindowResizedNotification: - self = .windowResized - case kAXWindowMiniaturizedNotification: - self = .windowMiniaturized - case kAXWindowDeminiaturizedNotification: - self = .windowDeminiaturized - case kAXCreatedNotification: - self = .created - case kAXUIElementDestroyedNotification: - self = .uiElementDestroyed - default: - return nil - } - } - } - - @Published public fileprivate(set) var focusedWindow: XcodeWindowInspector? - @Published public fileprivate(set) var documentURL: URL? = nil - @Published public fileprivate(set) var workspaceURL: URL? = nil - @Published public fileprivate(set) var projectRootURL: URL? = nil - @Published public fileprivate(set) var workspaces = [WorkspaceIdentifier: Workspace]() - @Published public private(set) var completionPanel: AXUIElement? - public var realtimeWorkspaces: [WorkspaceIdentifier: WorkspaceInfo] { - updateWorkspaceInfo() - return workspaces.mapValues(\.info) - } - - public let axNotifications = AsyncPassthroughSubject() - - public var realtimeDocumentURL: URL? { - guard let window = appElement.focusedWindow, - window.identifier == "Xcode.WorkspaceWindow" - else { return nil } - - return WorkspaceXcodeWindowInspector.extractDocumentURL(windowElement: window) - } - - public var realtimeWorkspaceURL: URL? { - guard let window = appElement.focusedWindow, - window.identifier == "Xcode.WorkspaceWindow" - else { return nil } - - return WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) - } - - public var realtimeProjectURL: URL? { - let workspaceURL = realtimeWorkspaceURL - let documentURL = realtimeDocumentURL - return WorkspaceXcodeWindowInspector.extractProjectURL( - workspaceURL: workspaceURL, - documentURL: documentURL - ) - } - - var _version: String? - public var version: String? { - if let _version { return _version } - guard let plistPath = runningApplication.bundleURL? - .appendingPathComponent("Contents") - .appendingPathComponent("version.plist") - .path - else { return nil } - guard let plistData = FileManager.default.contents(atPath: plistPath) else { return nil } - var format = PropertyListSerialization.PropertyListFormat.xml - guard let plistDict = try? PropertyListSerialization.propertyList( - from: plistData, - options: .mutableContainersAndLeaves, - format: &format - ) as? [String: AnyObject] else { return nil } - let result = plistDict["CFBundleShortVersionString"] as? String - _version = result - return result - } - - private var longRunningTasks = Set>() - private var focusedWindowObservations = Set() - - deinit { - axNotifications.finish() - for task in longRunningTasks { task.cancel() } - } - - override init(runningApplication: NSRunningApplication) { - super.init(runningApplication: runningApplication) - - Task { @XcodeInspectorActor in - observeFocusedWindow() - observeAXNotifications() - - try await Task.sleep(nanoseconds: 3_000_000_000) - // Sometimes the focused window may not be ready on app launch. - if !(focusedWindow is WorkspaceXcodeWindowInspector) { - observeFocusedWindow() - } - } - } - - @XcodeInspectorActor - func refresh() { - if let focusedWindow = focusedWindow as? WorkspaceXcodeWindowInspector { - focusedWindow.refresh() - } else { - observeFocusedWindow() - } - } - - @XcodeInspectorActor - private func observeFocusedWindow() { - if let window = appElement.focusedWindow { - if window.identifier == "Xcode.WorkspaceWindow" { - let window = WorkspaceXcodeWindowInspector( - app: runningApplication, - uiElement: window, - axNotifications: axNotifications - ) - - focusedWindowObservations.forEach { $0.cancel() } - focusedWindowObservations.removeAll() - - Task { @MainActor in - focusedWindow = window - documentURL = window.documentURL - workspaceURL = window.workspaceURL - projectRootURL = window.projectRootURL - } - - window.$documentURL - .filter { $0 != .init(fileURLWithPath: "/") } - .receive(on: DispatchQueue.main) - .sink { [weak self] url in - self?.documentURL = url - }.store(in: &focusedWindowObservations) - window.$workspaceURL - .filter { $0 != .init(fileURLWithPath: "/") } - .receive(on: DispatchQueue.main) - .sink { [weak self] url in - self?.workspaceURL = url - }.store(in: &focusedWindowObservations) - window.$projectRootURL - .filter { $0 != .init(fileURLWithPath: "/") } - .receive(on: DispatchQueue.main) - .sink { [weak self] url in - self?.projectRootURL = url - }.store(in: &focusedWindowObservations) - - } else { - let window = XcodeWindowInspector(uiElement: window) - Task { @MainActor in - focusedWindow = window - } - } - } else { - Task { @MainActor in - focusedWindow = nil - } - } - } - - @XcodeInspectorActor - func observeAXNotifications() { - longRunningTasks.forEach { $0.cancel() } - longRunningTasks = [] - - let axNotificationStream = AXNotificationStream( - app: runningApplication, - notificationNames: - kAXTitleChangedNotification, - kAXApplicationActivatedNotification, - kAXApplicationDeactivatedNotification, - kAXMovedNotification, - kAXResizedNotification, - kAXMainWindowChangedNotification, - kAXFocusedWindowChangedNotification, - kAXFocusedUIElementChangedNotification, - kAXWindowMovedNotification, - kAXWindowResizedNotification, - kAXWindowMiniaturizedNotification, - kAXWindowDeminiaturizedNotification, - kAXCreatedNotification, - kAXUIElementDestroyedNotification - ) - - let observeAXNotificationTask = Task { @XcodeInspectorActor [weak self] in - var updateWorkspaceInfoTask: Task? - - for await notification in axNotificationStream { - guard let self else { return } - try Task.checkCancellation() - await Task.yield() - - guard let event = AXNotificationKind(rawValue: notification.name) else { - continue - } - - self.axNotifications.send(.init(kind: event, element: notification.element)) - - if event == .focusedWindowChanged { - observeFocusedWindow() - } - - if event == .focusedUIElementChanged || event == .applicationDeactivated { - updateWorkspaceInfoTask?.cancel() - updateWorkspaceInfoTask = Task { [weak self] in - guard let self else { return } - try await Task.sleep(nanoseconds: 2_000_000_000) - try Task.checkCancellation() - self.updateWorkspaceInfo() - } - } - - if event == .created || event == .uiElementDestroyed { - let isCompletionPanel = { - notification.element.identifier == "_XC_COMPLETION_TABLE_" - || notification.element.firstChild { element in - element.identifier == "_XC_COMPLETION_TABLE_" - } != nil - } - - switch event { - case .created: - if isCompletionPanel() { - await MainActor.run { - self.completionPanel = notification.element - self.completionPanel?.setMessagingTimeout(1) - self.axNotifications.send(.init( - kind: .xcodeCompletionPanelChanged, - element: notification.element - )) - } - } - case .uiElementDestroyed: - if isCompletionPanel() { - await MainActor.run { - self.completionPanel = nil - self.axNotifications.send(.init( - kind: .xcodeCompletionPanelChanged, - element: notification.element - )) - } - } - default: continue - } - } - } - } - - longRunningTasks.insert(observeAXNotificationTask) - - updateWorkspaceInfo() - } -} - -// MARK: - Workspace Info - -extension XcodeAppInstanceInspector { - public enum WorkspaceIdentifier: Hashable { - case url(URL) - case unknown - } - - public class Workspace { - public let element: AXUIElement - public var info: WorkspaceInfo - - /// When a window is closed, all it's properties will be set to nil. - /// Since we can't get notification for window closing, - /// we will use it to check if the window is closed. - var isValid: Bool { - element.parent != nil - } - - init(element: AXUIElement) { - self.element = element - info = .init(tabs: []) - } - } - - public struct WorkspaceInfo { - public let tabs: Set - - public func combined(with info: WorkspaceInfo) -> WorkspaceInfo { - return .init(tabs: tabs.union(info.tabs)) - } - } - - func updateWorkspaceInfo() { - let workspaceInfoInVisibleSpace = Self.fetchVisibleWorkspaces(runningApplication) - let workspaces = Self.updateWorkspace(workspaces, with: workspaceInfoInVisibleSpace) - Task { @MainActor in - self.workspaces = workspaces - } - } - - /// Use the project path as the workspace identifier. - static func workspaceIdentifier(_ window: AXUIElement) -> WorkspaceIdentifier { - if let url = WorkspaceXcodeWindowInspector.extractWorkspaceURL(windowElement: window) { - return WorkspaceIdentifier.url(url) - } - return WorkspaceIdentifier.unknown - } - - /// With Accessibility API, we can ONLY get the information of visible windows. - static func fetchVisibleWorkspaces( - _ app: NSRunningApplication - ) -> [WorkspaceIdentifier: Workspace] { - let app = AXUIElementCreateApplication(app.processIdentifier) - let windows = app.windows.filter { $0.identifier == "Xcode.WorkspaceWindow" } - - var dict = [WorkspaceIdentifier: Workspace]() - - for window in windows { - let workspaceIdentifier = workspaceIdentifier(window) - var traverseCount = 0 - - let tabs = { - guard let editArea = window.firstChild(where: { $0.description == "editor area" }) - else { return Set() } - var allTabs = Set() - let tabBars = editArea.tabBars - for tabBar in tabBars { - tabBar.traverse { element, _ in - traverseCount += 1 - if element.roleDescription == "tab" { - allTabs.insert(element.title) - return .skipDescendants - } - return .continueSearching - } - } - return allTabs - }() - - let workspace = Workspace(element: window) - workspace.info = .init(tabs: tabs) - dict[workspaceIdentifier] = workspace - } - return dict - } - - static func updateWorkspace( - _ old: [WorkspaceIdentifier: Workspace], - with new: [WorkspaceIdentifier: Workspace] - ) -> [WorkspaceIdentifier: Workspace] { - var updated = old.filter { $0.value.isValid } // remove closed windows. - for (identifier, workspace) in new { - if let existed = updated[identifier] { - existed.info = workspace.info - } else { - updated[identifier] = workspace - } - } - return updated - } - - // The screen that Xcode App located at - public var appScreen: NSScreen? { - appElement.focusedWindow?.maxIntersectionScreen - } -} - -public extension AXUIElement { - var tabBars: [AXUIElement] { - // Searching by traversing with AXUIElement is (Xcode) resource consuming, we should skip - // as much as possible! - - guard let editArea: AXUIElement = { - if description == "editor area" { return self } - return firstChild(where: { $0.description == "editor area" }) - }() else { return [] } - - var tabBars = [AXUIElement]() - editArea.traverse { element, _ in - let description = element.description - if description == "Tab Bar" { - element.traverse { element, _ in - if element.description == "tab bar" { - tabBars.append(element) - return .stopSearching - } - return .continueSearching - } - return .skipDescendantsAndSiblings - } - - if element.identifier == "editor context" { - return .skipDescendantsAndSiblings - } - if element.isSourceEditor { - return .skipDescendantsAndSiblings - } - if description == "Code Coverage Ribbon" { - return .skipDescendants - } - if description == "Debug Area" { - return .skipDescendants - } - - if description == "debug bar" { - return .skipDescendants - } - return .continueSearching - } - return tabBars - } - - var maxIntersectionScreen: NSScreen? { - guard let rect = rect else { return nil } - - var bestScreen: NSScreen? - var maxIntersectionArea: CGFloat = 0 - - for screen in NSScreen.screens { - // Skip screens that are in full-screen mode - // Full-screen detection: visible frame equals total frame (no menu bar/dock) - if screen.frame == screen.visibleFrame { - continue - } - - // Calculate intersection area between Xcode frame and screen frame - let intersection = rect.intersection(screen.frame) - let intersectionArea = intersection.width * intersection.height - - // Update best screen if this intersection is larger - if intersectionArea > maxIntersectionArea { - maxIntersectionArea = intersectionArea - bestScreen = screen - } - } - - return bestScreen - } -} diff --git a/Tool/Sources/XcodeInspector/DisabledLanguageList.swift b/Tool/Sources/XcodeInspector/DisabledLanguageList.swift deleted file mode 100644 index ce723568..00000000 --- a/Tool/Sources/XcodeInspector/DisabledLanguageList.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import Preferences -import SuggestionBasic - -public class DisabledLanguageList { - public static let shared = DisabledLanguageList() - - private init() {} - - public var activeDocumentLanguage: CodeLanguage? { - let activeURL = XcodeInspector.shared.activeDocumentURL - return activeURL.map(languageIdentifierFromFileURL) - } - - public var list: [String] { - UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) - } - - public func isEnabled(_ language: CodeLanguage) -> Bool { - return !list.contains(language.rawValue) - } - - public func enable(_ language: CodeLanguage) { - UserDefaults.shared.set( - list.filter { $0 != language.rawValue }, - for: \.suggestionFeatureDisabledLanguageList - ) - } - - public func disable(_ language: CodeLanguage) { - let currentList = list - - if !currentList.contains(language.rawValue) { - UserDefaults.shared.set( - currentList + [language.rawValue], - for: \.suggestionFeatureDisabledLanguageList - ) - } - } -} diff --git a/Tool/Sources/XcodeInspector/Helpers.swift b/Tool/Sources/XcodeInspector/Helpers.swift deleted file mode 100644 index eab2b002..00000000 --- a/Tool/Sources/XcodeInspector/Helpers.swift +++ /dev/null @@ -1,18 +0,0 @@ -import AppKit -import Foundation - -public extension NSRunningApplication { - var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" } - var isCopilotForXcodeExtensionService: Bool { - bundleIdentifier == Bundle.main.bundleIdentifier - } -} - -public extension FileManager { - func fileIsDirectory(atPath path: String) -> Bool { - var isDirectory: ObjCBool = false - let exists = fileExists(atPath: path, isDirectory: &isDirectory) - return isDirectory.boolValue && exists - } -} - diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift deleted file mode 100644 index 6082a324..00000000 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ /dev/null @@ -1,296 +0,0 @@ -import AppKit -import AsyncPassthroughSubject -import AXNotificationStream -import Foundation -import Logger -import Status -import SuggestionBasic - -/// Representing a source editor inside Xcode. -public class SourceEditor { - public typealias Content = EditorInformation.SourceEditorContent - - public struct AXNotification: Hashable { - public var kind: AXNotificationKind - public var element: AXUIElement - - public func hash(into hasher: inout Hasher) { - kind.hash(into: &hasher) - } - } - - public enum AXNotificationKind: Hashable, Equatable { - case selectedTextChanged - case valueChanged - case scrollPositionChanged - case evaluatedContentChanged - } - - let runningApplication: NSRunningApplication - public let element: AXUIElement - var observeAXNotificationsTask: Task? - public let axNotifications = AsyncPassthroughSubject() - - /// To prevent expensive calculations in ``getContent()``. - private let cache = Cache() - - public func getLatestEvaluatedContent() -> Content { - let selectionRange = element.selectedTextRange - let (content, lines, selections) = cache.latest() - let lineAnnotationElements = element.children.filter { $0.identifier == "Line Annotation" } - let lineAnnotations = lineAnnotationElements.map(\.description) - - return .init( - content: content, - lines: lines, - selections: selections, - cursorPosition: selections.first?.start ?? .outOfScope, - cursorOffset: selectionRange?.lowerBound ?? 0, - lineAnnotations: lineAnnotations - ) - } - - /// Get the content of the source editor. - /// - /// - note: This method is expensive. It needs to convert index based ranges to line based - /// ranges. - public func getContent() -> Content { - let content = getElementValueAndRecordStatus() - let selectionRange = element.selectedTextRange - let (lines, selections) = cache.get(content: content, selectedTextRange: selectionRange) - - let lineAnnotationElements = element.children.filter { $0.identifier == "Line Annotation" } - let lineAnnotations = lineAnnotationElements.map(\.description) - - axNotifications.send(.init(kind: .evaluatedContentChanged, element: element)) - - return .init( - content: content, - lines: lines, - selections: selections, - cursorPosition: selections.first?.start ?? .outOfScope, - cursorOffset: selectionRange?.lowerBound ?? 0, - lineAnnotations: lineAnnotations - ) - } - - private func getElementValueAndRecordStatus() -> String { - do { - let value: String = try element.copyValue(key: kAXValueAttribute) - Task { await Status.shared.updateAXStatus(.granted) } - return value - } catch AXError.apiDisabled { - Task { await Status.shared.updateAXStatus(.notGranted) } - } catch { - // ignore - } - return "" - } - - public init(runningApplication: NSRunningApplication, element: AXUIElement) { - self.runningApplication = runningApplication - self.element = element - element.setMessagingTimeout(2) - observeAXNotifications() - } - - private func observeAXNotifications() { - observeAXNotificationsTask?.cancel() - observeAXNotificationsTask = Task { @XcodeInspectorActor [weak self] in - guard let self else { return } - await withThrowingTaskGroup(of: Void.self) { [weak self] group in - guard let self else { return } - let editorNotifications = AXNotificationStream( - app: runningApplication, - element: element, - notificationNames: - kAXSelectedTextChangedNotification, - kAXValueChangedNotification - ) - - group.addTask { [weak self] in - for await notification in editorNotifications { - try Task.checkCancellation() - await Task.yield() - guard let self else { return } - if let kind: AXNotificationKind = { - switch notification.name { - case kAXSelectedTextChangedNotification: return .selectedTextChanged - case kAXValueChangedNotification: return .valueChanged - default: return nil - } - }() { - self.axNotifications.send(.init( - kind: kind, - element: notification.element - )) - } - } - } - - if let scrollView = element.parent, let scrollBar = scrollView.verticalScrollBar { - let scrollViewNotifications = AXNotificationStream( - app: runningApplication, - element: scrollBar, - notificationNames: kAXValueChangedNotification - ) - - group.addTask { [weak self] in - for await notification in scrollViewNotifications { - try Task.checkCancellation() - await Task.yield() - guard let self else { return } - self.axNotifications.send(.init( - kind: .scrollPositionChanged, - element: notification.element - )) - } - } - } - - try? await group.waitForAll() - } - } - } -} - -extension SourceEditor { - final class Cache { - static let queue = DispatchQueue(label: "SourceEditor.Cache") - - private var sourceContent: String? - private var cachedLines = [String]() - private var sourceSelectedTextRange: ClosedRange? - private var cachedSelections = [CursorRange]() - - init( - sourceContent: String? = nil, - cachedLines: [String] = [String](), - sourceSelectedTextRange: ClosedRange? = nil, - cachedSelections: [CursorRange] = [CursorRange]() - ) { - self.sourceContent = sourceContent - self.cachedLines = cachedLines - self.sourceSelectedTextRange = sourceSelectedTextRange - self.cachedSelections = cachedSelections - } - - func get(content: String, selectedTextRange: ClosedRange?) -> ( - lines: [String], - selections: [CursorRange] - ) { - Self.queue.sync { - let contentMatch = content == sourceContent - let selectedRangeMatch = selectedTextRange == sourceSelectedTextRange - let lines: [String] = { - if contentMatch { - return cachedLines - } - return content.breakLines(appendLineBreakToLastLine: false) - }() - let selections: [CursorRange] = { - if contentMatch, selectedRangeMatch { - return cachedSelections - } - if let selectedTextRange { - return [SourceEditor.convertRangeToCursorRange( - selectedTextRange, - in: lines - )] - } - return [] - }() - - sourceContent = content - cachedLines = lines - sourceSelectedTextRange = selectedTextRange - cachedSelections = selections - - return (lines, selections) - } - } - - func latest() -> (content: String, lines: [String], selections: [CursorRange]) { - Self.queue.sync { - (sourceContent ?? "", cachedLines, cachedSelections) - } - } - } -} - -// MARK: - Helpers - -public extension SourceEditor { - static func convertCursorRangeToRange( - _ cursorRange: CursorRange, - in lines: [String] - ) -> CFRange { - var countS = 0 - var countE = 0 - var range = CFRange(location: 0, length: 0) - for (i, line) in lines.enumerated() { - if i == cursorRange.start.line { - countS = countS + cursorRange.start.character - range.location = countS - } - if i == cursorRange.end.line { - countE = countE + cursorRange.end.character - range.length = max(countE - range.location, 0) - break - } - countS += line.utf16.count - countE += line.utf16.count - } - return range - } - - static func convertCursorRangeToRange( - _ cursorRange: CursorRange, - in content: String - ) -> CFRange { - let lines = content.breakLines(appendLineBreakToLastLine: false) - return convertCursorRangeToRange(cursorRange, in: lines) - } - - static func convertRangeToCursorRange( - _ range: ClosedRange, - in lines: [String] - ) -> CursorRange { - guard !lines.isEmpty else { return CursorRange(start: .zero, end: .zero) } - var countS = 0 - var countE = 0 - var cursorRange = CursorRange(start: .zero, end: .outOfScope) - for (i, line) in lines.enumerated() { - if countS <= range.lowerBound, - // when equal, means the cursor is located at the lowerBound - range.lowerBound <= countS + line.utf16.count - { - cursorRange.start = .init(line: i, character: range.lowerBound - countS) - } - if countE <= range.upperBound, - range.upperBound < countE + line.utf16.count - { - cursorRange.end = .init(line: i, character: range.upperBound - countE) - break - } - countS += line.utf16.count - countE += line.utf16.count - } - if cursorRange.end == .outOfScope { - cursorRange.end = .init( - line: lines.endIndex - 1, - character: lines.last?.utf16.count ?? 0 - ) - } - return cursorRange - } - - static func convertRangeToCursorRange( - _ range: ClosedRange, - in content: String - ) -> CursorRange { - let lines = content.breakLines(appendLineBreakToLastLine: false) - return convertRangeToCursorRange(range, in: lines) - } -} - diff --git a/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift b/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift deleted file mode 100644 index f7779b87..00000000 --- a/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift +++ /dev/null @@ -1,207 +0,0 @@ -import AppKit -import AXExtension -import Foundation -import Logger -import Status - -public extension XcodeAppInstanceInspector { - func triggerCopilotCommand(name: String, activateXcode: Bool = true) async throws { - let bundleName = Bundle.main.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String - let status = await getExtensionStatus(bundleName: bundleName) - guard status == .granted else { - let reason: String - switch status { - case .notGranted: - reason = "No bundle found for \(bundleName)." - case .disabled: - reason = "\(bundleName) is found but disabled." - default: - reason = "" - } - throw CantRunCommand(path: "Editor/\(bundleName)/\(name)", reason: reason) - } - - try await triggerMenuItem(path: ["Editor", bundleName, name], activateApp: activateXcode) - } - - private func getExtensionStatus(bundleName: String) async -> ExtensionPermissionStatus { - let app = AXUIElementCreateApplication(runningApplication.processIdentifier) - - guard let menuBar = app.menuBar, - let editorMenu = menuBar.child(title: "Editor") else { - return .notGranted - } - - if let bundleMenuItem = editorMenu.child(title: bundleName, role: "AXMenuItem") { - var enabled: CFTypeRef? - let error = AXUIElementCopyAttributeValue(bundleMenuItem, kAXEnabledAttribute as CFString, &enabled) - if error == .success, let isEnabled = enabled as? Bool { - return isEnabled ? .granted : .disabled - } - return .disabled - } - - return .notGranted - } -} - -public extension AppInstanceInspector { - struct CantRunCommand: Error, LocalizedError { - let path: String - let reason: String - public var errorDescription: String { - "Can't run command \(path): \(reason)" - } - } - - @MainActor - func triggerMenuItem(path: [String], activateApp: Bool) async throws { - let sourcePath = path.joined(separator: "/") - func cantRunCommand(_ reason: String) -> CantRunCommand { - return CantRunCommand(path: sourcePath, reason: reason) - } - - guard path.count >= 2 else { throw cantRunCommand("Path too short.") } - - if activateApp { - if !runningApplication.activate() { - Logger.service.error(""" - Trigger menu item \(sourcePath): \ - Xcode not activated. - """) - } - } else { - if !runningApplication.isActive { - Logger.service.error(""" - Trigger menu item \(sourcePath): \ - Xcode not activated. - """) - } - } - - await Task.yield() - - if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) { - let app = AXUIElementCreateApplication(runningApplication.processIdentifier) - - guard let menuBar = app.menuBar else { - Logger.service.error(""" - Trigger menu item \(sourcePath) failed: \ - Menu not found. - """) - throw cantRunCommand("Menu not found.") - } - var path = path - var currentMenu = menuBar - while !path.isEmpty { - let item = path.removeFirst() - - if path.isEmpty, let button = currentMenu.child(title: item, role: "AXMenuItem") { - let error = AXUIElementPerformAction(button, kAXPressAction as CFString) - if error != AXError.success { - Logger.service.error(""" - Trigger menu item \(sourcePath) failed: \ - \(error.localizedDescription) - """) - throw cantRunCommand(error.localizedDescription) - } else { - #if DEBUG - Logger.service.info(""" - Trigger menu item \(sourcePath) succeeded. - """) - #endif - return - } - } else if let menu = currentMenu.child(title: item) { - #if DEBUG - Logger.service.info(""" - Trigger menu item \(sourcePath): Move to \(item). - """) - #endif - currentMenu = menu - } else { - Logger.service.error(""" - Trigger menu item \(sourcePath) failed: \ - \(item) is not found. - """) - throw cantRunCommand("\(item) is not found.") - } - } - } else { - let clickTask = { - var path = path - let button = path.removeLast() - let menuBarItem = path.removeFirst() - let list = path - .reversed() - .map { "menu 1 of menu item \"\($0)\"" } - .joined(separator: " of ") - return """ - click menu item "\(button)" of \(list) \ - of menu bar item "\(menuBarItem)" \ - of menu bar 1 - """ - }() - /// check if menu is open, if not, click the menu item. - let appleScript = """ - tell application "System Events" - set theprocs to every process whose unix id is \ - \(runningApplication.processIdentifier) - repeat with proc in theprocs - tell proc - repeat with theMenu in menus of menu bar 1 - set theValue to value of attribute "AXVisibleChildren" of theMenu - if theValue is not {} then - return - end if - end repeat - \(clickTask) - end tell - end repeat - end tell - """ - - do { - try await runAppleScript(appleScript) - } catch { - Logger.service.error(""" - Trigger menu item \(path.joined(separator: "/")) failed: \ - \(error.localizedDescription) - """) - throw cantRunCommand(error.localizedDescription) - } - } - } -} - -@discardableResult -func runAppleScript(_ appleScript: String) async throws -> String { - let task = Process() - task.launchPath = "/usr/bin/osascript" - task.arguments = ["-e", appleScript] - let outpipe = Pipe() - task.standardOutput = outpipe - task.standardError = Pipe() - - return try await withUnsafeThrowingContinuation { continuation in - do { - task.terminationHandler = { _ in - do { - if let data = try outpipe.fileHandleForReading.readToEnd(), - let content = String(data: data, encoding: .utf8) - { - continuation.resume(returning: content) - return - } - continuation.resume(returning: "") - } catch { - continuation.resume(throwing: error) - } - } - try task.run() - } catch { - continuation.resume(throwing: error) - } - } -} - diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift deleted file mode 100644 index 2b2ea1e8..00000000 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ /dev/null @@ -1,432 +0,0 @@ -import AppKit -import AsyncAlgorithms -import AXExtension -import Combine -import Foundation -import Logger -import Preferences -import Status -import SuggestionBasic -import Toast - -public extension Notification.Name { - static let accessibilityAPIMalfunctioning = Notification.Name("accessibilityAPIMalfunctioning") -} - -@globalActor -public enum XcodeInspectorActor: GlobalActor { - public actor Actor {} - public static let shared = Actor() -} - -#warning("TODO: Consider rewriting it with Swift Observation") -public final class XcodeInspector: ObservableObject { - public static let shared = XcodeInspector() - - @XcodeInspectorActor - @dynamicMemberLookup - public class Safe { - var inspector: XcodeInspector { .shared } - nonisolated init() {} - public subscript(dynamicMember member: KeyPath) -> T { - inspector[keyPath: member] - } - } - - private var toast: ToastController { ToastControllerDependencyKey.liveValue } - - private var cancellable = Set() - private var activeXcodeObservations = Set>() - private var appChangeObservations = Set>() - private var activeXcodeCancellable = Set() - - #warning("TODO: Find a good way to make XcodeInspector thread safe!") - public var safe = Safe() - - @Published public fileprivate(set) var activeApplication: AppInstanceInspector? - @Published public fileprivate(set) var previousActiveApplication: AppInstanceInspector? - @Published public fileprivate(set) var activeXcode: XcodeAppInstanceInspector? - @Published public fileprivate(set) var latestActiveXcode: XcodeAppInstanceInspector? - @Published public fileprivate(set) var xcodes: [XcodeAppInstanceInspector] = [] - @Published public fileprivate(set) var activeProjectRootURL: URL? = nil - @Published public fileprivate(set) var activeDocumentURL: URL? = nil - @Published public fileprivate(set) var activeWorkspaceURL: URL? = nil - @Published public fileprivate(set) var focusedWindow: XcodeWindowInspector? - @Published public fileprivate(set) var focusedEditor: SourceEditor? - @Published public fileprivate(set) var focusedElement: AXUIElement? - @Published public fileprivate(set) var completionPanel: AXUIElement? - @Published public fileprivate(set) var latestNonRootWorkspaceURL: URL? = nil - - /// Get the content of the source editor. - /// - /// - note: This method is expensive. It needs to convert index based ranges to line based - /// ranges. - @XcodeInspectorActor - public func getFocusedEditorContent() async -> EditorInformation? { - guard let documentURL = realtimeActiveDocumentURL, - let workspaceURL = realtimeActiveWorkspaceURL, - let projectURL = activeProjectRootURL - else { return nil } - - let editorContent = focusedEditor?.getContent() - let language = languageIdentifierFromFileURL(documentURL) - let relativePath = documentURL.path.replacingOccurrences(of: projectURL.path, with: "") - - if let editorContent, let range = editorContent.selections.first { - let (selectedContent, selectedLines) = EditorInformation.code( - in: editorContent.lines, - inside: range - ) - return .init( - editorContent: editorContent, - selectedContent: selectedContent, - selectedLines: selectedLines, - documentURL: documentURL, - workspaceURL: workspaceURL, - projectRootURL: projectURL, - relativePath: relativePath, - language: language - ) - } - - return .init( - editorContent: editorContent, - selectedContent: "", - selectedLines: [], - documentURL: documentURL, - workspaceURL: workspaceURL, - projectRootURL: projectURL, - relativePath: relativePath, - language: language - ) - } - - public var realtimeActiveDocumentURL: URL? { - latestActiveXcode?.realtimeDocumentURL ?? activeDocumentURL - } - - public var realtimeActiveWorkspaceURL: URL? { - latestActiveXcode?.realtimeWorkspaceURL ?? activeWorkspaceURL - } - - public var realtimeActiveProjectURL: URL? { - latestActiveXcode?.realtimeProjectURL ?? activeProjectRootURL - } - - init() { - AXUIElement.setGlobalMessagingTimeout(3) - Task { @XcodeInspectorActor in - restart() - } - } - - @XcodeInspectorActor - public func restart(cleanUp: Bool = false) { - if cleanUp { - activeXcodeObservations.forEach { $0.cancel() } - activeXcodeObservations.removeAll() - activeXcodeCancellable.forEach { $0.cancel() } - activeXcodeCancellable.removeAll() - activeXcode = nil - latestActiveXcode = nil - activeApplication = nil - activeProjectRootURL = nil - activeDocumentURL = nil - activeWorkspaceURL = nil - focusedWindow = nil - focusedEditor = nil - focusedElement = nil - completionPanel = nil - latestNonRootWorkspaceURL = nil - } - - let runningApplications = NSWorkspace.shared.runningApplications - xcodes = runningApplications - .filter { $0.isXcode } - .map(XcodeAppInstanceInspector.init(runningApplication:)) - let activeXcode = xcodes.first(where: \.isActive) - latestActiveXcode = activeXcode ?? xcodes.first - activeApplication = activeXcode ?? runningApplications - .first(where: \.isActive) - .map(AppInstanceInspector.init(runningApplication:)) - - appChangeObservations.forEach { $0.cancel() } - appChangeObservations.removeAll() - - let appChangeTask = Task(priority: .utility) { [weak self] in - guard let self else { return } - if let activeXcode { - setActiveXcode(activeXcode) - } - - await withThrowingTaskGroup(of: Void.self) { [weak self] group in - group.addTask { [weak self] in // Did activate app - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didActivateApplicationNotification) - for await notification in sequence { - try Task.checkCancellation() - guard let self else { return } - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication - else { continue } - if app.isXcode { - if let existed = xcodes.first(where: { - $0.processIdentifier == app.processIdentifier && !$0.isTerminated - }) { - Task { @XcodeInspectorActor in - self.setActiveXcode(existed) - } - } else { - let new = XcodeAppInstanceInspector(runningApplication: app) - Task { @XcodeInspectorActor in - self.xcodes.append(new) - self.setActiveXcode(new) - } - } - } else { - let appInspector = AppInstanceInspector(runningApplication: app) - Task { @XcodeInspectorActor in - self.previousActiveApplication = self.activeApplication - self.activeApplication = appInspector - } - } - } - } - - group.addTask { [weak self] in // Did terminate app - let sequence = NSWorkspace.shared.notificationCenter - .notifications(named: NSWorkspace.didTerminateApplicationNotification) - for await notification in sequence { - try Task.checkCancellation() - guard let self else { return } - guard let app = notification - .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication - else { continue } - if app.isXcode { - let processIdentifier = app.processIdentifier - Task { @XcodeInspectorActor in - self.xcodes.removeAll { - $0.processIdentifier == processIdentifier || $0.isTerminated - } - if self.latestActiveXcode?.runningApplication - .processIdentifier == processIdentifier - { - self.latestActiveXcode = nil - } - - if let activeXcode = self.xcodes.first(where: \.isActive) { - self.setActiveXcode(activeXcode) - } - } - } - } - } - - if UserDefaults.shared - .value(for: \.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning) - { - group.addTask { [weak self] in - while true { - guard let self else { return } - if UserDefaults.shared.value( - for: \.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer - ) { - return - } - - try await Task.sleep(nanoseconds: 10_000_000_000) - Task { @XcodeInspectorActor in - self.checkForAccessibilityMalfunction("Timer") - } - } - } - } - - group.addTask { [weak self] in // malfunctioning - let sequence = NotificationCenter.default - .notifications(named: .accessibilityAPIMalfunctioning) - for await notification in sequence { - try Task.checkCancellation() - guard let self else { return } - await self - .recoverFromAccessibilityMalfunctioning(notification.object as? String) - } - } - } - } - - appChangeObservations.insert(appChangeTask) - } - - public func reactivateObservationsToXcode() { - Task { @XcodeInspectorActor in - if let activeXcode { - setActiveXcode(activeXcode) - activeXcode.observeAXNotifications() - } - } - } - - @XcodeInspectorActor - private func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { - previousActiveApplication = activeApplication - activeApplication = xcode - xcode.refresh() - for task in activeXcodeObservations { task.cancel() } - for cancellable in activeXcodeCancellable { cancellable.cancel() } - activeXcodeObservations.removeAll() - activeXcodeCancellable.removeAll() - - activeXcode = xcode - latestActiveXcode = xcode - activeDocumentURL = xcode.documentURL - focusedWindow = xcode.focusedWindow - completionPanel = xcode.completionPanel - activeProjectRootURL = xcode.projectRootURL - activeWorkspaceURL = xcode.workspaceURL - focusedWindow = xcode.focusedWindow - storeLatestNonRootWorkspaceURL(xcode.workspaceURL) // Add this call - - let setFocusedElement = { @XcodeInspectorActor [weak self] in - guard let self else { return } - - func getFocusedElementAndRecordStatus(_ element: AXUIElement) -> AXUIElement? { - do { - let focused: AXUIElement = try element.copyValue(key: kAXFocusedUIElementAttribute) - Task { await Status.shared.updateAXStatus(.granted) } - return focused - } catch AXError.apiDisabled { - Task { await Status.shared.updateAXStatus(.notGranted) } - } catch { - // ignore - } - return nil - } - - focusedElement = getFocusedElementAndRecordStatus(xcode.appElement) - if let editorElement = focusedElement, editorElement.isSourceEditor { - focusedEditor = .init( - runningApplication: xcode.runningApplication, - element: editorElement - ) - } else if let element = focusedElement, - let editorElement = element.firstParent(where: \.isSourceEditor) - { - focusedEditor = .init( - runningApplication: xcode.runningApplication, - element: editorElement - ) - } else { - focusedEditor = nil - } - } - - setFocusedElement() - let focusedElementChanged = Task { @XcodeInspectorActor in - for await notification in await xcode.axNotifications.notifications() { - if notification.kind == .focusedUIElementChanged { - try Task.checkCancellation() - setFocusedElement() - } - } - } - - activeXcodeObservations.insert(focusedElementChanged) - - if UserDefaults.shared - .value(for: \.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning) - { - let malfunctionCheck = Task { @XcodeInspectorActor [weak self] in - if #available(macOS 13.0, *) { - let notifications = await xcode.axNotifications.notifications().filter { - $0.kind == .uiElementDestroyed - }.debounce(for: .milliseconds(1000)) - for await _ in notifications { - guard let self else { return } - try Task.checkCancellation() - self.checkForAccessibilityMalfunction("Element Destroyed") - } - } - } - - activeXcodeObservations.insert(malfunctionCheck) - - checkForAccessibilityMalfunction("Reactivate Xcode") - } - - xcode.$completionPanel.sink { [weak self] element in - Task { @XcodeInspectorActor in self?.completionPanel = element } - }.store(in: &activeXcodeCancellable) - - xcode.$documentURL.sink { [weak self] url in - Task { @XcodeInspectorActor in self?.activeDocumentURL = url } - }.store(in: &activeXcodeCancellable) - - xcode.$workspaceURL.sink { [weak self] url in - Task { @XcodeInspectorActor in - self?.activeWorkspaceURL = url - self?.storeLatestNonRootWorkspaceURL(url) - } - }.store(in: &activeXcodeCancellable) - - xcode.$projectRootURL.sink { [weak self] url in - Task { @XcodeInspectorActor in self?.activeProjectRootURL = url } - }.store(in: &activeXcodeCancellable) - - xcode.$focusedWindow.sink { [weak self] window in - Task { @XcodeInspectorActor in self?.focusedWindow = window } - }.store(in: &activeXcodeCancellable) - } - - private var lastRecoveryFromAccessibilityMalfunctioningTimeStamp = Date() - - @XcodeInspectorActor - private func checkForAccessibilityMalfunction(_ source: String) { - guard Date().timeIntervalSince(lastRecoveryFromAccessibilityMalfunctioningTimeStamp) > 5 - else { return } - - if let editor = focusedEditor, !editor.element.isSourceEditor { - NotificationCenter.default.post( - name: .accessibilityAPIMalfunctioning, - object: "Source Editor Element Corrupted: \(source)" - ) - } else if let element = activeXcode?.appElement.focusedElement { - if element.description != focusedElement?.description || - element.role != focusedElement?.role - { - NotificationCenter.default.post( - name: .accessibilityAPIMalfunctioning, - object: "Element Inconsistency: \(source)" - ) - } - } - } - - @XcodeInspectorActor - private func recoverFromAccessibilityMalfunctioning(_ source: String?) { - let message = """ - Accessibility API malfunction detected: \ - \(source ?? ""). - Resetting active Xcode. - """ - - if UserDefaults.shared.value(for: \.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted) { - toast.toast(content: message, level: .warning) - } else { - Logger.service.info(message) - } - if let activeXcode { - lastRecoveryFromAccessibilityMalfunctioningTimeStamp = Date() - setActiveXcode(activeXcode) - activeXcode.observeAXNotifications() - } - } - - @XcodeInspectorActor - private func storeLatestNonRootWorkspaceURL(_ newWorkspaceURL: URL?) { - if let url = newWorkspaceURL, url.path != "/" { - self.latestNonRootWorkspaceURL = url - } - // If newWorkspaceURL is nil or its path is "/", latestNonRootWorkspaceURL remains unchanged. - } -} diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift deleted file mode 100644 index d2506822..00000000 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ /dev/null @@ -1,155 +0,0 @@ -import AppKit -import AsyncPassthroughSubject -import AXExtension -import Combine -import Foundation -import Logger - -public class XcodeWindowInspector: ObservableObject { - public let uiElement: AXUIElement - - init(uiElement: AXUIElement) { - self.uiElement = uiElement - uiElement.setMessagingTimeout(2) - } -} - -public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { - let app: NSRunningApplication - @Published public internal(set) var documentURL: URL = .init(fileURLWithPath: "/") - @Published public internal(set) var workspaceURL: URL = .init(fileURLWithPath: "/") - @Published public internal(set) var projectRootURL: URL = .init(fileURLWithPath: "/") - private var focusedElementChangedTask: Task? - - public func refresh() { - Task { @XcodeInspectorActor in updateURLs() } - } - - public init( - app: NSRunningApplication, - uiElement: AXUIElement, - axNotifications: AsyncPassthroughSubject - ) { - self.app = app - super.init(uiElement: uiElement) - - focusedElementChangedTask = Task { [weak self, axNotifications] in - await self?.updateURLs() - - await withThrowingTaskGroup(of: Void.self) { [weak self] group in - group.addTask { [weak self] in - // prevent that documentURL may not be available yet - try await Task.sleep(nanoseconds: 500_000_000) - if self?.documentURL == .init(fileURLWithPath: "/") { - await self?.updateURLs() - } - } - - group.addTask { [weak self] in - for await notification in await axNotifications.notifications() { - guard notification.kind == .focusedUIElementChanged - || notification.kind == .titleChanged - else { continue } - guard let self else { return } - try Task.checkCancellation() - await Task.yield() - await self.updateURLs() - } - } - } - } - } - - @XcodeInspectorActor - func updateURLs() { - let documentURL = Self.extractDocumentURL(windowElement: uiElement) - if let documentURL { - Task { @MainActor in - self.documentURL = documentURL - } - } - let workspaceURL = Self.extractWorkspaceURL(windowElement: uiElement) - if let workspaceURL { - Task { @MainActor in - self.workspaceURL = workspaceURL - } - } - let projectURL = Self.extractProjectURL( - workspaceURL: workspaceURL, - documentURL: documentURL - ) - if let projectURL { - Task { @MainActor in - self.projectRootURL = projectURL - } - } - } - - static func extractDocumentURL( - windowElement: AXUIElement - ) -> URL? { - // fetch file path of the frontmost window of Xcode through Accessibility API. - let path = windowElement.document - if let path = path?.removingPercentEncoding { - let url = URL( - fileURLWithPath: path - .replacingOccurrences(of: "file://", with: "") - ) - return adjustFileURL(url) - } - return nil - } - - static func extractWorkspaceURL( - windowElement: AXUIElement - ) -> URL? { - for child in windowElement.children { - if child.description.starts(with: "/"), child.description.count > 1 { - let path = child.description - let trimmedNewLine = path.trimmingCharacters(in: .newlines) - let url = URL(fileURLWithPath: trimmedNewLine) - return url - } - } - return nil - } - - public static func extractProjectURL( - workspaceURL: URL?, - documentURL: URL? - ) -> URL? { - guard var currentURL = workspaceURL ?? documentURL else { return nil } - var firstDirectoryURL: URL? - var lastGitDirectoryURL: URL? - while currentURL.pathComponents.count > 1 { - defer { currentURL.deleteLastPathComponent() } - guard FileManager.default.fileIsDirectory(atPath: currentURL.path) else { continue } - guard currentURL.pathExtension != "xcodeproj" else { continue } - guard currentURL.pathExtension != "xcworkspace" else { continue } - guard currentURL.pathExtension != "playground" else { continue } - if firstDirectoryURL == nil { firstDirectoryURL = currentURL } - let gitURL = currentURL.appendingPathComponent(".git") - if FileManager.default.fileIsDirectory(atPath: gitURL.path) { - lastGitDirectoryURL = currentURL - } else if let text = try? String(contentsOf: gitURL) { - if !text.hasPrefix("gitdir: ../"), // it's not a sub module - text.range(of: "/.git/worktrees/") != nil // it's a git worktree - { - lastGitDirectoryURL = currentURL - } - } - } - - return lastGitDirectoryURL ?? firstDirectoryURL ?? workspaceURL - } - - static func adjustFileURL(_ url: URL) -> URL { - if url.pathExtension == "playground", - FileManager.default.fileIsDirectory(atPath: url.path) - { - return url.appendingPathComponent("Contents.swift") - } - return url - } -} - diff --git a/Tool/Tests/ASTParserTests/CursorDeepFirstSearchTests.swift b/Tool/Tests/ASTParserTests/CursorDeepFirstSearchTests.swift deleted file mode 100644 index cb70853a..00000000 --- a/Tool/Tests/ASTParserTests/CursorDeepFirstSearchTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Foundation -import XCTest - -@testable import ASTParser - -class CursorDeepFirstSearchTests: XCTestCase { - class TN { - var parent: TN? - var value: Int - var children: [TN] = [] - - init(_ value: Int, _ children: [TN] = []) { - self.value = value - self.children = children - children.forEach { $0.parent = self } - } - } - - class ACursor: Cursor { - var currentNode: TN? - init(currentNode: TN?) { - self.currentNode = currentNode - } - - func goToFirstChild() -> Bool { - if let first = currentNode?.children.first { - currentNode = first - return true - } - return false - } - - func goToNextSibling() -> Bool { - if let parent = currentNode?.parent, - let index = parent.children.firstIndex(where: { $0 === currentNode }), - index < parent.children.count - 1 { - currentNode = parent.children[index + 1] - return true - } - return false - } - - func goToParent() -> Bool { - if let parent = currentNode?.parent { - currentNode = parent - return true - } - return false - } - } - - func test_deep_first_search() { - let root = TN(0, [ - TN(1, [ - TN(2), - TN(3) - ]), - TN(4, [ - TN(5, [TN(6, [TN(7)])]), - TN(8) - ]) - ]) - let cursor = ACursor(currentNode: root) - var result = [Int]() - for node in CursorDeepFirstSearchSequence(cursor: cursor, skipChildren: { _ in true }) { - result.append(node.value) - } - - XCTAssertEqual(result, result.sorted()) - } - - func test_deep_first_search_skip_children() { - let root = TN(0, [ - TN(1, [ - TN(2), - TN(3) - ]), - TN(4, [ - TN(5, [TN(6, [TN(7)])]), - TN(8) - ]) - ]) - let cursor = ACursor(currentNode: root) - var result = [Int]() - for node in CursorDeepFirstSearchSequence(cursor: cursor, skipChildren: { $0.value == 5 }) { - result.append(node.value) - } - - XCTAssertEqual(result, [0, 1, 2, 3, 4, 5, 8]) - } -} diff --git a/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift b/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift deleted file mode 100644 index 8b137891..00000000 --- a/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift deleted file mode 100644 index af9d3c02..00000000 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ /dev/null @@ -1,462 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest - -@testable import FocusedCodeFinder - -final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { - func test_selecting_a_line_inside_the_method_the_scope_should_be_the_method() { - let code = """ - @implementation Foo - - (void)fooWith:(NSInteger)foo { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - @end - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 2, character: 4) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@implementation Foo", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (6, 4)) - ), - .init( - signature: "- (void)fooWith:(NSInteger)foo", - name: "fooWith:(NSInteger)foo", - range: .init(startPair: (1, 0), endPair: (5, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 4)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - NSInteger foo = 0; - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_a_function_the_scope_should_be_the_function() { - let code = """ - void foo(char name[]) { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - """ - let range = CursorRange(startPair: (2, 0), endPair: (2, 4)) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "void foo(char name[])", - name: "foo", - range: .init(startPair: (0, 0), endPair: (4, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 1)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - NSLog(@"Hello"); - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_method_inside_an_implementation_the_scope_should_be_the_implementation() { - let code = """ - __attribute__((objc_nonlazy_class)) - @implementation Foo (Category) - - (void)fooWith:(NSInteger)foo { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - @end - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 6, character: 1) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "__attribute__((objc_nonlazy_class)) @implementation Foo (Category)", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (7, 4)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (7, 4)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - - (void)fooWith:(NSInteger)foo { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_an_interface_the_scope_should_be_the_interface() { - let code = """ - @interface ViewController >: NSObject - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - @end - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@interface ViewController>: NSObject", - name: "ViewController", - range: .init(startPair: (0, 0), endPair: (4, 4)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 4)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_an_interface_category_the_scope_should_be_the_interface() { - let code = """ - @interface __GENERICS(NSArray, ObjectType) (BlocksKit) - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - @end - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@interface __GENERICS(NSArray, ObjectType) (BlocksKit)", - name: "NSArray", - range: .init(startPair: (0, 0), endPair: (4, 4)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 4)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_a_protocol_the_scope_should_be_the_protocol() { - let code = """ - @protocol Foo - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - @end - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@protocol Foo", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (4, 4)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 4)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_a_struct_the_scope_should_be_the_struct() { - let code = """ - struct Foo { - NSInteger foo; - NSInteger bar; - NSInteger baz; - } - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "struct Foo", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (4, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 1)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - NSInteger foo; - NSInteger bar; - NSInteger baz; - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_a_enum_the_scope_should_be_the_enum() { - let code = """ - enum Foo { - foo, - bar, - baz - }; - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "enum Foo", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (4, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 1)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - foo, - bar, - baz - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_an_NSEnum_the_scope_should_be_the_enum() { - let code = """ - typedef NS_ENUM(NSInteger, Foo) { - foo, - bar, - baz - }; - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 3, character: 31) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "typedef NS_ENUM(NSInteger, Foo)", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (4, 2)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (4, 2)), - smallestContextRange: range, - focusedRange: range, - focusedCode: """ - foo, - bar, - baz - - """, - imports: [], - includes: [] - )) - } -} - -final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { - func test_get_focused_code_inside_method_the_method_should_be_the_focused_code() { - let code = """ - @implementation Foo - - (void)fooWith:(NSInteger)foo { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - @end - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 2, character: 0) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@implementation Foo", - name: "Foo", - range: .init(startPair: (0, 0), endPair: (6, 4)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 4)), - smallestContextRange: .init(startPair: (1, 0), endPair: (5, 1)), - focusedRange: .init(startPair: (1, 0), endPair: (5, 1)), - focusedCode: """ - - (void)fooWith:(NSInteger)foo { - NSInteger foo = 0; - NSLog(@"Hello"); - NSLog(@"World"); - } - - """, - imports: [], - includes: [] - )) - } - - func test_get_focused_code_inside_an_interface_category_the_focused_code_should_be_the_interface( - ) { - let code = """ - @interface __GENERICS(NSArray, ObjectType) (BlocksKit) - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - @end - - @implementation Foo - @end - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 1, character: 0) - ) - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .file, - contextRange: .init(startPair: (0, 0), endPair: (0, 0)), - smallestContextRange: .init(startPair: (0, 0), endPair: (4, 4)), - focusedRange: .init(startPair: (0, 0), endPair: (4, 4)), - focusedCode: """ - @interface __GENERICS(NSArray, ObjectType) (BlocksKit) - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - - (void)fooWith:(NSInteger)foo; - @end - - """, - imports: [], - includes: [] - )) - } -} - -final class ObjectiveCFocusedCodeFinder_Imports_Tests: XCTestCase { - func test_parsing_imports() { - let code = """ - #import - @import UIKit; - #import "Foo.h" - #include "Bar.h" - """ - - let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: .zero - ) - - XCTAssertEqual(context.imports, [ - "", - "UIKit", - "\"Foo.h\"", - ]) - XCTAssertEqual(context.includes, [ - "\"Bar.h\"", - ]) - } -} - diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift deleted file mode 100644 index 666b614c..00000000 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ /dev/null @@ -1,507 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest - -@testable import FocusedCodeFinder - -func document(code: String) -> FocusedCodeFinder.Document { - .init( - documentURL: URL(fileURLWithPath: "/"), - content: code, - lines: code.components(separatedBy: "\n").map { "\($0)\n" } - ) -} - -final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { - func test_selecting_a_line_inside_the_function_the_scope_should_be_the_function() { - let code = """ - public struct A: B, C { - @ViewBuilder private func f(_ a: String) -> String { - let a = 1 - let b = 2 - let c = 3 - let d = 4 - let e = 5 - } - } - """ - let range = CursorRange( - start: CursorPosition(line: 4, character: 0), - end: CursorPosition(line: 4, character: 13) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "public struct A: B, C", - name: "A", - range: .init(startPair: (0, 0), endPair: (8, 1)) - ), - .init( - signature: "@ViewBuilder private func f(_ a: String) -> String", - name: "f", - range: .init(startPair: (1, 4), endPair: (7, 5)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (8, 1)), - smallestContextRange: .init(startPair: (4, 0), endPair: (4, 13)), - focusedRange: .init(startPair: (4, 0), endPair: (4, 13)), - focusedCode: """ - let c = 3 - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_function_inside_a_struct_the_scope_should_be_the_struct() { - let code = """ - @MainActor - public struct A: B, C { - func f() { - let a = 1 - let b = 2 - let c = 3 - let d = 4 - let e = 5 - } - } - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 7, character: 5) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@MainActor public struct A: B, C", - name: "A", - range: .init(startPair: (0, 0), endPair: (9, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (9, 1)), - smallestContextRange: .init(startPair: (2, 0), endPair: (7, 5)), - focusedRange: .init(startPair: (2, 0), endPair: (7, 5)), - focusedCode: """ - func f() { - let a = 1 - let b = 2 - let c = 3 - let d = 4 - let e = 5 - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_variable_inside_a_class_the_scope_should_be_the_class() { - let code = """ - @MainActor final public class A: P, K { - var a = 1 - var b = 2 - var c = 3 - var d = 4 - var e = 5 - } - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 1, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@MainActor final public class A: P, K", - name: "A", - range: .init(startPair: (0, 0), endPair: (6, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 1)), - smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedCode: """ - var a = 1 - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_function_inside_a_protocol_the_scope_should_be_the_protocol() { - let code = """ - public protocol A: Hashable { - func f() - func g() - func h() - func i() - func j() - } - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 1, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "public protocol A: Hashable", - name: "A", - range: .init(startPair: (0, 0), endPair: (6, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 1)), - smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedCode: """ - func f() - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_variable_inside_an_extension_the_scope_should_be_the_extension() { - let code = """ - private extension A: Equatable { - var a = 1 - var b = 2 - var c = 3 - var d = 4 - var e = 5 - } - """ - let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 1, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "private extension A: Equatable", - name: "A", - range: .init(startPair: (0, 0), endPair: (6, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 1)), - smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), - focusedCode: """ - var a = 1 - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_static_function_from_an_actor_the_scope_should_be_the_actor() { - let code = """ - @gloablActor - public actor A { - static func f() {} - static func g() {} - static func h() {} - static func i() {} - static func j() {} - } - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 2, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@gloablActor public actor A", - name: "A", - range: .init(startPair: (0, 0), endPair: (7, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (7, 1)), - smallestContextRange: .init(startPair: (2, 0), endPair: (2, 9)), - focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), - focusedCode: """ - static func f() {} - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_case_inside_an_enum_the_scope_should_be_the_enum() { - let code = """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - """ - let range = CursorRange( - start: CursorPosition(line: 3, character: 0), - end: CursorPosition(line: 3, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "@MainActor public indirect enum A", - name: "A", - range: .init(startPair: (0, 0), endPair: (8, 1)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (8, 1)), - smallestContextRange: .init(startPair: (3, 0), endPair: (3, 9)), - focusedRange: .init(startPair: (3, 0), endPair: (3, 9)), - focusedCode: """ - case a - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_inside_computed_variable_the_scope_should_be_the_variable() { - let code = """ - struct A { - @SomeWrapper public private(set) var a: Int { - let a = 1 - let b = 2 - let c = 3 - let d = 4 - let e = 5 - } - } - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 2, character: 9) - ) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .scope(signature: [ - .init( - signature: "struct A", - name: "A", - range: .init(startPair: (0, 0), endPair: (8, 1)) - ), - .init( - signature: "@SomeWrapper public private(set) var a: Int", - name: "a", - range: .init(startPair: (1, 4), endPair: (7, 5)) - ), - ]), - contextRange: .init(startPair: (0, 0), endPair: (8, 1)), - smallestContextRange: .init(startPair: (2, 0), endPair: (2, 9)), - focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), - focusedCode: """ - let a = 1 - - """, - imports: [], - includes: [] - )) - } - - func test_selecting_a_line_in_freestanding_macro_the_scope_should_be_the_macro() { - // TODO: - } -} - -final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { - func test_get_focused_code_on_top_level_should_fallback_to_unknown_language() { - let code = """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - - func hello() { - print("hello") - print("hello") - } - """ - let range = CursorRange(startPair: (0, 0), endPair: (0, 0)) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 1000).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (0, 0), endPair: (13, 2)), - smallestContextRange: .init(startPair: (0, 0), endPair: (13, 2)), - focusedRange: .init(startPair: (0, 0), endPair: (13, 2)), - focusedCode: """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - - func hello() { - print("hello") - print("hello") - } - - """, - imports: [], - includes: [] - )) - } - - func test_get_focused_code_inside_enum_the_whole_enum_will_be_the_focused_code() { - let code = """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - """ - let range = CursorRange(startPair: (3, 0), endPair: (3, 0)) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 1000).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .file, - contextRange: .init(startPair: (0, 0), endPair: (0, 0)), - smallestContextRange: .init(startPair: (0, 0), endPair: (8, 1)), - focusedRange: .init(startPair: (0, 0), endPair: (8, 1)), - focusedCode: """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - - """, - imports: [], - includes: [] - )) - } - - func test_get_focused_code_inside_enum_with_limited_max_line_count() { - let code = """ - @MainActor - public - indirect enum A { - case a - case b - case c - case d - case e - } - """ - let range = CursorRange(startPair: (3, 0), endPair: (3, 0)) - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 3).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context, .init( - scope: .file, - contextRange: .init(startPair: (0, 0), endPair: (0, 0)), - smallestContextRange: .init(startPair: (0, 0), endPair: (8, 1)), - focusedRange: .init(startPair: (2, 0), endPair: (4, 11)), - focusedCode: """ - indirect enum A { - case a - case b - - """, - imports: [], - includes: [] - )) - } -} - -final class SwiftFocusedCodeFinder_Import_Tests: XCTestCase { - func test_parsing_imports() { - let code = """ - import OnTop - import Second - import Third - - struct Foo { - - } - - import BelowStructFoo - - class Bar { - - } - - import BelowClassBar - """ - - let range = CursorRange.zero - let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 3).findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context.imports, [ - "OnTop", - "Second", - "Third", - "BelowStructFoo", - "BelowClassBar", - ]) - } -} - diff --git a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift deleted file mode 100644 index fa975db9..00000000 --- a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest - -@testable import FocusedCodeFinder - -class UnknownLanguageFocusedCodeFinderTests: XCTestCase { - func test_the_code_is_long_enough_for_the_search_range() { - let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() - let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - .findFocusedCode( - in: document(code: code), - containingRange: .init(startPair: (50, 0), endPair: (50, 0)) - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (40, 0), endPair: (60, 3)), - smallestContextRange: .init(startPair: (40, 0), endPair: (60, 3)), - focusedRange: .init(startPair: (45, 0), endPair: (55, 3)), - focusedCode: stride(from: 45, through: 55, by: 1).map { "\($0)\n" }.joined(), - imports: [], - includes: [] - )) - } - - func test_the_upper_side_is_not_long_enough_expand_the_lower_end() { - let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() - let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - .findFocusedCode( - in: document(code: code), - containingRange: .init(startPair: (2, 0), endPair: (2, 0)) - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (0, 0), endPair: (15, 3)), - smallestContextRange: .init(startPair: (0, 0), endPair: (15, 3)), - focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), - focusedCode: stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined(), - imports: [], - includes: [] - )) - } - - func test_the_lower_side_is_not_long_enough_do_not_expand_the_upper_end() { - let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() - let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - .findFocusedCode( - in: document(code: code), - containingRange: .init(startPair: (99, 0), endPair: (99, 0)) - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (89, 0), endPair: (101, 1)), - smallestContextRange: .init(startPair: (89, 0), endPair: (101, 1)), - focusedRange: .init(startPair: (94, 0), endPair: (101, 1)), - focusedCode: stride(from: 94, through: 100, by: 1).map { "\($0)\n" }.joined() + "\n", - imports: [], - includes: [] - )) - } - - func test_both_sides_are_just_long_enough() { - let code = stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined() - let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - .findFocusedCode( - in: document(code: code), - containingRange: .init(startPair: (5, 0), endPair: (5, 0)) - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (0, 0), endPair: (11, 1)), - smallestContextRange: .init(startPair: (0, 0), endPair: (11, 1)), - focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), - focusedCode: code, - imports: [], - includes: [] - )) - } - - func test_both_sides_are_not_long_enough() { - let code = stride(from: 0, through: 4, by: 1).map { "\($0)\n" }.joined() - let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - .findFocusedCode( - in: document(code: code), - containingRange: .init(startPair: (3, 0), endPair: (3, 0)) - ) - XCTAssertEqual(context, .init( - scope: .top, - contextRange: .init(startPair: (0, 0), endPair: (5, 1)), - smallestContextRange: .init(startPair: (0, 0), endPair: (5, 1)), - focusedRange: .init(startPair: (0, 0), endPair: (5, 1)), - focusedCode: code + "\n", - imports: [], - includes: [] - )) - } -} - diff --git a/Tool/Tests/GitHelperTests/GitHunkTests.swift b/Tool/Tests/GitHelperTests/GitHunkTests.swift deleted file mode 100644 index 03e79a2f..00000000 --- a/Tool/Tests/GitHelperTests/GitHunkTests.swift +++ /dev/null @@ -1,272 +0,0 @@ -import XCTest -import GitHelper - -class GitHunkTests: XCTestCase { - - func testParseDiffSingleHunk() { - let diff = """ - @@ -1,3 +1,4 @@ - line1 - +added line - line2 - line3 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) - XCTAssertEqual(hunk.deletedLines, 3) - XCTAssertEqual(hunk.startAddedLine, 1) - XCTAssertEqual(hunk.addedLines, 4) - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 2) - XCTAssertEqual(hunk.additions[0].length, 1) - XCTAssertEqual(hunk.diffText, " line1\n+added line\n line2\n line3") - } - - func testParseDiffMultipleHunks() { - let diff = """ - @@ -1,2 +1,3 @@ - line1 - +added line1 - line2 - @@ -10,2 +11,3 @@ - line10 - +added line10 - line11 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 2) - - // First hunk - let hunk1 = hunks[0] - XCTAssertEqual(hunk1.startDeletedLine, 1) - XCTAssertEqual(hunk1.deletedLines, 2) - XCTAssertEqual(hunk1.startAddedLine, 1) - XCTAssertEqual(hunk1.addedLines, 3) - XCTAssertEqual(hunk1.additions.count, 1) - XCTAssertEqual(hunk1.additions[0].start, 2) - XCTAssertEqual(hunk1.additions[0].length, 1) - - // Second hunk - let hunk2 = hunks[1] - XCTAssertEqual(hunk2.startDeletedLine, 10) - XCTAssertEqual(hunk2.deletedLines, 2) - XCTAssertEqual(hunk2.startAddedLine, 11) - XCTAssertEqual(hunk2.addedLines, 3) - XCTAssertEqual(hunk2.additions.count, 1) - XCTAssertEqual(hunk2.additions[0].start, 12) - XCTAssertEqual(hunk2.additions[0].length, 1) - } - - func testParseDiffMultipleAdditions() { - let diff = """ - @@ -1,5 +1,7 @@ - line1 - +added line1 - +added line2 - line2 - line3 - +added line3 - line4 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.additions.count, 2) - - // First addition block - XCTAssertEqual(hunk.additions[0].start, 2) - XCTAssertEqual(hunk.additions[0].length, 2) - - // Second addition block - XCTAssertEqual(hunk.additions[1].start, 6) - XCTAssertEqual(hunk.additions[1].length, 1) - } - - func testParseDiffWithDeletions() { - let diff = """ - @@ -1,4 +1,2 @@ - line1 - -deleted line1 - -deleted line2 - line2 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) - XCTAssertEqual(hunk.deletedLines, 4) - XCTAssertEqual(hunk.startAddedLine, 1) - XCTAssertEqual(hunk.addedLines, 2) - XCTAssertEqual(hunk.additions.count, 0) // No additions, only deletions - } - - func testParseDiffNewFile() { - let diff = """ - @@ -0,0 +1,3 @@ - +line1 - +line2 - +line3 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) // Should be adjusted from 0 to 1 - XCTAssertEqual(hunk.deletedLines, 0) - XCTAssertEqual(hunk.startAddedLine, 1) // Should be adjusted from 0 to 1 - XCTAssertEqual(hunk.addedLines, 3) - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 1) - XCTAssertEqual(hunk.additions[0].length, 3) - } - - func testParseDiffDeletedFile() { - let diff = """ - @@ -1,3 +0,0 @@ - -line1 - -line2 - -line3 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) - XCTAssertEqual(hunk.deletedLines, 3) - XCTAssertEqual(hunk.startAddedLine, 1) // Should be adjusted from 0 to 1 - XCTAssertEqual(hunk.addedLines, 0) - XCTAssertEqual(hunk.additions.count, 0) - } - - func testParseDiffSingleLineContext() { - let diff = """ - @@ -1 +1,2 @@ - line1 - +added line - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) - XCTAssertEqual(hunk.deletedLines, 1) // Default when not specified - XCTAssertEqual(hunk.startAddedLine, 1) - XCTAssertEqual(hunk.addedLines, 2) - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 2) - XCTAssertEqual(hunk.additions[0].length, 1) - } - - func testParseDiffEmptyString() { - let diff = "" - let hunks = GitHunk.parseDiff(diff) - XCTAssertEqual(hunks.count, 0) - } - - func testParseDiffInvalidFormat() { - let diff = """ - invalid diff format - no hunk headers - """ - - let hunks = GitHunk.parseDiff(diff) - XCTAssertEqual(hunks.count, 0) - } - - func testParseDiffTrailingNewline() { - let diff = """ - @@ -1,2 +1,3 @@ - line1 - +added line - line2 - - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.diffText, " line1\n+added line\n line2") - XCTAssertFalse(hunk.diffText.hasSuffix("\n")) - } - - func testParseDiffConsecutiveAdditions() { - let diff = """ - @@ -1,3 +1,6 @@ - line1 - +added1 - +added2 - +added3 - line2 - line3 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 2) - XCTAssertEqual(hunk.additions[0].length, 3) - } - - func testParseDiffMixedChanges() { - let diff = """ - @@ -1,6 +1,7 @@ - line1 - -deleted line - +added line1 - +added line2 - line2 - line3 - line4 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1) - XCTAssertEqual(hunk.deletedLines, 6) - XCTAssertEqual(hunk.startAddedLine, 1) - XCTAssertEqual(hunk.addedLines, 7) - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 2) - XCTAssertEqual(hunk.additions[0].length, 2) - } - - func testParseDiffLargeLineNumbers() { - let diff = """ - @@ -1000,5 +1000,6 @@ - line1000 - +added line - line1001 - line1002 - line1003 - line1004 - """ - - let hunks = GitHunk.parseDiff(diff) - - XCTAssertEqual(hunks.count, 1) - let hunk = hunks[0] - XCTAssertEqual(hunk.startDeletedLine, 1000) - XCTAssertEqual(hunk.startAddedLine, 1000) - XCTAssertEqual(hunk.additions.count, 1) - XCTAssertEqual(hunk.additions[0].start, 1001) - XCTAssertEqual(hunk.additions[0].length, 1) - } -} diff --git a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift deleted file mode 100644 index d6cdcbff..00000000 --- a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -import CopilotForXcodeKit -import LanguageServerProtocol -import XCTest - -@testable import Workspace -@testable import GitHubCopilotService - -struct TestServiceLocator: ServiceLocatorType { - let server: GitHubCopilotLSP - func getService(from workspace: WorkspaceInfo) async -> GitHubCopilotService? { - .init(designatedServer: server) - } -} - -final class FetchSuggestionTests: XCTestCase { - func test_process_suggestions_from_server() async throws { - struct TestServer: GitHubCopilotLSP { - func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { - throw CancellationError() - } - - func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { - return GitHubCopilotRequest.InlineCompletion.Response(items: [ - .init( - insertText: "Hello World\n", - filterText: nil, - range: .init(start: .init((0, 0)), end: .init((0, 4))), - command: nil - ), - .init( - insertText: " ", - filterText: nil, - range: .init(start: .init((0, 0)), end: .init((0, 1))), - command: nil - ), - .init( - insertText: " \n", - filterText: nil, - range: .init(start: .init((0, 0)), end: .init((0, 2))), - command: nil - ), - ]) as! E.Response - } - func sendRequest(_: E, timeout: TimeInterval) async throws -> E.Response where E: GitHubCopilotRequestType { - return GitHubCopilotRequest.InlineCompletion.Response(items: []) as! E.Response - } - var eventSequence: ServerConnection.EventSequence { - let result = ServerConnection.EventSequence.makeStream() - result.continuation.finish() - return result.stream - } - } - let service = GitHubCopilotSuggestionService(serviceLocator: TestServiceLocator(server: TestServer())) - let completions = try await service.getSuggestions( - .init( - fileURL: .init(fileURLWithPath: "/file.swift"), - relativePath: "", - language: .builtIn(.swift), - content: "", - originalContent: "", - cursorPosition: .outOfScope, - tabSize: 4, - indentSize: 4, - usesTabsForIndentation: false, - relevantCodeSnippets: [] - ), - workspace: .init( - workspaceURL: .init(fileURLWithPath: "/"), - projectURL: .init(fileURLWithPath: "/file.swift") - ) - ) - XCTAssertEqual(completions.count, 3) - } - - func test_if_language_identifier_is_unknown_returns_correctly() async throws { - class TestServer: GitHubCopilotLSP { - func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { - // unimplemented - } - - func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { - return GitHubCopilotRequest.InlineCompletion.Response(items: [ - .init( - insertText: "Hello World\n", - filterText: nil, - range: .init(start: .init((0, 0)), end: .init((0, 4))), - command: nil - ), - ]) as! E.Response - } - - func sendRequest(_ endpoint: E, timeout: TimeInterval) async throws -> E.Response where E : GitHubCopilotRequestType { - return GitHubCopilotRequest.InlineCompletion.Response(items: []) as! E.Response - } - var eventSequence: ServerConnection.EventSequence { - let result = ServerConnection.EventSequence.makeStream() - result.continuation.finish() - return result.stream - } - } - let testServer = TestServer() - let service = GitHubCopilotSuggestionService(serviceLocator: TestServiceLocator(server: testServer)) - let completions = try await service.getSuggestions( - .init( - fileURL: .init(fileURLWithPath: "/"), - relativePath: "", - language: .builtIn(.swift), - content: "", - originalContent: "", - cursorPosition: .outOfScope, - tabSize: 4, - indentSize: 4, - usesTabsForIndentation: false, - relevantCodeSnippets: [] - ), - workspace: .init( - workspaceURL: .init(fileURLWithPath: "/"), - projectURL: .init(fileURLWithPath: "/file.swift") - ) - ) - XCTAssertEqual(completions.count, 1) - XCTAssertEqual(completions.first?.text, "Hello World\n") - } -} - diff --git a/Tool/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift deleted file mode 100644 index 1054e452..00000000 --- a/Tool/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -import LanguageServerProtocol -import XCTest - -@testable import GitHubCopilotService - -final class FileExtensionToLanguageIdentifierTests: XCTestCase { - func test_no_conflicts_in_map() { - var dict = [String: [String]]() - for languageId in LanguageIdentifier.allCases { - for e in languageId.fileExtensions { - if dict[e] == nil { - dict[e] = [] - } - dict[e]?.append(languageId.rawValue) - } - } - - let confilicts = dict.filter { $0.value.count > 1 } - XCTAssertEqual(confilicts, [:]) - } -} diff --git a/Tool/Tests/SharedUIComponentsTests/ConvertToCodeLinesTests.swift b/Tool/Tests/SharedUIComponentsTests/ConvertToCodeLinesTests.swift deleted file mode 100644 index 7ad54127..00000000 --- a/Tool/Tests/SharedUIComponentsTests/ConvertToCodeLinesTests.swift +++ /dev/null @@ -1,159 +0,0 @@ -import XCTest - -@testable import SharedUIComponents - -final class ConvertToCodeLinesTests: XCTestCase { - func test_do_not_remove_common_leading_spaces() async throws { - let code = """ - struct Cat { - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "swift", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: false, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 0) - print(code.replacingOccurrences(of: " ", with: "·")) - XCTAssertEqual(result.map(\.string), [ - " struct Cat {", - " }", - ]) - } - - func test_wont_remove_common_leading_spaces_2_spaces() async throws { - let code = """ - struct Cat { - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 0) - XCTAssertEqual(result.map(\.string), [ - " struct Cat {", - " }", - ]) - } - - func test_remove_common_leading_spaces_4_spaces() async throws { - let code = """ - struct Cat { - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 4) - XCTAssertEqual(result.map(\.string), [ - "struct Cat {", - "}", - ]) - } - - func test_remove_common_leading_spaces_8_spaces() async throws { - let code = """ - struct Cat { - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 8) - XCTAssertEqual(result.map(\.string), [ - "struct Cat {", - "}", - ]) - } - - func test_remove_common_leading_spaces_one_line_is_empty() async throws { - let code = """ - struct Cat { - - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 4) - XCTAssertEqual(result.map(\.string), [ - "struct Cat {", - "", - "}", - ]) - } - - func test_remove_common_leading_spaces_one_line_has_no_leading_spaces() async throws { - let code = """ - struct Cat { - // - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 0) - XCTAssertEqual(result.map(\.string), [ - " struct Cat {", - "//", - " }", - ]) - } - - func test_remove_common_leading_spaces_one_line_has_fewer_leading_spaces() async throws { - let code = """ - struct Cat { - // - } - """ - let (result, spaceCount) = CodeHighlighting.highlighted( - code: code, - language: "md", - scenario: "a", - brightMode: true, - droppingLeadingSpaces: true, - font: .systemFont(ofSize: 14) - ) - - XCTAssertEqual(spaceCount, 4) - XCTAssertEqual(result.map(\.string), [ - " struct Cat {", - "//", - " }", - ]) - } -} diff --git a/Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift b/Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift deleted file mode 100644 index 6f2ff5a7..00000000 --- a/Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import XCTest -@testable import SuggestionBasic - -final class BreakLinePerformanceTests: XCTestCase { - func test_breakLines() { - let string = String(repeating: """ - Hello - World - - """, count: 50000) - - measure { - let _ = string.breakLines() - } - } -} - diff --git a/Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift b/Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift deleted file mode 100644 index 8ff71f9c..00000000 --- a/Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import XCTest - -@testable import SuggestionBasic - -class LineAnnotationParsingTests: XCTestCase { - func test_parse_line_annotation() { - let annotation = "Error Line 25: FileName.swift:25 Cannot convert Type" - let parsed = EditorInformation.parseLineAnnotation(annotation) - XCTAssertEqual(parsed.type, "Error") - XCTAssertEqual(parsed.line, 25) - XCTAssertEqual(parsed.message, "Cannot convert Type") - } -} diff --git a/Tool/Tests/SuggestionBasicTests/ModificationTests.swift b/Tool/Tests/SuggestionBasicTests/ModificationTests.swift deleted file mode 100644 index 0de61490..00000000 --- a/Tool/Tests/SuggestionBasicTests/ModificationTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -import XCTest - -@testable import SuggestionBasic - -final class ModificationTests: XCTestCase { - func test_nsmutablearray_deleting_an_element() { - let a = NSMutableArray(array: ["a", "b", "c"]) - a.apply([.deleted(0...0)]) - XCTAssertEqual(a as! [String], ["b", "c"]) - } - - func test_nsmutablearray_deleting_all_element() { - let a = NSMutableArray(array: ["a", "b", "c"]) - a.apply([.deleted(0...2)]) - XCTAssertEqual(a as! [String], []) - } - - func test_nsmutablearray_deleting_too_much_element() { - let a = NSMutableArray(array: ["a", "b", "c"]) - a.apply([.deleted(0...100)]) - XCTAssertEqual(a as! [String], []) - } - - func test_nsmutablearray_inserting_elements() { - let a = NSMutableArray(array: ["a", "b", "c"]) - a.apply([.inserted(0, ["y", "z"])]) - XCTAssertEqual(a as! [String], ["y", "z", "a", "b", "c"]) - a.apply([.inserted(1, ["0", "1"])]) - XCTAssertEqual(a as! [String], ["y", "0", "1", "z", "a", "b", "c"]) - } - - func test_nsmutablearray_inserting_elements_at_index_out_of_range() { - let a = NSMutableArray(array: ["a", "b", "c"]) - a.apply([.inserted(1000, ["z"])]) - XCTAssertEqual(a as! [String], ["a", "b", "c", "z"]) - } -} diff --git a/Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift b/Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift deleted file mode 100644 index 7b1fa007..00000000 --- a/Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -import Foundation -import XCTest -@testable import SuggestionBasic - -final class TextExtrationFromCodeTests: XCTestCase { - func test_empty_selection() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 0), - end: CursorPosition(line: 0, character: 0) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "") - XCTAssertEqual(result.lines, ["let foo = 1\n"]) - } - - func test_single_line_selection() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "foo = ") - XCTAssertEqual(result.lines, ["let foo = 1\n"]) - } - - func test_single_line_selection_with_emoji() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let 🎆🎆o = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "🎆🎆o ") - XCTAssertEqual(result.lines, ["let 🎆🎆o = 1\n"]) - } - - func test_single_line_selection_cutting_emoji() { - // undefined behavior - - let selection = CursorRange( - start: CursorPosition(line: 0, character: 5), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let 🎆🎆o = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.lines, ["let 🎆🎆o = 1\n"]) - } - - func test_single_line_selection_at_line_end() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 8), - end: CursorPosition(line: 0, character: 11) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "= 1") - XCTAssertEqual(result.lines, ["let foo = 1\n"]) - } - - func test_multi_line_selection() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 1, character: 11) - ) - let lines = ["let foo = 1\n", "let bar = 2\n", "let baz = 3\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "foo = 1\nlet bar = 2") - XCTAssertEqual(result.lines, ["let foo = 1\n", "let bar = 2\n"]) - } - - func test_multi_line_selection_with_emoji() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 1, character: 11) - ) - let lines = ["🎆🎆 foo = 1\n", "let bar = 2\n", "let baz = 3\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, " foo = 1\nlet bar = 2") - XCTAssertEqual(result.lines, ["🎆🎆 foo = 1\n", "let bar = 2\n"]) - } - - func test_invalid_selection() { - let selection = CursorRange( - start: CursorPosition(line: 1, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let foo = 1", "let bar = 2"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: false - ) - XCTAssertEqual(result.code, "") - XCTAssertEqual(result.lines, []) - } - - func test_single_line_selection_ignoring_column() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 0, character: 10) - ) - let lines = ["let foo = 1\n", "let bar = 2\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: true - ) - XCTAssertEqual(result.code, "let foo = 1\n") - XCTAssertEqual(result.lines, ["let foo = 1\n"]) - } - - func test_multi_line_selection_ignoring_column() { - let selection = CursorRange( - start: CursorPosition(line: 0, character: 4), - end: CursorPosition(line: 1, character: 11) - ) - let lines = ["let foo = 1\n", "let bar = 2\n", "let baz = 3\n"] - let result = EditorInformation.code( - in: lines, - inside: selection, - ignoreColumns: true - ) - XCTAssertEqual(result.code, "let foo = 1\nlet bar = 2\n") - XCTAssertEqual(result.lines, ["let foo = 1\n", "let bar = 2\n"]) - } -} - diff --git a/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift b/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift deleted file mode 100644 index ac389951..00000000 --- a/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift +++ /dev/null @@ -1,188 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest - -@testable import SuggestionProvider - -class PostProcessingSuggestionServiceMiddlewareTests: XCTestCase { - func createRequest( - _ code: String = "", - _ cursorPosition: CursorPosition = .zero - ) -> SuggestionRequest { - let lines = code.breakLines() - return SuggestionRequest( - fileURL: URL(fileURLWithPath: "/path/to/file.swift"), - relativePath: "file.swift", - content: code, - originalContent: code, - lines: lines, - cursorPosition: cursorPosition, - cursorOffset: { - if cursorPosition == .outOfScope { return 0 } - let prefixLines = if cursorPosition.line > 0 { - lines[0.. - } - let offset = prefixLines.reduce(0) { $0 + $1.utf8.count } - return offset - + lines[cursorPosition.line].prefix(cursorPosition.character).utf8.count - }(), - tabSize: 4, - indentSize: 4, - usesTabsForIndentation: false, - relevantCodeSnippets: [] - ) - } - - func test_trailing_whitespaces_and_new_lines_should_be_removed() async throws { - let middleware = PostProcessingSuggestionServiceMiddleware() - - let handler: PostProcessingSuggestionServiceMiddleware.Next = { _ in - [ - .init( - id: "1", - text: "hello world \n \n", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "2", - text: " \n hello world \n \n", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - ] - } - - let suggestions = try await middleware.getSuggestion( - createRequest(), - configuration: .init( - acceptsRelevantCodeSnippets: true, - mixRelevantCodeSnippetsInSource: true, - acceptsRelevantSnippetsFromOpenedFiles: true - ), - next: handler - ) - - XCTAssertEqual(suggestions, [ - .init( - id: "1", - text: "hello world", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "2", - text: " \n hello world", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - ]) - } - - func test_remove_suggestions_that_contains_only_whitespaces_and_new_lines() async throws { - let middleware = PostProcessingSuggestionServiceMiddleware() - - let handler: PostProcessingSuggestionServiceMiddleware.Next = { _ in - [ - .init( - id: "1", - text: "hello world \n \n", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "2", - text: " \n\n\r", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "3", - text: " ", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "4", - text: "\n\n\n", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - ] - } - - let suggestions = try await middleware.getSuggestion( - createRequest(), - configuration: .init( - acceptsRelevantCodeSnippets: true, - mixRelevantCodeSnippetsInSource: true, - acceptsRelevantSnippetsFromOpenedFiles: true - ), - next: handler - ) - - XCTAssertEqual(suggestions, [ - .init( - id: "1", - text: "hello world", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - ]) - } - - func test_remove_suggestion_that_takes_no_effect_after_being_accepted() async throws { - let middleware = PostProcessingSuggestionServiceMiddleware() - - let handler: PostProcessingSuggestionServiceMiddleware.Next = { _ in - [ - .init( - id: "1", - text: "hello world \n \n", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "2", - text: "let cat = 100", - position: .init(line: 0, character: 13), - range: .init(startPair: (0, 0), endPair: (0, 13)) - ), - .init( - id: "3", - text: "let cat = 10", - position: .init(line: 0, character: 13), - range: .init(startPair: (0, 0), endPair: (0, 13)) - ), - ] - } - - let suggestions = try await middleware.getSuggestion( - createRequest("let cat = 100", .init(line: 0, character: 3)), - configuration: .init( - acceptsRelevantCodeSnippets: true, - mixRelevantCodeSnippetsInSource: true, - acceptsRelevantSnippetsFromOpenedFiles: true - ), - next: handler - ) - - XCTAssertEqual(suggestions, [ - .init( - id: "1", - text: "hello world", - position: .init(line: 0, character: 0), - range: .init(startPair: (0, 0), endPair: (0, 0)) - ), - .init( - id: "3", - text: "let cat = 10", - position: .init(line: 0, character: 13), - range: .init(startPair: (0, 0), endPair: (0, 13)) - ), - ]) - } -} - diff --git a/Tool/Tests/SystemUtilsTests/SystemUtilsTests.swift b/Tool/Tests/SystemUtilsTests/SystemUtilsTests.swift deleted file mode 100644 index 4dae3722..00000000 --- a/Tool/Tests/SystemUtilsTests/SystemUtilsTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -import XCTest - -@testable import SystemUtils - -final class SystemUtilsTests: XCTestCase { - func test_get_xcode_version() async throws { - guard let version = SystemUtils.xcodeVersion else { - XCTFail("The Xcode version should not be nil.") - return - } - let versionPattern = "^\\d+(\\.\\d+)*$" - let versionTest = NSPredicate(format: "SELF MATCHES %@", versionPattern) - - XCTAssertTrue(versionTest.evaluate(with: version), "The Xcode version should match the expected format.") - XCTAssertFalse(version.isEmpty, "The Xcode version should not be an empty string.") - } - - func test_getLoginShellEnvironment() throws { - // Test with a valid shell path - let validShellPath = "/bin/zsh" - let env = SystemUtils.shared.getLoginShellEnvironment(shellPath: validShellPath) - - XCTAssertNotNil(env, "Environment should not be nil for valid shell path") - XCTAssertFalse(env?.isEmpty ?? true, "Environment should contain variables") - - // Check for essential environment variables - XCTAssertNotNil(env?["PATH"], "PATH should be present in environment") - XCTAssertNotNil(env?["HOME"], "HOME should be present in environment") - XCTAssertNotNil(env?["USER"], "USER should be present in environment") - - // Test with an invalid shell path - let invalidShellPath = "/nonexistent/shell" - let invalidEnv = SystemUtils.shared.getLoginShellEnvironment(shellPath: invalidShellPath) - XCTAssertNil(invalidEnv, "Environment should be nil for invalid shell path") - } - - func test_appendCommonBinPaths() { - // Test with an empty path - let appendedEmptyPath = SystemUtils.shared.appendCommonBinPaths(path: "") - XCTAssertFalse(appendedEmptyPath.isEmpty, "Result should not be empty when starting with empty path") - XCTAssertTrue(appendedEmptyPath.contains("/usr/bin"), "Common path /usr/bin should be added") - XCTAssertFalse(appendedEmptyPath.hasPrefix(":"), "Result should not start with ':'") - - // Test with a custom path - let customPath = "/custom/bin:/another/custom/bin" - let appendedCustomPath = SystemUtils.shared.appendCommonBinPaths(path: customPath) - - // Verify original paths are preserved - XCTAssertTrue(appendedCustomPath.hasPrefix(customPath), "Original paths should be preserved") - - // Verify common paths are added - XCTAssertTrue(appendedCustomPath.contains(":/usr/local/bin"), "Should contain /usr/local/bin") - XCTAssertTrue(appendedCustomPath.contains(":/usr/bin"), "Should contain /usr/bin") - XCTAssertTrue(appendedCustomPath.contains(":/bin"), "Should contain /bin") - - // Test with a path that already includes some common paths - let existingCommonPath = "/usr/bin:/custom/bin" - let appendedExistingPath = SystemUtils.shared.appendCommonBinPaths(path: existingCommonPath) - - // Check that /usr/bin wasn't added again - let pathComponents = appendedExistingPath.split(separator: ":") - let usrBinCount = pathComponents.filter { $0 == "/usr/bin" }.count - XCTAssertEqual(usrBinCount, 1, "Common path should not be duplicated") - - // Make sure the result is a valid PATH string - // First component should be the initial path components - XCTAssertTrue(appendedExistingPath.hasPrefix(existingCommonPath), "Should preserve original path at the beginning") - } - - func test_executeCommand() throws { - // Test with a simple echo command - let testMessage = "Hello, World!" - let output = try SystemUtils.executeCommand(path: "/bin/echo", arguments: [testMessage]) - - XCTAssertNotNil(output, "Output should not be nil for valid command") - XCTAssertEqual( - output?.trimmingCharacters(in: .whitespacesAndNewlines), - testMessage, "Output should match the expected message" - ) - - // Test with a command that returns multiple lines - let multilineOutput = try SystemUtils.executeCommand(path: "/bin/echo", arguments: ["-e", "line1\\nline2"]) - XCTAssertNotNil(multilineOutput, "Output should not be nil for multiline command") - XCTAssertTrue(multilineOutput?.contains("line1") ?? false, "Output should contain 'line1'") - XCTAssertTrue(multilineOutput?.contains("line2") ?? false, "Output should contain 'line2'") - - // Test with a command that has no output - let noOutput = try SystemUtils.executeCommand(path: "/usr/bin/true", arguments: []) - XCTAssertNotNil(noOutput, "Output should not be nil even for commands with no output") - XCTAssertTrue(noOutput?.isEmpty ?? false, "Output should be empty for /usr/bin/true") - - // Test with an invalid command path should throw an error - XCTAssertThrowsError( - try SystemUtils.executeCommand(path: "/nonexistent/command", arguments: []), - "Should throw error for invalid command path" - ) - } -} diff --git a/Tool/Tests/WorkspaceSuggestionServiceTests/FilespaceSuggestionSnapshotTests.swift b/Tool/Tests/WorkspaceSuggestionServiceTests/FilespaceSuggestionSnapshotTests.swift deleted file mode 100644 index f20fa662..00000000 --- a/Tool/Tests/WorkspaceSuggestionServiceTests/FilespaceSuggestionSnapshotTests.swift +++ /dev/null @@ -1,97 +0,0 @@ -import XCTest -import SuggestionBasic -import WorkspaceSuggestionService - -final class FilespaceSuggestionSnapshotTests: XCTestCase { - - func testSameContent_IsEqual() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - - XCTAssertTrue(a == b) - } - - func testDifferenentContent_IsNotEqual() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["on","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - - XCTAssertFalse(a == b) - } - - func testEqualOrCurrentLineDiffers_WithNoChange_ReturnsTrue() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - - XCTAssertTrue(a.equalOrOnlyCurrentLineDiffers(comparedTo: b)) - } - - func testEqualOrCurrentLineDiffers_WithOnlyCurrentChange_ReturnsTrue() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one","two","t"], - cursorPosition: CursorPosition(line: 2, character: 1) - ) - - XCTAssertTrue(a.equalOrOnlyCurrentLineDiffers(comparedTo: b)) - } - - func testEqualOrCurrentLineDiffers_WithPositionChange_ReturnsFalse() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 1, character: 0) - ) - - XCTAssertFalse(a.equalOrOnlyCurrentLineDiffers(comparedTo: b)) - } - - func testEqualOrCurrentLineDiffers_WithPrefixChange_ReturnsFalse() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","two",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one","one",""], - cursorPosition: CursorPosition(line: 2, character: 0) - ) - - XCTAssertFalse(a.equalOrOnlyCurrentLineDiffers(comparedTo: b)) - } - - func testEqualOrCurrentLineDiffers_WithSuffixChange_ReturnsFalse() throws { - let a = FilespaceSuggestionSnapshot( - lines: ["one","","three"], - cursorPosition: CursorPosition(line: 1, character: 0) - ) - let b = FilespaceSuggestionSnapshot( - lines: ["one",""], - cursorPosition: CursorPosition(line: 1, character: 0) - ) - - XCTAssertFalse(a.equalOrOnlyCurrentLineDiffers(comparedTo: b)) - } -} diff --git a/Tool/Tests/WorkspaceSuggestionServiceTests/LineEditTests.swift b/Tool/Tests/WorkspaceSuggestionServiceTests/LineEditTests.swift deleted file mode 100644 index 0cfb1ec1..00000000 --- a/Tool/Tests/WorkspaceSuggestionServiceTests/LineEditTests.swift +++ /dev/null @@ -1,161 +0,0 @@ -import SuggestionBasic -import WorkspaceSuggestionService -import XCTest - -final class LineEditTests: XCTestCase { - - func lineAndCursorPos(from str: String) -> (String, CursorPosition) { - let parts = str.split(separator: "|", maxSplits: 1, omittingEmptySubsequences: false) - let pos = CursorPosition(line: 0, character: parts.first?.count ?? 0) - return (parts.joined(), pos) - } - - func suggestion(_ line: String, rangeLength: Int = 0) -> CodeSuggestion { - let (text, position) = lineAndCursorPos(from: line) - return CodeSuggestion( - id: "", - text: text, - position: position, - range: .init(startPair: (0, 0), endPair: (0, rangeLength)) - ) - } - - func edit(from: String, to: String, suggested: String) -> LineEdit { - // replacement range is the full length of the original line (minus line ending and cursor placeholder) - return edit(from: from, to: to, suggested: suggested, rangeLength: max(0, from.count - 2)) - } - - func edit(from: String, to: String, suggested: String, rangeLength: Int) -> LineEdit { - let (fromLine, fromPos) = lineAndCursorPos(from: from) - let (toLine, toPos) = lineAndCursorPos(from: to) - - return LineEdit( - snapshot: .init( - lines: [fromLine], - cursorPosition: fromPos - ), - suggestion: suggestion(suggested, rangeLength: rangeLength), - lines: [toLine], - cursor: toPos - ) - } - - // MARK: .init - - func testInit_EmptyLine() throws { - let edit = edit(from: "|", to: "|", suggested: "|// hello") - - XCTAssertEqual(edit.line, "") - XCTAssertEqual(edit.userEntered, "") - XCTAssertEqual(edit.head, "") - XCTAssertEqual(edit.tail, "") - } - - func testInit_NoTail() throws { - let edit = edit(from: "let one |\n", to: "let one =|\n", suggested: "let one |= 1") - - XCTAssertEqual(edit.line, "let one =") - XCTAssertEqual(edit.userEntered, "let one =") - XCTAssertEqual(edit.head, "let one =") - XCTAssertEqual(edit.tail, "") - } - - func testInit_PreservesExistingTail() throws { - let edit = edit( - from: "let fourTuple = (1, |)\n", - to: "let fourTuple = (1, 2|)\n", - suggested: "let fourTuple = (1, |2, 3, 4)" - ) - - XCTAssertEqual(edit.line, "let fourTuple = (1, 2)") - XCTAssertEqual(edit.userEntered, "let fourTuple = (1, 2") - XCTAssertEqual(edit.head, "let fourTuple = (1, 2") - XCTAssertEqual(edit.tail, ")") - } - - func testInit_NewBraceCompletionIncludedInTail() throws { - let edit = edit( - from: "let nestedTuple = (1, |)\n", - to: "let nestedTuple = (1, (2, (3|)))\n", - suggested: "let nestedTuple = (1, |(2, (3, 4)))" - ) - - XCTAssertEqual(edit.line, "let nestedTuple = (1, (2, (3)))") - XCTAssertEqual(edit.userEntered, "let nestedTuple = (1, (2, (3") - XCTAssertEqual(edit.head, "let nestedTuple = (1, (2, (3") - XCTAssertEqual(edit.tail, ")))") - } - - func testInit_NonBraceCompletionNotIncludedInTail() throws { - let edit = edit( - from: "let nestedTuple = (1, |)\n", - to: "let nestedTuple = (1, (|2))\n", - suggested: "let nestedTuple = (1, |(2, (3, 4)))" - ) - - XCTAssertEqual(edit.line, "let nestedTuple = (1, (2))") - XCTAssertEqual(edit.userEntered, "let nestedTuple = (1, (2") - XCTAssertEqual(edit.head, "let nestedTuple = (1, (") - XCTAssertEqual(edit.tail, "))") - } - - // MARK: .updateSuggestions - - func testUpdateSuggestions_WithNoChanges_ReturnsSameSuggestions() { - let edit = edit(from: "|\n", to: "|\n", suggested: "|// hello") - - let suggestions = [suggestion("|// hello"), suggestion("|// hello there")] - let updated = edit.updateSuggestions(suggestions) - - XCTAssertEqual(updated, suggestions) - } - - func testUpdateSuggestions_WithTypingIntoSuggetion_AdjustsCursorPositionAndRange() { - let edit = edit(from: "|\n", to: "//|\n", suggested: "|// hello") - - let suggestions = [suggestion("|// hello"), suggestion("|// hello there")] - let updated = edit.updateSuggestions(suggestions) - - XCTAssertEqual(updated, [ - suggestion("//| hello", rangeLength: 2), - suggestion("//| hello there", rangeLength: 2) - ]) - } - - func testUpdateSuggestions_WithSameTail_PreservesSelectedRange() { - let edit = edit( - from: "let pos = (1|)\n", - to: "let pos = (1, |)\n", - suggested: "let pos = (1|, 1)" - ) - - let updated = edit.updateSuggestions([edit.suggestion]) - - XCTAssertEqual(updated, [suggestion("let pos = (1, |1)", rangeLength: 15)]) - } - - func testUpdateSuggestions_WithPartialLineRange_PreservesUnselectedPortion() { - let edit = edit( - from: "let pos = (1|) //\n", - to: "let pos = (1, |) //\n", - suggested: "let pos = (1|, 1)", - rangeLength: 13 - ) - - let updated = edit.updateSuggestions([edit.suggestion]) - - XCTAssertEqual(updated, [suggestion("let pos = (1, |1)", rangeLength: 15)]) - } - - func testUpdateSuggestions_WithNewBraceCompletion_ExtendsSelectedRange() { - let edit = edit( - from: "let nested = (1|)\n", - to: "let nested = (1, (2, |))\n", - suggested: "let nested = (1|, (2, 3))" - ) - - let updated = edit.updateSuggestions([edit.suggestion]) - - XCTAssertEqual(updated, [suggestion("let nested = (1, (2, |3))", rangeLength: 23)]) - } -} diff --git a/Tool/Tests/WorkspaceTests/FileChangeWatcherTests.swift b/Tool/Tests/WorkspaceTests/FileChangeWatcherTests.swift deleted file mode 100644 index 02d35acd..00000000 --- a/Tool/Tests/WorkspaceTests/FileChangeWatcherTests.swift +++ /dev/null @@ -1,385 +0,0 @@ -import ConversationServiceProvider -import CoreServices -import Foundation -import LanguageServerProtocol -@testable import Workspace -import XCTest - -// MARK: - Mocks for Testing - -class MockFSEventProvider: FSEventProvider { - var createdStream: FSEventStreamRef? - var didStartStream = false - var didStopStream = false - var didInvalidateStream = false - var didReleaseStream = false - var didSetDispatchQueue = false - var registeredCallback: FSEventStreamCallback? - var registeredContext: UnsafeMutablePointer? - - var simulatedFiles: [String] = [] - - func createEventStream( - paths: CFArray, - latency: CFTimeInterval, - flags: UInt32, - callback: @escaping FSEventStreamCallback, - context: UnsafeMutablePointer - ) -> FSEventStreamRef? { - registeredCallback = callback - registeredContext = context - let stream = unsafeBitCast(1, to: FSEventStreamRef.self) - createdStream = stream - return stream - } - - func startStream(_ stream: FSEventStreamRef) { - didStartStream = true - } - - func stopStream(_ stream: FSEventStreamRef) { - didStopStream = true - } - - func invalidateStream(_ stream: FSEventStreamRef) { - didInvalidateStream = true - } - - func releaseStream(_ stream: FSEventStreamRef) { - didReleaseStream = true - } - - func setDispatchQueue(_ stream: FSEventStreamRef, queue: DispatchQueue) { - didSetDispatchQueue = true - } -} - -class MockWorkspaceFileProvider: WorkspaceFileProvider { - var subprojects: [URL] = [] - var filesInWorkspace: [FileReference] = [] - var xcProjectPaths: Set = [] - var xcWorkspacePaths: Set = [] - - func getProjects(by workspaceURL: URL) -> [URL] { - return subprojects - } - - func getFilesInActiveWorkspace(workspaceURL: URL, workspaceRootURL: URL) -> [FileReference] { - return filesInWorkspace - } - - func isXCProject(_ url: URL) -> Bool { - return xcProjectPaths.contains(url.path) - } - - func isXCWorkspace(_ url: URL) -> Bool { - return xcWorkspacePaths.contains(url.path) - } - - func fileExists(atPath: String) -> Bool { - return true - } -} - -class MockFileWatcher: FileWatcherProtocol { - var fileURL: URL - var dispatchQueue: DispatchQueue? - var onFileModified: (() -> Void)? - var onFileDeleted: (() -> Void)? - var onFileRenamed: (() -> Void)? - - static var watchers = [URL: MockFileWatcher]() - - init(fileURL: URL, dispatchQueue: DispatchQueue? = nil, onFileModified: (() -> Void)? = nil, onFileDeleted: (() -> Void)? = nil, onFileRenamed: (() -> Void)? = nil) { - self.fileURL = fileURL - self.dispatchQueue = dispatchQueue - self.onFileModified = onFileModified - self.onFileDeleted = onFileDeleted - self.onFileRenamed = onFileRenamed - MockFileWatcher.watchers[fileURL] = self - } - - func startWatching() -> Bool { - return true - } - - func stopWatching() { - MockFileWatcher.watchers[fileURL] = nil - } - - static func triggerFileDelete(for fileURL: URL) { - guard let watcher = watchers[fileURL] else { return } - watcher.onFileDeleted?() - } -} - -class MockFileWatcherFactory: FileWatcherFactory { - func createFileWatcher(fileURL: URL, dispatchQueue: DispatchQueue?, onFileModified: (() -> Void)?, onFileDeleted: (() -> Void)?, onFileRenamed: (() -> Void)?) -> FileWatcherProtocol { - return MockFileWatcher(fileURL: fileURL, dispatchQueue: dispatchQueue, onFileModified: onFileModified, onFileDeleted: onFileDeleted, onFileRenamed: onFileRenamed) - } - - func createDirectoryWatcher(watchedPaths: [URL], changePublisher: @escaping PublisherType, publishInterval: TimeInterval) -> DirectoryWatcherProtocol { - return BatchingFileChangeWatcher( - watchedPaths: watchedPaths, - changePublisher: changePublisher, - fsEventProvider: MockFSEventProvider() - ) - } -} - -// MARK: - Tests for BatchingFileChangeWatcher - -final class BatchingFileChangeWatcherTests: XCTestCase { - var mockFSEventProvider: MockFSEventProvider! - var publishedEvents: [[FileEvent]] = [] - - override func setUp() { - super.setUp() - mockFSEventProvider = MockFSEventProvider() - publishedEvents = [] - } - - func createWatcher(projectURL: URL = URL(fileURLWithPath: "/test/project")) -> BatchingFileChangeWatcher { - return BatchingFileChangeWatcher( - watchedPaths: [projectURL], - changePublisher: { [weak self] events in - self?.publishedEvents.append(events) - }, - publishInterval: 0.1, - fsEventProvider: mockFSEventProvider - ) - } - - func testInitSetsUpTimerAndFileWatching() { - let _ = createWatcher() - - XCTAssertNotNil(mockFSEventProvider.createdStream) - XCTAssertTrue(mockFSEventProvider.didStartStream) - } - - func testDeinitCleansUpResources() { - var watcher: BatchingFileChangeWatcher? = createWatcher() - weak var weakWatcher = watcher - - watcher = nil - - // Wait for the watcher to be deallocated - let startTime = Date() - let timeout: TimeInterval = 1.0 - - while weakWatcher != nil && Date().timeIntervalSince(startTime) < timeout { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01)) - } - - XCTAssertTrue(mockFSEventProvider.didStopStream) - XCTAssertTrue(mockFSEventProvider.didInvalidateStream) - XCTAssertTrue(mockFSEventProvider.didReleaseStream) - } - - func testAddingEventsAndPublishing() { - let watcher = createWatcher() - let fileURL = URL(fileURLWithPath: "/test/project/file.swift") - - watcher.onFileCreated(file: fileURL) - - // No events should be published yet - XCTAssertTrue(publishedEvents.isEmpty) - - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - // Only verify array contents if we have events - guard !publishedEvents.isEmpty else { return } - - XCTAssertEqual(publishedEvents[0].count, 1) - XCTAssertEqual(publishedEvents[0][0].uri, fileURL.absoluteString) - XCTAssertEqual(publishedEvents[0][0].type, .created) - } - - func testProcessingFSEvents() { - let watcher = createWatcher() - let fileURL = URL(fileURLWithPath: "/test/project/file.swift") - - // Test file creation - directly call methods instead of simulating FS events - watcher.onFileCreated(file: fileURL) - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - guard !publishedEvents.isEmpty else { return } - XCTAssertEqual(publishedEvents[0].count, 1) - XCTAssertEqual(publishedEvents[0][0].type, .created) - - // Test file modification - publishedEvents = [] - watcher.onFileChanged(file: fileURL) - - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - guard !publishedEvents.isEmpty else { return } - XCTAssertEqual(publishedEvents[0].count, 1) - XCTAssertEqual(publishedEvents[0][0].type, .changed) - - // Test file deletion - publishedEvents = [] - watcher.onFileDeleted(file: fileURL) - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - guard !publishedEvents.isEmpty else { return } - XCTAssertEqual(publishedEvents[0].count, 1) - XCTAssertEqual(publishedEvents[0][0].type, .deleted) - } -} - -extension BatchingFileChangeWatcherTests { - func waitForPublishedEvents(timeout: TimeInterval = 1.0) -> Bool { - let start = Date() - while publishedEvents.isEmpty && Date().timeIntervalSince(start) < timeout { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - return !publishedEvents.isEmpty - } -} - -// MARK: - Tests for FileChangeWatcherService - -final class FileChangeWatcherServiceTests: XCTestCase { - var mockWorkspaceFileProvider: MockWorkspaceFileProvider! - var publishedEvents: [[FileEvent]] = [] - - override func setUp() { - super.setUp() - mockWorkspaceFileProvider = MockWorkspaceFileProvider() - publishedEvents = [] - } - - func createService(workspaceURL: URL = URL(fileURLWithPath: "/test/workspace")) -> FileChangeWatcherService { - return FileChangeWatcherService( - workspaceURL, - publisher: { [weak self] events in - self?.publishedEvents.append(events) - }, - publishInterval: 0.1, - workspaceFileProvider: mockWorkspaceFileProvider, - watcherFactory: MockFileWatcherFactory() - ) - } - - func testStartWatchingCreatesWatchersForProjects() { - let project1 = URL(fileURLWithPath: "/test/workspace/project1") - let project2 = URL(fileURLWithPath: "/test/workspace/project2") - mockWorkspaceFileProvider.subprojects = [project1, project2] - - let service = createService() - service.startWatching() - - XCTAssertNotNil(service.watcher) - XCTAssertEqual(service.watcher?.paths().count, 2) - XCTAssertEqual(service.watcher?.paths(), [project1, project2]) - } - - func testStartWatchingDoesNotCreateWatcherForRootDirectory() { - let service = createService(workspaceURL: URL(fileURLWithPath: "/")) - service.startWatching() - - XCTAssertNil(service.watcher) - } - - func testProjectMonitoringDetectsAddedProjects() { - let workspace = URL(fileURLWithPath: "/test/workspace") - let project1 = URL(fileURLWithPath: "/test/workspace/project1") - mockWorkspaceFileProvider.subprojects = [project1] - mockWorkspaceFileProvider.xcWorkspacePaths = [workspace.path] - - let service = createService(workspaceURL: workspace) - service.startWatching() - - XCTAssertNotNil(service.watcher) - - // Simulate adding a new project - let project2 = URL(fileURLWithPath: "/test/workspace/project2") - mockWorkspaceFileProvider.subprojects = [project1, project2] - - // Set up mock files for the added project - let file1URL = URL(fileURLWithPath: "/test/workspace/project2/file1.swift") - let file1 = FileReference( - url: file1URL, - relativePath: file1URL.relativePath, - fileName: file1URL.lastPathComponent - ) - let file2URL = URL(fileURLWithPath: "/test/workspace/project2/file2.swift") - let file2 = FileReference( - url: file2URL, - relativePath: file2URL.relativePath, - fileName: file2URL.lastPathComponent - ) - mockWorkspaceFileProvider.filesInWorkspace = [file1, file2] - - MockFileWatcher.triggerFileDelete(for: workspace.appendingPathComponent("contents.xcworkspacedata")) - - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - guard !publishedEvents.isEmpty else { return } - - // Verify file events were published - XCTAssertEqual(publishedEvents[0].count, 2) - - // Verify both files were reported as created - XCTAssertEqual(publishedEvents[0][0].type, .created) - XCTAssertEqual(publishedEvents[0][1].type, .created) - } - - func testProjectMonitoringDetectsRemovedProjects() { - let workspace = URL(fileURLWithPath: "/test/workspace") - let project1 = URL(fileURLWithPath: "/test/workspace/project1") - let project2 = URL(fileURLWithPath: "/test/workspace/project2") - mockWorkspaceFileProvider.subprojects = [project1, project2] - mockWorkspaceFileProvider.xcWorkspacePaths = [workspace.path] - - let service = createService(workspaceURL: workspace) - service.startWatching() - - XCTAssertNotNil(service.watcher) - - // Simulate removing a project - mockWorkspaceFileProvider.subprojects = [project1] - - // Set up mock files for the removed project - let file1URL = URL(fileURLWithPath: "/test/workspace/project2/file1.swift") - let file1 = FileReference( - url: file1URL, - relativePath: file1URL.relativePath, - fileName: file1URL.lastPathComponent - ) - let file2URL = URL(fileURLWithPath: "/test/workspace/project2/file2.swift") - let file2 = FileReference( - url: file2URL, - relativePath: file2URL.relativePath, - fileName: file2URL.lastPathComponent - ) - mockWorkspaceFileProvider.filesInWorkspace = [file1, file2] - - // Clear published events from setup - publishedEvents = [] - - MockFileWatcher.triggerFileDelete(for: workspace.appendingPathComponent("contents.xcworkspacedata")) - - XCTAssertTrue(waitForPublishedEvents(), "No events were published within timeout") - - guard !publishedEvents.isEmpty else { return } - - // Verify file events were published - XCTAssertEqual(publishedEvents[0].count, 2) - - // Verify both files were reported as deleted - XCTAssertEqual(publishedEvents[0][0].type, .deleted) - XCTAssertEqual(publishedEvents[0][1].type, .deleted) - } -} - -extension FileChangeWatcherServiceTests { - func waitForPublishedEvents(timeout: TimeInterval = 3.0) -> Bool { - let start = Date() - while publishedEvents.isEmpty && Date().timeIntervalSince(start) < timeout { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - return !publishedEvents.isEmpty - } -} diff --git a/Tool/Tests/WorkspaceTests/WorkspaceTests.swift b/Tool/Tests/WorkspaceTests/WorkspaceTests.swift deleted file mode 100644 index 87276a06..00000000 --- a/Tool/Tests/WorkspaceTests/WorkspaceTests.swift +++ /dev/null @@ -1,460 +0,0 @@ -import XCTest -import Foundation -@testable import Workspace - -class WorkspaceFileTests: XCTestCase { - func testMatchesPatterns() { - let url1 = URL(fileURLWithPath: "/path/to/file.swift") - let url2 = URL(fileURLWithPath: "/path/to/.git") - let patterns = [".git", ".svn"] - - XCTAssertTrue(WorkspaceFile.matchesPatterns(url2, patterns: patterns)) - XCTAssertFalse(WorkspaceFile.matchesPatterns(url1, patterns: patterns)) - } - - func testIsXCWorkspace() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - let xcworkspaceURL = try createSubdirectory(in: tmpDir, withName: "myWorkspace.xcworkspace") - XCTAssertFalse(WorkspaceFile.isXCWorkspace(xcworkspaceURL)) - let xcworkspaceDataURL = try createFile(in: xcworkspaceURL, withName: "contents.xcworkspacedata", contents: "") - XCTAssertTrue(WorkspaceFile.isXCWorkspace(xcworkspaceURL)) - } catch { - throw error - } - } - - func testIsXCProject() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - let xcprojectURL = try createSubdirectory(in: tmpDir, withName: "myProject.xcodeproj") - XCTAssertFalse(WorkspaceFile.isXCProject(xcprojectURL)) - let xcprojectDataURL = try createFile(in: xcprojectURL, withName: "project.pbxproj", contents: "") - XCTAssertTrue(WorkspaceFile.isXCProject(xcprojectURL)) - } catch { - throw error - } - } - - func testGetFilesInActiveProject() throws { - let tmpDir = try createTemporaryDirectory() - do { - let xcprojectURL = try createXCProjectFolder(in: tmpDir, withName: "myProject.xcodeproj") - _ = try createFile(in: tmpDir, withName: "file1.swift", contents: "") - _ = try createFile(in: tmpDir, withName: "file2.swift", contents: "") - _ = try createSubdirectory(in: tmpDir, withName: ".git") - let files = WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: xcprojectURL, workspaceRootURL: tmpDir) - let fileNames = files.map { $0.url.lastPathComponent } - XCTAssertEqual(files.count, 2) - XCTAssertTrue(fileNames.contains("file1.swift")) - XCTAssertTrue(fileNames.contains("file2.swift")) - } catch { - deleteDirectoryIfExists(at: tmpDir) - throw error - } - deleteDirectoryIfExists(at: tmpDir) - } - - func testGetFilesInActiveWorkspace() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - let myWorkspaceRoot = try createSubdirectory(in: tmpDir, withName: "myWorkspace") - let xcWorkspaceURL = try createXCWorkspaceFolder(in: myWorkspaceRoot, withName: "myWorkspace.xcworkspace", fileRefs: [ - "container:myProject.xcodeproj", - "group:../notExistedDir/notExistedProject.xcodeproj", - "group:../myDependency",]) - let xcprojectURL = try createXCProjectFolder(in: myWorkspaceRoot, withName: "myProject.xcodeproj") - let myDependencyURL = try createSubdirectory(in: tmpDir, withName: "myDependency") - - // Files under workspace should be included - _ = try createFile(in: myWorkspaceRoot, withName: "file1.swift", contents: "") - // unsupported patterns and file extension should be excluded - _ = try createFile(in: myWorkspaceRoot, withName: "unsupportedFileExtension.xyz", contents: "") - _ = try createSubdirectory(in: myWorkspaceRoot, withName: ".git") - - // Files under project metadata folder should be excluded - _ = try createFile(in: xcprojectURL, withName: "fileUnderProjectMetadata.swift", contents: "") - - // Files under dependency should be included - _ = try createFile(in: myDependencyURL, withName: "depFile1.swift", contents: "") - // Should be excluded - _ = try createSubdirectory(in: myDependencyURL, withName: ".git") - - // Files under unrelated directories should be excluded - _ = try createFile(in: tmpDir, withName: "unrelatedFile1.swift", contents: "") - - let files = WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: xcWorkspaceURL, workspaceRootURL: myWorkspaceRoot) - let fileNames = files.map { $0.url.lastPathComponent } - XCTAssertEqual(files.count, 2) - XCTAssertTrue(fileNames.contains("file1.swift")) - XCTAssertTrue(fileNames.contains("depFile1.swift")) - } catch { - throw error - } - } - - func testGetSubprojectURLsFromXCWorkspace() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - - let workspaceDir = try createSubdirectory(in: tmpDir, withName: "workspace") - - // Create tryapp directory and project - let tryappDir = try createSubdirectory(in: tmpDir, withName: "tryapp") - _ = try createXCProjectFolder(in: tryappDir, withName: "tryapp.xcodeproj") - - // Create Copilot for Xcode project - _ = try createXCProjectFolder(in: workspaceDir, withName: "Copilot for Xcode.xcodeproj") - - // Create Test1 directory - let test1Dir = try createSubdirectory(in: tmpDir, withName: "Test1") - - // Create Test2 directory and project - let test2Dir = try createSubdirectory(in: tmpDir, withName: "Test2") - _ = try createXCProjectFolder(in: test2Dir, withName: "project2.xcodeproj") - - // Create the workspace data file with our references - let xcworkspaceData = """ - - - - - - - - - - - - - - """ - let workspaceURL = try createXCWorkspaceFolder(in: workspaceDir, withName: "workspace.xcworkspace", xcworkspacedata: xcworkspaceData) - - let subprojectURLs = WorkspaceFile.getSubprojectURLs(in: workspaceURL) - - XCTAssertEqual(subprojectURLs.count, 4) - let resolvedPaths = subprojectURLs.map { $0.path } - let expectedPaths = [ - tryappDir.path, - workspaceDir.path, // For Copilot for Xcode.xcodeproj - test1Dir.path, - test2Dir.path - ] - XCTAssertEqual(resolvedPaths, expectedPaths) - } - - func testGetSubprojectURLsFromEmbeddedXCWorkspace() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - - // Create the workspace data file with a self reference - let xcworkspaceData = """ - - - - - - """ - - // Create the MyApp directory structure - let myAppDir = try createSubdirectory(in: tmpDir, withName: "MyApp") - let xcodeProjectDir = try createXCProjectFolder(in: myAppDir, withName: "MyApp.xcodeproj") - let embeddedWorkspaceDir = try createXCWorkspaceFolder(in: xcodeProjectDir, withName: "MyApp.xcworkspace", xcworkspacedata: xcworkspaceData) - - let subprojectURLs = WorkspaceFile.getSubprojectURLs(in: embeddedWorkspaceDir) - XCTAssertEqual(subprojectURLs.count, 1) - XCTAssertEqual(subprojectURLs[0].lastPathComponent, "MyApp") - XCTAssertEqual(subprojectURLs[0].path, myAppDir.path) - } - - func testGetSubprojectURLsFromXCWorkspaceOrganizedByGroup() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - - // Create directories for the projects and groups - let tryappDir = try createSubdirectory(in: tmpDir, withName: "tryapp") - _ = try createXCProjectFolder(in: tryappDir, withName: "tryapp.xcodeproj") - - let webLibraryDir = try createSubdirectory(in: tmpDir, withName: "WebLibrary") - - // Create the group directories - let group1Dir = try createSubdirectory(in: tmpDir, withName: "group1") - let group2Dir = try createSubdirectory(in: group1Dir, withName: "group2") - _ = try createSubdirectory(in: group2Dir, withName: "group3") - _ = try createSubdirectory(in: group1Dir, withName: "group4") - - // Create the MyProjects directory - let myProjectsDir = try createSubdirectory(in: tmpDir, withName: "MyProjects") - - // Create the copilot-xcode directory and project - let copilotXcodeDir = try createSubdirectory(in: myProjectsDir, withName: "copilot-xcode") - _ = try createXCProjectFolder(in: copilotXcodeDir, withName: "Copilot for Xcode.xcodeproj") - - // Create the SwiftLanguageWeather directory and project - let swiftWeatherDir = try createSubdirectory(in: myProjectsDir, withName: "SwiftLanguageWeather") - _ = try createXCProjectFolder(in: swiftWeatherDir, withName: "SwiftWeather.xcodeproj") - - // Create the workspace data file with a complex group structure - let xcworkspaceData = """ - - - - - - - - - - - - - - - - - - - - """ - - // Create a test workspace structure - let workspaceURL = try createXCWorkspaceFolder(in: tmpDir, withName: "workspace.xcworkspace", xcworkspacedata: xcworkspaceData) - - let subprojectURLs = WorkspaceFile.getSubprojectURLs(in: workspaceURL) - XCTAssertEqual(subprojectURLs.count, 4) - let expectedPaths = [ - tryappDir.path, - webLibraryDir.path, - copilotXcodeDir.path, - swiftWeatherDir.path - ] - for expectedPath in expectedPaths { - XCTAssertTrue(subprojectURLs.contains { $0.path == expectedPath }, "Expected path not found: \(expectedPath)") - } - } - - func deleteDirectoryIfExists(at url: URL) { - if FileManager.default.fileExists(atPath: url.path) { - do { - try FileManager.default.removeItem(at: url) - } catch { - print("Failed to delete directory at \(url.path)") - } - } - } - - func createTemporaryDirectory() throws -> URL { - let temporaryDirectoryURL = FileManager.default.temporaryDirectory - let directoryName = UUID().uuidString - let directoryURL = temporaryDirectoryURL.appendingPathComponent(directoryName) - try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - #if DEBUG - print("Create temp directory \(directoryURL.path)") - #endif - return directoryURL - } - - func createSubdirectory(in directory: URL, withName name: String) throws -> URL { - let subdirectoryURL = directory.appendingPathComponent(name) - try FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil) - return subdirectoryURL - } - - func createFile(in directory: URL, withName name: String, contents: String) throws -> URL { - let fileURL = directory.appendingPathComponent(name) - let data = contents.data(using: .utf8) - FileManager.default.createFile(atPath: fileURL.path, contents: data, attributes: nil) - return fileURL - } - - func createXCProjectFolder(in baseDirectory: URL, withName projectName: String) throws -> URL { - let projectURL = try createSubdirectory(in: baseDirectory, withName: projectName) - if projectName.hasSuffix(".xcodeproj") { - _ = try createFile(in: projectURL, withName: "project.pbxproj", contents: "// Project file contents") - } - return projectURL - } - - func createXCWorkspaceFolder(in baseDirectory: URL, withName workspaceName: String, fileRefs: [String]?) throws -> URL { - let xcworkspaceURL = try createSubdirectory(in: baseDirectory, withName: workspaceName) - if let fileRefs { - _ = try createXCworkspacedataFile(directory: xcworkspaceURL, fileRefs: fileRefs) - } - return xcworkspaceURL - } - - func createXCWorkspaceFolder(in baseDirectory: URL, withName workspaceName: String, xcworkspacedata: String) throws -> URL { - let xcworkspaceURL = try createSubdirectory(in: baseDirectory, withName: workspaceName) - _ = try createFile(in: xcworkspaceURL, withName: "contents.xcworkspacedata", contents: xcworkspacedata) - return xcworkspaceURL - } - - func createXCworkspacedataFile(directory: URL, fileRefs: [String]) throws -> URL { - let contents = generateXCWorkspacedataContents(fileRefs: fileRefs) - return try createFile(in: directory, withName: "contents.xcworkspacedata", contents: contents) - } - - func generateXCWorkspacedataContents(fileRefs: [String]) -> String { - var contents = """ - - - """ - for fileRef in fileRefs { - contents += """ - - - """ - } - contents += "" - return contents - } - - func testIsValidFile() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - // Test valid Swift file - let swiftFileURL = try createFile(in: tmpDir, withName: "ValidFile.swift", contents: "// Swift code") - XCTAssertTrue(try WorkspaceFile.isValidFile(swiftFileURL)) - - // Test valid files with different supported extensions - let jsFileURL = try createFile(in: tmpDir, withName: "script.js", contents: "// JavaScript") - XCTAssertTrue(try WorkspaceFile.isValidFile(jsFileURL)) - - let mdFileURL = try createFile(in: tmpDir, withName: "README.md", contents: "# Markdown") - XCTAssertTrue(try WorkspaceFile.isValidFile(mdFileURL)) - - let jsonFileURL = try createFile(in: tmpDir, withName: "config.json", contents: "{}") - XCTAssertTrue(try WorkspaceFile.isValidFile(jsonFileURL)) - - // Test case insensitive extension matching - let swiftUpperURL = try createFile(in: tmpDir, withName: "File.SWIFT", contents: "// Swift") - XCTAssertTrue(try WorkspaceFile.isValidFile(swiftUpperURL)) - - // Test unsupported file extension - let unsupportedFileURL = try createFile(in: tmpDir, withName: "file.xyz", contents: "unsupported") - XCTAssertFalse(try WorkspaceFile.isValidFile(unsupportedFileURL)) - - // Test files matching skip patterns - let gitFileURL = try createFile(in: tmpDir, withName: ".git", contents: "") - XCTAssertFalse(try WorkspaceFile.isValidFile(gitFileURL)) - - let dsStoreURL = try createFile(in: tmpDir, withName: ".DS_Store", contents: "") - XCTAssertFalse(try WorkspaceFile.isValidFile(dsStoreURL)) - - let nodeModulesURL = try createFile(in: tmpDir, withName: "node_modules", contents: "") - XCTAssertFalse(try WorkspaceFile.isValidFile(nodeModulesURL)) - - // Test directory (should return false) - let subdirURL = try createSubdirectory(in: tmpDir, withName: "subdir") - XCTAssertFalse(try WorkspaceFile.isValidFile(subdirURL)) - - // Test Xcode workspace (should return false) - let xcworkspaceURL = try createSubdirectory(in: tmpDir, withName: "test.xcworkspace") - _ = try createFile(in: xcworkspaceURL, withName: "contents.xcworkspacedata", contents: "") - XCTAssertFalse(try WorkspaceFile.isValidFile(xcworkspaceURL)) - - // Test Xcode project (should return false) - let xcprojectURL = try createSubdirectory(in: tmpDir, withName: "test.xcodeproj") - _ = try createFile(in: xcprojectURL, withName: "project.pbxproj", contents: "") - XCTAssertFalse(try WorkspaceFile.isValidFile(xcprojectURL)) - - } catch { - throw error - } - } - - func testIsValidFileWithCustomExclusionFilter() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - let swiftFileURL = try createFile(in: tmpDir, withName: "TestFile.swift", contents: "// Swift code") - let jsFileURL = try createFile(in: tmpDir, withName: "script.js", contents: "// JavaScript") - - // Test without custom exclusion filter - XCTAssertTrue(try WorkspaceFile.isValidFile(swiftFileURL)) - XCTAssertTrue(try WorkspaceFile.isValidFile(jsFileURL)) - - // Test with custom exclusion filter that excludes Swift files - let excludeSwiftFilter: (URL) -> Bool = { url in - return url.pathExtension.lowercased() == "swift" - } - - XCTAssertFalse(try WorkspaceFile.isValidFile(swiftFileURL, shouldExcludeFile: excludeSwiftFilter)) - XCTAssertTrue(try WorkspaceFile.isValidFile(jsFileURL, shouldExcludeFile: excludeSwiftFilter)) - - // Test with custom exclusion filter that excludes files with "Test" in name - let excludeTestFilter: (URL) -> Bool = { url in - return url.lastPathComponent.contains("Test") - } - - XCTAssertFalse(try WorkspaceFile.isValidFile(swiftFileURL, shouldExcludeFile: excludeTestFilter)) - XCTAssertTrue(try WorkspaceFile.isValidFile(jsFileURL, shouldExcludeFile: excludeTestFilter)) - - } catch { - throw error - } - } - - func testIsValidFileWithAllSupportedExtensions() throws { - let tmpDir = try createTemporaryDirectory() - defer { - deleteDirectoryIfExists(at: tmpDir) - } - do { - let supportedExtensions = supportedFileExtensions - - for (index, ext) in supportedExtensions.enumerated() { - let fileName = "testfile\(index).\(ext)" - let fileURL = try createFile(in: tmpDir, withName: fileName, contents: "test content") - XCTAssertTrue(try WorkspaceFile.isValidFile(fileURL), "File with extension .\(ext) should be valid") - } - - } catch { - throw error - } - } -} diff --git a/Tool/Tests/XcodeInspectorTests/DisabledLanguageListTests.swift b/Tool/Tests/XcodeInspectorTests/DisabledLanguageListTests.swift deleted file mode 100644 index 8bc20dba..00000000 --- a/Tool/Tests/XcodeInspectorTests/DisabledLanguageListTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Preferences -import SuggestionBasic -import XCTest -import XcodeInspector - -public class DisabledLanguageListTests: XCTestCase { - - var savedDisabledList: [String] = [] - - public override func setUp() { - savedDisabledList = UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) - UserDefaults.shared.set(["yaml", "plaintext"], for: \.suggestionFeatureDisabledLanguageList) - } - - public override func tearDown() { - UserDefaults.shared.set(savedDisabledList, for: \.suggestionFeatureDisabledLanguageList) - } - - // MARK: - isEnabled - - public func testIsEnabled_ReturnsTrue_ForLanguageNotOnDisabledList() { - XCTAssertTrue(DisabledLanguageList.shared.isEnabled(.builtIn(.swift))) - } - - public func testIsEnabled_ReturnsFalse_ForLanguageOnDisabledList() { - XCTAssertFalse(DisabledLanguageList.shared.isEnabled(.plaintext)) - } - - // MARK: - enable - - public func testEnable_RemovesLanguageFromDisabledList() { - DisabledLanguageList.shared.enable(.plaintext) - - XCTAssertEqual(DisabledLanguageList.shared.list, ["yaml"]) - } - - public func testEnable_IgnoresLanguageNotOnDisabledList() { - DisabledLanguageList.shared.enable(.builtIn(.swift)) - - XCTAssertEqual(DisabledLanguageList.shared.list, ["yaml", "plaintext"]) - } - - // MARK: - disable - - public func testEnable_AddsLanguageToDisabledList() { - DisabledLanguageList.shared.disable(.builtIn(.scala)) - - XCTAssertEqual(DisabledLanguageList.shared.list, ["yaml", "plaintext", "scala"]) - } - - public func testEnable_IgnoresLanguageOnDisabledList() { - DisabledLanguageList.shared.disable(.plaintext) - - XCTAssertEqual(DisabledLanguageList.shared.list, ["yaml", "plaintext"]) - } -} diff --git a/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift b/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift deleted file mode 100644 index d62d0c0b..00000000 --- a/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -import Foundation -import SuggestionBasic -import XCTest - -@testable import XcodeInspector - -class SourceEditorRangeConversionTests: XCTestCase { - // MARK: - Convert to CursorRange - - func test_convert_multiline_range() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let range = 21...39 - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - XCTAssertEqual(cursorRange.start, .init(line: 1, character: 3)) - XCTAssertEqual(cursorRange.end, .init(line: 3, character: 6)) - } - - func test_convert_multiline_range_with_special_line_endings() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """.replacingOccurrences(of: "\n", with: "\r\n") - - let range = 21...39 - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - XCTAssertEqual(cursorRange.start, .init(line: 1, character: 2)) - XCTAssertEqual(cursorRange.end, .init(line: 3, character: 3)) - } - - func test_convert_multiline_range_with_emoji() { - let code = """ - import Foundation - import 🎆🎆🎆🎆🎆🎆 - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let range = 21...42 - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - XCTAssertEqual(cursorRange.start, .init(line: 1, character: 3)) - XCTAssertEqual(cursorRange.end, .init(line: 3, character: 3)) - } - - func test_convert_multiline_range_cutting_emoji() { - // undefined behavior - - let code = """ - import Foundation - import 🎆🎆🎆🎆🎆🎆 - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let range = 26...42 // in the middle of the emoji - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - XCTAssertEqual(cursorRange.start, .init(line: 1, character: 8)) - XCTAssertEqual(cursorRange.end, .init(line: 3, character: 3)) - } - - func test_convert_range_with_no_code() { - let code = "" - let range = 21...39 - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - XCTAssertEqual(cursorRange.start, .zero) - XCTAssertEqual(cursorRange.end, .zero) - } - - func test_convert_multiline_range_with_out_of_range_cursor() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let range = 999...1000 - let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code) - - // undefined behavior - - XCTAssertEqual(cursorRange.start, .zero) - XCTAssertEqual(cursorRange.end, .init(line: 8, character: 0)) - } - - // MARK: - Convert to CFRange - - func test_back_convert_multiline_cursor_range() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let cursorRange = CursorRange( - start: .init(line: 1, character: 3), - end: .init(line: 3, character: 6) - ) - let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code) - - XCTAssertEqual(range.range, 21...39) - } - - func test_back_convert_multiline_range_with_out_of_range_cursor() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let cursorRange = CursorRange( - start: .init(line: 999, character: 0), - end: .init(line: 1000, character: 0) - ) - let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code) - - // undefined behavior - - XCTAssertEqual(range.range, 0...0) - } - - func test_back_convert_multiline_range_with_special_line_endings() { - let code = """ - import Foundation - import XCTest - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """.replacingOccurrences(of: "\n", with: "\r\n") - - let cursorRange = CursorRange( - start: .init(line: 1, character: 2), - end: .init(line: 3, character: 3) - ) - let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code) - - XCTAssertEqual(range.range, 21...39) - } - - func test_back_convert_multiline_range_with_emoji() { - let code = """ - import Foundation - import 🎆🎆🎆🎆🎆🎆 - - class SourceEditorRangeConversionTests { - func testSomething() { - // test - } - } - - """ - - let cursorRange = CursorRange( - start: .init(line: 1, character: 3), - end: .init(line: 3, character: 3) - ) - let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code) - XCTAssertEqual(range.range, 21...42) - } - - func test_back_convert_range_with_no_code() { - let code = "" - let range = 21...39 - let cursorRange = SourceEditor.convertCursorRangeToRange( - SourceEditor.convertRangeToCursorRange(range, in: code), - in: code - ) - - XCTAssertEqual(cursorRange.range, 0...0) - } -} - -private extension CFRange { - var range: ClosedRange { - return location...(location + length) - } -} - diff --git a/Tool/Tests/XcodeInspectorTests/SourceEditorCachePerformanceTests.swift b/Tool/Tests/XcodeInspectorTests/SourceEditorCachePerformanceTests.swift deleted file mode 100644 index f3632d83..00000000 --- a/Tool/Tests/XcodeInspectorTests/SourceEditorCachePerformanceTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import XCTest - -@testable import XcodeInspector - -class SourceEditorCachePerformanceTests: XCTestCase { - func test_source_editor_cache_get_content_comparison() { - let content = String(repeating: """ - struct Cat: Animal { - var name: String - } - - """, count: 500) - let cache = SourceEditor.Cache(sourceContent: content + "Yes") - - measure { - for _ in 1 ... 10000 { - _ = cache.get(content: content, selectedTextRange: nil) - } - } - } -} - diff --git a/Tool/Tests/XcodeInspectorTests/SourceEditorCacheTests.swift b/Tool/Tests/XcodeInspectorTests/SourceEditorCacheTests.swift deleted file mode 100644 index 3649e3ef..00000000 --- a/Tool/Tests/XcodeInspectorTests/SourceEditorCacheTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import XCTest - -@testable import XcodeInspector - -class SourceEditorCacheTests: XCTestCase { - func test_source_editor_cache_get_content_thread_safe() { - func randomContent() -> String { - String(repeating: """ - struct Cat: Animal { - var name: String - } - - """, count: Int.random(in: 2...10)) - } - - func randomSelectionRange() -> ClosedRange { - let random = Int.random(in: 0...20) - return random...random - } - - let cache = SourceEditor.Cache() - - let max = 5000 - let exp = expectation(description: "test_source_editor_cache_get_content_thread_safe") - DispatchQueue.concurrentPerform(iterations: max) { count in - let content = randomContent() - let selectionRange = randomSelectionRange() - let result = cache.get(content: content, selectedTextRange: selectionRange) - - XCTAssertEqual(result.lines, content.breakLines(appendLineBreakToLastLine: false)) - XCTAssertEqual(result.selections, [SourceEditor.convertRangeToCursorRange( - selectionRange, - in: result.lines - )]) - - if max == count + 1 { - exp.fulfill() - } - } - - wait(for: [exp], timeout: 10) - } -} - diff --git a/Version.xcconfig b/Version.xcconfig deleted file mode 100644 index 82b2f1a9..00000000 --- a/Version.xcconfig +++ /dev/null @@ -1,3 +0,0 @@ -APP_VERSION = 0.0.0 -APP_BUILD = $(APP_VERSION) - diff --git a/bridgeLaunchAgent.plist b/bridgeLaunchAgent.plist deleted file mode 100644 index a052db44..00000000 --- a/bridgeLaunchAgent.plist +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Label - com.github.CopilotForXcode.CommunicationBridge - Program - /Applications/GitHub Copilot for Xcode.app/Contents/Applications/CommunicationBridge - MachServices - - com.github.CopilotForXcode.CommunicationBridge - - - - diff --git a/export-options.plist b/export-options.plist deleted file mode 100644 index f8d0b35b..00000000 --- a/export-options.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - teamID - VEKTX9H2N7 - method - developer-id - - diff --git a/launchAgent.plist b/launchAgent.plist deleted file mode 100644 index 4770316e..00000000 --- a/launchAgent.plist +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Label - com.github.CopilotForXcode.ExtensionService - Program - /Applications/GitHub Copilot for Xcode.app/Contents/Applications/GitHub Copilot for Xcode Extension.app/Contents/MacOS/GitHub Copilot for Xcode Extension - MachServices - - com.github.CopilotForXcode.ExtensionService - - - -