diff --git a/.devcontainer/default/Dockerfile b/.devcontainer/default/Dockerfile deleted file mode 100644 index 7cb443594..000000000 --- a/.devcontainer/default/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift.org open source project -## -## Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -## Licensed under Apache License v2.0 with Runtime Library Exception -## -## See https://swift.org/LICENSE.txt for license information -## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -## -##===----------------------------------------------------------------------===## - -FROM swiftlang/swift:nightly-main-jammy - -RUN \ - # Disable apt interactive prompts for this RUN command - export DEBIAN_FRONTEND="noninteractive" && \ - # Update apt package list - apt-get update && \ - # Install sourcekit-lsp dependencies - apt-get install -y libsqlite3-dev libncurses5-dev python3 - diff --git a/.devcontainer/default/devcontainer.json b/.devcontainer/default/devcontainer.json deleted file mode 100644 index 7f2e5fe85..000000000 --- a/.devcontainer/default/devcontainer.json +++ /dev/null @@ -1,30 +0,0 @@ -// Reference: https://containers.dev/implementors/json_reference/ -{ - "name": "SourceKit-LSP", - "dockerFile": "Dockerfile", - - // Allow the processes in the container to attach a debugger - "capAdd": [ "SYS_PTRACE" ], - "securityOpt": [ "seccomp=unconfined" ], - - "mounts": [ - // Use a named volume for the build products for optimal performance (https://code.visualstudio.com/remote/advancedcontainers/improve-performance#_use-a-targeted-named-volume) - "source=${localWorkspaceFolderBasename}-build,target=${containerWorkspaceFolder}/.build,type=volume" - ], - "customizations": { - "vscode": { - "extensions": [ - "sswg.swift-lang" - ], - "settings": { - "lldb.library": "/usr/lib/liblldb.so", - "swift.buildArguments": [ - "-Xcxx", - "-I/usr/lib/swift", - "-Xcxx", - "-I/usr/lib/swift/Block" - ] - } - } - } -} diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8b1f93e0e..000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] - -ignore = - E501, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bdb02647e..564a3ae7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # This file is a list of the people responsible for ensuring that patches for a -# particular part of SourceKit-LSP are reviewed, either by themselves or by +# particular part of swift-tools-protocols are reviewed, either by themselves or by # someone else. They are also the gatekeepers for their part of Swift, with the # final word on what goes in or not. @@ -8,4 +8,4 @@ # Order is important. The last matching pattern has the most precedence. # Owner of anything in SourceKit-LSP not owned by anyone else. -* @ahoppen @bnbarham @hamishknight @rintaro +* @ahoppen @bnbarham @hamishknight @rintaro @owenv diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml deleted file mode 100644 index d87b1ce61..000000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Bug Report -description: Something isn't working as expected -labels: [bug] -body: - - type: input - id: version - attributes: - label: Swift version - description: Which version of Swift are you using? If you are unsure, insert the output of `path/to/swift --version` - placeholder: Eg. swiftlang-5.10.0.13, swift-DEVELOPMENT-SNAPSHOT-2024-05-01-a - - type: input - id: platform - attributes: - label: Platform - description: What operating system are you seeing the issue on? - placeholder: Eg. Ubuntu 22.04, Windows 11, macOS 14 - - type: input - id: editor - attributes: - label: Editor - description: Which text editor are you using (and LSP extension/plugin if applicable)? - placeholder: Eg. Visual Studio Code with Swift extension 1.9.0, Neovim - - type: textarea - id: description - attributes: - label: Description - description: | - A short description of the incorrect behavior. - If you think this issue has been recently introduced and did not occur in an earlier version, please note that. If possible, include the last version that the behavior was correct in addition to your current version. - - type: textarea - id: steps-to-reproduce - attributes: - label: Steps to Reproduce - description: If you have steps that reproduce the issue, please add them here. If you can share a project that reproduces the issue, please attach it. - - type: textarea - id: logging - attributes: - label: Logging - description: | - Run `sourcekit-lsp diagnose` in terminal and attach the generated bundle to help us diagnose the issue. - The generated bundle may contain paths to files on disk as well as portions of your source code. This greatly helps in reproducing issues, but you should only attach it if you feel comfortable doing so. diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml deleted file mode 100644 index 30f157e55..000000000 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Feature Request -description: A suggestion for a new feature -labels: [enhancement] -body: - - type: textarea - id: description - attributes: - label: Description - description: | - A description of your proposed feature. - Examples that show what's missing, or what new capabilities will be possible, are very helpful! - If this feature unlocks new use-cases please describe them. - Provide links to existing issues or external references/discussions, if appropriate. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 00d5ca0a0..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,12 +0,0 @@ -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2024 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See https://swift.org/LICENSE.txt for license information -# See https://swift.org/CONTRIBUTORS.txt for Swift project authors - -contact_links: - - name: Discussion Forum - url: https://forums.swift.org/c/development/sourcekit-lsp - about: Ask and answer questions about SourceKit-LSP diff --git a/.licenseignore b/.licenseignore index a41d8a3b1..efe1141e9 100644 --- a/.licenseignore +++ b/.licenseignore @@ -1,3 +1 @@ *Package.swift -Utilities/ubsan_supressions.supp -sourcekit-lsp-dev-utils diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index bf881902f..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "configurations": [ - { - "type": "swift", - "request": "launch", - "name": "Debug sourcekit-lsp", - "program": "${workspaceFolder:sourcekit-lsp}/.build/debug/sourcekit-lsp", - "args": [], - "cwd": "${workspaceFolder:sourcekit-lsp}", - "preLaunchTask": "swift: Build Debug sourcekit-lsp" - }, - { - "type": "swift", - "request": "launch", - "name": "Release sourcekit-lsp", - "program": "${workspaceFolder:sourcekit-lsp}/.build/release/sourcekit-lsp", - "args": [], - "cwd": "${workspaceFolder:sourcekit-lsp}", - "preLaunchTask": "swift: Build Release sourcekit-lsp" - }, - { - "type": "swift", - "request": "attach", - "name": "Attach sourcekit-lsp (debug)", - "program": "${workspaceFolder:sourcekit-lsp}/.build/debug/sourcekit-lsp", - "waitFor": true - }, - { - "type": "swift", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:sourcekit-lsp}/SourceKitLSPDevUtils", - "name": "Debug sourcekit-lsp-dev-utils (SourceKitLSPDevUtils)", - "program": "${workspaceFolder:sourcekit-lsp}/SourceKitLSPDevUtils/.build/debug/sourcekit-lsp-dev-utils", - "preLaunchTask": "swift: Build Debug sourcekit-lsp-dev-utils (SourceKitLSPDevUtils)" - }, - { - "type": "swift", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:sourcekit-lsp}/SourceKitLSPDevUtils", - "name": "Release sourcekit-lsp-dev-utils (SourceKitLSPDevUtils)", - "program": "${workspaceFolder:sourcekit-lsp}/SourceKitLSPDevUtils/.build/release/sourcekit-lsp-dev-utils", - "preLaunchTask": "swift: Build Release sourcekit-lsp-dev-utils (SourceKitLSPDevUtils)" - } - ] -} diff --git a/CMakeLists.txt b/CMakeLists.txt index d23ea604d..4e0592110 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16.1) list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) -project(SourceKit-LSP +project(swift-tools-protocols LANGUAGES C Swift) set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) @@ -21,17 +21,8 @@ add_compile_options( find_package(dispatch QUIET) find_package(Foundation QUIET) -find_package(TSC QUIET) -find_package(LMDB QUIET) -find_package(IndexStoreDB QUIET) -find_package(SwiftPM QUIET) -find_package(LLBuild QUIET) -find_package(ArgumentParser CONFIG REQUIRED) -find_package(SwiftCollections QUIET) -find_package(SwiftSyntax CONFIG REQUIRED) -find_package(SwiftASN1 CONFIG REQUIRED) -find_package(SwiftCrypto CONFIG REQUIRED) include(SwiftSupport) add_subdirectory(Sources) +add_subdirectory(cmake/modules) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78d6ee38a..a5bf16cca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,131 +1,3 @@ -# Contributing - -This document contains notes about development and testing of SourceKit-LSP, the [Contributor Documentation](Contributor%20Documentation/) folder has some more detailed documentation. - -## Building & Testing - -SourceKit-LSP is a SwiftPM package, so you can build and test it using anything that supports packages - opening in Xcode, Visual Studio Code with [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode) installed, or through the command line using `swift build` and `swift test`. See below for extra instructions for Linux and Windows - -SourceKit-LSP builds with the latest released Swift version and all its tests pass or, if unsupported by the latest Swift version, are skipped. Using the `main` development branch of SourceKit-LSP with an older Swift versions is not supported. - -> [!TIP] -> SourceKit-LSP’s logging is usually very useful to debug test failures. On macOS these logs are written to the system log by default. To redirect them to stderr, build SourceKit-LSP with the `SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER` environment variable set to `1`: -> - In VS Code: Add the following to your `settings.json`: -> ```json -> "swift.swiftEnvironmentVariables": { "SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER": "1" }, -> ``` -> - In Xcode -> 1. Product -> Scheme -> Edit Scheme… -> 2. Select the Arguments tab in the Run section -> 3. Add a `SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER` environment variable with value `1` -> - On the command line: Set the `SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER` environment variable to `1` when running tests, e.g by running `SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER=1 swift test --parallel` - -> [!TIP] -> Other useful environment variables during test execution are: -> - `SKIP_LONG_TESTS`: Skips tests that usually take longer than 1 second to execute. This significantly speeds up test time, especially with `swift test --parallel` -> - `SOURCEKIT_LSP_KEEP_TEST_SCRATCH_DIR`: Does not delete the temporary files created during test execution. Allows inspection of the test projects after the test finishes. - -### Linux - -The following dependencies of SourceKit-LSP need to be installed on your system -- libsqlite3-dev libncurses5-dev python3 - -You need to add `/usr/lib/swift` and `/usr/lib/swift/Block` C++ search paths to your `swift build` invocation that SourceKit-LSP’s dependencies build correctly. Assuming that your Swift toolchain is installed to `/`, the build command is - -```sh -$ swift build -Xcxx -I/usr/lib/swift -Xcxx -I/usr/lib/swift/Block -``` - -### Windows - -To build SourceKit-LSP on Windows, the swift-syntax libraries need to be built as dynamic libraries so we do not exceed the maximum symbol limit in a single binary. Additionally, the equivalent search paths to the linux build need to be passed. Run the following in PowerShell. - -```ps -> $env:SWIFTSYNTAX_BUILD_DYNAMIC_LIBRARIES = 1; swift test -Xcc -I -Xcc $env:SDKROOT\usr\include -Xcc -I -Xcc $env:SDKROOT\usr\include\Block -``` - -To work on SourceKit-LSP in VS Code, add the following to your `settings.json`, for other editors ensure that the `SWIFTSYNTAX_BUILD_DYNAMIC_LIBRARY` environment variable is set when launching `sourcekit-lsp`. - -```json -"swift.swiftEnvironmentVariables": { - "SWIFTSYNTAX_BUILD_DYNAMIC_LIBRARY": "1" -}, -``` - -### Devcontainer - -You can develop SourceKit-LSP inside a devcontainer, which is essentially a Linux container that has all of SourceKit-LSP’s dependencies pre-installed. The [official tutorial](https://code.visualstudio.com/docs/devcontainers/tutorial) contains information of how to set up devcontainers in VS Code. - -Recommended Docker settings for macOS are: -- General - - "Choose file sharing implementation for your containers": VirtioFS (better IO performance) -- Resources - - CPUs: Allow docker to use most or all of your CPUs - - Memory: Allow docker to use most or all of your memory - -## Using a locally-built sourcekit-lsp in an editor - -If you want test your changes to SourceKit-LSP inside your editor, you can point it to your locally-built `sourcekit-lsp` executable. The exact steps vary by editor. For VS Code, you can add the following to your `settings.json`. - -```json -"swift.sourcekit-lsp.serverPath": "/path/to/sourcekit-lsp/.build/arm64-apple-macosx/debug/sourcekit-lsp", -``` - -> [!NOTE] -> VS Code will note that that the `swift.sourcekit-lsp.serverPath` setting is deprecated. That’s because mixing and matching versions of sourcekit-lsp and Swift toolchains is generally not supported, so the settings is reserved for developers of SourceKit-LSP, which includes you. You can ignore this warning, If you have the `swift.path` setting to a recent [Swift Development Snapshot](https://www.swift.org/install). - -> [!TIP] -> The easiest way to debug SourceKit-LSP is usually to write a test case that reproduces the behavior and then debug that. If that’s not possible, you can attach LLDB to the sourcekit-lsp launched by your and set breakpoints to debug. To do so on the command line, run -> ```bash -> $ lldb --wait-for --attach-name sourcekit-lsp -> ``` -> -> If you are developing SourceKit-LSP in Xcode, go to Debug -> Attach to Process by PID or Name. - -## Selecting a Toolchain - -When SourceKit-LSP is installed as part of a toolchain, it finds the Swift version to use relative to itself. When building SourceKit-LSP locally, it picks a default toolchain on your system, which usually corresponds to the toolchain that is used if you invoke `swift` without any specified path. - -To adjust the toolchain that should be used by SourceKit-LSP (eg. because you want to use new `sourcekitd` features that are only available in a Swift open source toolchain snapshot but not your default toolchain), set the `SOURCEKIT_TOOLCHAIN_PATH` environment variable to your toolchain when running SourceKit-LSP. - -## Logging - -SourceKit-LSP has extensive logging to the system log on macOS and to `~/.sourcekit-lsp/logs/` or stderr on other platforms. - -To show the logs on macOS, run -```sh -log show --last 1h --predicate 'subsystem CONTAINS "org.swift.sourcekit-lsp"' --info --debug -``` -Or to stream the logs as they are produced: -``` -log stream --predicate 'subsystem CONTAINS "org.swift.sourcekit-lsp"' --level debug -``` -On non-Apple platforms, you can use common commands like `tail` to read the logs or stream them as they are produced: -``` -tail -F ~/.sourcekit-lsp/logs/* -``` - -SourceKit-LSP masks data that may contain private information such as source file names and contents by default. To enable logging of this information, follow the instructions in [Diagnose Bundle.md](Documentation/Diagnose%20Bundle.md). - -## Formatting - -SourceKit-LSP is formatted using [swift-format](http://github.com/swiftlang/swift-format) to ensure a consistent style. - -To format your changes run the formatter using the following command -```bash -swift format -ipr . -``` - -If you are developing SourceKit-LSP in VS Code, you can also run the *Run swift-format* task from *Tasks: Run tasks* in the command palette. - -## Generate configuration schema - -If you modify the configuration options in [`SKOptions`](./Sources/SKOptions), you need to regenerate the configuration the JSON schema and the documentation by running the following command: - -```bash -./sourcekit-lsp-dev-utils generate-config-schema -``` - ## Authoring commits Prefer to squash the commits of your PR (*pull request*) and avoid adding commits like “Address review comments”. This creates a clearer git history, which doesn’t need to record the history of how the PR evolved. diff --git a/Contributor Documentation/BSP Extensions.md b/Contributor Documentation/BSP Extensions.md deleted file mode 100644 index 88c234fbd..000000000 --- a/Contributor Documentation/BSP Extensions.md +++ /dev/null @@ -1,245 +0,0 @@ -# BSP Extensions - -SourceKit-LSP extends the BSP protocol in the following ways. - -## `$/cancelRequest` - -Notification sent from SourceKit-LSP to the BSP server to cancel a request. If a BSP server doesn’t support cancellation, it should ignore this notification. - -Definition is the same as in LSP. - -## `build/initialize` - -`InitializeBuildResultDataKind` can be `sourceKit`, in which case `InitializeBuildResultData` contains the following data. - -```ts -export interface SourceKitInitializeBuildResponseData { - /** The path at which SourceKit-LSP can store its index database, aggregating data from `indexStorePath`. - * This should point to a directory that can be exclusively managed by SourceKit-LSP. Its exact location can be arbitrary. */ - indexDatabasePath?: string; - - /** The directory to which the index store is written during compilation, ie. the path passed to `-index-store-path` - * for `swiftc` or `clang` invocations **/ - indexStorePath?: string; - - /** Whether the server set the `outputPath` property in the `buildTarget/sources` request */ - outputPathsProvider?: bool; - - /** Whether the build server supports the `buildTarget/prepare` request */ - prepareProvider?: bool; - - /** Whether the server implements the `textDocument/sourceKitOptions` request. */ - sourceKitOptionsProvider?: bool; - - /** The files to watch for changes. - * Changes to these files are sent to the BSP server using `workspace/didChangeWatchedFiles`. - * `FileSystemWatcher` is the same as in LSP. */ - watchers: [FileSystemWatcher]? -} -``` - -## `build/logMessage` - -Added fields: - -```ts -/** - * Extends BSPs log message grouping by explicitly starting and ending the log for a specific task ID. - */ -structure?: StructuredLogBegin | StructuredLogReport | StructuredLogEnd; -``` - -With - -```ts -/** - * Indicates the beginning of a new task that may receive updates with `StructuredLogReport` or `StructuredLogEnd` - * payloads. - */ -export interface StructuredLogBegin { - kind: 'begin' - - /** - * A succinct title that can be used to describe the task that started this structured. - */ - title: string; -} - - -/** - * Adds a new log message to a structured log without ending it. - */ -export interface StructuredLogReport { - kind: 'report'; -} - -/** - * Ends a structured log. No more `StructuredLogReport` updates should be sent for this task ID. - * - * The task ID may be re-used for new structured logs by beginning a new structured log for that task. - */ -export interface StructuredLogEnd { - kind: 'end'; -} -``` - -## `build/taskStart` - -If `data` contains a string value for the `workDoneProgressTitle` key, then the task's message will be displayed in the client as a work done progress with that title. - -## `buildTarget/didChange` - -`changes` can be `null` to indicate that all targets have changed. - -## `buildTarget/prepare` - -The prepare build target request is sent from the client to the server to prepare the given list of build targets for editor functionality. - -To do so, the build server should perform any work that is necessary to typecheck the files in the given target. This includes, but is not limited to: Building Swift modules for all dependencies and running code generation scripts. Compared to a full build, the build server may skip actions that are not necessary for type checking, such as object file generation but the exact steps necessary are dependent on the build system. SwiftPM implements this step using the `swift build --experimental-prepare-for-indexing` command. - -The server communicates during the initialize handshake whether this method is supported or not by setting `prepareProvider: true` in `SourceKitInitializeBuildResponseData`. - -- method: `buildTarget/prepare` -- params: `PrepareParams` -- result: `void` - -```ts -export interface PrepareParams { - /** A list of build targets to prepare. */ - targets: BuildTargetIdentifier[]; - - /** A unique identifier generated by the client to identify this request. - * The server may include this id in triggered notifications or responses. **/ - originId?: OriginId; -} -``` - -## `buildTarget/sources` - -`SourceItemDataKind` can be `sourceKit` in which case `SourceItem.data` contains the following data. - -```ts -export interface SourceKitSourceItemData { - /** The language of the source file. If `nil`, the language is inferred from the file extension. */ - language?: LanguageId; - - /** - * The kind of source file that this source item represents. If omitted, the item is assumed to be a normal source - * file, ie. omitting this key is equivalent to specifying it as `source`. - */ - kind?: "source" | "header" | "doccCatalog"; - - /** - * The output path that is a string that uniquely identifies the index output of this file in this target. If an index - * store should be re-used between build and background indexing, it must match the `-o` path or - * `-index-unit-output-path` used during the build. - * - * The index unit output path historically matched the path used for `-o` during compilation but has since evolved to - * be an opaque string. In particular, it does not have to match to any file on disk. - * - * This allows SourceKit-LSP to remove index entries for source files that are removed from a target but remain - * present on disk and to index a file that is part of multiple targets in the context of each target. - * - * The server communicates during the initialize handshake whether it populates this property by setting - * `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`. - */ - outputPath?: string; - - /** - * If this source item gets copied to a different destination during preparation, the destinations it will be copied - * to. - * - * If a user action would jump to one of these copied files, this allows SourceKit-LSP to redirect the navigation to - * the original source file instead of jumping to a file in the build directory. - */ - copyDestinations?: DocumentURI[]; -} -``` - -## `textDocument/sourceKitOptions` - -The `TextDocumentSourceKitOptionsRequest` request is sent from the client to the server to query for the list of compiler options necessary to compile this file in the given target. - -The build settings are considered up-to-date and can be cached by SourceKit-LSP until a `buildTarget/didChange` is sent for the requested target. - -The request may return `nil` if it doesn't have any build settings for this file in the given target. - -- method: `textDocument/sourceKitOptions` -- params: `TextDocumentSourceKitOptionsParams` -- result: `TextDocumentSourceKitOptionsResult` - -```ts -export interface TextDocumentSourceKitOptionsRequest { - /** The URI of the document to get options for */ - textDocument: TextDocumentIdentifier; - - /** The target for which the build setting should be returned. - * - * A source file might be part of multiple targets and might have different compiler arguments in those two targets, - * thus the target is necessary in this request. **/ - target: BuildTargetIdentifier; - - /** The language with which the document was opened in the editor. */ - language: LanguageId; -} - -export interface TextDocumentSourceKitOptionsResult { - /** The compiler options required for the requested file. */ - compilerArguments: string[]; - - /** The working directory for the compile command. */ - workingDirectory?: string; - - /** Additional data that will not be interpreted by SourceKit-LSP but made available to clients in the - * `workspace/_sourceKitOptions` LSP requests. */ - data?: LSPAny; -} -``` - -## `workspace/buildTargets` - -`BuildTargetTag` can have the following additional values - -```ts -export interface BuildTargetTag { - // ... - - /** This is a target of a dependency from the project the user opened, eg. a target that builds a SwiftPM dependency. */ - export const Dependency = "dependency"; - - /** This target only exists to provide compiler arguments for SourceKit-LSP can't be built standalone. - * - * For example, a SwiftPM package manifest is in a non-buildable target. **/ - export const NotBuildable = "not-buildable"; -} -``` - -`BuildTargetDataKind` can be `sourceKit` in which case `BuildTarget.data` contains the following data. - - -```ts -export interface SourceKitBuildTarget { - /** The toolchain that should be used to build this target. The URI should point to the directory that contains the - * `usr` directory. On macOS, this is typically a bundle ending in `.xctoolchain`. If the toolchain is installed to - * `/` on Linux, the toolchain URI would point to `/`. - * - * If no toolchain is given, SourceKit-LSP will pick a toolchain to use for this target. **/ - toolchain?: URI; -} -``` - -## `workspace/didChangeWatchedFiles` - -Notification sent from SourceKit-LSP to the build server to indicate that files within the project have been modified. - -SourceKit-LSP may send file change notifications for a superset of the files that the BSP server requested to watch in `watchers`. It is the BSP server’s responsibility to filter the file watch notifications for the ones it is actually interested in. - -Definition is the same as in LSP. - -## `workspace/waitForBuildSystemUpdates` - -This request is a no-op and doesn't have any effects. - -If the build server is currently updating the build graph, this request should return after those updates have finished processing. - -- method: `workspace/waitForBuildSystemUpdates` diff --git a/Contributor Documentation/Background Indexing.md b/Contributor Documentation/Background Indexing.md deleted file mode 100644 index 70fd43d80..000000000 --- a/Contributor Documentation/Background Indexing.md +++ /dev/null @@ -1,40 +0,0 @@ -# Background Indexing - -Background indexing is a collective term for two related but fundamentally independent operations that support cross-file and cross-module functionality within a project: -1. Module preparation of a target generates the Swift modules of all the dependent targets so that `import` statements can be resolved, eg. to produce diagnostics for a source file open in the editor. -2. Updating the index store for a source file essentially type checks the source file and writes information about symbol occurrences and their relations to the index store, which will be picked up by indexstore-db. Updating the index store for a source file requires its target to be prepared so that the import statements within can be resolved. - -A lot of semantic editor functionality doesn’t require an up-to-date index: For example, code completion operates exclusively on the source file’s AST and thus only requires the file’s target to be prepared but doesn’t use the index store. As a rule of thumb, all functionality that only looks up definitions (like jump-to-definition of a variable) can operate without the index, any functionality that looks for references to a symbol needs the index (like call hierarchy). - -## Target preparation - -Preparation of a target should perform the minimal amount of work to build all `.swiftmodule` files that the target transitively depends on, so that import statements can be resolved. Errors in dependent targets should not stop downstream modules from being built (this is different to a normal build in which the build is usually stopped if a dependent modules couldn’t be built). For SwiftPM, this is done by invoking `swift build` with the `--experimental-prepare-for-indexing` parameter. - -### Status tracking - -When SourceKit-LSP is launched, all targets are considered to be out-of-date. This needs to be done because source files might have changed since SourceKit-LSP was last started – if the module wasn’t modified since the last SourceKit-LSP launch, we re-prepare the target and rely on the build server to produce a fast null build. - -After we have prepared a target, we mark it as being up-to-date in `SemanticIndexManager.preparationUpToDateTracker`. That way we don’t need to invoke the build server every time we want to perform semantic functionality of a source file, which saves us the time of a null build (which can hundreds of milliseconds for SwiftPM). If a source file is changed (as noted through file watching), all of its target’s dependencies are marked as out-of-date. Note that the target that the source file belongs to is not marked as out-of-date – preparation of a target builds all dependencies but does not need to build the target’s module itself. The next operation that requires the target to be prepared will trigger a preparation job. - -## Updating the index store - -Updating the index store is done by invoking the compiler for the source file (either `swiftc` or `clang`) with flags that write index data about declared symbols and their occurrences (ie. locations that reference them) to the index store as unit and record files. These unit and record files are watched for by indexstore-db, which is a database on top of the raw index data that allows eg. efficient lookup of the record files that contain the reference to a symbol. - -### Status tracking - -Similarly to target preparation, when SourceKit-LSP is launched, the index of all source files is considered out-of-date and the initial index of the project is triggered, which indexes every file. - -When a source file should be indexed, we first check if a unit file for this source file already exists and whether the date of that unit file is later than the last modification date of the source file. If this is the case, we know that the source file hasn’t been modified since it was last indexed and thus, no work needs to be done – avoiding re-indexing the entire project if it is closed and re-opened. If no unit file exists or if the source file has been modified, the compiler is launched to update the file’s index. - -Once we know that a source file’s index is up-to-date, we mark it in `SemanticIndexManager.indexStoreUpToDateTracker`, similar to preparation status tracking. This way we don’t need to hit the file system to check if a source file has an up-to-date index. - -## Scheduling of index operations - -Target preparation and updating of the index store are guarded on two levels: When the request to prepare a target or update the index of a source file comes in, `SemanticIndexManager` performs a fast check to see if the target or source file is already known to be up-to-date. If so, we can immediately return without having to schedule a task in the `TaskScheduler`. - -If we don’t hit this fast path, a new task will be added to the `TaskScheduler` and all dependency tracking, scheduling considerations or in-progress up-to-date tracking is handed over to the task scheduler, the semantic index manager doesn’t need to worry about them. For example: -- The task scheduler will prevent two preparation tasks from running simultaneously because they would access the same build folder. -- The task scheduler will execute tasks with higher priority first. -- The task scheduler will not run two jobs that index the same file simultaneously. -- If two tasks to index the same file have been added to the task scheduler (eg. because the source file was modified twice before the first update job was scheduled), the task scheduler will execute the first index task first. When the second index task is executed, it checks whether the source file’s index is now up-to-date, which is the case here, so the second index operation is a no-op and doesn’t launch a compiler process. - diff --git a/Contributor Documentation/Debugging Memory Leaks.md b/Contributor Documentation/Debugging Memory Leaks.md deleted file mode 100644 index 58fea7298..000000000 --- a/Contributor Documentation/Debugging Memory Leaks.md +++ /dev/null @@ -1,21 +0,0 @@ -# Debugging Memory Leaks - -https://www.swift.org/documentation/server/guides/memory-leaks-and-usage.html is a good document with instructions on how to debug memory leaks in Swift. Below are some steps tailored to debug SourceKit-LSP. - -## macOS - -At any point during SourceKit-LSP’s execution, you can collect a memory graph using - -```bash -leaks --outputGraph=/tmp sourcekit-lsp -``` - -This memory graph can then be opened in Xcode to inspect which objects are alive at that point in time and which objects reference them (thus keeping them alive). - -## Linux - -[heaptrack](https://github.com/KDE/heaptrack) is a helpful tool to monitor memory allocations and leaks. To debug a memory leak on Linux you need to have SourceKit-LSP (and potentially sourcekitd) built from source, because heaptrack requires debug information for the binaries it inspects. To debug a memory issue on Linux. - -- Install heaptrack: `apt install heaptrack heaptrack-gui` -- Attach heaptrack to SourceKit-LSP: `heaptrack --record-only --pid $(pidof sourcekit-lsp)` -- Run the command that heaptrack suggests to analyze the file diff --git a/Contributor Documentation/Environment Variables.md b/Contributor Documentation/Environment Variables.md deleted file mode 100644 index b1d36259b..000000000 --- a/Contributor Documentation/Environment Variables.md +++ /dev/null @@ -1,21 +0,0 @@ -# Environment variables - -The following environment variables can be used to control some behavior in SourceKit-LSP - -## Build time - -- `SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER`: Use the `NonDarwinLogger` to log to stderr, even when building SourceKit-LSP on macOS. This is useful when running tests using `swift test` because it writes the log messages to stderr, which is displayed during the `swift test` invocation. -- `SOURCEKIT_LSP_CI_INSTALL`: Modifies rpaths in a way that’s necessary to build SourceKit-LSP to be included in a distributed toolchain. Should not be used locally. -- `SWIFTCI_USE_LOCAL_DEPS`: Assume that all of SourceKit-LSP’s dependencies are checked out next to it and use those instead of cloning the repositories. Primarily intended for CI environments that check out related branches. - -## Runtime - -- `SOURCEKIT_LSP_LOG_LEVEL`: When using `NonDarwinLogger`, specify the level at which messages should be logged. Defaults to `debug` in debug build and `default` in release builds. Primarily used to increase the log level when running tests from a release build in Swift CI. To adjust the logging on user devices, use the [Configuration file](../Documentation/Configuration%20File.md). -- `SOURCEKIT_LSP_LOG_PRIVACY_LEVEL`: When using `NonDarwinLogger`, specifies whether information that might contain sensitive information (essentially source code) should be logged. Defaults to `private` in debug build and `public` in release builds. Primarily used to log sensitive information when running tests from a release build in Swift CI. To adjust the logging on user devices, use the [Configuration file](../Documentation/Configuration%20File.md). - -## Testing -- `SKIP_LONG_TESTS`: Skip tests that typically take more than 1s to execute. -- `SOURCEKIT_LSP_KEEP_TEST_SCRATCH_DIR`: Does not delete the temporary files created during test execution. Allows inspection of the test projects after the test finishes. -- `SOURCEKIT_LSP_TEST_MODULE_CACHE`: Specifies where tests should store their shared module cache. Defaults to writing the module cache to a temporary directory. Intended so that CI systems can clean the module cache directory after running. -- `SOURCEKIT_LSP_TEST_TIMEOUT`: Override the timeout duration for tests, in seconds. -- `SOURCEKIT_LSP_TEST_PLUGIN_PATHS`: Load the SourceKit plugins from this path instead of relative to the package's build folder. If set to `RELATIVE_TO_SOURCEKITD`, the SourceKit plugin is found from the toolchain, relative to sourcekitd. diff --git a/Contributor Documentation/Files To Reindex.md b/Contributor Documentation/Files To Reindex.md deleted file mode 100644 index 27d682664..000000000 --- a/Contributor Documentation/Files To Reindex.md +++ /dev/null @@ -1,35 +0,0 @@ -# Which files to re-index when a file is modified - -## `.swift` - -Obviously affects the file itself, which we do re-index. - -If an internal declaration was changed, all files within the module might be also affected. If a public declaration was changed, all modules that depend on it might be affected. The effect can only really be in three ways: -1. It might change overload resolution in another file, which is fairly unlikely -2. A new declaration is introduced in this file that is already referenced by another file -3. A declaration is removed in this file that was referenced from other files. In those cases the other files now have an invalid reference. - -We decided to not re-index any files other than the file itself because naively re-indexing all files that might depend on the modified file requires too much processing power that will likely have no or very little effect on the index – we are trading accuracy for CPU time here. -We mark the targets of the changed file as well as any dependent targets as out-of-date. The assumption is that most likely the user will go back to any of the affected files shortly afterwards and modify them again. When that happens, the affected file will get re-indexed and bring the index back up to date. - -Alternatives would be: -- We could check the file’s interface hash and re-index other files based on whether it has changed. But at that point we are somewhat implementing a build system. And even if a new public method was introduced it’s very likely that the user hasn’t actually used it anywhere yet, which means that re-indexing all dependent modules would still be doing unnecessary work. -- To cover case (2) from above, we could re-index only dependencies that previously indexed with errors. This is an alternative that hasn’t been fully explored. - -## `.h` - -All files that include the header (including via other headers) might be affected by the change, similar to how all `.swift` files that import a module might be affected. Similar to modules, we choose to not re-index all files that include the header because of the same considerations mentioned above. - -To re-index the header, we pick one main file that includes the header and re-index that, which will effectively update the index for the header. For existing headers, we know which files import a header from the existing index. For new header files, we assume that it hasn’t been included in any file yet and thus don't index it. If the user wrote an include to the new header before creating the header itself, we don't know about that include from the existing index. But it’s likely that the user will modify the file that includes the new header file shortly after, which will index the header and establish the header to main file connection. - -## `.c` / `.cpp` / `.m` - -This is the easy case since only the file itself is affected. - -## Compiler settings (`compile_commands.json` / `Package.swift`) - -Ideally, we would like to consider a file as changed when its compile commands have changed, if they changed in a meaningful way (ie. in a way that would also trigger re-compilation in an incremental build). Non-meaningful changes would be: -- If compiler arguments that aren't order dependent are shuffled around. We could have a really quick check for compiler arguments equality by comparing them unordered. Any real compiler argument change will most likely do more than rearranging the arguments. -- The addition of a new Swift file to a target is equivalent to that file being modified and shouldn’t trigger a re-index of the entire target. - -At the moment, unit files don’t include information about the compiler arguments with which they were created, so it’s impossible to know whether the compiler arguments have changed when a project is opened. Thus, for now, we don’t re-index any files on compiler settings changing. diff --git a/Contributor Documentation/Implementing a BSP server.md b/Contributor Documentation/Implementing a BSP server.md deleted file mode 100644 index ed0b09827..000000000 --- a/Contributor Documentation/Implementing a BSP server.md +++ /dev/null @@ -1,46 +0,0 @@ -# Implementing a BSP server - -SourceKit-LSP can connect to any build system to provide semantic functionality through the [Build Server Protocol (BSP)](https://build-server-protocol.github.io). This is a short guide of the requests and notifications that a BSP server for SourceKit-LSP should implement. For more detailed information, refer to the [BSP spec](https://build-server-protocol.github.io/docs/specification) and the [SourceKit-LSP BSP Extensions](BSP%20Extensions.md). This document just references BSP methods and properties and those specification documents contain their documentation. - -## Required lifecycle methods - -In order to be launched and shut down successfully, the BSP server must implement the following methods - -- `build/initialize` -- `build/initialized` -- `build/shutdown` -- `build/exit` - -The `build/initialize` response must have `dataKind: "sourceKit"` and `data.sourceKitOptionsProvider: true`. In order to provide global code navigation features such as call hierarchy and global rename, the build server must set `data.indexDatabasePath` and `data.indexStorePath`. - -## Retrieving build settings - -In order to provide semantic functionality for source files, the BSP server must provide the following methods: - -- `workspace/buildTargets` -- `buildTarget/sources` -- `textDocument/sourceKitOptions` -- `buildTarget/didChange` -- `workspace/waitForBuildSystemUpdates` - -The `workspace/buildTargets`, `buildTarget/sources`, and `textDocument/sourceKitOption` requests should query the current state of the build server and return as quickly as possible to ensure smooth operation of SourceKit-LSP operations. Returning a response should not be blocked by expensive background computation. For example, if the BSP server receives a `workspace/buildTargets` request when it hasn’t computed a build graph yet, it is preferable that the build server returns an empty list of targets and sends a `buildTarget/didChange` notification when the build graph has been computed instead of waiting for build graph computation to finish before replying to the `workspace/buildTargets` request. - -If the build system does not have a notion of targets, eg. because it provides build settings from a file akin to a JSON compilation database, it may use a single dummy target for all source files or a separate target for each source file, either choice will work. - -If the build system loads the entire build graph during initialization, it may immediately return from `workspace/waitForBuildSystemUpdates`. - -## Supporting background indexing - -To support background indexing, the build server must set `data.prepareProvider: true` in the `build/initialize` response and implement the `buildTarget/prepare` method. The compiler options used to prepare a target should match those sent for `textDocument/sourceKitOptions` in order to avoid mismatches when loading modules. - -## Optional methods - -The following methods are not necessary to implement for SourceKit-LSP to work but might help with the implementation of the build server. - -- `build/logMessage` -- `build/taskStart`, `build/taskProgress`, and `build/taskFinish` -- `workspace/didChangeWatchedFiles` - -## Build server discovery - -To make your build server discoverable, create a [BSP connection specification](https://build-server-protocol.github.io/docs/overview/server-discovery) file named `buildServer.json` in the root of your project. diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md deleted file mode 100644 index d99af467b..000000000 --- a/Contributor Documentation/LSP Extensions.md +++ /dev/null @@ -1,765 +0,0 @@ -# LSP Extensions - -SourceKit-LSP extends the LSP protocol in the following ways. - -To enable some of these extensions, the client needs to communicate that it can support them. To do so, it should pass a dictionary for the `capabilities.experimental` field in the `initialize` request. For each capability to enable, it should pass an entry as follows. - -```json -"": { - "supported": true -} -``` - -## `PublishDiagnosticsClientCapabilities` - -Added field (this is an extension from clangd that SourceKit-LSP re-exposes): - -```ts -/** - * Requests that SourceKit-LSP send `Diagnostic.codeActions`. - */ -codeActionsInline?: bool -``` - -## `Diagnostic` - -Added field (this is an extension from clangd that SourceKit-LSP re-exposes): - -```ts -/** - * All the code actions that address this diagnostic. - */ -codeActions: CodeAction[]? -``` - -## `DiagnosticRelatedInformation` - -Added field (this is an extension from clangd that SourceKit-LSP re-exposes): - -```ts -/** - * All the code actions that address the parent diagnostic via this note. - */ -codeActions: CodeAction[]? -``` - -## Semantic token modifiers - -Added the following cases from clangd - -```ts -deduced = 'deduced' -virtual = 'virtual' -dependentName = 'dependentName' -usedAsMutableReference = 'usedAsMutableReference' -usedAsMutablePointer = 'usedAsMutablePointer' -constructorOrDestructor = 'constructorOrDestructor' -userDefined = 'userDefined' -functionScope = 'functionScope' -classScope = 'classScope' -fileScope = 'fileScope' -globalScope = 'globalScope' -``` - -## Semantic token types - -Added the following cases from clangd - -```ts -bracket = 'bracket' -label = 'label' -concept = 'concept' -unknown = 'unknown' -``` - -Added case - -```ts -/** - * An identifier that hasn't been further classified - */ -identifier = 'identifier' -``` - -## `textDocument/completion` - -Added field: - -```ts -/** - * Options to control code completion behavior in Swift - */ -sourcekitlspOptions?: SKCompletionOptions; -``` - -with - -```ts -interface SKCompletionOptions { - /** - * The maximum number of completion results to return, or `null` for unlimited. - */ - maxResults?: int; -} -``` - -## `textDocument/doccDocumentation` - -New request that generates documentation for a symbol at a given cursor location. - -Primarily designed to support live preview of Swift documentation in editors. - -This request looks up the nearest documentable symbol (if any) at a given cursor location within -a text document and returns a `DoccDocumentationResponse`. The response contains a string -representing single JSON encoded DocC RenderNode. This RenderNode can then be rendered in an -editor via https://github.com/swiftlang/swift-docc-render. - -The position may be omitted for documentation within DocC markdown and tutorial files as they -represent a single documentation page. It is only required for generating documentation within -Swift files as they usually contain multiple documentable symbols. - -Documentation can fail to be generated for a number of reasons. The most common of which being -that no documentable symbol could be found. In such cases the request will fail with a request -failed LSP error code (-32803) that contains a human-readable error message. This error message can -be displayed within the live preview editor to indicate that something has gone wrong. - -At the moment this request is only available on macOS and Linux. SourceKit-LSP will advertise -`textDocument/doccDocumentation` in its experimental server capabilities if it supports it. - -- params: `DoccDocumentationParams` -- result: `DoccDocumentationResponse` - -```ts -export interface DoccDocumentationParams { - /** - * The document to generate documentation for. - */ - textDocument: TextDocumentIdentifier; - - /** - * The cursor position within the document. (optional) - * - * This parameter is only used in Swift files to determine which symbol to render. - * The position is ignored for markdown and tutorial documents. - */ - position?: Position; -} - -export interface DoccDocumentationResponse { - /** - * The JSON encoded RenderNode that can be rendered by swift-docc-render. - */ - renderNode: string; -} -``` - -## `textDocument/symbolInfo` - -New request for semantic information about the symbol at a given location. - -This request looks up the symbol (if any) at a given text document location and returns -SymbolDetails for that location, including information such as the symbol's USR. The symbolInfo -request is not primarily designed for editors, but instead as an implementation detail of how -one LSP implementation (e.g. SourceKit) gets information from another (e.g. clangd) to use in -performing index queries or otherwise implementing the higher level requests such as definition. - -This request is an extension of the `textDocument/symbolInfo` request defined by clangd. - -- params: `SymbolInfoParams` -- result: `SymbolDetails[]` - - -```ts -export interface SymbolInfoParams { - /** - * The document in which to lookup the symbol location. - */ - textDocument: TextDocumentIdentifier; - - /** - * The document location at which to lookup symbol information. - */ - position: Position; -} - -interface ModuleInfo { - /** - * The name of the module in which the symbol is defined. - */ - moduleName: string; - - /** - * If the symbol is defined within a subgroup of a module, the name of the group. Otherwise `nil`. - */ - groupName?: string; -} - -interface SymbolDetails { - /** - * The name of the symbol, if any. - */ - name?: string; - - /** - * The name of the containing type for the symbol, if any. - * - * For example, in the following snippet, the `containerName` of `foo()` is `C`. - * - * ```c++ - * class C { - * void foo() {} - * } - */ - containerName?: string; - - /** - * The USR of the symbol, if any. - */ - usr?: string; - - /** - * Best known declaration or definition location without global knowledge. - * - * For a local or private variable, this is generally the canonical definition location - - * appropriate as a response to a `textDocument/definition` request. For global symbols this is - * the best known location within a single compilation unit. For example, in C++ this might be - * the declaration location from a header as opposed to the definition in some other - * translation unit. - */ - bestLocalDeclaration?: Location; - - /** - * The kind of the symbol - */ - kind?: SymbolKind; - - /** - * Whether the symbol is a dynamic call for which it isn't known which method will be invoked at runtime. This is - * the case for protocol methods and class functions. - * - * Optional because `clangd` does not return whether a symbol is dynamic. - */ - isDynamic?: bool; - - /** - * Whether this symbol is defined in the SDK or standard library. - * - * This property only applies to Swift symbols - */ - isSystem?: bool; - - /** - * If the symbol is dynamic, the USRs of the types that might be called. - * - * This is relevant in the following cases - * ```swift - * class A { - * func doThing() {} - * } - * class B: A {} - * class C: B { - * override func doThing() {} - * } - * class D: A { - * override func doThing() {} - * } - * func test(value: B) { - * value.doThing() - * } - * ``` - * - * The USR of the called function in `value.doThing` is `A.doThing` (or its - * mangled form) but it can never call `D.doThing`. In this case, the - * receiver USR would be `B`, indicating that only overrides of subtypes in - * `B` may be called dynamically. - */ - receiverUsrs?: string[]; - - /** - * If the symbol is defined in a module that doesn't have source information associated with it, the name and group - * and group name that defines this symbol. - * - * This property only applies to Swift symbols. - */ - systemModule?: ModuleInfo; -} -``` - -## `textDocument/tests` - -New request that returns symbols for all the test classes and test methods within a file. - -- params: `DocumentTestsParams` -- result: `TestItem[]` - -```ts -interface TestTag { - /** - * ID of the test tag. `TestTag` instances with the same ID are considered to be identical. - */ - id: string; -} - -/** - * A test item that can be shown an a client's test explorer or used to identify tests alongside a source file. - * - * A `TestItem` can represent either a test suite or a test itself, since they both have similar capabilities. - * - * This type matches the `TestItem` type in Visual Studio Code to a fair degree. - */ -interface TestItem { - /** - * Identifier for the `TestItem`. - * - * This identifier uniquely identifies the test case or test suite. It can be used to run an individual test (suite). - */ - id: string; - - /** - * Display name describing the test. - */ - label: string; - - /** - * Optional description that appears next to the label. - */ - description?: string; - - /** - * A string that should be used when comparing this item with other items. - * - * When `nil` the `label` is used. - */ - sortText?: string; - - /** - * Whether the test is disabled. - */ - disabled: bool; - - /** - * The type of test, eg. the testing framework that was used to declare the test. - */ - style: string; - - /** - * The location of the test item in the source code. - */ - location: Location; - - /** - * The children of this test item. - * - * For a test suite, this may contain the individual test cases or nested suites. - */ - children: TestItem[]; - - /** - * Tags associated with this test item. - */ - tags: TestTag[]; -} - -export interface DocumentTestsParams { - /** - * The text document. - */ - textDocument: TextDocumentIdentifier; -} -``` - -## `sourceKit/_isIndexing` - -Request from the client to the server querying whether SourceKit-LSP is currently performing an background indexing tasks, including target preparation. - -> [!IMPORTANT] -> This request is experimental and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - -- params: `IsIndexingParams` -- result: `IsIndexingResult` - -```ts -export interface IsIndexingParams {} - -export interface IsIndexingResult { - /** - * Whether SourceKit-LSP is currently performing an indexing task. - */ - indexing: boolean; -} -``` - -## `window/didChangeActiveDocument` - -New notification from the client to the server, telling SourceKit-LSP which document is the currently active primary document. - -This notification should only be called for documents that the editor has opened in SourceKit-LSP using the `textDocument/didOpen` notification. - -By default, SourceKit-LSP infers the currently active editor document from the last document that received a request. -If the client supports active reporting of the currently active document, it should check for the -`window/didChangeActiveDocument` experimental server capability. If that capability is present, it should respond with -the `window/didChangeActiveDocument` experimental client capability and send this notification whenever the currently -active document changes. - -- params: `DidChangeActiveDocumentParams` - -```ts -export interface DidChangeActiveDocumentParams { - /** - * The document that is being displayed in the active editor or `null` to indicate that either no document is active - * or that the currently open document is not handled by SourceKit-LSP. - */ - textDocument?: TextDocumentIdentifier; -} -``` - -## `window/logMessage` - -Added fields: - -```ts -/** - * Asks the client to log the message to a log with this name, to organize log messages from different aspects (eg. general logging vs. logs from background indexing). - * - * Clients may ignore this parameter and add the message to the global log - */ -logName?: string; - - -/** - * If specified, allows grouping log messages that belong to the same originating task together instead of logging - * them in chronological order in which they were produced. - * - * LSP Extension guarded by the experimental `structured-logs` feature. - */ -structure?: StructuredLogBegin | StructuredLogReport | StructuredLogEnd; -``` - -With - -```ts -/** - * Indicates the beginning of a new task that may receive updates with `StructuredLogReport` or `StructuredLogEnd` - * payloads. - */ -export interface StructuredLogBegin { - kind: 'begin'; - - /** - * A succinct title that can be used to describe the task that started this structured. - */ - title: string; - - /** - * A unique identifier, identifying the task this structured log message belongs to. - */ - taskID: string; -} - - -/** - * Adds a new log message to a structured log without ending it. - */ -export interface StructuredLogReport { - kind: 'report'; -} - -/** - * Ends a structured log. No more `StructuredLogReport` updates should be sent for this task ID. - * - * The task ID may be re-used for new structured logs by beginning a new structured log for that task. - */ -export interface StructuredLogEnd { - kind: 'end'; -} -``` - -## `workspace/_setOptions` - -New request to modify runtime options of SourceKit-LSP. - -Any options not specified in this request will be left as-is. - -> [!IMPORTANT] -> This request is experimental, guarded behind the `set-options-request` experimental feature, and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - -- params: `SetOptionsParams` -- result: `void` - -```ts -export interface SetOptionsParams { - /** - * `true` to pause background indexing or `false` to resume background indexing. - */ - backgroundIndexingPaused?: bool; -} -``` - -## `workspace/_sourceKitOptions` - -New request from the client to the server to retrieve the compiler arguments that SourceKit-LSP uses to process the document. - -This request does not require the document to be opened in SourceKit-LSP. This is also why it has the `workspace/` instead of the `textDocument/` prefix. - -> [!IMPORTANT] -> This request is experimental, guarded behind the `sourcekit-options-request` experimental feature, and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - - -- params: `SourceKitOptionsRequest` -- result: `SourceKitOptionsResult` - -```ts -export interface SourceKitOptionsRequest { - /** - * The document to get options for - */ - textDocument: TextDocumentIdentifier; - - /** - * If specified, explicitly request the compiler arguments when interpreting the document in the context of the given - * target. - * - * The target URI must match the URI that is used by the BSP server to identify the target. This option thus only - * makes sense to specify if the client also controls the BSP server. - * - * When this is `null`, SourceKit-LSP returns the compiler arguments it uses when the the document is opened in the - * client, ie. it infers a canonical target for the document. - */ - target?: DocumentURI; - - /** - * Whether SourceKit-LSP should ensure that the document's target is prepared before returning build settings. - * - * There is a tradeoff whether the target should be prepared: Preparing a target may take significant time but if the - * target is not prepared, the build settings might eg. refer to modules that haven't been built yet. - */ - prepareTarget: bool; - - /** - * If set to `true` and build settings could not be determined within a timeout (see `buildSettingsTimeout` in the - * SourceKit-LSP configuration file), this request returns fallback build settings. - * - * If set to `true` the request only finishes when build settings were provided by the build server. - */ - allowFallbackSettings: bool -} - -/** - * The kind of options that were returned by the `workspace/_sourceKitOptions` request, ie. whether they are fallback - * options or the real compiler options for the file. - */ -export namespace SourceKitOptionsKind { - /** - * The SourceKit options are known to SourceKit-LSP and returned them. - */ - export const normal = "normal" - - /** - * SourceKit-LSP was unable to determine the build settings for this file and synthesized fallback settings. - */ - export const fallback = "fallback" -} - -export interface SourceKitOptionsResult { - /** - * The compiler options required for the requested file. - */ - compilerArguments: string[]; - - /** - * The working directory for the compile command. - */ - workingDirectory?: string; - - /** - * Whether SourceKit-LSP was able to determine the build settings or synthesized fallback settings. - */ - kind: SourceKitOptionsKind; - - /** - * - `true` If the request requested the file's target to be prepared and the target needed preparing - * - `false` If the request requested the file's target to be prepared and the target was up to date - * - `nil`: If the request did not request the file's target to be prepared or the target could not be prepared for - * other reasons - */ - didPrepareTarget?: bool - - /** - * Additional data that the BSP server returned in the `textDocument/sourceKitOptions` BSP request. This data is not - * interpreted by SourceKit-LSP. - */ - data?: LSPAny -} -``` - -## `workspace/_outputPaths` - -New request from the client to the server to retrieve the output paths of a target (see the `buildTarget/outputPaths` BSP request). - -This request will only succeed if the build server supports the `buildTarget/outputPaths` request. - -> [!IMPORTANT] -> This request is experimental, guarded behind the `output-paths-request` experimental feature, and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - - -- params: `OutputPathsRequest` -- result: `OutputPathsResult` - -```ts -export interface OutputPathsRequest { - /** - * The target whose output file paths to get. - */ - target: DocumentURI; - - /** - * The URI of the workspace to which the target belongs. - */ - workspace: DocumentURI; -} - -export interface OutputPathsResult { - /** - * The output paths for all source files in the target - */ - outputPaths: string[]; -} -``` - -## `workspace/getReferenceDocument` - -Request from the client to the server asking for contents of a URI having a custom scheme. -For example: "sourcekit-lsp:" - -Enable the experimental client capability `"workspace/getReferenceDocument"` so that the server responds with reference document URLs for certain requests or commands whenever possible. - -- params: `GetReferenceDocumentParams` - -- result: `GetReferenceDocumentResponse` - -```ts -export interface GetReferenceDocumentParams { - /** - * The `DocumentUri` of the custom scheme url for which content is required - */ - uri: DocumentUri; -} - -/** - * Response containing `content` of `GetReferenceDocumentRequest` - */ -export interface GetReferenceDocumentResult { - content: string; -} -``` - -## `workspace/peekDocuments` - -Request from the server to the client to show the given documents in a "peeked" editor. - -This request is handled by the client to show the given documents in a "peeked" editor (i.e. inline with / inside the editor canvas). - -It requires the experimental client capability `"workspace/peekDocuments"` to use. - -- params: `PeekDocumentsParams` -- result: `PeekDocumentsResult` - -```ts -export interface PeekDocumentsParams { - /** - * The `DocumentUri` of the text document in which to show the "peeked" editor - */ - uri: DocumentUri; - - /** - * The `Position` in the given text document in which to show the "peeked editor" - */ - position: Position; - - /** - * An array `DocumentUri` of the documents to appear inside the "peeked" editor - */ - locations: DocumentUri[]; -} - -/** - * Response to indicate the `success` of the `PeekDocumentsRequest` - */ -export interface PeekDocumentsResult { - success: boolean; -} -``` - -## `workspace/synchronize` - -Request from the client to the server to wait for SourceKit-LSP to handle all ongoing requests and, optionally, wait for background activity to finish. - -This method is intended to be used in automated environments which need to wait for background activity to finish before executing requests that rely on that background activity to finish. Examples of such cases are: - - Automated tests that need to wait for background indexing to finish and then checking the result of request results - - Automated tests that need to wait for requests like file changes to be handled and checking behavior after those have been processed - - Code analysis tools that want to use SourceKit-LSP to gather information about the project but can only do so after the index has been loaded - -Because this request waits for all other SourceKit-LSP requests to finish, it limits parallel request handling and is ill-suited for any kind of interactive environment. In those environments, it is preferable to quickly give the user a result based on the data that is available and (let the user) re-perform the action if the underlying index data has changed. - -- params: `SynchronizeParams` -- result: `void` - -```ts -export interface SynchronizeParams { - /** - * Wait for the build server to have an up-to-date build graph by sending a `workspace/waitForBuildSystemUpdates` to - * it. - * - * This is implied by `index = true`. - * - * This option is experimental, guarded behind the `synchronize-for-build-system-updates` experimental feature, and - * may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - */ - buildServerUpdates?: bool; - - /** - * Wait for the build server to update its internal mapping of copied files to their original location. - * - * This option is experimental, guarded behind the `synchronize-copy-file-map` experimental feature, and may be - * modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. - */ - copyFileMap?: bool; - - /** - * Wait for background indexing to finish and all index unit files to be loaded into indexstore-db. - */ - index?: bool; -} -``` - -## `workspace/tests` - -New request that returns symbols for all the test classes and test methods within the current workspace. - -- params: `WorkspaceTestsParams` -- result: `TestItem[]` - -```ts -export interface WorkspaceTestsParams {} -``` - -## `workspace/triggerReindex` - -New request to re-index all files open in the SourceKit-LSP server. - -Users should not need to rely on this request. The index should always be updated automatically in the background. Having to invoke this request means there is a bug in SourceKit-LSP's automatic re-indexing. It does, however, offer a workaround to re-index files when such a bug occurs where otherwise there would be no workaround. - - -- params: `TriggerReindexParams` -- result: `void` - -```ts -export interface TriggerReindexParams {} -``` - -## Languages - -Added a new language with the identifier `tutorial` to support the `*.tutorial` files that -Swift DocC uses to define tutorials and tutorial overviews in its documentation catalogs. -It is expected that editors send document events for `tutorial` and `markdown` files if -they wish to request information about these files from SourceKit-LSP. diff --git a/Contributor Documentation/Logging.md b/Contributor Documentation/Logging.md deleted file mode 100644 index 5b1c6229d..000000000 --- a/Contributor Documentation/Logging.md +++ /dev/null @@ -1,67 +0,0 @@ -# Logging - -The following is a guide of how to write log messages and which level they should be logged at. There may be reasons to deviate from these guides but they should be a good starting point. - -## Log Levels - -The following log levels should be used by log messages in default. The [Explore logging in Swift](https://developer.apple.com/wwdc20/10168?time=604) session from WWDC20 has some explanation of how these levels are persisted by OS log and we follow those guidelines. - -### Fault - -> From Explore Logging in Swift: _Bug in program_ - -A bug in SourceKit-LSP, sourcekitd or any other project included in the Swift toolchain that should never happen and is not due to malformed user input. The fix for faults should always be in a project controlled by the Swift toolchain (most likely in SourceKit-LSP itself) and we should never close them as a third party to resolve. Think of these as light assertions that don’t crash sourcekit-lsp because it is able to recover in some way. - -Examples: -- Some global state invariant is broken like a file to `startProgress` not being followed by `endProgress` -- sourcekitd crashes -- Two targets in SwiftPM have the same name - -### Error - -> From Explore Logging in Swift: _Error seen during execution_ - -An error that is due to user input or eg. stale state of files on disk. It indicates that something is going wrong which might explain unexpected behavior. Errors could be due to malformed user input such as invalid requests from the editor or due to stale state that will eventually converge again. - -Examples: -- The client sends an invalid request -- Preparation of a file failed due to a syntax error in the user’s code -- The index contains a reference to a source file but the source fail has been modified since the index was last updated and is thus no longer valid - -## Log/Notice/Default - -`logger.default` logs at the `default` aka `notice` level. - -> From Explore Logging in Swift: _Essential for troubleshooting_ - -Essential state transitions during SourceKit-LSP’s execution that allow use to determine what interactions the user performed. These logs will most likely be included in diagnose bundles from `sourcekit-lsp` diagnose and should help us solve most problems. - -Examples: -- The user sends an LSP request -- Indexing of a file starts or finishes -- New build settings for a file have been computed -- Responses from sourcekitd - -## Info - -> From Explore Logging in Swift: _Helpful but not essential for troubleshooting_ (not persisted, logged to memory) - -Internal state transitions that are helpful. If eg. a request fails and it’s not immediately obvious at which it failed, these should help us narrow down the code that it failed in. These messages might be missing from the logs generated by `sourcekit-lsp diagnose` and should not generally be needed to fix issues - -Examples: -- Requests sent to `sourcekitd` or `clangd` -- Logging the main file for a header file - -## Debug - -> From Explore Logging in Swift: _Useful only during debugging_ (only logged during debugging) - -Log messages that are useful eg. when debugging a test failure but that is not needed for diagnosing most real-world issues, like detailed information about when a function starts executing to diagnose race conditions. - -Examples: -- Tasks start and finish executing in `TaskScheduler` - -## Log messages - -Log messages should resemble English sentences and start with an uppercase letter. If the log is a single sentence it should not have a period at its end. - diff --git a/Contributor Documentation/Modules.md b/Contributor Documentation/Modules.md deleted file mode 100644 index ae454ba2f..000000000 --- a/Contributor Documentation/Modules.md +++ /dev/null @@ -1,90 +0,0 @@ -# Modules - -The SourceKit-LSP package contains the following non-testing modules. - -### BuildServerProtocol - -Swift types to represent the [Build Server Protocol (BSP) specification](https://build-server-protocol.github.io/docs/specification). These types should also be usable when implementing a BSP client and thus this module should not have any dependencies other than the LanguageServerProtocol module, with which it shares some types. - -### BuildServerIntegration - -Defines the queries SourceKit-LSP can ask of a build server, like getting compiler arguments for a file, finding a target’s dependencies or preparing a target. - -### CAtomics - -Implementation of atomics for Swift using C. Once we can raise our deployment target to use the `Atomic` type from the Swift standard library, this module should be removed. - -### CSKTestSupport - -For testing, overrides `__cxa_atexit` to prevent registration of static destructors due to work around https://github.com/swiftlang/swift/issues/55112. - - -### Csourcekitd - -Header file defining the interface to sourcekitd. This should stay in sync with [sourcekitd.h](https://github.com/swiftlang/swift/blob/main/tools/SourceKit/tools/sourcekitd/include/sourcekitd/sourcekitd.h) in the Swift repository. - -### Diagnose - -A collection of subcommands to the `sourcekit-lsp` executable that help SourceKit-LSP diagnose issues. - -### InProcessClient - -A simple type that allows launching a SourceKit-LSP server in-process, communicating in terms of structs from the `LanguageServerProtocol` module. - -This should be the dedicated entry point for clients that want to run SourceKit-LSP in-process instead of launching a SourceKit-LSP server out-of-process and communicating with it using JSON RPC. - -### LanguageServerProtocol - -Swift types to represent the [Language Server Protocol (LSP) specification, version 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/). These types should also be usable when implementing an LSP client and thus this module should not have any dependencies. - -### LanguageServerProtocolExtensions - -Extensions on top of `LanguageServerProtocol` and `LanguageServerProtocolJSONRPC` that might require other modules defined in sourcekit-lsp. - -### LanguageServerProtocolJSONRPC - -A connection to or from a SourceKit-LSP server. Since message parsing can fail, it needs to handle errors in some way and the design decision here is to use SKLogging, which hardcodes `org.swift.sourcekit-lsp` as the default logging subsystem and thus makes the module unsuitable for generic clients. - -### SemanticIndex - -Contains the interface with which SourceKit-LSP queries the semantic index, adding up-to-date checks on top of the indexstore-db API. Also implements the types that manage background indexing. - -### SKLogging - -Types that are API-compatible with OSLog that allow logging to OSLog when building for Apple platforms and logging to stderr or files on non-Apple platforms. This should not be dependent on any LSP specific types and be portable to other packages. - -### SKUtilities - -Types that should be sharable by the different modules that implement SourceKit-LSP but that are not generic enough to fit into `SwiftExtensions` or that need to depend on `SKLogging` and thus can’t live in `SwiftExtensions`. - -### SKOptions - -Configuration options to change how SourceKit-LSP behaves, based on [Configuration files](../Documentation/Configuration%20File.md). - -### SKTestSupport - -A collection of utilities useful for writing tests for SourceKit-LSP and which are not specific to a single test module. - -### sourcekit-lsp - -This executable target that produces the `sourcekit-lsp` binary. - -### SourceKitD - -A Swift interface to talk to sourcekitd. - -### SourceKitLSP - -This is the core module that implements the SourceKit-LSP server. - -### SwiftExtensions - -Extensions to the Swift standard library and Foundation. Should not have any other dependencies. Any types in here should theoretically make senses to put in the Swift standard library or Foundation and they shouldn't be specific to SourceKit-LSP - -#### ToolchainRegistry - -Discovers Swift toolchains on the system. - -### TSCExtensions - -Extensions on top of `swift-tools-support-core` that might integrate with modules from sourcekit-lsp. diff --git a/Contributor Documentation/Overview.md b/Contributor Documentation/Overview.md deleted file mode 100644 index 81aeea182..000000000 --- a/Contributor Documentation/Overview.md +++ /dev/null @@ -1,34 +0,0 @@ -# Design Overview - -This document gives a general overview of the major design concepts in SourceKit-LSP and how components fit together. Detailed doc comments can usually be found on the individual types. - -## Message handling - -LSP Messages (ie. LSP requests or notifications) sent to SourceKit-LSP via stdin are handled using `JSONRPCConnection`, which decodes the JSON in the message to a type from `LanguageServerProtocol`. We need to guarantee that LSP messages are generally handled in-order. Re-ordering eg. `textDocument/didChange` notifications would be a major issue since it results in out-of-sync document contents. Since Swift concurrency does not make any ordering guarantees (Tasks and async calls can be executed in any order), the `Connection` types are serial and don’t use Swift Concurrency features. - -After the message has been decoded, the `SourceKitLSPServer` handles it on its `messageHandlingQueue`. `SourceKitLSPServer` is capable of interpreting the semantic meaning of requests and is able to decide whether requests can be executed in parallel, modelled by `MessageHandlingDependencyTracker`. For example, edits to different documents don’t have any dependencies and can be handled concurrently or out-of-order. Similarly requests to the same document that aren’t modifying it can be handled concurrently. - -## Language services - -To provide language-specific functionality `SourceKitLSPServer` delegates to the types that implement `LanguageService`. This is either `SwiftLanguageService`, which implements language functionality for Swift files based on sourcekitd and swift-syntax, or `ClangLanguageService`, which launches a `clangd` process to provide functionality for C, C++, Objective-C and Objective-C++. - -For requests that require knowledge about the project’s index, like call hierarchy or rename of global symbols, `SourceKitLSPServer` enriches the information returned from the language service with information from the [index](http://github.com/swiftlang/indexstore-db/). - -### sourcekitd - -[sourcekitd](https://github.com/apple/swift/tree/main/tools/SourceKit) is implemented in the Swift compiler’s repository and uses the Swift compiler’s understanding of Swift code to provide semantic functionality. On macOS, sourcekitd is run as an XPC service and all other platforms sourcekitd is run in the sourcekit-lsp process. This means that on macOS, a crash of sourcekitd can be recovered by re-launching sourcekitd, while on other platforms a crash inside sourcekitd crashes sourcekit-lsp. - -## Build servers - -Basically all semantic functionality requires knowledge about how a file is being built (like module search paths to be able to resolve `import` statements). SourceKit-LSP consults a *build server* to get this information. Build servers that are currently supported are: -- `SwiftPMBuildServer`: Reads a `Package.swift` at the project’s root -- `CompilationDatabaseBuildServer`: Reads a `compile_commands.json` file to provide build settings for files. `compile_commands.json` can eg. be generated by CMake. -- `ExternalBuildServerAdapter`: Launches an external BSP server process to provide build settings. - -## Logging - -SourceKit-LSP has fairly extensive logging to help diagnose issues. The way logging works depends on the platform that SourceKit-LSP is running on. - -On macOS, SourceKit-LSP logs to the system log. [CONTRIBUTING.md](../CONTRIBUTING.md#logging) contains some information about how to read the system logs. Since [OSLog](https://developer.apple.com/documentation/os/logging) cannot be wrapped, the decision to log to macOS’s system log is done at build time and cannot be modified at runtime. - -On other platforms, the `NonDarwinLogger` types are used to log messages. These types are API-compatible with OSLog. Log messages are written to stderr by default. When possible, `SourceKitLSP.run` will redirect the log messages to log files in `~/.sourcekit-lsp` on launch. diff --git a/Contributor Documentation/README.md b/Contributor Documentation/README.md deleted file mode 100644 index c3601564a..000000000 --- a/Contributor Documentation/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Developer Documentation - -The following documentation documents are primarily intended for developers of SourceKit-LSP. - -- [Background Indexing](Background%20Indexing.md) -- [BSP Extension](BSP%20Extensions.md) -- [Debugging Memory Leaks](Debugging%20Memory%20Leaks.md) -- [Environment Variables](Environment%20Variables.md) -- [Files To Reindex](Files%20To%20Reindex.md) -- [Implementing a BSP server](Implementing%20a%20BSP%20server.md) -- [Logging](Logging.md) -- [LSP Extensions](LSP%20Extensions.md) -- [Modules](Modules.md) -- [Overview](Overview.md) -- [Swift Version](Swift%20Version.md) -- [Testing](Testing.md) diff --git a/Contributor Documentation/Swift Version.md b/Contributor Documentation/Swift Version.md deleted file mode 100644 index dbca987b8..000000000 --- a/Contributor Documentation/Swift Version.md +++ /dev/null @@ -1,3 +0,0 @@ -# Swift Version - -SourceKit-LSP currently needs to be able to build and pass tests using a Swift 5.10 compiler. \ No newline at end of file diff --git a/Contributor Documentation/Testing.md b/Contributor Documentation/Testing.md deleted file mode 100644 index 3742f4cde..000000000 --- a/Contributor Documentation/Testing.md +++ /dev/null @@ -1,19 +0,0 @@ -# Testing - -Most tests in SourceKit-LSP are integration tests that create a `SourceKitLSPServer` instance in-process, initialize it and send messages to it. The test support modules essentially define four ways of creating test projects. - -### `TestSourceKitLSPClient` - -Launches a `SourceKitLSPServer` in-process. Documents can be opened within it but these documents don't have any representation on the file system. `TestSourceKitLSPClient` has the lowest overhead and is the basis for all the other test projects. Because there are no files on disk, this type cannot test anything that requires cross-file functionality or exercise requests that require an index. - -### `IndexedSingleSwiftFileTestProject` - -Creates a single `.swift` file on disk, indexes it and then opens it using a `TestSourceKitLSPClient`. This is the best choice for tests that require an index but don’t need to exercise any cross-file functionality. - -### `SwiftPMTestProject` - -Creates a SwiftPM project on disk that allows testing of cross-file and cross-module functionality. By default the `SwiftPMTestProject` does not build an index or build any Swift modules, which is often sufficient when testing cross-file functionality within a single module. When cross-module functionality or an index is needed, background indexing can be enabled using `enableBackgroundIndexing: true`, which waits for background indexing to finish before allowing any requests. - -## `MultiFileTestProject` - -This is the most flexible test type that writes arbitrary files to disk. It provides less functionality out-of-the-box but is capable of eg. representing workspaces with multiple SwiftPM projects or projects that have `compile_commands.json`. diff --git a/Documentation/Configuration File.md b/Documentation/Configuration File.md deleted file mode 100644 index 6ca95bac6..000000000 --- a/Documentation/Configuration File.md +++ /dev/null @@ -1,62 +0,0 @@ - - -# Configuration File - -`.sourcekit-lsp/config.json` configuration files can be used to modify the behavior of SourceKit-LSP in various ways. The following locations are checked. Settings in later configuration files override settings in earlier configuration files -- `~/.sourcekit-lsp/config.json` -- On macOS: `~/Library/Application Support/org.swift.sourcekit-lsp/config.json` from the various `Library` folders on the system -- If the `XDG_CONFIG_HOME` environment variable is set: `$XDG_CONFIG_HOME/sourcekit-lsp/config.json` -- Initialization options passed in the initialize request -- A `.sourcekit-lsp/config.json` file in a workspace’s root - -The structure of the file is currently not guaranteed to be stable. Options may be removed or renamed. - -## Structure - -`config.json` is a JSON file with the following structure. All keys are optional and unknown keys are ignored. -- `swiftPM`: Options for SwiftPM workspaces. - - `configuration: "debug"|"release"`: The configuration to build the project for during background indexing and the configuration whose build folder should be used for Swift modules if background indexing is disabled. Equivalent to SwiftPM's `--configuration` option. - - `scratchPath: string`: Build artifacts directory path. If nil, the build system may choose a default value. This path can be specified as a relative path, which will be interpreted relative to the project root. Equivalent to SwiftPM's `--scratch-path` option. - - `swiftSDKsDirectory: string`: Equivalent to SwiftPM's `--swift-sdks-path` option. - - `swiftSDK: string`: Equivalent to SwiftPM's `--swift-sdk` option. - - `triple: string`: Equivalent to SwiftPM's `--triple` option. - - `toolsets: string[]`: Equivalent to SwiftPM's `--toolset` option. - - `traits: string[]`: Traits to enable for the package. Equivalent to SwiftPM's `--traits` option. - - `cCompilerFlags: string[]`: Extra arguments passed to the compiler for C files. Equivalent to SwiftPM's `-Xcc` option. - - `cxxCompilerFlags: string[]`: Extra arguments passed to the compiler for C++ files. Equivalent to SwiftPM's `-Xcxx` option. - - `swiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files. Equivalent to SwiftPM's `-Xswiftc` option. - - `linkerFlags: string[]`: Extra arguments passed to the linker. Equivalent to SwiftPM's `-Xlinker` option. - - `buildToolsSwiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files or plugins. Equivalent to SwiftPM's `-Xbuild-tools-swiftc` option. - - `disableSandbox: boolean`: Disables running subprocesses from SwiftPM in a sandbox. Equivalent to SwiftPM's `--disable-sandbox` option. Useful when running `sourcekit-lsp` in a sandbox because nested sandboxes are not supported. -- `compilationDatabase`: Dictionary with the following keys, defining options for workspaces with a compilation database. - - `searchPaths: string[]`: Additional paths to search for a compilation database, relative to a workspace root. -- `fallbackBuildSystem`: Dictionary with the following keys, defining options for files that aren't managed by any build server. - - `cCompilerFlags: string[]`: Extra arguments passed to the compiler for C files. - - `cxxCompilerFlags: string[]`: Extra arguments passed to the compiler for C++ files. - - `swiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files. - - `sdk: string`: The SDK to use for fallback arguments. Default is to infer the SDK using `xcrun`. -- `buildSettingsTimeout: integer`: Number of milliseconds to wait for build settings from the build server before using fallback build settings. -- `clangdOptions: string[]`: Extra command line arguments passed to `clangd` when launching it. -- `index`: Options related to indexing. - - `indexPrefixMap: [string: string]`: Path remappings for remapping index data for local use. - - `updateIndexStoreTimeout: integer`: Number of seconds to wait for an update index store task to finish before terminating it. -- `logging`: Options related to logging, changing SourceKit-LSP’s logging behavior on non-Apple platforms. On Apple platforms, logging is done through the [system log](Diagnose%20Bundle.md#Enable%20Extended%20Logging). These options can only be set globally and not per workspace. - - `level: "debug"|"info"|"default"|"error"|"fault"`: The level from which one onwards log messages should be written. - - `privacyLevel: "public"|"private"|"sensitive"`: Whether potentially sensitive information should be redacted. Default is `public`, which redacts potentially sensitive information. - - `inputMirrorDirectory: string`: Write all input received by SourceKit-LSP on stdin to a file in this directory. Useful to record and replay an entire SourceKit-LSP session. - - `outputMirrorDirectory: string`: Write all data sent from SourceKit-LSP to the client to a file in this directory. Useful to record the raw communication between SourceKit-LSP and the client on a low level. -- `sourcekitd`: Options modifying the behavior of sourcekitd. -- `defaultWorkspaceType: "buildServer"|"compilationDatabase"|"swiftPM"`: Default workspace type. Overrides workspace type selection logic. -- `generatedFilesPath: string`: Directory in which generated interfaces and macro expansions should be stored. -- `backgroundIndexing: boolean`: Whether background indexing is enabled. -- `backgroundPreparationMode: "build"|"noLazy"|"enabled"`: Determines how background indexing should prepare a target. - - `build`: Build a target to prepare it. - - `noLazy`: Prepare a target without generating object files but do not do lazy type checking and function body skipping. This uses SwiftPM's `--experimental-prepare-for-indexing-no-lazy` flag. - - `enabled`: Prepare a target without generating object files. -- `cancelTextDocumentRequestsOnEditAndClose: boolean`: Whether sending a `textDocument/didChange` or `textDocument/didClose` notification for a document should cancel all pending requests for that document. -- `experimentalFeatures: ("on-type-formatting"|"structured-logs")[]`: Experimental features that are enabled. -- `swiftPublishDiagnosticsDebounceDuration: number`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`. -- `workDoneProgressDebounceDuration: number`: When a task is started that should be displayed to the client as a work done progress, how many milliseconds to wait before actually starting the work done progress. This prevents flickering of the work done progress in the client for short-lived index tasks which end within this duration. -- `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd. -- `semanticServiceRestartTimeout: number`: If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it. -- `buildServerWorkspaceRequestsTimeout: number`: Duration how long to wait for responses to `workspace/buildTargets` or `buildTarget/sources` request by the build server before defaulting to an empty response. diff --git a/Documentation/Diagnose Bundle.md b/Documentation/Diagnose Bundle.md deleted file mode 100644 index 60a2a06bc..000000000 --- a/Documentation/Diagnose Bundle.md +++ /dev/null @@ -1,19 +0,0 @@ -# Diagnose Bundle - -A diagnose bundle is designed to help SourceKit-LSP developers diagnose and fix reported issues. -You can generate a diagnose bundle with: -```sh -sourcekit-lsp diagnose -``` - -And then attach the resulting `sourcekit-lsp-diagnose-*` bundle to any bug reports. - -You may want to inspect the bundle to determine whether you're willing to share the collected information. At a high level they contain: -- Crash logs from SourceKit - - From Xcode toolchains, just a stack trace. - - For assert compilers (ie. nightly toolchains) also sometimes some source code that was currently compiled to cause the crash. -- Log messages emitted by SourceKit - - We mark all information that may contain private information (source code, file names, …) as private by default, so all of that will be redacted. Private logging can be enabled for SourceKit-LSP as described in [Enable Extended Logging](Enable%20Extended%20Logging.md). On macOS these extended log messages are also included in a sysdiagnose. -- Versions of Swift installed on your system -- If possible, a minimized project that caused SourceKit to crash -- If possible, a minimized project that caused the Swift compiler to crash diff --git a/Documentation/Editor Integration.md b/Documentation/Editor Integration.md deleted file mode 100644 index 24a358415..000000000 --- a/Documentation/Editor Integration.md +++ /dev/null @@ -1,65 +0,0 @@ -# Editor Integration - -Most modern text editors support the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP) and many have support for Swift through SourceKit-LSP. https://www.swift.org/tools has a list of some popular editors and how to set them up. This page covers any editors not listed there. - - - -## BBEdit - -Support for LSP is built in to BBEdit 14.0 and later. - -If `sourcekit-lsp` is in your `$PATH` or is discoverable by using `xcrun --find sourcekit-lsp`, BBEdit will use it automatically. Otherwise you can manually configure BBEdit to use a suitable `sourcekit-lsp` as needed. - -You can read more about BBEdit's LSP support and configuration hints [here](https://www.barebones.com/support/bbedit/lsp-notes.html). - -## Nova - -You can use SourceKit-LSP with Nova by using the [Icarus](http://panic.com/open-in-nova/extension?id=panic.Icarus) extension. - -By default, Icarus will try to discover `sourcekit-lsp` automatically (using `xcrun --find sourcekit-lsp`), but can be configured to look at an installed Swift toolchain package (using `xcrun --toolchain swift --find sourcekit-lsp`) or custom path. To do so, open the extension settings from Nova's Extensions menu: Extension Library… -> Icarus -> Settings -> Toolchain. - -The Icarus source is located within [its repository](https://github.com/panicinc/icarus). - -## Sublime Text - -Before using SourceKit-LSP with Sublime Text, you will need to install the [LSP](https://packagecontrol.io/packages/LSP), [LSP-SourceKit](https://github.com/sublimelsp/LSP-SourceKit) and [Swift-Next](https://github.com/Swift-Next/Swift-Next) packages from Package Control. Then toggle the server on by typing in command palette `LSP: Enable Language Server Globally` or `LSP: Enable Language Server in Project`. - -## Theia Cloud IDE - -You can use SourceKit-LSP with Theia by using the `theiaide/theia-swift` image. To use the image you need to have [Docker](https://docs.docker.com/get-started/) installed first. - -The following command pulls the image and runs Theia IDE on http://localhost:3000 with the current directory as a workspace. - -```bash -$ docker run -it -p 3000:3000 -v "$(pwd):/home/project:cached" theiaide/theia-swift:next -``` - -You can pass additional arguments to Theia after the image name, for example to enable debugging: - -```bash -$ docker run -it -p 3000:3000 --expose 9229 -p 9229:9229 -v "$(pwd):/home/project:cached" theiaide/theia-swift:next --inspect=0.0.0.0:9229 -``` - -Image Variants -- `theiaide/theia-swift:latest`: This image is based on the latest stable released version. -- `theiaide/theia-swift:next`: This image is based on the nightly published version. - -The `theia-swift-docker` source is located at [theia-apps](https://github.com/theia-ide/theia-apps). - -## Other Editors - -SourceKit-LSP should work with any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). Each editor has its own mechanism for configuring an LSP server, so consult your editor's documentation for the specifics. In general, you can configure your editor to use SourceKit-LSP for Swift, C, C++, Objective-C and Objective-C++ files; the editor will need to be configured to find the `sourcekit-lsp` executable from your installed Swift toolchain, which expects to communicate with the editor over `stdin` and `stdout`. - -## Building a new SourceKit-LSP Client - -If you are building a new SourceKit-LSP client for an editor, here are some critical hints that may help you. Some of these are general hints for development of an LSP client. - -- SourceKit-LSP has extensive logging. See the [Logging section in CONTRIBUTING.md](../CONTRIBUTING.md#logging) for more information. -- You have to [open a document](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen) before sending document-based requests. -- [Don't](https://forums.swift.org/t/how-do-you-build-a-sandboxed-editor-that-uses-sourcekit-lsp/40906) attempt to use Sourcekit-LSP in [a sandboxed context](https://developer.apple.com/documentation/xcode/configuring-the-macos-app-sandbox): - - As with most developer tooling, SourceKit-LSP relies on other system- and language tools that it would not be allowed to access from within an app sandbox. -- Strictly adhere to the format specification of LSP packets, including their [header- and content part](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#headerPart). -- Piece LSP packets together from the stdout data: - - SourceKit-LSP outputs LSP packets to stdout, but: Single chunks of data output do not correspond to single packets. stdout rather delivers a stream of data. You have to buffer that stream in some form and detect individual LSP packets in it. -- Provide the current system environment variables to SourceKit-LSP: - - SourceKit-LSP must read some current environment variables of the system, so [don't wipe them all out](https://forums.swift.org/t/making-a-sourcekit-lsp-client-find-references-fails-solved/57426) when providing modified or additional variables. diff --git a/Documentation/Enable Experimental Background Indexing.md b/Documentation/Enable Experimental Background Indexing.md deleted file mode 100644 index 3c6cb70a3..000000000 --- a/Documentation/Enable Experimental Background Indexing.md +++ /dev/null @@ -1,35 +0,0 @@ -# Enable Experimental Background Indexing - -> [!IMPORTANT] -> Background indexing is enabled by default in Swift 6.1 toolchains and above. This guide only refers to Swift 6.0 toolchains. - -Background indexing in SourceKit-LSP is enabled by default in Swift 6.1 toolchains and is available as an experimental feature for Swift 6.0 toolchains. - -## Behavior Without Background Indexing - -With background indexing disabled, SourceKit-LSP does not update its global index in the background or build Swift modules in the background. Thus, a lot of cross-module or global functionality is limited if the project hasn't been built recently. For example, consider two modules: `Lib` and `Exec`, where `Exec` depends on `Lib`: Without background indexing, if a function is added to `Lib`, completion/jump to definition/etc in `Exec` would not be able to see that function until after a build. Background indexing solves that issue. - -## Set Up - -1. Install the Swift 6.0 toolchain or install [Xcode 16](https://developer.apple.com/xcode/). -2. Enable the experimental `background-indexing` feature by creating a [configuration file](Configuration%20File.md) at `~/.sourcekit-lsp/config.json` with the following contents: -```json -{ - "backgroundIndexing": true, - "backgroundPreparationMode": "enabled" -} -``` - -## Known issues - -- Background Indexing is only supported for SwiftPM projects [#1269](https://github.com/swiftlang/sourcekit-lsp/issues/1269), [#1271](https://github.com/swiftlang/sourcekit-lsp/issues/1271) -- If you change a function in a way that changes its USR but keeps it API compatible (such as adding a defaulted parameter), references to it will be lost and not re-indexed automatically [#1264](https://github.com/swiftlang/sourcekit-lsp/issues/1264) - - Workaround: Make some edit to the files that had references to re-index them -- The index build is currently completely separate from the command line build generated using `swift build`. Building *does not* update the index (break your habits of always building!) [#1270](https://github.com/swiftlang/sourcekit-lsp/issues/1270) -- The initial indexing might take 2-3x more time than a regular build [#1262](https://github.com/swiftlang/sourcekit-lsp/issues/1262), [#1268](https://github.com/swiftlang/sourcekit-lsp/issues/1268) - -## Filing issues - -If you hit any issues that are not mentioned above, please [file a GitHub issue](https://github.com/swiftlang/sourcekit-lsp/issues/new/choose) and attach the following information, if possible: -- A diagnostic bundle generated by running `path/to/sourcekit-lsp diagnose`. -- Your project including the `.build` folder, if possible. diff --git a/Documentation/Enable Extended Logging.md b/Documentation/Enable Extended Logging.md deleted file mode 100644 index 9e1bc3efd..000000000 --- a/Documentation/Enable Extended Logging.md +++ /dev/null @@ -1,23 +0,0 @@ -# Enable Extended logging - -By default, SourceKit-LSP redacts information about your source code, directory structure, and similar potentially sensitive information from its logs. If you are comfortable with sharing such information, you can enable SourceKit-LSP’s extended logging, which improves the ability of SourceKit-LSP developers to understand and fix issues. - -When extended logging is enabled, it will log extended information from that point onwards. To capture the extended logs for an issue you are seeing, please reproduce the issue after enabling extended logging and capture a diagnose bundle by running `sourcekit-lsp diagnose` in terminal. - -## macOS - -To enable extended logging on macOS, install the configuration profile from https://github.com/swiftlang/sourcekit-lsp/blob/main/Documentation/Enable%20Extended%20Logging.mobileconfig as described in https://support.apple.com/guide/mac-help/configuration-profiles-standardize-settings-mh35561/mac#mchlp41bd550. SourceKit-LSP will immediately stop redacting information and include them in the system log. - -To disable extended logging again, remove the configuration profile as described in https://support.apple.com/guide/mac-help/configuration-profiles-standardize-settings-mh35561/mac#mchlpa04df41. - -## Non-Apple platforms - -To enable extended logging on non-Apple platforms, create a [configuration file](Configuration%20File.md) at `~/.sourcekit-lsp/config.json` with the following contents: -```json -{ - "logging": { - "level": "debug", - "privacyLevel": "private" - } -} -``` diff --git a/Documentation/Enable Extended Logging.mobileconfig b/Documentation/Enable Extended Logging.mobileconfig deleted file mode 100644 index 776ed5528..000000000 --- a/Documentation/Enable Extended Logging.mobileconfig +++ /dev/null @@ -1,66 +0,0 @@ - - - - - PayloadContent - - - PayloadDisplayName - Enable Extended Logging - PayloadEnabled - - PayloadIdentifier - com.apple.system.logging.C6E6C174-6B72-42FE-AF4D-2EBEB7080F6A - PayloadType - com.apple.system.logging - PayloadUUID - C6E6C174-6B72-42FE-AF4D-2EBEB7080F6A - PayloadVersion - 1 - Subsystems - - org.swift.sourcekit-lsp - - DEFAULT-OPTIONS - - Enable-Private-Data - - - - org.swift.sourcekit-lsp.indexing - - DEFAULT-OPTIONS - - Enable-Private-Data - - - - org.swift.sourcekit-lsp.task-scheduler - - DEFAULT-OPTIONS - - Enable-Private-Data - - - - - - - PayloadDescription - Enables extended logging from SourceKit-LSP, which is not enabled by default because it contains information about your source code, directory structure and similar potentially sensitive information. - PayloadDisplayName - Extended SourceKit-LSP Logging - PayloadIdentifier - org.swift.sourcekit-lsp.enable-extended-logging - PayloadRemovalDisallowed - - PayloadScope - System - PayloadType - Configuration - PayloadUUID - AB0B405D-E807-4BFA-8D83-765A8AAC6EC7 - PayloadVersion - 1 - - diff --git a/Documentation/README.md b/Documentation/README.md deleted file mode 100644 index 34a610171..000000000 --- a/Documentation/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Documentation - -The following documentation documents are intended for users of SourceKit-LSP. - -- [Configuration File](Configuration%20File.md) -- [Diagnose Bundle](Diagnose%20Bundle.md) -- [Editor Integration](Editor%20Integration.md) -- [Enable Experimental Background Indexing](Enable%20Experimental%20Background%20Indexing.md) -- [Enable Extended Logging](Enable%20Extended%20Logging.md) -- [Using SourceKit-LSP with Embedded Projects](Using%20SourceKit-LSP%20with%20Embedded%20Projects.md) diff --git a/Documentation/Using SourceKit-LSP with Embedded Projects.md b/Documentation/Using SourceKit-LSP with Embedded Projects.md deleted file mode 100644 index 03b06fdb3..000000000 --- a/Documentation/Using SourceKit-LSP with Embedded Projects.md +++ /dev/null @@ -1,26 +0,0 @@ -# Using SourceKit-LSP with Embedded Projects - -If you need to pass additional options in the `swift build` invocation to build your project, SourceKit-LSP needs to know about these as well to fully understand your project. To tell SourceKit-LSP about these options, add a `.sourcekit-lsp/config.json` file to your project’s root folder and add the arguments you pass to `swift build` to that configuration file as described [here](Configuration%20File.md). - -For example, if you use the following invocation to build your project - -```sh -swift build \ - --configuration release \ - --triple armv7em-apple-none-macho \ - -Xcc -D__APPLE__ -Xcc -D__MACH__ \ - -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector -``` - -Then the `.sourcekit-lsp/config.json` file should contain - -```json -{ - "swiftPM": { - "configuration": "release", - "triple": "armv7em-apple-none-macho", - "cCompilerFlags": ["-D__APPLE__", "-D__MACH__"], - "swiftCompilerFlags": ["-Xfrontend", "-disable-stack-protector"] - } -} -``` diff --git a/Package.swift b/Package.swift index 818d4fd6f..a2cb1cb2f 100644 --- a/Package.swift +++ b/Package.swift @@ -5,26 +5,23 @@ import PackageDescription /// Swift settings that should be applied to every Swift target. var globalSwiftSettings: [SwiftSetting] { - var result: [SwiftSetting] = [ + let result: [SwiftSetting] = [ .enableUpcomingFeature("InternalImportsByDefault"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("InferIsolatedConformances"), .enableUpcomingFeature("NonisolatedNonsendingByDefault"), ] - if noSwiftPMDependency { - result += [.define("NO_SWIFTPM_DEPENDENCY")] - } return result } var products: [Product] = [ - .executable(name: "sourcekit-lsp", targets: ["sourcekit-lsp"]), - .library(name: "_SourceKitLSP", targets: ["SourceKitLSP"]), .library(name: "BuildServerProtocol", targets: ["BuildServerProtocol"]), - .library(name: "LSPBindings", targets: ["LanguageServerProtocol", "LanguageServerProtocolJSONRPC"]), - .library(name: "InProcessClient", targets: ["InProcessClient"]), - .library(name: "SwiftSourceKitPlugin", type: .dynamic, targets: ["SwiftSourceKitPlugin"]), - .library(name: "SwiftSourceKitClientPlugin", type: .dynamic, targets: ["SwiftSourceKitClientPlugin"]), + .library(name: "LanguageServerProtocol", targets: ["LanguageServerProtocol"]), + .library(name: "LanguageServerProtocolTransport", targets: ["LanguageServerProtocolTransport"]), + .library(name: "SKLogging", targets: ["SKLogging"]), + .library(name: "_SKLoggingForPlugin", targets: ["_SKLoggingForPlugin"]), + .library(name: "SwiftExtensions", targets: ["SwiftExtensions"]), + .library(name: "_SwiftExtensionsForPlugin", targets: ["_SwiftExtensionsForPlugin"]), ] var targets: [Target] = [ @@ -34,28 +31,6 @@ var targets: [Target] = [ // - Dependencies are listed on separate lines // - All array elements are sorted alphabetically - // MARK: sourcekit-lsp - - .executableTarget( - name: "sourcekit-lsp", - dependencies: [ - "BuildServerIntegration", - "Diagnose", - "InProcessClient", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", - "SKOptions", - "SourceKitLSP", - "ToolchainRegistry", - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings, - linkerSettings: sourcekitLSPLinkSettings - ), - // MARK: BuildServerProtocol .target( @@ -72,214 +47,18 @@ var targets: [Target] = [ dependencies: [ "BuildServerProtocol", "LanguageServerProtocol", - "SKTestSupport", + "ToolsProtocolsTestSupport", ], swiftSettings: globalSwiftSettings ), - // MARK: BuildServerIntegration - - .target( - name: "BuildServerIntegration", - dependencies: [ - "BuildServerProtocol", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", - "SKLogging", - "SKOptions", - "SKUtilities", - "SourceKitD", - "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ] - + swiftPMDependency([ - .product(name: "SwiftPM-auto", package: "swift-package-manager"), - .product(name: "SwiftPMDataModel-auto", package: "swift-package-manager"), - ]), - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "BuildServerIntegrationTests", - dependencies: [ - "BuildServerIntegration", - "LanguageServerProtocol", - "SKOptions", - "SKTestSupport", - "SourceKitLSP", - "ToolchainRegistry", - "TSCExtensions", - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: CAtomics - - .target( - name: "CAtomics", - dependencies: [] - ), - - .target( - name: "CCompletionScoring", - dependencies: [] - ), - - // MARK: ClangLanguageService - - .target( - name: "ClangLanguageService", - dependencies: [ - "BuildServerIntegration", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", - "SKLogging", - "SKOptions", - "SourceKitLSP", - "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - ] + swiftSyntaxDependencies(["SwiftSyntax"]), - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - // MARK: CompletionScoring - - .target( - name: "CompletionScoring", - dependencies: ["CCompletionScoring"], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), + // MARK: ToolsProtocolsCAtomics .target( - name: "CompletionScoringForPlugin", - dependencies: ["CCompletionScoring"], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "CompletionScoringTests", - dependencies: ["CompletionScoring", "CompletionScoringTestSupport", "SwiftExtensions"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "CompletionScoringPerfTests", - dependencies: ["CompletionScoring", "CompletionScoringTestSupport", "SwiftExtensions"], - swiftSettings: globalSwiftSettings - ), - - // MARK: CompletionScoringTestSupport - - .target( - name: "CompletionScoringTestSupport", - dependencies: ["CompletionScoring", "SwiftExtensions"], - resources: [.copy("INPUTS")], - swiftSettings: globalSwiftSettings - ), - - // MARK: CSKTestSupport - - .target( - name: "CSKTestSupport", + name: "ToolsProtocolsCAtomics", dependencies: [] ), - // MARK: Csourcekitd - - .target( - name: "Csourcekitd", - dependencies: [], - exclude: ["CMakeLists.txt"] - ), - - // MARK: Diagnose - - .target( - name: "Diagnose", - dependencies: [ - "BuildServerIntegration", - "InProcessClient", - "LanguageServerProtocolExtensions", - "SKLogging", - "SKOptions", - "SKUtilities", - "SourceKitD", - "SourceKitLSP", - "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ] + swiftSyntaxDependencies(["SwiftIDEUtils", "SwiftSyntax", "SwiftParser"]), - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "DiagnoseTests", - dependencies: [ - "BuildServerIntegration", - "Diagnose", - "SKLogging", - "SKTestSupport", - "SourceKitD", - "ToolchainRegistry", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: DocumentationLanguageService - - .target( - name: "DocumentationLanguageService", - dependencies: [ - "BuildServerIntegration", - "BuildServerProtocol", - "LanguageServerProtocol", - "SemanticIndex", - "SKLogging", - "SKUtilities", - "SourceKitLSP", - "SwiftExtensions", - .product(name: "IndexStoreDB", package: "indexstore-db"), - .product(name: "Markdown", package: "swift-markdown"), - .product(name: "SwiftDocC", package: "swift-docc"), - .product(name: "SymbolKit", package: "swift-docc-symbolkit"), - ], - exclude: [], - swiftSettings: globalSwiftSettings - ), - - // MARK: InProcessClient - - .target( - name: "InProcessClient", - dependencies: [ - "BuildServerIntegration", - "ClangLanguageService", - "DocumentationLanguageService", - "LanguageServerProtocol", - "SKLogging", - "SKOptions", - "SourceKitLSP", - "SwiftLanguageService", - "ToolchainRegistry", - "TSCExtensions", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - // MARK: LanguageServerProtocol .target( @@ -293,74 +72,32 @@ var targets: [Target] = [ name: "LanguageServerProtocolTests", dependencies: [ "LanguageServerProtocol", - "SKTestSupport", - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: LanguageServerProtocolExtensions - - .target( - name: "LanguageServerProtocolExtensions", - dependencies: [ - "LanguageServerProtocol", - "LanguageServerProtocolJSONRPC", "SKLogging", - "SourceKitD", - "SwiftExtensions", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - // MARK: LanguageServerProtocolJSONRPC - - .target( - name: "LanguageServerProtocolJSONRPC", - dependencies: [ - "LanguageServerProtocol", - "SKLogging", - "SwiftExtensions", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "LanguageServerProtocolJSONRPCTests", - dependencies: [ - "LanguageServerProtocolJSONRPC", - "SKTestSupport", + "ToolsProtocolsTestSupport", ], swiftSettings: globalSwiftSettings ), - // MARK: SemanticIndex + // MARK: LanguageServerProtocolTransport .target( - name: "SemanticIndex", + name: "LanguageServerProtocolTransport", dependencies: [ "BuildServerProtocol", - "BuildServerIntegration", "LanguageServerProtocol", - "LanguageServerProtocolExtensions", "SKLogging", "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "IndexStoreDB", package: "indexstore-db"), ], exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings ), .testTarget( - name: "SemanticIndexTests", + name: "LanguageServerProtocolTransportTests", dependencies: [ - "SemanticIndex", + "LanguageServerProtocolTransport", "SKLogging", - "SKTestSupport", + "ToolsProtocolsTestSupport", ], swiftSettings: globalSwiftSettings ), @@ -371,24 +108,21 @@ var targets: [Target] = [ name: "SKLogging", dependencies: [ "SwiftExtensions", - .product(name: "Crypto", package: "swift-crypto"), ], exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings + lspLoggingSwiftSettings ), + // SourceKit-LSP SPI target. Builds SKLogging with an alternate module name to avoid runtime type collisions. .target( - name: "SKLoggingForPlugin", + name: "_SKLoggingForPlugin", dependencies: [ - "SwiftExtensionsForPlugin" + "_SwiftExtensionsForPlugin" ], exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings + lspLoggingSwiftSettings + [ - // We can't depend on swift-crypto in the plugin because we can't module-alias it due to https://github.com/swiftlang/swift-package-manager/issues/8119 - .define("NO_CRYPTO_DEPENDENCY"), - .define("SKLOGGING_FOR_PLUGIN"), .unsafeFlags([ - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", + "-module-alias", "SwiftExtensions=_SwiftExtensionsForPlugin", ]), ] ), @@ -397,192 +131,37 @@ var targets: [Target] = [ name: "SKLoggingTests", dependencies: [ "SKLogging", - "SKTestSupport", - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: SKOptions - - .target( - name: "SKOptions", - dependencies: [ - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "SKLogging", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - // MARK: SKUtilities - - .target( - name: "SKUtilities", - dependencies: [ - "SKLogging", - "SwiftExtensions", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .target( - name: "SKUtilitiesForPlugin", - dependencies: [ - "SKLoggingForPlugin", - "SwiftExtensionsForPlugin", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings + [ - .unsafeFlags([ - "-module-alias", "SKLogging=SKLoggingForPlugin", - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", - ]) - ] - ), - - .testTarget( - name: "SKUtilitiesTests", - dependencies: [ - "SKUtilities", - "SKTestSupport", + "ToolsProtocolsTestSupport", ], swiftSettings: globalSwiftSettings ), - // MARK: SKTestSupport + // MARK: ToolsProtocolsTestSupport .target( - name: "SKTestSupport", + name: "ToolsProtocolsTestSupport", dependencies: [ - "BuildServerIntegration", - "CSKTestSupport", - "Csourcekitd", - "InProcessClient", "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", + "LanguageServerProtocolTransport", "SKLogging", - "SKOptions", - "SKUtilities", - "SourceKitD", - "SourceKitLSP", - "SwiftExtensions", - "SwiftLanguageService", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - resources: [.copy("INPUTS")], - swiftSettings: globalSwiftSettings - ), - - // MARK: SourceKitD - - .target( - name: "SourceKitD", - dependencies: [ - "Csourcekitd", - "SKLogging", - "SwiftExtensions", - ], - exclude: ["CMakeLists.txt", "sourcekitd_uids.swift.gyb"], - swiftSettings: globalSwiftSettings - ), - - .target( - name: "SourceKitDForPlugin", - dependencies: [ - "Csourcekitd", - "SKLoggingForPlugin", - "SwiftExtensionsForPlugin", - ], - exclude: ["CMakeLists.txt", "sourcekitd_uids.swift.gyb"], - swiftSettings: globalSwiftSettings + [ - .unsafeFlags([ - "-module-alias", "SKLogging=SKLoggingForPlugin", - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", - ]) - ] - ), - - .testTarget( - name: "SourceKitDTests", - dependencies: [ - "BuildServerIntegration", - "SourceKitD", - "SKTestSupport", "SwiftExtensions", - "ToolchainRegistry", ], swiftSettings: globalSwiftSettings ), - // MARK: SourceKitLSP - - .target( - name: "SourceKitLSP", - dependencies: [ - "BuildServerProtocol", - "BuildServerIntegration", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", - "SemanticIndex", - "SKLogging", - "SKOptions", - "SKUtilities", - "SourceKitD", - "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "IndexStoreDB", package: "indexstore-db"), - .product(name: "Markdown", package: "swift-markdown"), - ] + swiftSyntaxDependencies(["SwiftSyntax"]), - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "SourceKitLSPTests", - dependencies: [ - "BuildServerProtocol", - "BuildServerIntegration", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "SemanticIndex", - "SKLogging", - "SKOptions", - "SKTestSupport", - "SKUtilities", - "SourceKitD", - "SourceKitLSP", - "ToolchainRegistry", - .product(name: "IndexStoreDB", package: "indexstore-db"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - // Depend on `SwiftCompilerPlugin` and `SwiftSyntaxMacros` so the modules are built before running tests and can - // be used by test cases that test macros (see `SwiftPMTestProject.macroPackageManifest`) - ] - + swiftSyntaxDependencies([ - "SwiftIfConfig", "SwiftParser", "SwiftSyntax", "SwiftCompilerPlugin", "SwiftSyntaxMacros", - ]), - swiftSettings: globalSwiftSettings - ), - // MARK: SwiftExtensions .target( name: "SwiftExtensions", - dependencies: ["CAtomics"], + dependencies: ["ToolsProtocolsCAtomics"], exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings ), + // SourceKit-LSP SPI target. Builds SwiftExtensions with an alternate module name to avoid runtime type collisions. .target( - name: "SwiftExtensionsForPlugin", - dependencies: ["CAtomics"], + name: "_SwiftExtensionsForPlugin", + dependencies: ["ToolsProtocolsCAtomics"], exclude: ["CMakeLists.txt"], swiftSettings: globalSwiftSettings ), @@ -591,175 +170,19 @@ var targets: [Target] = [ name: "SwiftExtensionsTests", dependencies: [ "SKLogging", - "SKTestSupport", + "ToolsProtocolsTestSupport", "SwiftExtensions", ], swiftSettings: globalSwiftSettings ), - // MARK: SwiftLanguageService - - .target( - name: "SwiftLanguageService", - dependencies: [ - "BuildServerProtocol", - "BuildServerIntegration", - "Csourcekitd", - "LanguageServerProtocol", - "LanguageServerProtocolExtensions", - "LanguageServerProtocolJSONRPC", - "SemanticIndex", - "SKLogging", - "SKOptions", - "SKUtilities", - "SourceKitD", - "SourceKitLSP", - "SwiftExtensions", - "ToolchainRegistry", - "TSCExtensions", - .product(name: "IndexStoreDB", package: "indexstore-db"), - .product(name: "Crypto", package: "swift-crypto"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ] - + swiftSyntaxDependencies([ - "SwiftBasicFormat", - "SwiftDiagnostics", - "SwiftIDEUtils", - "SwiftParser", - "SwiftParserDiagnostics", - "SwiftRefactor", - "SwiftSyntax", - "SwiftSyntaxBuilder", - ]), - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - // MARK: SwiftSourceKitClientPlugin - - .target( - name: "SwiftSourceKitClientPlugin", - dependencies: [ - "Csourcekitd", - "SourceKitDForPlugin", - "SwiftExtensionsForPlugin", - "SwiftSourceKitPluginCommon", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings + [ - .unsafeFlags([ - "-module-alias", "SourceKitD=SourceKitDForPlugin", - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", - ]) - ], - linkerSettings: sourcekitLSPLinkSettings - ), - - // MARK: SwiftSourceKitPluginCommon - - .target( - name: "SwiftSourceKitPluginCommon", - dependencies: [ - "Csourcekitd", - "SourceKitDForPlugin", - "SwiftExtensionsForPlugin", - "SKLoggingForPlugin", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings + [ - .unsafeFlags([ - "-module-alias", "SourceKitD=SourceKitDForPlugin", - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", - "-module-alias", "SKLogging=SKLoggingForPlugin", - ]) - ] - ), - - // MARK: SwiftSourceKitPlugin - - .target( - name: "SwiftSourceKitPlugin", - dependencies: [ - "Csourcekitd", - "CompletionScoringForPlugin", - "SKUtilitiesForPlugin", - "SKLoggingForPlugin", - "SourceKitDForPlugin", - "SwiftSourceKitPluginCommon", - "SwiftExtensionsForPlugin", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings + [ - .unsafeFlags([ - "-module-alias", "CompletionScoring=CompletionScoringForPlugin", - "-module-alias", "SKUtilities=SKUtilitiesForPlugin", - "-module-alias", "SourceKitD=SourceKitDForPlugin", - "-module-alias", "SKLogging=SKLoggingForPlugin", - "-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin", - ]) - ], - linkerSettings: sourcekitLSPLinkSettings - ), - - .testTarget( - name: "SwiftSourceKitPluginTests", - dependencies: [ - "BuildServerIntegration", - "CompletionScoring", - "Csourcekitd", - "LanguageServerProtocol", - "SKTestSupport", - "SourceKitD", - "SwiftExtensions", - "ToolchainRegistry", - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: ToolchainRegistry - - .target( - name: "ToolchainRegistry", - dependencies: [ - "SKLogging", - "SKUtilities", - "SwiftExtensions", - "TSCExtensions", - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "ToolchainRegistryTests", - dependencies: [ - "SKTestSupport", - "ToolchainRegistry", - ], - swiftSettings: globalSwiftSettings - ), - - // MARK: TSCExtensions - - .target( - name: "TSCExtensions", - dependencies: [ - "SKLogging", - "SwiftExtensions", - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - ], - exclude: ["CMakeLists.txt"], - swiftSettings: globalSwiftSettings - ), - - .testTarget( - name: "TSCExtensionsTests", - dependencies: [ - "SKTestSupport", - "SwiftExtensions", - "TSCExtensions", - ], - swiftSettings: globalSwiftSettings + // MARK: Command plugins + .plugin( + name: "cmake-smoke-test", + capability: .command(intent: .custom( + verb: "cmake-smoke-test", + description: "Build Swift Build using CMake for validation purposes" + )) ), ] @@ -780,7 +203,7 @@ if buildOnlyTests { } let package = Package( - name: "SourceKitLSP", + name: "swift-tools-protocols", platforms: [.macOS(.v14)], products: products, dependencies: dependencies, @@ -788,23 +211,6 @@ let package = Package( swiftLanguageModes: [.v6] ) -@MainActor -func swiftSyntaxDependencies(_ names: [String]) -> [Target.Dependency] { - if buildDynamicSwiftSyntaxLibrary { - return [.product(name: "_SwiftSyntaxDynamic", package: "swift-syntax")] - } else { - return names.map { .product(name: $0, package: "swift-syntax") } - } -} - -@MainActor -func swiftPMDependency(_ values: [T]) -> [T] { - if noSwiftPMDependency { - return [] - } - return values -} - // MARK: - Parse build arguments func hasEnvironmentVariable(_ name: String) -> Bool { @@ -825,13 +231,6 @@ var installAction: Bool { hasEnvironmentVariable("SOURCEKIT_LSP_CI_INSTALL") } /// remote dependency. var useLocalDependencies: Bool { hasEnvironmentVariable("SWIFTCI_USE_LOCAL_DEPS") } -/// Whether swift-syntax is being built as a single dynamic library instead of as a separate library per module. -/// -/// This means that the swift-syntax symbols don't need to be statically linked, which allows us to stay below the -/// maximum number of exported symbols on Windows, in turn allowing us to build sourcekit-lsp using SwiftPM on Windows -/// and run its tests. -var buildDynamicSwiftSyntaxLibrary: Bool { hasEnvironmentVariable("SWIFTSYNTAX_BUILD_DYNAMIC_LIBRARY") } - /// Build only tests targets and test support modules. /// /// This is used to test swift-format on Windows, where the modules required for the `swift-format` executable are @@ -839,9 +238,6 @@ var buildDynamicSwiftSyntaxLibrary: Bool { hasEnvironmentVariable("SWIFTSYNTAX_B /// the `swift test` invocation so that all pre-built modules can be found. var buildOnlyTests: Bool { hasEnvironmentVariable("SOURCEKIT_LSP_BUILD_ONLY_TESTS") } -/// Build SourceKit-LSP without a dependency on SwiftPM, ie. without support for SwiftPM projects. -var noSwiftPMDependency: Bool { hasEnvironmentVariable("SOURCEKIT_LSP_NO_SWIFTPM_DEPENDENCY") } - // MARK: - Dependencies // When building with the swift build-script, use local dependencies whose contents are controlled @@ -852,34 +248,14 @@ var dependencies: [Package.Dependency] { if buildOnlyTests { return [] } else if useLocalDependencies { - return [ - .package(path: "../indexstore-db"), - .package(path: "../swift-docc"), - .package(path: "../swift-docc-symbolkit"), - .package(path: "../swift-markdown"), - .package(path: "../swift-tools-support-core"), - .package(path: "../swift-argument-parser"), - .package(path: "../swift-syntax"), - .package(path: "../swift-crypto"), - ] + swiftPMDependency([.package(name: "swift-package-manager", path: "../swiftpm")]) + return [] } else { let relatedDependenciesBranch = "main" return [ - .package(url: "https://github.com/swiftlang/indexstore-db.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/swiftlang/swift-docc.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/swiftlang/swift-docc-symbolkit.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/swiftlang/swift-markdown.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/swiftlang/swift-tools-support-core.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), // Not a build dependency. Used so the "Format Source Code" command plugin can be used to format sourcekit-lsp .package(url: "https://github.com/swiftlang/swift-format.git", branch: relatedDependenciesBranch), ] - + swiftPMDependency([ - .package(url: "https://github.com/swiftlang/swift-package-manager.git", branch: relatedDependenciesBranch) - ]) } } diff --git a/Plugins/cmake-smoke-test/cmake-smoke-test.swift b/Plugins/cmake-smoke-test/cmake-smoke-test.swift new file mode 100644 index 000000000..e84a39c24 --- /dev/null +++ b/Plugins/cmake-smoke-test/cmake-smoke-test.swift @@ -0,0 +1,203 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 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 the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import PackagePlugin +import Foundation + +@main +struct CMakeSmokeTest: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + var args = ArgumentExtractor(arguments) + + guard args.extractFlag(named: "disable-sandbox") > 0 else { + throw Errors.missingRequiredOption("--disable-sandbox") + } + + guard let cmakePath = args.extractOption(named: "cmake-path").last else { throw Errors.missingRequiredOption("--cmake-path") } + Diagnostics.progress("using cmake at \(cmakePath)") + let cmakeURL = URL(filePath: cmakePath) + guard let ninjaPath = args.extractOption(named: "ninja-path").last else { throw Errors.missingRequiredOption("--ninja-path") } + Diagnostics.progress("using ninja at \(ninjaPath)") + let ninjaURL = URL(filePath: ninjaPath) + let sysrootPath = args.extractOption(named: "sysroot-path").last + if let sysrootPath { + Diagnostics.progress("using sysroot at \(sysrootPath)") + } + + let extraCMakeArgs = args.extractOption(named: "extra-cmake-arg") + Diagnostics.progress("Extra cmake args: \(extraCMakeArgs.joined(separator: " "))") + + let moduleCachePath = try context.pluginWorkDirectoryURL.appending(component: "module-cache").filePath + + let swiftToolsProtocolsURL = context.package.directoryURL + let swiftToolsProtocolsBuildURL = context.pluginWorkDirectoryURL.appending(component: "swift-build") + try Diagnostics.progress("swift-tools-protocols: \(swiftToolsProtocolsURL.filePath)") + + try FileManager.default.createDirectory(at: swiftToolsProtocolsBuildURL, withIntermediateDirectories: true) + + var sharedSwiftFlags = [ + "-module-cache-path", moduleCachePath + ] + + if let sysrootPath { + sharedSwiftFlags += ["-sdk", sysrootPath] + } + + let sharedCMakeArgs = [ + "-G", "Ninja", + "-DCMAKE_MAKE_PROGRAM=\(ninjaPath)", + "-DCMAKE_BUILD_TYPE:=Debug", + "-DCMAKE_Swift_FLAGS='\(sharedSwiftFlags.joined(separator: " "))'" + ] + extraCMakeArgs + + Diagnostics.progress("Building swift-tools-protocols") + try await Process.checkNonZeroExit(url: cmakeURL, arguments: sharedCMakeArgs + [swiftToolsProtocolsURL.filePath], workingDirectory: swiftToolsProtocolsBuildURL) + try await Process.checkNonZeroExit(url: ninjaURL, arguments: [], workingDirectory: swiftToolsProtocolsBuildURL) + Diagnostics.progress("Built swift-tools-protocols") + } +} + +enum Errors: Error { + case processError(terminationReason: Process.TerminationReason, terminationStatus: Int32) + case missingRequiredOption(String) + case miscError(String) +} + +extension URL { + var filePath: String { + get throws { + try withUnsafeFileSystemRepresentation { path in + guard let path else { + throw Errors.miscError("cannot get file path for URL: \(self)") + } + return String(cString: path) + } + } + } +} + +extension Process { + func run() async throws { + try await withCheckedThrowingContinuation { continuation in + terminationHandler = { _ in + continuation.resume() + } + + do { + try run() + } catch { + terminationHandler = nil + continuation.resume(throwing: error) + } + } + } + + static func checkNonZeroExit(url: URL, arguments: [String], workingDirectory: URL, environment: [String: String]? = nil) async throws { + try Diagnostics.progress("\(url.filePath) \(arguments.joined(separator: " "))") + #if USE_PROCESS_SPAWNING_WORKAROUND && !os(Windows) + Diagnostics.progress("Using process spawning workaround") + // Linux workaround for https://github.com/swiftlang/swift-corelibs-foundation/issues/4772 + // Foundation.Process on Linux seems to inherit the Process.run()-calling thread's signal mask, creating processes that even have SIGTERM blocked + // This manifests as CMake getting stuck when invoking 'uname' with incorrectly configured signal handlers. + var fileActions = posix_spawn_file_actions_t() + defer { posix_spawn_file_actions_destroy(&fileActions) } + var attrs: posix_spawnattr_t = posix_spawnattr_t() + defer { posix_spawnattr_destroy(&attrs) } + posix_spawn_file_actions_init(&fileActions) + try posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory.filePath) + + posix_spawnattr_init(&attrs) + posix_spawnattr_setpgroup(&attrs, 0) + var noSignals = sigset_t() + sigemptyset(&noSignals) + posix_spawnattr_setsigmask(&attrs, &noSignals) + + var mostSignals = sigset_t() + sigemptyset(&mostSignals) + for i in 1 ..< SIGSYS { + if i == SIGKILL || i == SIGSTOP { + continue + } + sigaddset(&mostSignals, i) + } + posix_spawnattr_setsigdefault(&attrs, &mostSignals) + posix_spawnattr_setflags(&attrs, numericCast(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK)) + var pid: pid_t = -1 + try withArrayOfCStrings([url.filePath] + arguments) { arguments in + try withArrayOfCStrings((environment ?? [:]).map { key, value in "\(key)=\(value)" }) { environment in + let spawnResult = try posix_spawn(&pid, url.filePath, /*file_actions=*/&fileActions, /*attrp=*/&attrs, arguments, nil); + var exitCode: Int32 = -1 + var result = wait4(pid, &exitCode, 0, nil); + while (result == -1 && errno == EINTR) { + result = wait4(pid, &exitCode, 0, nil) + } + guard result != -1 else { + throw Errors.miscError("wait failed") + } + guard exitCode == 0 else { + throw Errors.miscError("exit code nonzero") + } + } + } + #else + let process = Process() + process.executableURL = url + process.arguments = arguments + process.currentDirectoryURL = workingDirectory + process.environment = environment + try await process.run() + if process.terminationStatus != 0 { + throw Errors.processError(terminationReason: process.terminationReason, terminationStatus: process.terminationStatus) + } + #endif + } +} + +#if USE_PROCESS_SPAWNING_WORKAROUND && !os(Windows) +func scan(_ seq: S, _ initial: U, _ combine: (U, S.Element) -> U) -> [U] { + var result: [U] = [] + result.reserveCapacity(seq.underestimatedCount) + var runningResult = initial + for element in seq { + runningResult = combine(runningResult, element) + result.append(runningResult) + } + return result +} + +func withArrayOfCStrings( + _ args: [String], + _ body: (UnsafePointer?>) throws -> T +) throws -> T { + let argsCounts = Array(args.map { $0.utf8.count + 1 }) + let argsOffsets = [0] + scan(argsCounts, 0, +) + let argsBufferSize = argsOffsets.last! + var argsBuffer: [UInt8] = [] + argsBuffer.reserveCapacity(argsBufferSize) + for arg in args { + argsBuffer.append(contentsOf: arg.utf8) + argsBuffer.append(0) + } + return try argsBuffer.withUnsafeMutableBufferPointer { + (argsBuffer) in + let ptr = UnsafeRawPointer(argsBuffer.baseAddress!).bindMemory( + to: Int8.self, capacity: argsBuffer.count) + var cStrings: [UnsafePointer?] = argsOffsets.map { ptr + $0 } + cStrings[cStrings.count - 1] = nil + return try cStrings.withUnsafeMutableBufferPointer { + let unsafeString = UnsafeMutableRawPointer($0.baseAddress!).bindMemory( + to: UnsafeMutablePointer?.self, capacity: $0.count) + return try body(unsafeString) + } + } +} +#endif diff --git a/README.md b/README.md index acc8bf801..cfdfd3ed0 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,17 @@ -# SourceKit-LSP +# swift-tools-protocols -SourceKit-LSP is an implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP) for Swift and C-based languages. It provides intelligent editor functionality like code-completion and jump-to-definition to editors that support LSP. SourceKit-LSP is built on top of [sourcekitd](https://github.com/apple/swift/tree/main/tools/SourceKit) and [clangd](https://clang.llvm.org/extra/clangd.html) for high-fidelity language support, and provides a powerful source code index as well as cross-language support. SourceKit-LSP supports projects that use the Swift Package Manager and projects that generate a `compile_commands.json` file, such as CMake. +`swift-tools-protocols` provides basic model types and a transport implementation for the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP) and [Build Server Protocol](https://build-server-protocol.github.io) (BSP). -## Getting Started - -SourceKit-LSP is included in the the Swift toolchains available on [swift.org](http://swift.org/install/) and is bundled with [Xcode](http://developer.apple.com/xcode/). - -[swift.org/tools](https://www.swift.org/tools) has a list of popular editors that support LSP and can thus be hooked up to SourceKit-LSP to provide intelligent editor functionality as well as set-up guides. +This package is intended to be a reusable component suitable for adoption by other projects as a semantically versioned dependency. Clients can build on this foundation to implement a client or server for either protocol, like [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp) or a BSP which integrates with it. The implementation is optimized for the Swift toolchain's use cases rather than attempting to serve as a canonical implenentation of LSP or BSP in Swift. -> [!IMPORTANT] -> SourceKit-LSP does not update its global index in the background or build Swift modules in the background. Thus, a lot of cross-module or global functionality is limited if the project hasn't been built recently. To update the index or rebuild the Swift modules, build your project or enable the experimental background indexing as described in [Enable Experimental Background Indexing](Documentation/Enable%20Experimental%20Background%20Indexing.md). - -To learn more about SourceKit-LSP, refer to the [Documentation](Documentation). +## Getting Started -> [!NOTE] -> If you are using SourceKit-LSP with a SwiftPM project in which you need to pass additional arguments to the `swift build` invocation, as is commonly the case for embedded projects, you need to teach SourceKit-LSP about those arguments as described in [Using SourceKit-LSP with Embedded Projects](Documentation/Using%20SourceKit-LSP%20with%20Embedded%20Projects.md). +Build the package using `swift build` and run the unit tests using `swift test`. ## Reporting Issues -If you should hit any issues while using SourceKit-LSP, we appreciate bug reports on [GitHub Issue](https://github.com/swiftlang/sourcekit-lsp/issues/new/choose). +If you should hit any issues while using the package, we appreciate bug reports on [GitHub Issue](https://github.com/swiftlang/swift-tools-protocols/issues/new/). ## Contributing -If you want to contribute code to SourceKit-LSP, see [CONTRIBUTING.md](CONTRIBUTING.md) for more information. +If you want to contribute, see [CONTRIBUTING.md](CONTRIBUTING.md) for more information. diff --git a/SourceKitLSPDevUtils/Package.swift b/SourceKitLSPDevUtils/Package.swift deleted file mode 100644 index 59456953c..000000000 --- a/SourceKitLSPDevUtils/Package.swift +++ /dev/null @@ -1,40 +0,0 @@ -// swift-tools-version: 6.2 - -import Foundation -import PackageDescription - -let package = Package( - name: "SourceKitLSPDevUtils", - platforms: [.macOS(.v14)], - products: [ - .executable(name: "sourcekit-lsp-dev-utils", targets: ["SourceKitLSPDevUtils"]) - ], - targets: [ - .executableTarget( - name: "SourceKitLSPDevUtils", - dependencies: [ - "ConfigSchemaGen", - .product(name: "ArgumentParser", package: "swift-argument-parser"), - ] - ), - .target( - name: "ConfigSchemaGen", - dependencies: [ - .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftParser", package: "swift-syntax"), - ] - ), - ], - swiftLanguageModes: [.v6] -) - -let dependencies: [(url: String, path: String, fromVersion: Version)] = [ - ("https://github.com/swiftlang/swift-syntax.git", "../../swift-syntax", "600.0.1"), - ("https://github.com/apple/swift-argument-parser.git", "../../swift-argument-parser", "1.5.0"), -] - -if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { - package.dependencies += dependencies.map { .package(url: $0.url, from: $0.fromVersion) } -} else { - package.dependencies += dependencies.map { .package(url: $0.path, from: $0.fromVersion) } -} diff --git a/SourceKitLSPDevUtils/README.md b/SourceKitLSPDevUtils/README.md deleted file mode 100644 index 5b5829060..000000000 --- a/SourceKitLSPDevUtils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# sourcekit-lsp-dev-utils - -This directory contains utilities for developing SourceKit-LSP. [CONTRIBUTING.md](../CONTRIBUTING.md) covers how to use these utilities. diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/ConfigSchemaGen.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/ConfigSchemaGen.swift deleted file mode 100644 index 48c51ef6c..000000000 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/ConfigSchemaGen.swift +++ /dev/null @@ -1,149 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SwiftParser -import SwiftSyntax - -/// The main entry point for generating a JSON schema and Markdown documentation -/// for the SourceKit-LSP configuration file format -/// (`.sourcekit-lsp/config.json`) from the Swift type definitions in -/// `SKOptions` Swift module. -package struct ConfigSchemaGen { - private struct WritePlan { - fileprivate let category: String - fileprivate let path: URL - fileprivate let contents: () throws -> Data - - fileprivate func write() throws { - try contents().write(to: path) - } - } - - private static let projectRoot = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - private static let sourceDir = - projectRoot - .appending(components: "Sources", "SKOptions") - private static let configSchemaJSONPath = - projectRoot - .appending(component: "config.schema.json") - private static let configSchemaDocPath = - projectRoot - .appending(components: "Documentation", "Configuration File.md") - - /// Generates and writes the JSON schema and documentation for the SourceKit-LSP configuration file format. - package static func generate() throws { - let plans = try plan() - for plan in plans { - print("Writing \(plan.category) to \"\(plan.path.path)\"") - try plan.write() - } - } - - /// Verifies that the generated JSON schema and documentation in the current source tree - /// are up-to-date with the Swift type definitions in `SKOptions`. - /// - Returns: `true` if the generated files are up-to-date, `false` otherwise. - package static func verify() throws -> Bool { - let plans = try plan() - for plan in plans { - print("Verifying \(plan.category) at \"\(plan.path.path)\"") - let expectedContents = try plan.contents() - let actualContents = try Data(contentsOf: plan.path) - guard expectedContents == actualContents else { - print("error: \(plan.category) is out-of-date!") - print("Please run `./sourcekit-lsp-dev-utils generate-config-schema` to update it.") - return false - } - } - return true - } - - private static func plan() throws -> [WritePlan] { - let sourceFiles = FileManager.default.enumerator(at: sourceDir, includingPropertiesForKeys: nil)! - let typeNameResolver = TypeDeclResolver() - - for case let fileURL as URL in sourceFiles { - guard fileURL.pathExtension == "swift" else { - continue - } - let sourceText = try String(contentsOf: fileURL) - let sourceFile = Parser.parse(source: sourceText) - typeNameResolver.collect(from: sourceFile) - } - let rootTypeDecl = try typeNameResolver.lookupType(fullyQualified: ["SourceKitLSPOptions"]) - let context = OptionSchemaContext(typeNameResolver: typeNameResolver) - var schema = try context.buildSchema(from: rootTypeDecl) - - // Manually annotate the logging level enum since LogLevel type exists - // outside of the SKOptions module - schema["logging"]?["level"]?.kind = .enum( - OptionTypeSchama.Enum( - name: "LogLevel", - cases: ["debug", "info", "default", "error", "fault"].map { - OptionTypeSchama.Case(name: $0) - } - ) - ) - schema["logging"]?["privacyLevel"]?.kind = .enum( - OptionTypeSchama.Enum( - name: "PrivacyLevel", - cases: ["public", "private", "sensitive"].map { - OptionTypeSchama.Case(name: $0) - } - ) - ) - - return [ - WritePlan( - category: "JSON Schema", - path: configSchemaJSONPath, - contents: { try generateJSONSchema(from: schema, context: context) } - ), - WritePlan( - category: "Schema Documentation", - path: configSchemaDocPath, - contents: { try generateDocumentation(from: schema, context: context) } - ), - ] - } - - private static func generateJSONSchema(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data { - let schemaBuilder = JSONSchemaBuilder(context: context) - var jsonSchema = try schemaBuilder.build(from: schema) - jsonSchema.title = "SourceKit-LSP Configuration" - jsonSchema.comment = "DO NOT EDIT THIS FILE. This file is generated by \(#fileID)." - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] - return try encoder.encode(jsonSchema) - } - - private static func generateDocumentation(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data - { - let docBuilder = OptionDocumentBuilder(context: context) - guard let data = try docBuilder.build(from: schema).data(using: .utf8) else { - throw ConfigSchemaGenError("Failed to encode documentation as UTF-8") - } - return data - } -} - -struct ConfigSchemaGenError: Error, CustomStringConvertible { - let description: String - - init(_ description: String) { - self.description = description - } -} diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift deleted file mode 100644 index 55f578633..000000000 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift +++ /dev/null @@ -1,140 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -@propertyWrapper -final class HeapBox { - var wrappedValue: T - init(wrappedValue: T) { - self.wrappedValue = wrappedValue - } -} - -/// JSON Schema representation for version draft-07 -/// https://json-schema.org/draft-07/draft-handrews-json-schema-01 -/// -/// NOTE: draft-07 is the latest version of JSON Schema that is supported by -/// most of the tools. We may need to update this schema in the future. -struct JSONSchema: Encodable { - enum CodingKeys: String, CodingKey { - case _schema = "$schema" - case id = "$id" - case comment = "$comment" - case title - case type - case description - case properties - case required - case `enum` - case items - case additionalProperties - case markdownDescription - case markdownEnumDescriptions - } - var _schema: String? - var id: String? - var comment: String? - var title: String? - var type: String? - var description: String? - var properties: [String: JSONSchema]? - var required: [String]? - var `enum`: [String]? - @HeapBox - var items: JSONSchema? - @HeapBox - var additionalProperties: JSONSchema? - - /// VSCode extension: Markdown formatted description for rich hover - /// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md - var markdownDescription: String? - /// VSCode extension: Markdown formatted descriptions for rich hover for enum values - /// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md - var markdownEnumDescriptions: [String]? - - func encode(to encoder: any Encoder) throws { - // Manually implement encoding to use `encodeIfPresent` for HeapBox-ed fields - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(_schema, forKey: ._schema) - try container.encodeIfPresent(id, forKey: .id) - try container.encodeIfPresent(comment, forKey: .comment) - try container.encodeIfPresent(title, forKey: .title) - try container.encodeIfPresent(type, forKey: .type) - try container.encodeIfPresent(description, forKey: .description) - if let properties = properties, !properties.isEmpty { - try container.encode(properties, forKey: .properties) - } - if let required = required, !required.isEmpty { - try container.encode(required, forKey: .required) - } - try container.encodeIfPresent(`enum`, forKey: .enum) - try container.encodeIfPresent(items, forKey: .items) - try container.encodeIfPresent(additionalProperties, forKey: .additionalProperties) - try container.encodeIfPresent(markdownDescription, forKey: .markdownDescription) - if let markdownEnumDescriptions { - try container.encode(markdownEnumDescriptions, forKey: .markdownEnumDescriptions) - } - } -} - -struct JSONSchemaBuilder { - let context: OptionSchemaContext - - func build(from typeSchema: OptionTypeSchama) throws -> JSONSchema { - var schema = try buildJSONSchema(from: typeSchema) - schema._schema = "http://json-schema.org/draft-07/schema#" - return schema - } - - private func buildJSONSchema(from typeSchema: OptionTypeSchama) throws -> JSONSchema { - var schema = JSONSchema() - switch typeSchema.kind { - case .boolean: schema.type = "boolean" - case .integer: schema.type = "integer" - case .number: schema.type = "number" - case .string: schema.type = "string" - case .array(let value): - schema.type = "array" - schema.items = try buildJSONSchema(from: value) - case .dictionary(let value): - schema.type = "object" - schema.additionalProperties = try buildJSONSchema(from: value) - case .struct(let structInfo): - schema.type = "object" - var properties: [String: JSONSchema] = [:] - var required: [String] = [] - for property in structInfo.properties { - let propertyType = property.type - var propertySchema = try buildJSONSchema(from: propertyType) - propertySchema.description = property.description - // As we usually use Markdown syntax for doc comments, set `markdownDescription` - // too for better rendering in VSCode. - propertySchema.markdownDescription = property.description - properties[property.name] = propertySchema - if !propertyType.isOptional { - required.append(property.name) - } - } - schema.properties = properties - schema.required = required - case .enum(let enumInfo): - schema.type = "string" - schema.enum = enumInfo.cases.map(\.name) - // Set `markdownEnumDescriptions` for better rendering in VSCode rich hover - // Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec, - // so we only set `markdownEnumDescriptions` here. - if enumInfo.cases.contains(where: { $0.description != nil }) { - schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" } - } - } - return schema - } -} diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift deleted file mode 100644 index 23271d64c..000000000 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift +++ /dev/null @@ -1,107 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// Generates a markdown document for the configuration file based on the schema. -struct OptionDocumentBuilder { - static let preamble = """ - - - # Configuration File - - `.sourcekit-lsp/config.json` configuration files can be used to modify the behavior of SourceKit-LSP in various ways. The following locations are checked. Settings in later configuration files override settings in earlier configuration files - - `~/.sourcekit-lsp/config.json` - - On macOS: `~/Library/Application Support/org.swift.sourcekit-lsp/config.json` from the various `Library` folders on the system - - If the `XDG_CONFIG_HOME` environment variable is set: `$XDG_CONFIG_HOME/sourcekit-lsp/config.json` - - Initialization options passed in the initialize request - - A `.sourcekit-lsp/config.json` file in a workspace’s root - - The structure of the file is currently not guaranteed to be stable. Options may be removed or renamed. - - ## Structure - - `config.json` is a JSON file with the following structure. All keys are optional and unknown keys are ignored. - - """ - - let context: OptionSchemaContext - - /// Builds a markdown document for the configuration file based on the schema. - func build(from schema: OptionTypeSchama) throws -> String { - var doc = Self.preamble - - func appendProperty(_ property: OptionTypeSchama.Property, indentLevel: Int) throws { - let indent = String(repeating: " ", count: indentLevel) - let name = property.name - doc += "\(indent)- `\(name)" - let type = property.type - let typeDescription: String? - switch type.kind { - case .struct: - // Skip struct type as we describe its properties in the next level - typeDescription = nil - default: - typeDescription = Self.typeToDisplay(type) - } - if let typeDescription { - doc += ": \(typeDescription)`:" - } else { - doc += "`:" - } - if let description = property.description { - doc += " " + description.split(separator: "\n").joined(separator: "\n\(indent) ") - } - doc += "\n" - switch type.kind { - case .struct(let schema): - for property in schema.properties { - try appendProperty(property, indentLevel: indentLevel + 1) - } - case .enum(let schema): - for caseInfo in schema.cases { - // Add detailed description for each case if available - guard let description = caseInfo.description else { - continue - } - doc += "\(indent) - `\(caseInfo.name)`" - doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ") - doc += "\n" - } - default: break - } - } - guard case .struct(let schema) = schema.kind else { - throw ConfigSchemaGenError("Root schema must be a struct") - } - for property in schema.properties { - try appendProperty(property, indentLevel: 0) - } - return doc - } - - static func typeToDisplay(_ type: OptionTypeSchama, shouldWrap: Bool = false) -> String { - switch type.kind { - case .boolean: return "boolean" - case .integer: return "integer" - case .number: return "number" - case .string: return "string" - case .array(let value): - return "\(typeToDisplay(value, shouldWrap: true))[]" - case .dictionary(let value): - return "[string: \(typeToDisplay(value))]" - case .struct(let structInfo): - return structInfo.name - case .enum(let enumInfo): - let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|") - return shouldWrap ? "(\(cases))" : cases - } - } -} diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift deleted file mode 100644 index 64876c1db..000000000 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift +++ /dev/null @@ -1,237 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax - -/// Intermediate type schema representation for option types derived from Swift -/// syntax nodes -struct OptionTypeSchama { - struct Property { - var name: String - var type: OptionTypeSchama - var description: String? - var defaultValue: String? - } - - struct Struct { - var name: String - /// Properties of the object, preserving the order of declaration - var properties: [Property] - } - - struct Case { - var name: String - var description: String? - } - - struct Enum { - var name: String - var cases: [Case] - } - - enum Kind { - case boolean - case integer - case number - case string - indirect case array(value: OptionTypeSchama) - indirect case dictionary(value: OptionTypeSchama) - case `struct`(Struct) - case `enum`(Enum) - } - - var kind: Kind - var isOptional: Bool - - init(kind: Kind, isOptional: Bool = false) { - self.kind = kind - self.isOptional = isOptional - } - - /// Accesses the property schema by name - subscript(_ key: String) -> OptionTypeSchama? { - get { - guard case .struct(let structInfo) = kind else { - return nil - } - return structInfo.properties.first { $0.name == key }?.type - } - set { - guard case .struct(var structInfo) = kind else { - fatalError("Cannot set property on non-object type") - } - guard let index = structInfo.properties.firstIndex(where: { $0.name == key }) else { - fatalError("Property not found: \(key)") - } - guard let newValue = newValue else { - fatalError("Cannot set property to nil") - } - structInfo.properties[index].type = newValue - kind = .struct(structInfo) - } - } -} - -/// Context for resolving option schema from Swift syntax nodes -struct OptionSchemaContext { - private let typeNameResolver: TypeDeclResolver - - init(typeNameResolver: TypeDeclResolver) { - self.typeNameResolver = typeNameResolver - } - - /// Builds a schema from a type declaration - func buildSchema(from typeDecl: TypeDeclResolver.TypeDecl) throws -> OptionTypeSchama { - switch DeclSyntax(typeDecl).as(DeclSyntaxEnum.self) { - case .structDecl(let decl): - let structInfo = try buildStructProperties(decl) - return OptionTypeSchama(kind: .struct(structInfo)) - case .enumDecl(let decl): - let enumInfo = try buildEnumCases(decl) - return OptionTypeSchama(kind: .enum(enumInfo)) - default: - throw ConfigSchemaGenError("Unsupported type declaration: \(typeDecl)") - } - } - - /// Resolves the type of a given type usage - private func resolveType(_ type: TypeSyntax) throws -> OptionTypeSchama { - switch type.as(TypeSyntaxEnum.self) { - case .optionalType(let type): - var wrapped = try resolveType(type.wrappedType) - guard !wrapped.isOptional else { - throw ConfigSchemaGenError("Nested optional type is not supported") - } - wrapped.isOptional = true - return wrapped - case .arrayType(let type): - let value = try resolveType(type.element) - return OptionTypeSchama(kind: .array(value: value)) - case .dictionaryType(let type): - guard type.key.trimmedDescription == "String" else { - throw ConfigSchemaGenError("Dictionary key type must be String: \(type.key)") - } - let value = try resolveType(type.value) - return OptionTypeSchama(kind: .dictionary(value: value)) - case .identifierType(let type): - let primitiveTypes: [String: OptionTypeSchama.Kind] = [ - "String": .string, - "Int": .integer, - "Double": .number, - "Bool": .boolean, - ] - if let primitiveType = primitiveTypes[type.trimmedDescription] { - return OptionTypeSchama(kind: primitiveType) - } else if type.name.trimmedDescription == "Set" { - guard let elementType = type.genericArgumentClause?.arguments.first?.argument else { - throw ConfigSchemaGenError("Set type must have one generic argument: \(type)") - } - return OptionTypeSchama(kind: .array(value: try resolveType(elementType))) - } else { - let type = try typeNameResolver.lookupType(for: type) - return try buildSchema(from: type) - } - default: - throw ConfigSchemaGenError("Unsupported type syntax: \(type)") - } - } - - private func buildEnumCases(_ node: EnumDeclSyntax) throws -> OptionTypeSchama.Enum { - let cases = try node.memberBlock.members.flatMap { member -> [OptionTypeSchama.Case] in - guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { - return [] - } - return try caseDecl.elements.compactMap { - guard $0.parameterClause == nil else { - throw ConfigSchemaGenError("Associated values in enum cases are not supported: \(caseDecl)") - } - let name: String - if let rawValue = $0.rawValue?.value { - if let stringLiteral = rawValue.as(StringLiteralExprSyntax.self), - let literalValue = stringLiteral.representedLiteralValue - { - name = literalValue - } else { - throw ConfigSchemaGenError( - "Only string literals without interpolation are supported as enum case raw values: \(caseDecl)" - ) - } - } else { - name = $0.name.text - } - let description = Self.extractDocComment(caseDecl.leadingTrivia) - if description?.contains("- Note: Internal option") ?? false { - return nil - } - return OptionTypeSchama.Case(name: name, description: description) - } - } - let typeName = node.name.text - return .init(name: typeName, cases: cases) - } - - private func buildStructProperties(_ node: StructDeclSyntax) throws -> OptionTypeSchama.Struct { - var properties: [OptionTypeSchama.Property] = [] - for member in node.memberBlock.members { - // Skip computed properties - guard let variable = member.decl.as(VariableDeclSyntax.self), - let binding = variable.bindings.first, - let type = binding.typeAnnotation, - binding.accessorBlock == nil - else { continue } - - let name = binding.pattern.trimmed.description - let defaultValue = binding.initializer?.value.description - let description = Self.extractDocComment(variable.leadingTrivia) - if description?.contains("- Note: Internal option") ?? false { - continue - } - let typeInfo = try resolveType(type.type) - properties.append( - .init(name: name, type: typeInfo, description: description, defaultValue: defaultValue) - ) - } - let typeName = node.name.text - return .init(name: typeName, properties: properties) - } - - private static func extractDocComment(_ trivia: Trivia) -> String? { - var docLines = trivia.flatMap { piece in - switch piece { - case .docBlockComment(let text): - // Remove `/**` and `*/` - assert(text.hasPrefix("/**") && text.hasSuffix("*/"), "Unexpected doc block comment format: \(text)") - return text.dropFirst(3).dropLast(2).split { $0.isNewline } - case .docLineComment(let text): - // Remove `///` and leading space - assert(text.hasPrefix("///"), "Unexpected doc line comment format: \(text)") - let text = text.dropFirst(3) - return [text] - default: - return [] - } - } - guard !docLines.isEmpty else { - return nil - } - // Trim leading spaces for each line and skip empty lines - docLines = docLines.compactMap { - guard !$0.isEmpty else { return nil } - var trimmed = $0 - while trimmed.first?.isWhitespace == true { - trimmed = trimmed.dropFirst() - } - return trimmed - } - return docLines.joined(separator: " ") - } -} diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/TypeDeclResolver.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/TypeDeclResolver.swift deleted file mode 100644 index 8c8bd089e..000000000 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/TypeDeclResolver.swift +++ /dev/null @@ -1,118 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax - -/// Resolves type declarations from Swift syntax nodes -class TypeDeclResolver { - typealias TypeDecl = NamedDeclSyntax & DeclGroupSyntax & DeclSyntaxProtocol - /// A representation of a qualified name of a type declaration - /// - /// `Outer.Inner` type declaration is represented as ["Outer", "Inner"] - typealias QualifiedName = [String] - private var typeDeclByQualifiedName: [QualifiedName: TypeDecl] = [:] - - enum Error: Swift.Error { - case typeNotFound(QualifiedName) - } - - private class TypeDeclCollector: SyntaxVisitor { - let resolver: TypeDeclResolver - var scope: [TypeDecl] = [] - var rootTypeDecls: [TypeDecl] = [] - - init(resolver: TypeDeclResolver) { - self.resolver = resolver - super.init(viewMode: .all) - } - - func visitNominalDecl(_ node: TypeDecl) -> SyntaxVisitorContinueKind { - let name = node.name.text - let qualifiedName = scope.map(\.name.text) + [name] - resolver.typeDeclByQualifiedName[qualifiedName] = node - scope.append(node) - return .visitChildren - } - - func visitPostNominalDecl() { - let type = scope.removeLast() - if scope.isEmpty { - rootTypeDecls.append(type) - } - } - - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNominalDecl(node) - } - override func visitPost(_ node: StructDeclSyntax) { - visitPostNominalDecl() - } - override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - return visitNominalDecl(node) - } - override func visitPost(_ node: EnumDeclSyntax) { - visitPostNominalDecl() - } - } - - /// Collects type declarations from a parsed Swift source file - func collect(from schema: SourceFileSyntax) { - let collector = TypeDeclCollector(resolver: self) - collector.walk(schema) - } - - /// Builds the type name scope for a given type usage - private func buildScope(type: IdentifierTypeSyntax) -> QualifiedName { - var innerToOuter: [String] = [] - var context: SyntaxProtocol = type - while let parent = context.parent { - if let parent = parent.asProtocol(NamedDeclSyntax.self), parent.isProtocol(DeclGroupSyntax.self) { - innerToOuter.append(parent.name.text) - } - context = parent - } - return innerToOuter.reversed() - } - - /// Looks up a qualified name of a type declaration by its unqualified type usage - /// Returns the qualified name hierarchy of the type declaration - /// If the type declaration is not found, returns the unqualified name - private func tryQualify(type: IdentifierTypeSyntax) -> QualifiedName { - let name = type.name.text - let scope = buildScope(type: type) - /// Search for the type declaration from the innermost scope to the outermost scope - for i in (0...scope.count).reversed() { - let qualifiedName = Array(scope[0.. TypeDecl { - let qualifiedName = tryQualify(type: type) - guard let typeDecl = typeDeclByQualifiedName[qualifiedName] else { - throw Error.typeNotFound(qualifiedName) - } - return typeDecl - } - - /// Looks up a type declaration by its fully qualified name - func lookupType(fullyQualified: QualifiedName) throws -> TypeDecl { - guard let typeDecl = typeDeclByQualifiedName[fullyQualified] else { - throw Error.typeNotFound(fullyQualified) - } - return typeDecl - } -} diff --git a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/GenerateConfigSchema.swift b/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/GenerateConfigSchema.swift deleted file mode 100644 index cf20e7724..000000000 --- a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/GenerateConfigSchema.swift +++ /dev/null @@ -1,24 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import ArgumentParser -import ConfigSchemaGen - -struct GenerateConfigSchema: ParsableCommand { - static let configuration = CommandConfiguration( - abstract: "Generate a JSON schema and documentation for the SourceKit-LSP configuration file" - ) - - func run() throws { - try ConfigSchemaGen.generate() - } -} diff --git a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/VerifyConfigSchema.swift b/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/VerifyConfigSchema.swift deleted file mode 100644 index 40a5a8c11..000000000 --- a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/Commands/VerifyConfigSchema.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import ArgumentParser -import ConfigSchemaGen -import Foundation - -struct VerifyConfigSchema: ParsableCommand { - static let configuration = CommandConfiguration( - abstract: - "Verify that the generated JSON schema and documentation for the SourceKit-LSP configuration file are up-to-date" - ) - - func run() throws { - guard try ConfigSchemaGen.verify() else { - throw ExitCode.failure - } - print("All schemas are up-to-date!") - } -} diff --git a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/SourceKitLSPDevUtils.swift b/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/SourceKitLSPDevUtils.swift deleted file mode 100644 index ddd960085..000000000 --- a/SourceKitLSPDevUtils/Sources/SourceKitLSPDevUtils/SourceKitLSPDevUtils.swift +++ /dev/null @@ -1,25 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import ArgumentParser - -@main -struct SourceKitLSPDevUtils: ParsableCommand { - static let configuration = CommandConfiguration( - commandName: "sourcekit-lsp-dev-utils", - abstract: "Utilities for developing SourceKit-LSP", - subcommands: [ - GenerateConfigSchema.self, - VerifyConfigSchema.self, - ] - ) -} diff --git a/Sources/BuildServerIntegration/BuildServerHooks.swift b/Sources/BuildServerIntegration/BuildServerHooks.swift deleted file mode 100644 index cb2d14d93..000000000 --- a/Sources/BuildServerIntegration/BuildServerHooks.swift +++ /dev/null @@ -1,53 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol - -package struct SwiftPMTestHooks: Sendable { - package var reloadPackageDidStart: (@Sendable () async -> Void)? - package var reloadPackageDidFinish: (@Sendable () async -> Void)? - - package init( - reloadPackageDidStart: (@Sendable () async -> Void)? = nil, - reloadPackageDidFinish: (@Sendable () async -> Void)? = nil - ) { - self.reloadPackageDidStart = reloadPackageDidStart - self.reloadPackageDidFinish = reloadPackageDidFinish - } -} - -package struct BuildServerHooks: Sendable { - package var swiftPMTestHooks: SwiftPMTestHooks - - /// A hook that will be executed before a request is handled by a `BuiltInBuildServer`. - /// - /// This allows requests to be artificially delayed. - package var preHandleRequest: (@Sendable (any RequestType) async -> Void)? - - /// When running SourceKit-LSP in-process, allows the creator of `SourceKitLSPServer` to create a message handler that - /// handles BSP requests instead of SourceKit-LSP creating build server as needed. - package var injectBuildServer: - (@Sendable (_ projectRoot: URL, _ connectionToSourceKitLSP: any Connection) async -> any Connection)? - - package init( - swiftPMTestHooks: SwiftPMTestHooks = SwiftPMTestHooks(), - preHandleRequest: (@Sendable (any RequestType) async -> Void)? = nil, - injectBuildServer: ( - @Sendable (_ projectRoot: URL, _ connectionToSourceKitLSP: any Connection) async -> any Connection - )? = nil - ) { - self.swiftPMTestHooks = swiftPMTestHooks - self.preHandleRequest = preHandleRequest - self.injectBuildServer = injectBuildServer - } -} diff --git a/Sources/BuildServerIntegration/BuildServerManager.swift b/Sources/BuildServerIntegration/BuildServerManager.swift deleted file mode 100644 index 480994d72..000000000 --- a/Sources/BuildServerIntegration/BuildServerManager.swift +++ /dev/null @@ -1,1843 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -import Dispatch -package import Foundation -package import LanguageServerProtocol -package import LanguageServerProtocolExtensions -import SKLogging -package import SKOptions -import SKUtilities -package import SwiftExtensions -import TSCExtensions -package import ToolchainRegistry - -import struct TSCBasic.RelativePath - -private typealias RequestCache = Cache - -/// An output path returned from the build server in the `SourceItem.data.outputPath` field. -package enum OutputPath: Hashable, Comparable, CustomLogStringConvertible { - /// An output path returned from the build server. - case path(String) - - /// The build server does not support output paths. - case notSupported - - package var description: String { - switch self { - case .notSupported: return "" - case .path(let path): return path - } - } - - package var redactedDescription: String { - switch self { - case .notSupported: return "" - case .path(let path): return path.hashForLogging - } - } -} - -package struct SourceFileInfo: Sendable { - /// Maps the targets that this source file is a member of to the output path the file has within that target. - /// - /// The value in the dictionary can be: - /// - `.path` if the build server supports output paths and produced a result - /// - `.notSupported` if the build server does not support output paths. - /// - `nil` if the build server supports output paths but did not return an output path for this file in this target. - package var targetsToOutputPath: [BuildTargetIdentifier: OutputPath?] - - /// The targets that this source file is a member of - package var targets: some Collection & Sendable { targetsToOutputPath.keys } - - /// `true` if this file belongs to the root project that the user is working on. It is false, if the file belongs - /// to a dependency of the project. - package var isPartOfRootProject: Bool - - /// Whether the file might contain test cases. This property is an over-approximation. It might be true for files - /// from non-test targets or files that don't actually contain any tests. - package var mayContainTests: Bool - - /// Source files returned here fall into two categories: - /// - Buildable source files are files that can be built by the build server and that make sense to background index - /// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient - /// compiler arguments for these files to provide semantic editor functionality but we can't build them. - package var isBuildable: Bool - - /// If this source item gets copied to a different destination during preparation, the destinations it will be copied - /// to. - package var copyDestinations: Set - - fileprivate func merging(_ other: SourceFileInfo?) -> SourceFileInfo { - guard let other else { - return self - } - let mergedTargetsToOutputPaths = targetsToOutputPath.merging( - other.targetsToOutputPath, - uniquingKeysWith: { lhs, rhs in - if lhs == rhs { - return lhs - } - logger.error("Received mismatching output files: \(lhs?.forLogging) vs \(rhs?.forLogging)") - // Deterministically pick an output file if they mismatch. But really, this shouldn't happen. - switch (lhs, rhs) { - case (let lhs?, nil): return lhs - case (nil, let rhs?): return rhs - case (nil, nil): return nil // Should be handled above already - case (let lhs?, let rhs?): return min(lhs, rhs) - } - } - ) - return SourceFileInfo( - targetsToOutputPath: mergedTargetsToOutputPaths, - isPartOfRootProject: other.isPartOfRootProject || isPartOfRootProject, - mayContainTests: other.mayContainTests || mayContainTests, - isBuildable: other.isBuildable || isBuildable, - copyDestinations: copyDestinations.union(other.copyDestinations) - ) - } -} - -private struct BuildTargetInfo { - /// The build target itself. - var target: BuildTarget - - /// The maximum depth at which this target occurs at the build graph, ie. the number of edges on the longest path - /// from this target to a root target (eg. an executable) - var depth: Int - - /// The targets that depend on this target, ie. the inverse of `BuildTarget.dependencies`. - var dependents: Set -} - -fileprivate extension BuildTarget { - var sourceKitData: SourceKitBuildTarget? { - guard dataKind == .sourceKit else { - return nil - } - return SourceKitBuildTarget(fromLSPAny: data) - } -} - -fileprivate extension InitializeBuildResponse { - var sourceKitData: SourceKitInitializeBuildResponseData? { - guard dataKind == nil || dataKind == .sourceKit else { - return nil - } - return SourceKitInitializeBuildResponseData(fromLSPAny: data) - } -} - -/// A build server adapter is responsible for receiving messages from the `BuildServerManager` and forwarding them to -/// the build server. For built-in build servers, this means that we need to translate the BSP messages to methods in -/// the `BuiltInBuildServer` protocol. For external (aka. out-of-process, aka. BSP servers) build servers, this means -/// that we need to manage the external build server's lifetime. -private enum BuildServerAdapter { - case builtIn(BuiltInBuildServerAdapter, connectionToBuildServer: any Connection) - case external(ExternalBuildServerAdapter) - /// A message handler that was created by `injectBuildServer` and will handle all BSP messages. - case injected(any Connection) - - /// Send a notification to the build server. - func send(_ notification: some NotificationType) async { - switch self { - case .builtIn(_, let connectionToBuildServer): - connectionToBuildServer.send(notification) - case .external(let external): - await external.send(notification) - case .injected(let connection): - connection.send(notification) - } - } - - /// Send a request to the build server. - func send(_ request: Request) async throws -> Request.Response { - switch self { - case .builtIn(_, let connectionToBuildServer): - return try await connectionToBuildServer.send(request) - case .external(let external): - return try await external.send(request) - case .injected(let messageHandler): - // After we sent the request, the ID of the request. - // When we send a `CancelRequestNotification` this is reset to `nil` so that we don't send another cancellation - // notification. - let requestID = ThreadSafeBox(initialValue: nil) - - return try await withTaskCancellationHandler { - return try await withCheckedThrowingContinuation { continuation in - if Task.isCancelled { - return continuation.resume(throwing: CancellationError()) - } - requestID.value = messageHandler.send(request) { response in - continuation.resume(with: response) - } - if Task.isCancelled { - // The task might have been cancelled after we checked `Task.isCancelled` above but before `requestID.value` - // is set, we won't send a `CancelRequestNotification` from the `onCancel` handler. Send it from here. - if let requestID = requestID.takeValue() { - messageHandler.send(CancelRequestNotification(id: requestID)) - } - } - } - } onCancel: { - if let requestID = requestID.takeValue() { - messageHandler.send(CancelRequestNotification(id: requestID)) - } - } - } - } -} - -private extension BuildServerSpec { - private func createBuiltInBuildServerAdapter( - messagesToSourceKitLSPHandler: any MessageHandler, - buildServerHooks: BuildServerHooks, - _ createBuildServer: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildServer? - ) async -> BuildServerAdapter? { - let connectionToSourceKitLSP = LocalConnection( - receiverName: "BuildServerManager for \(projectRoot.lastPathComponent)", - handler: messagesToSourceKitLSPHandler - ) - - let buildServer = await orLog("Creating build server") { - try await createBuildServer(connectionToSourceKitLSP) - } - guard let buildServer else { - logger.log("Failed to create build server at \(projectRoot)") - return nil - } - logger.log("Created \(type(of: buildServer), privacy: .public) at \(projectRoot)") - let buildServerAdapter = BuiltInBuildServerAdapter( - underlyingBuildServer: buildServer, - connectionToSourceKitLSP: connectionToSourceKitLSP, - buildServerHooks: buildServerHooks - ) - let connectionToBuildServer = LocalConnection( - receiverName: "\(type(of: buildServer)) for \(projectRoot.lastPathComponent)", - handler: buildServerAdapter - ) - return .builtIn(buildServerAdapter, connectionToBuildServer: connectionToBuildServer) - } - - /// Create a `BuildServerAdapter` that manages a build server of this kind and return a connection that can be used - /// to send messages to the build server. - func createBuildServerAdapter( - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions, - buildServerHooks: BuildServerHooks, - messagesToSourceKitLSPHandler: any MessageHandler - ) async -> BuildServerAdapter? { - switch self.kind { - case .externalBuildServer: - let buildServer = await orLog("Creating external build server") { - try await ExternalBuildServerAdapter( - projectRoot: projectRoot, - configPath: configPath, - messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler - ) - } - guard let buildServer else { - logger.log("Failed to create external build server at \(projectRoot)") - return nil - } - logger.log("Created external build server at \(projectRoot)") - return .external(buildServer) - case .jsonCompilationDatabase: - return await createBuiltInBuildServerAdapter( - messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler, - buildServerHooks: buildServerHooks - ) { connectionToSourceKitLSP in - try JSONCompilationDatabaseBuildServer( - configPath: configPath, - toolchainRegistry: toolchainRegistry, - connectionToSourceKitLSP: connectionToSourceKitLSP - ) - } - case .fixedCompilationDatabase: - return await createBuiltInBuildServerAdapter( - messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler, - buildServerHooks: buildServerHooks - ) { connectionToSourceKitLSP in - try FixedCompilationDatabaseBuildServer( - configPath: configPath, - connectionToSourceKitLSP: connectionToSourceKitLSP - ) - } - case .swiftPM: - #if !NO_SWIFTPM_DEPENDENCY - return await createBuiltInBuildServerAdapter( - messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler, - buildServerHooks: buildServerHooks - ) { connectionToSourceKitLSP in - try await SwiftPMBuildServer( - projectRoot: projectRoot, - toolchainRegistry: toolchainRegistry, - options: options, - connectionToSourceKitLSP: connectionToSourceKitLSP, - testHooks: buildServerHooks.swiftPMTestHooks - ) - } - #else - return nil - #endif - case .injected(let injector): - let connectionToSourceKitLSP = LocalConnection( - receiverName: "BuildServerManager for \(projectRoot.lastPathComponent)", - handler: messagesToSourceKitLSPHandler - ) - return .injected( - await injector(projectRoot, connectionToSourceKitLSP) - ) - } - } -} - -/// Entry point for all build server queries. -package actor BuildServerManager: QueueBasedMessageHandler { - package let messageHandlingHelper = QueueBasedMessageHandlerHelper( - signpostLoggingCategory: "build-server-manager-message-handling", - createLoggingScope: false - ) - - package let messageHandlingQueue = AsyncQueue() - - /// The path to the main configuration file (or directory) that this build server manages. - /// - /// Some examples: - /// - The path to `Package.swift` for SwiftPM packages - /// - The path to `compile_commands.json` for a JSON compilation database - /// - /// `nil` if the `BuildServerManager` does not have an underlying build server. - package let configPath: URL? - - /// The files for which the delegate has requested change notifications, ie. the files for which the delegate wants to - /// get `fileBuildSettingsChanged` and `filesDependenciesUpdated` callbacks. - private var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:] - - private var connectionToClient: BuildServerManagerConnectionToClient - - /// The build serer adapter that is used to answer build server queries. - private var buildServerAdapter: BuildServerAdapter? - - /// The build server adapter after initialization finishes. When sending messages to the BSP server, this should be - /// preferred over `buildServerAdapter` because no messages must be sent to the build server before initialization - /// finishes. - private var buildServerAdapterAfterInitialized: BuildServerAdapter? { - get async throws { - guard await initializeResult.value != nil else { - throw ResponseError.unknown("Build server failed to initialize") - } - return buildServerAdapter - } - } - - /// Provider of file to main file mappings. - /// - /// Force-unwrapped optional because initializing it requires access to `self`. - private var mainFilesProvider: Task! { - didSet { - // Must only be set once - precondition(oldValue == nil) - precondition(mainFilesProvider != nil) - } - } - - package func mainFilesProvider(as: T.Type) async -> T? { - guard let mainFilesProvider = mainFilesProvider else { - return nil - } - guard let index = await mainFilesProvider.value as? T else { - logger.fault("Expected the main files provider of the build server manager to be an `\(T.self)`") - return nil - } - return index - } - - /// Build server delegate that will receive notifications about setting changes, etc. - private weak var delegate: BuildServerManagerDelegate? - - private let buildSettingsLogger = BuildSettingsLogger() - - /// The list of toolchains that are available. - /// - /// Used to determine which toolchain to use for a given document. - private let toolchainRegistry: ToolchainRegistry - - private let options: SourceKitLSPOptions - - /// A task that stores the result of the `build/initialize` request once it is received. - /// - /// Force-unwrapped optional because initializing it requires access to `self`. - private var initializeResult: Task! { - didSet { - // Must only be set once - precondition(oldValue == nil) - precondition(initializeResult != nil) - } - } - - /// For tasks from the build server that should create a work done progress in the client, a mapping from the `TaskId` - /// in the build server to a `WorkDoneProgressManager` that manages that work done progress in the client. - private var workDoneProgressManagers: [TaskIdentifier: WorkDoneProgressManager] = [:] - - /// Debounces calls to `delegate.filesDependenciesUpdated`. - /// - /// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not - /// debounce `workspace/didChangeWatchedFiles` and sends a separate notification eg. for every file within a target as - /// it's being updated by a git checkout, which would cause other files within that target to receive a - /// `fileDependenciesUpdated` call once for every updated file within the target. - /// - /// Force-unwrapped optional because initializing it requires access to `self`. - private var filesDependenciesUpdatedDebouncer: Debouncer>! = nil { - didSet { - // Must only be set once - precondition(oldValue == nil) - precondition(filesDependenciesUpdatedDebouncer != nil) - } - } - - /// Debounces calls to `delegate.fileBuildSettingsChanged`. - /// - /// This helps in the following situation: A build server takes 5s to return build settings for a file and we have 10 - /// requests for those build settings coming in that time period. Once we get build settings, we get 10 calls to - /// `resultReceivedAfterTimeout` in `buildSettings(for:in:language:fallbackAfterTimeout:)`, all for the same document. - /// But calling `fileBuildSettingsChanged` once is totally sufficient. - /// - /// Force-unwrapped optional because initializing it requires access to `self`. - private var filesBuildSettingsChangedDebouncer: Debouncer>! = nil { - didSet { - // Must only be set once - precondition(oldValue == nil) - precondition(filesBuildSettingsChangedDebouncer != nil) - } - } - - private var cachedAdjustedSourceKitOptions = RequestCache() - - private var cachedBuildTargets = Cache() - - private var cachedTargetSources = RequestCache() - - /// `SourceFilesAndDirectories` is a global property that only gets reset when the build targets change and thus - /// has no real key. - private struct SourceFilesAndDirectoriesKey: Hashable {} - - private struct SourceFilesAndDirectories { - /// The source files in the workspace, ie. all `SourceItem`s that have `kind == .file`. - let files: [DocumentURI: SourceFileInfo] - - /// The source directories in the workspace, ie. all `SourceItem`s that have `kind == .directory`. - /// - /// `pathComponents` is the result of `key.fileURL?.pathComponents`. We frequently need these path components to - /// determine if a file is descendent of the directory and computing them from the `DocumentURI` is expensive. - let directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] - - /// Same as `Set(files.filter(\.value.isBuildable).keys)`. Pre-computed because we need this pretty frequently in - /// `SemanticIndexManager.filesToIndex`. - let buildableSourceFiles: Set - - internal init( - files: [DocumentURI: SourceFileInfo], - directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] - ) { - self.files = files - self.directories = directories - self.buildableSourceFiles = Set(files.filter(\.value.isBuildable).keys) - } - - } - - private let cachedSourceFilesAndDirectories = Cache() - - /// The latest map of copied file URIs to their original source locations. - /// - /// We don't use a `Cache` for this because we can provide reasonable functionality even without or with an - /// out-of-date copied file map - in the worst case we jump to a file in the build directory instead of the source - /// directory. - /// We don't want to block requests like definition on receiving up-to-date index information from the build server. - private var cachedCopiedFileMap: [DocumentURI: DocumentURI] = [:] - - /// The latest task to update the `cachedCopiedFileMap`. This allows us to cancel previous tasks to update the copied - /// file map when a new update is requested. - private var copiedFileMapUpdateTask: Task? - - /// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any. - package var initializationData: SourceKitInitializeBuildResponseData? { - get async { - return await initializeResult.value?.sourceKitData - } - } - - package init( - buildServerSpec: BuildServerSpec?, - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions, - connectionToClient: BuildServerManagerConnectionToClient, - buildServerHooks: BuildServerHooks, - createMainFilesProvider: - @escaping @Sendable ( - SourceKitInitializeBuildResponseData?, _ mainFilesChangedCallback: @escaping @Sendable () async -> Void - ) async -> MainFilesProvider? - ) async { - self.toolchainRegistry = toolchainRegistry - self.options = options - self.connectionToClient = connectionToClient - self.configPath = buildServerSpec?.configPath - self.buildServerAdapter = await buildServerSpec?.createBuildServerAdapter( - toolchainRegistry: toolchainRegistry, - options: options, - buildServerHooks: buildServerHooks, - messagesToSourceKitLSPHandler: WeakMessageHandler(self) - ) - - // The debounce duration of 500ms was chosen arbitrarily without any measurements. - self.filesDependenciesUpdatedDebouncer = Debouncer( - debounceDuration: .milliseconds(500), - combineResults: { $0.union($1) }, - makeCall: { [weak self] (filesWithUpdatedDependencies) in - guard let self, let delegate = await self.delegate else { - logger.fault("Not calling filesDependenciesUpdated because no delegate exists in SwiftPMBuildServer") - return - } - let changedWatchedFiles = await self.watchedFilesReferencing(mainFiles: filesWithUpdatedDependencies) - if !changedWatchedFiles.isEmpty { - await delegate.filesDependenciesUpdated(changedWatchedFiles) - } - } - ) - - // We don't need a large debounce duration here. It just needs to be big enough to accumulate - // `resultReceivedAfterTimeout` calls for the same document (see comment on `filesBuildSettingsChangedDebouncer`). - // Since they should all come in at the same time, a couple of milliseconds should be sufficient here, an 20ms be - // plenty while still not causing a noticeable delay to the user. - self.filesBuildSettingsChangedDebouncer = Debouncer( - debounceDuration: .milliseconds(20), - combineResults: { $0.union($1) }, - makeCall: { [weak self] (filesWithChangedBuildSettings) in - guard let self, let delegate = await self.delegate else { - logger.fault("Not calling fileBuildSettingsChanged because no delegate exists in SwiftPMBuildServer") - return - } - if !filesWithChangedBuildSettings.isEmpty { - await delegate.fileBuildSettingsChanged(filesWithChangedBuildSettings) - } - } - ) - - // TODO: Forward file watch patterns from this initialize request to the client - // (https://github.com/swiftlang/sourcekit-lsp/issues/1671) - initializeResult = Task { () -> InitializeBuildResponse? in - guard let buildServerAdapter else { - return nil - } - guard let buildServerSpec else { - logger.fault("If we have a connectionToBuildServer, we must have had a buildServerSpec") - return nil - } - let initializeResponse: InitializeBuildResponse? - do { - initializeResponse = try await buildServerAdapter.send( - InitializeBuildRequest( - displayName: "SourceKit-LSP", - version: "", - bspVersion: "2.2.0", - rootUri: URI(buildServerSpec.projectRoot), - capabilities: BuildClientCapabilities(languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift]) - ) - ) - } catch { - initializeResponse = nil - let errorMessage: String - if let error = error as? ResponseError { - errorMessage = error.message - } else { - errorMessage = "\(error)" - } - connectionToClient.send( - ShowMessageNotification(type: .error, message: "Failed to initialize build server: \(errorMessage)") - ) - } - - if let initializeResponse, !(initializeResponse.sourceKitData?.sourceKitOptionsProvider ?? false), - case .external(let externalBuildServerAdapter) = buildServerAdapter - { - // The BSP server does not support the pull-based settings model. Inject a `LegacyBuildServerBuildServer` that - // offers the pull-based model to `BuildServerManager` and uses the push-based model to get build settings from - // the build server. - logger.log("Launched a legacy BSP server. Using push-based build settings model.") - let legacyBuildServer = await LegacyBuildServer( - projectRoot: buildServerSpec.projectRoot, - configPath: buildServerSpec.configPath, - initializationData: initializeResponse, - externalBuildServerAdapter - ) - let adapter = BuiltInBuildServerAdapter( - underlyingBuildServer: legacyBuildServer, - connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP, - buildServerHooks: buildServerHooks - ) - let connectionToBuildSerer = LocalConnection(receiverName: "Legacy BSP server", handler: adapter) - self.buildServerAdapter = .builtIn(adapter, connectionToBuildServer: connectionToBuildSerer) - } - Task { - var filesToWatch = initializeResponse?.sourceKitData?.watchers ?? [] - filesToWatch.append(FileSystemWatcher(globPattern: "**/*.swift", kind: [.change])) - if !options.backgroundIndexingOrDefault { - filesToWatch.append(FileSystemWatcher(globPattern: "**/*.swiftmodule", kind: [.create, .change, .delete])) - } - await connectionToClient.watchFiles(filesToWatch) - } - await buildServerAdapter.send(OnBuildInitializedNotification()) - return initializeResponse - } - self.mainFilesProvider = Task { - await createMainFilesProvider(initializationData) { [weak self] in - await self?.mainFilesChanged() - } - } - } - - /// Explicitly shut down the build server. - /// - /// The build server is automatically shut down using a background task when `BuildServerManager` is deallocated. - /// This, however, leads to possible race conditions where the shutdown task might not finish before the test is done, - /// which could result in the connection being reported as a leak. To avoid this problem, we want to explicitly shut - /// down the build server when the `SourceKitLSPServer` gets shut down. - package func shutdown() async { - // Clear any pending work done progresses from the build server. - self.workDoneProgressManagers.removeAll() - guard let buildServerAdapter = try? await self.buildServerAdapterAfterInitialized else { - return - } - await orLog("Sending shutdown request to build server") { - // Give the build server 2 seconds to shut down by itself. If it doesn't shut down within that time, terminate it. - try await withTimeout(.seconds(2)) { - _ = try await buildServerAdapter.send(BuildShutdownRequest()) - await buildServerAdapter.send(OnBuildExitNotification()) - } - } - if case .external(let externalBuildServerAdapter) = buildServerAdapter { - await orLog("Terminating external build server") { - // Give the build server 1 second to exit after receiving the `build/exit` notification. If it doesn't exit - // within that time, terminate it. - try await externalBuildServerAdapter.terminateIfRunning(after: .seconds(1)) - } - } - self.buildServerAdapter = nil - } - - deinit { - // Shut down the build server before closing the connection to it - Task { [buildServerAdapter, initializeResult] in - guard let buildServerAdapter else { - return - } - // We are accessing the raw connection to the build server, so we need to ensure that it has been initialized here - _ = await initializeResult?.value - await orLog("Sending shutdown request to build server") { - _ = try await buildServerAdapter.send(BuildShutdownRequest()) - await buildServerAdapter.send(OnBuildExitNotification()) - } - } - } - - /// - Note: Needed because `BuildSererManager` is created before `Workspace` is initialized and `Workspace` needs to - /// create the `BuildServerManager`, then initialize itself and then set itself as the delegate. - package func setDelegate(_ delegate: BuildServerManagerDelegate?) { - self.delegate = delegate - } - - // MARK: Handling messages from the build server - - package func handle(notification: some NotificationType) async { - switch notification { - case let notification as OnBuildTargetDidChangeNotification: - await self.didChangeBuildTarget(notification: notification) - case let notification as OnBuildLogMessageNotification: - await self.logMessage(notification: notification) - case let notification as TaskFinishNotification: - await self.taskFinish(notification: notification) - case let notification as TaskProgressNotification: - await self.taskProgress(notification: notification) - case let notification as TaskStartNotification: - await self.taskStart(notification: notification) - default: - logger.error("Ignoring unknown notification \(type(of: notification).method)") - } - } - - package func handle( - request: Request, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) async { - let request = RequestAndReply(request, reply: reply) - switch request { - default: - await request.reply { throw ResponseError.methodNotFound(Request.method) } - } - } - - private func didChangeBuildTarget(notification: OnBuildTargetDidChangeNotification) async { - let changedTargets: Set? = - if let changes = notification.changes { - Set(changes.map(\.target)) - } else { - nil - } - await self.buildTargetsDidChange(.didChangeBuildTargets(changedTargets: changedTargets)) - } - - private enum BuildTargetsChange { - case didChangeBuildTargets(changedTargets: Set?) - case buildTargetsReceivedResultAfterTimeout( - request: WorkspaceBuildTargetsRequest, - newResult: [BuildTargetIdentifier: BuildTargetInfo] - ) - case sourceFilesReceivedResultAfterTimeout( - request: BuildTargetSourcesRequest, - newResult: BuildTargetSourcesResponse - ) - } - - /// Update the cached state in `BuildServerManager` because new data was received from the BSP server. - /// - /// This handles a few seemingly unrelated reasons to ensure that we think about which caches to invalidate in the - /// other scenarios as well, when making changes in here. - private func buildTargetsDidChange(_ stateChange: BuildTargetsChange) async { - let changedTargets: Set? - - switch stateChange { - case .didChangeBuildTargets(let changedTargetsValue): - changedTargets = changedTargetsValue - self.cachedAdjustedSourceKitOptions.clear(isolation: self) { cacheKey in - guard let changedTargets else { - // All targets might have changed - return true - } - return changedTargets.contains(cacheKey.target) - } - self.cachedBuildTargets.clearAll(isolation: self) - self.cachedTargetSources.clear(isolation: self) { cacheKey in - guard let changedTargets else { - // All targets might have changed - return true - } - return !changedTargets.intersection(cacheKey.targets).isEmpty - } - case .buildTargetsReceivedResultAfterTimeout(let request, let newResult): - changedTargets = nil - - // Caches not invalidated: - // - cachedAdjustedSourceKitOptions: We would not have requested SourceKit options for targets that we didn't - // know about. Even if we did, the build server now telling us about the target should not change the options of - // the file within the target - // - cachedTargetSources: Similar to cachedAdjustedSourceKitOptions, we would not have requested sources for - // targets that we didn't know about and if we did, they wouldn't be affected - self.cachedBuildTargets.set(request, to: newResult) - case .sourceFilesReceivedResultAfterTimeout(let request, let newResult): - changedTargets = Set(request.targets) - - // Caches not invalidated: - // - cachedAdjustedSourceKitOptions: Same as for buildTargetsReceivedResultAfterTimeout. - // - cachedBuildTargets: Getting a result for the source files in a target doesn't change anything about the - // target's existence. - self.cachedTargetSources.set(request, to: newResult) - } - // Clear caches that capture global state and are affected by all changes - self.cachedSourceFilesAndDirectories.clearAll(isolation: self) - self.scheduleRecomputeCopyFileMap() - - await delegate?.buildTargetsChanged(changedTargets) - await filesBuildSettingsChangedDebouncer.scheduleCall(Set(watchedFiles.keys)) - } - - private func logMessage(notification: OnBuildLogMessageNotification) async { - await connectionToClient.waitUntilInitialized() - let type: WindowMessageType = - switch notification.type { - case .error: .error - case .warning: .warning - case .info: .info - case .log: .log - } - connectionToClient.logMessageToIndexLog( - message: notification.message, - type: type, - structure: notification.lspStructure - ) - } - - private func taskStart(notification: TaskStartNotification) async { - guard let workDoneProgressTitle = WorkDoneProgressTask(fromLSPAny: notification.data)?.title, - await connectionToClient.clientSupportsWorkDoneProgress - else { - return - } - - guard workDoneProgressManagers[notification.taskId.id] == nil else { - logger.error("Client is already tracking a work done progress for task \(notification.taskId.id)") - return - } - workDoneProgressManagers[notification.taskId.id] = WorkDoneProgressManager( - connectionToClient: connectionToClient, - waitUntilClientInitialized: connectionToClient.waitUntilInitialized, - tokenPrefix: notification.taskId.id, - initialDebounce: options.workDoneProgressDebounceDurationOrDefault, - title: workDoneProgressTitle - ) - } - - private func taskProgress(notification: TaskProgressNotification) async { - guard let progressManager = workDoneProgressManagers[notification.taskId.id] else { - return - } - let percentage: Int? = - if let progress = notification.progress, let total = notification.total { - Int((Double(progress) / Double(total) * 100).rounded()) - } else { - nil - } - await progressManager.update(message: notification.message, percentage: percentage) - } - - private func taskFinish(notification: TaskFinishNotification) async { - guard let progressManager = workDoneProgressManagers[notification.taskId.id] else { - return - } - await progressManager.end() - workDoneProgressManagers[notification.taskId.id] = nil - } - - // MARK: Build server queries - - /// Returns the toolchain that should be used to process the given target. - /// - /// If `target` is `nil` or the build server does not explicitly specify a toolchain for this target, the preferred - /// toolchain for the given language is returned. - package func toolchain( - for target: BuildTargetIdentifier?, - language: Language - ) async -> Toolchain? { - let toolchainPath = await orLog("Getting toolchain from build targets") { () -> URL? in - guard let target else { - return nil - } - let targets = try await self.buildTargets() - guard let target = targets[target]?.target else { - logger.error("Failed to find target \(target.forLogging) to determine toolchain") - return nil - } - guard let toolchain = target.sourceKitData?.toolchain else { - return nil - } - guard let toolchainUrl = toolchain.fileURL else { - logger.error("Toolchain is not a file URL") - return nil - } - return toolchainUrl - } - if let toolchainPath { - if let toolchain = await self.toolchainRegistry.toolchain(withPath: toolchainPath) { - return toolchain - } - logger.error("Toolchain at \(toolchainPath) not registered in toolchain registry.") - } - - switch language { - case .swift, .markdown, .tutorial: - return await toolchainRegistry.preferredToolchain(containing: [\.sourcekitd, \.swift, \.swiftc]) - case .c, .cpp, .objective_c, .objective_cpp: - return await toolchainRegistry.preferredToolchain(containing: [\.clang, \.clangd]) - default: - return nil - } - } - - /// Ask the build server if it explicitly specifies a language for this document. Return `nil` if it does not. - private func languageInferredFromBuildServer( - for document: DocumentURI, - in target: BuildTargetIdentifier - ) async throws -> Language? { - let sourcesItems = try await self.sourceFiles(in: [target]) - let sourceFiles = sourcesItems.flatMap(\.sources) - var result: Language? = nil - for sourceFile in sourceFiles where sourceFile.uri == document { - guard let language = sourceFile.sourceKitData?.language else { - continue - } - if result != nil && result != language { - logger.error("Conflicting languages for \(document.forLogging) in \(target)") - return nil - } - result = language - } - return result - } - - /// Returns the language that a document should be interpreted in for background tasks where the editor doesn't - /// specify the document's language. - package func defaultLanguage(for document: DocumentURI, in target: BuildTargetIdentifier) async -> Language? { - let languageFromBuildServer = await orLog("Getting source files to determine default language") { - try await languageInferredFromBuildServer(for: document, in: target) - } - return languageFromBuildServer ?? Language(inferredFromFileExtension: document) - } - - /// Returns the language that a document should be interpreted in for background tasks where the editor doesn't - /// specify the document's language. - /// - /// If the language could not be determined, this method throws an error. - package func defaultLanguageInCanonicalTarget(for document: DocumentURI) async throws -> Language { - struct UnableToInferLanguage: Error, CustomStringConvertible { - let document: DocumentURI - var description: String { "Unable to infer language for \(document)" } - } - - guard let canonicalTarget = await self.canonicalTarget(for: document) else { - guard let language = Language(inferredFromFileExtension: document) else { - throw UnableToInferLanguage(document: document) - } - return language - } - guard let language = await defaultLanguage(for: document, in: canonicalTarget) else { - throw UnableToInferLanguage(document: document) - } - return language - } - - /// Retrieve information about the given source file within the build server. - package func sourceFileInfo(for document: DocumentURI) async -> SourceFileInfo? { - return await orLog("Getting targets for source file") { - var result: SourceFileInfo? = nil - let filesAndDirectories = try await sourceFilesAndDirectories() - if let info = filesAndDirectories.files[document] { - result = result?.merging(info) ?? info - } - if !filesAndDirectories.directories.isEmpty, let documentPathComponents = document.fileURL?.pathComponents { - for (_, (directoryPathComponents, info)) in filesAndDirectories.directories { - guard let directoryPathComponents else { - continue - } - if isDescendant(documentPathComponents, of: directoryPathComponents) { - result = result?.merging(info) ?? info - } - } - } - return result - } - } - - /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to - /// the original source file. - package func locationAdjustedForCopiedFiles(_ location: Location) -> Location { - guard let originalUri = cachedCopiedFileMap[location.uri] else { - return location - } - // If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents - // of the lines touched by the location match and only return the original URI if they do. For now, we avoid this - // check due to its performance cost of reading files from disk. - return Location(uri: originalUri, range: location.range) - } - - /// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to - /// the original source file. - package func locationsAdjustedForCopiedFiles(_ locations: [Location]) -> [Location] { - return locations.map { locationAdjustedForCopiedFiles($0) } - } - - @discardableResult - package func scheduleRecomputeCopyFileMap() -> Task { - let task = Task { [previousUpdateTask = copiedFileMapUpdateTask] in - previousUpdateTask?.cancel() - await orLog("Re-computing copy file map") { - let sourceFilesAndDirectories = try await self.sourceFilesAndDirectories() - try Task.checkCancellation() - var copiedFileMap: [DocumentURI: DocumentURI] = [:] - for (file, fileInfo) in sourceFilesAndDirectories.files { - for copyDestination in fileInfo.copyDestinations { - copiedFileMap[copyDestination] = file - } - } - self.cachedCopiedFileMap = copiedFileMap - } - } - copiedFileMapUpdateTask = task - return task - } - - /// Returns all the targets that the document is part of. - package func targets(for document: DocumentURI) async -> [BuildTargetIdentifier] { - guard let targets = await sourceFileInfo(for: document)?.targets else { - return [] - } - return Array(targets) - } - - /// Returns the `BuildTargetIdentifier` that should be used for semantic functionality of the given document. - package func canonicalTarget(for document: DocumentURI) async -> BuildTargetIdentifier? { - // Sort the targets to deterministically pick the same `BuildTargetIdentifier` every time. - // We could allow the user to specify a preference of one target over another. - return await targets(for: document) - .sorted { $0.uri.stringValue < $1.uri.stringValue } - .first - } - - /// Returns the target's module name as parsed from the `BuildTargetIdentifier`'s compiler arguments. - package func moduleName(for document: DocumentURI, in target: BuildTargetIdentifier) async -> String? { - guard let language = await self.defaultLanguage(for: document, in: target), - let buildSettings = await buildSettings( - for: document, - in: target, - language: language, - fallbackAfterTimeout: false - ) - else { - return nil - } - - switch language { - case .swift: - // Module name is specified in the form -module-name MyLibrary - guard let moduleNameFlagIndex = buildSettings.compilerArguments.lastIndex(of: "-module-name") else { - return nil - } - return buildSettings.compilerArguments[safe: moduleNameFlagIndex + 1] - case .objective_c: - // Specified in the form -fmodule-name=MyLibrary - guard - let moduleNameArgument = buildSettings.compilerArguments.last(where: { $0.starts(with: "-fmodule-name=") }), - let moduleName = moduleNameArgument.split(separator: "=").last - else { - return nil - } - return String(moduleName) - default: - return nil - } - } - - /// Returns the build settings for `document` from `buildServer`. - /// - /// Implementation detail of `buildSettings(for:language:)`. - private func buildSettingsFromBuildServer( - for document: DocumentURI, - in target: BuildTargetIdentifier, - language: Language - ) async throws -> FileBuildSettings? { - guard let buildServerAdapter = try await buildServerAdapterAfterInitialized else { - return nil - } - let request = TextDocumentSourceKitOptionsRequest( - textDocument: TextDocumentIdentifier(document), - target: target, - language: language - ) - let response = try await cachedAdjustedSourceKitOptions.get(request, isolation: self) { request in - let options = try await buildServerAdapter.send(request) - switch language.semanticKind { - case .swift: - return options?.adjustArgsForSemanticSwiftFunctionality(fileToIndex: document) - case .clang: - return options?.adjustingArgsForSemanticClangFunctionality() - default: - return options - } - } - - guard let response else { - return nil - } - - return FileBuildSettings( - compilerArguments: response.compilerArguments, - workingDirectory: response.workingDirectory, - language: language, - data: response.data, - isFallback: false - ) - } - - /// Returns the build settings for the given file in the given target. - /// - /// If no target is given, this always returns fallback build settings. - /// - /// Only call this method if it is known that `document` is a main file. Prefer `buildSettingsInferredFromMainFile` - /// otherwise. If `document` is a header file, this will most likely return fallback settings because header files - /// don't have build settings by themselves. - /// - /// If `fallbackAfterTimeout` is true fallback build settings will be returned if no build settings can be found in - /// `SourceKitLSPOptions.buildSettingsTimeoutOrDefault`. - package func buildSettings( - for document: DocumentURI, - in target: BuildTargetIdentifier?, - language: Language, - fallbackAfterTimeout: Bool - ) async -> FileBuildSettings? { - if let target { - let buildSettingsFromBuildServer = await orLog("Getting build settings") { - if fallbackAfterTimeout { - try await withTimeout(options.buildSettingsTimeoutOrDefault) { - return try await self.buildSettingsFromBuildServer(for: document, in: target, language: language) - } resultReceivedAfterTimeout: { _ in - await self.filesBuildSettingsChangedDebouncer.scheduleCall([document]) - } - } else { - try await self.buildSettingsFromBuildServer(for: document, in: target, language: language) - } - } - if let buildSettingsFromBuildServer { - return buildSettingsFromBuildServer - } - } - - guard - var settings = fallbackBuildSettings( - for: document, - language: language, - options: options.fallbackBuildSystemOrDefault - ) - else { - return nil - } - if buildServerAdapter == nil { - // If there is no build server and we only have the fallback build server, we will never get real build settings. - // Consider the build settings non-fallback. - settings.isFallback = false - } - return settings - } - - /// Returns the build settings for the given document. - /// - /// If the document doesn't have builds settings by itself, eg. because it is a C header file, the build settings will - /// be inferred from the primary main file of the document. In practice this means that we will compute the build - /// settings of a C file that includes the header and replace any file references to that C file in the build settings - /// by the header file. - /// - /// When a target is passed in, the build settings for the document, interpreted as part of that target, are returned, - /// otherwise a canonical target is inferred for the source file. - /// - /// If no language is passed, this method tries to infer the language of the document from the build server. If that - /// fails, it returns `nil`. - package func buildSettingsInferredFromMainFile( - for document: DocumentURI, - target explicitlyRequestedTarget: BuildTargetIdentifier? = nil, - language: Language?, - fallbackAfterTimeout: Bool - ) async -> FileBuildSettings? { - func mainFileAndSettings( - basedOn document: DocumentURI - ) async -> (mainFile: DocumentURI, settings: FileBuildSettings)? { - let mainFile = await self.mainFile(for: document, language: language) - let settings: FileBuildSettings? = await orLog("Getting build settings") { - let target = - if let explicitlyRequestedTarget { - explicitlyRequestedTarget - } else { - try await withTimeout(options.buildSettingsTimeoutOrDefault) { - await self.canonicalTarget(for: mainFile) - } resultReceivedAfterTimeout: { _ in - await self.filesBuildSettingsChangedDebouncer.scheduleCall([document]) - } - } - var languageForFile: Language - if let language { - languageForFile = language - } else if let target, let language = await self.defaultLanguage(for: mainFile, in: target) { - languageForFile = language - } else if let language = Language(inferredFromFileExtension: mainFile) { - languageForFile = language - } else { - // We don't know the language as which to interpret the document, so we can't ask the build server for its - // settings. - return nil - } - return await self.buildSettings( - for: mainFile, - in: target, - language: languageForFile, - fallbackAfterTimeout: fallbackAfterTimeout - ) - } - guard let settings else { - return nil - } - return (mainFile, settings) - } - - var settings: FileBuildSettings? - var mainFile: DocumentURI? - if let mainFileAndSettings = await mainFileAndSettings(basedOn: document) { - (mainFile, settings) = mainFileAndSettings - } - if settings?.isFallback ?? true, let symlinkTarget = document.symlinkTarget, - let mainFileAndSettings = await mainFileAndSettings(basedOn: symlinkTarget) - { - (mainFile, settings) = mainFileAndSettings - } - guard var settings, let mainFile else { - return nil - } - - if mainFile != document { - // If the main file isn't the file itself, we need to patch the build settings - // to reference `document` instead of `mainFile`. - settings = settings.patching(newFile: document, originalFile: mainFile) - } - - await buildSettingsLogger.log(settings: settings, for: document) - return settings - } - - package func waitForUpToDateBuildGraph() async { - await orLog("Waiting for build server updates") { - let _: VoidResponse? = try await buildServerAdapterAfterInitialized?.send( - WorkspaceWaitForBuildSystemUpdatesRequest() - ) - } - // Handle any messages the build server might have sent us while updating. - await messageHandlingQueue.async(metadata: .stateChange) {}.valuePropagatingCancellation - - // Ensure that we send out all delegate calls so that everybody is informed about the changes. - await filesBuildSettingsChangedDebouncer.flush() - await filesDependenciesUpdatedDebouncer.flush() - } - - /// The root targets of the project have depth of 0 and all target dependencies have a greater depth than the target - /// itself. - private func targetDepthsAndDependents( - for buildTargets: [BuildTarget] - ) -> (depths: [BuildTargetIdentifier: Int], dependents: [BuildTargetIdentifier: Set]) { - var nonRoots: Set = [] - for buildTarget in buildTargets { - nonRoots.formUnion(buildTarget.dependencies) - } - let targetsById = Dictionary(elements: buildTargets, keyedBy: \.id) - var dependents: [BuildTargetIdentifier: Set] = [:] - var depths: [BuildTargetIdentifier: Int] = [:] - let rootTargets = buildTargets.filter { !nonRoots.contains($0.id) } - var worksList: [(target: BuildTargetIdentifier, depth: Int)] = rootTargets.map { ($0.id, 0) } - while let (target, depth) = worksList.popLast() { - depths[target] = max(depths[target, default: 0], depth) - for dependency in targetsById[target]?.dependencies ?? [] { - dependents[dependency, default: []].insert(target) - // Check if we have already recorded this target with a greater depth, in which case visiting it again will - // not increase its depth or any of its children. - if depths[dependency, default: 0] < depth + 1 { - worksList.append((dependency, depth + 1)) - } - } - } - return (depths, dependents) - } - - /// Sort the targets so that low-level targets occur before high-level targets. - /// - /// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows - /// index data to be available earlier. - package func topologicalSort(of targets: [BuildTargetIdentifier]) async throws -> [BuildTargetIdentifier] { - guard let buildTargets = await orLog("Getting build targets for topological sort", { try await buildTargets() }) - else { - return targets.sorted { $0.uri.stringValue < $1.uri.stringValue } - } - - return targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in - let lhsDepth = buildTargets[lhs]?.depth ?? 0 - let rhsDepth = buildTargets[rhs]?.depth ?? 0 - if lhsDepth != rhsDepth { - return lhsDepth > rhsDepth - } - return lhs.uri.stringValue < rhs.uri.stringValue - } - } - - /// Returns the list of targets that might depend on the given target and that need to be re-prepared when a file in - /// `target` is modified. - package func targets(dependingOn targetIds: some Collection) async -> [BuildTargetIdentifier] { - guard - let buildTargets = await orLog("Getting build targets for dependents", { try await self.buildTargets() }) - else { - return [] - } - - return transitiveClosure(of: targetIds, successors: { buildTargets[$0]?.dependents ?? [] }) - .sorted { $0.uri.stringValue < $1.uri.stringValue } - } - - package func prepare(targets: Set) async throws { - let _: VoidResponse? = try await buildServerAdapterAfterInitialized?.send( - BuildTargetPrepareRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) - ) - await orLog("Calling fileDependenciesUpdated") { - let filesInPreparedTargets = try await self.sourceFiles(in: targets).flatMap(\.sources).map(\.uri) - await filesDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets)) - } - } - - package func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - let mainFile = await mainFile(for: uri, language: language) - self.watchedFiles[uri] = (mainFile, language) - } - - package func unregisterForChangeNotifications(for uri: DocumentURI) async { - self.watchedFiles[uri] = nil - } - - private func buildTargets() async throws -> [BuildTargetIdentifier: BuildTargetInfo] { - let request = WorkspaceBuildTargetsRequest() - let result = try await cachedBuildTargets.get(request, isolation: self) { request in - let result = try await withTimeout(self.options.buildServerWorkspaceRequestsTimeoutOrDefault) { - guard let buildServerAdapter = try await self.buildServerAdapterAfterInitialized else { - return [:] - } - let buildTargets = try await buildServerAdapter.send(request).targets - let (depths, dependents) = await self.targetDepthsAndDependents(for: buildTargets) - var result: [BuildTargetIdentifier: BuildTargetInfo] = [:] - result.reserveCapacity(buildTargets.count) - for buildTarget in buildTargets { - guard result[buildTarget.id] == nil else { - logger.error("Found two targets with the same ID \(buildTarget.id)") - continue - } - let depth: Int - if let d = depths[buildTarget.id] { - depth = d - } else { - logger.fault("Did not compute depth for target \(buildTarget.id)") - depth = 0 - } - result[buildTarget.id] = BuildTargetInfo( - target: buildTarget, - depth: depth, - dependents: dependents[buildTarget.id] ?? [] - ) - } - return result - } resultReceivedAfterTimeout: { newResult in - await self.buildTargetsDidChange( - .buildTargetsReceivedResultAfterTimeout(request: request, newResult: newResult) - ) - } - guard let result else { - logger.error("Failed to get targets of workspace within timeout") - return [:] - } - return result - } - return result - } - - package func buildTarget(named identifier: BuildTargetIdentifier) async -> BuildTarget? { - return await orLog("Getting built target with ID") { - try await buildTargets()[identifier]?.target - } - } - - package func sourceFiles(in targets: Set) async throws -> [SourcesItem] { - guard !targets.isEmpty else { - return [] - } - - let request = BuildTargetSourcesRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue }) - - // If we have a cached request for a superset of the targets, serve the result from that cache entry. - let fromSuperset = await orLog("Getting source files from superset request") { - try await cachedTargetSources.getDerived( - isolation: self, - request, - canReuseKey: { targets.isSubset(of: $0.targets) }, - transform: { BuildTargetSourcesResponse(items: $0.items.filter { targets.contains($0.target) }) } - ) - } - if let fromSuperset { - return fromSuperset.items - } - - let response = try await cachedTargetSources.get(request, isolation: self) { request in - try await withTimeout(self.options.buildServerWorkspaceRequestsTimeoutOrDefault) { - guard let buildServerAdapter = try await self.buildServerAdapterAfterInitialized else { - return BuildTargetSourcesResponse(items: []) - } - return try await buildServerAdapter.send(request) - } resultReceivedAfterTimeout: { newResult in - await self.buildTargetsDidChange(.sourceFilesReceivedResultAfterTimeout(request: request, newResult: newResult)) - } ?? BuildTargetSourcesResponse(items: []) - } - return response.items - } - - /// Return the output paths for all source files known to the build server. - /// - /// See `SourceKitSourceItemData.outputFilePath` for details. - package func outputPathsInAllTargets() async throws -> [String] { - return try await outputPaths(in: Set(buildTargets().map(\.key))) - } - - /// For all source files in the given targets, return their output file paths. - /// - /// See `BuildTargetOutputPathsRequest` for details. - package func outputPaths(in targets: Set) async throws -> [String] { - return try await sourceFiles(in: targets).flatMap(\.sources).compactMap(\.sourceKitData?.outputPath) - } - - /// Returns all source files in the project. - /// - /// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means. - package func sourceFiles(includeNonBuildableFiles: Bool) async throws -> [DocumentURI: SourceFileInfo] { - let files = try await sourceFilesAndDirectories().files - if includeNonBuildableFiles { - return files - } else { - return files.filter(\.value.isBuildable) - } - } - - /// Returns all source files in the project that are considered buildable. - /// - /// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means. - package func buildableSourceFiles() async throws -> Set { - return try await sourceFilesAndDirectories().buildableSourceFiles - } - - /// Get all files and directories that are known to the build server, ie. that are returned by a `buildTarget/sources` - /// request for any target in the project. - /// - /// - Important: This method returns both buildable and non-buildable source files. Callers need to check - /// `SourceFileInfo.isBuildable` if they are only interested in buildable source files. - private func sourceFilesAndDirectories() async throws -> SourceFilesAndDirectories { - - return try await cachedSourceFilesAndDirectories.get( - SourceFilesAndDirectoriesKey(), - isolation: self - ) { key in - let targets = try await self.buildTargets() - let sourcesItems = try await self.sourceFiles(in: Set(targets.keys)) - - var files: [DocumentURI: SourceFileInfo] = [:] - var directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] = [:] - for sourcesItem in sourcesItems { - let target = targets[sourcesItem.target]?.target - let isPartOfRootProject = !(target?.tags.contains(.dependency) ?? false) - let mayContainTests = target?.tags.contains(.test) ?? true - for sourceItem in sourcesItem.sources { - let sourceKitData = sourceItem.sourceKitData - let outputPath: OutputPath? = - if !(await self.initializationData?.outputPathsProvider ?? false) { - .notSupported - } else if let outputPath = sourceKitData?.outputPath { - .path(outputPath) - } else { - nil - } - let info = SourceFileInfo( - targetsToOutputPath: [sourcesItem.target: outputPath], - isPartOfRootProject: isPartOfRootProject, - mayContainTests: mayContainTests, - isBuildable: !(target?.tags.contains(.notBuildable) ?? false) - && (sourceKitData?.kind ?? .source) == .source, - copyDestinations: Set(sourceKitData?.copyDestinations ?? []) - ) - switch sourceItem.kind { - case .file: - files[sourceItem.uri] = info.merging(files[sourceItem.uri]) - case .directory: - directories[sourceItem.uri] = ( - sourceItem.uri.fileURL?.pathComponents, info.merging(directories[sourceItem.uri]?.info) - ) - } - } - } - return SourceFilesAndDirectories(files: files, directories: directories) - } - } - - package func testFiles() async throws -> [DocumentURI] { - return try await sourceFiles(includeNonBuildableFiles: false).compactMap { (uri, info) -> DocumentURI? in - guard info.isPartOfRootProject, info.mayContainTests else { - return nil - } - return uri - } - } - - private func watchedFilesReferencing(mainFiles: Set) -> Set { - return Set( - watchedFiles.compactMap { (watchedFile, mainFileAndLanguage) in - if mainFiles.contains(mainFileAndLanguage.mainFile) { - return watchedFile - } else { - return nil - } - } - ) - } - - /// Return the main file that should be used to get build settings for `uri`. - /// - /// For Swift or normal C files, this will be the file itself. For header files, we pick a main file that includes the - /// header since header files don't have build settings by themselves. - /// - /// `language` is a hint of the document's language to speed up the `main` file lookup. Passing `nil` if the language - /// is unknown should always be safe. - package func mainFile(for uri: DocumentURI, language: Language?, useCache: Bool = true) async -> DocumentURI { - if language == .swift { - // Swift doesn't have main files. Skip the main file provider query. - return uri - } - if useCache, let mainFile = self.watchedFiles[uri]?.mainFile { - // Performance optimization: We did already compute the main file and have - // it cached. We can just return it. - return mainFile - } - - let mainFiles = await mainFiles(containing: uri) - if mainFiles.contains(uri) { - // If the main files contain the file itself, prefer to use that one - return uri - } else if let mainFile = mainFiles.min(by: { $0.pseudoPath < $1.pseudoPath }) { - // Pick the lexicographically first main file if it exists. - // This makes sure that picking a main file is deterministic. - return mainFile - } else { - return uri - } - } - - /// Returns all main files that include the given document. - /// - /// On Darwin platforms, this also performs the following normalization: indexstore-db by itself returns realpaths - /// but the build server might be using standardized Darwin paths (eg. realpath is `/private/tmp` but the standardized - /// path is `/tmp`). If the realpath that indexstore-db returns could not be found in the build server's source files - /// but the standardized path is part of the source files, return the standardized path instead. - package func mainFiles(containing uri: DocumentURI) async -> [DocumentURI] { - guard let mainFilesProvider = await mainFilesProvider.value else { - return [uri] - } - let mainFiles = Array(await mainFilesProvider.mainFiles(containing: uri, crossLanguage: false)) - if Platform.current == .darwin { - if let buildableSourceFiles = try? await self.buildableSourceFiles() { - return mainFiles.map { mainFile in - if mainFile == uri { - // Do not apply the standardized file normalization to the source file itself. Otherwise we would get the - // following behavior: - // - We have a build server that uses standardized file paths and index a file as /tmp/test.c - // - We are asking for the main files of /private/tmp/test.c - // - Since indexstore-db uses realpath for everything, we find the unit for /tmp/test.c as a unit containg - // /private/tmp/test.c, which has /private/tmp/test.c as the main file. - // - If we applied the path normalization, we would normalize /private/tmp/test.c to /tmp/test.c, thus - // reporting that /tmp/test.c is a main file containing /private/tmp/test.c, - // But that doesn't make sense (it would, in fact cause us to treat /private/tmp/test.c as a header file that - // we should index using /tmp/test.c as a main file. - return mainFile - } - if buildableSourceFiles.contains(mainFile) { - return mainFile - } - guard let fileURL = mainFile.fileURL else { - return mainFile - } - let standardized = DocumentURI(fileURL.standardizedFileURL) - if buildableSourceFiles.contains(standardized) { - return standardized - } - return mainFile - } - } - } - return mainFiles - } - - /// Returns the main file used for `uri`, if this is a registered file. - /// - /// For testing purposes only. - package func cachedMainFile(for uri: DocumentURI) -> DocumentURI? { - return self.watchedFiles[uri]?.mainFile - } - - // MARK: Informing BuildSererManager about changes - - package func filesDidChange(_ events: [FileEvent]) async { - if let buildServerAdapter = try? await buildServerAdapterAfterInitialized { - await buildServerAdapter.send(OnWatchedFilesDidChangeNotification(changes: events)) - } - - var targetsWithUpdatedDependencies: Set = [] - // If a Swift file within a target is updated, reload all the other files within the target since they might be - // referring to a function in the updated file. - let targetsWithChangedSwiftFiles = - await events - .filter { Language(inferredFromFileExtension: $0.uri) == .swift } - .asyncFlatMap { await self.targets(for: $0.uri) } - targetsWithUpdatedDependencies.formUnion(targetsWithChangedSwiftFiles) - - // If a `.swiftmodule` file is updated, this means that we have performed a build / are - // performing a build and files that depend on this module have updated dependencies. - // We don't have access to the build graph from the SwiftPM API offered to SourceKit-LSP to figure out which files - // depend on the updated module, so assume that all files have updated dependencies. - // The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being - // written to a directory within the project root. This is not necessarily true if the user specifies a build - // directory outside the source tree. - // If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when - // preparation of a target finishes. - if !options.backgroundIndexingOrDefault, - events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) - { - await orLog("Getting build targets") { - targetsWithUpdatedDependencies.formUnion(try await self.buildTargets().keys) - } - } - - var filesWithUpdatedDependencies: Set = [] - - await orLog("Getting source files in targets") { - let sourceFiles = try await self.sourceFiles(in: Set(targetsWithUpdatedDependencies)) - filesWithUpdatedDependencies.formUnion(sourceFiles.flatMap(\.sources).map(\.uri)) - } - - var mainFiles = await Set(events.asyncFlatMap { await self.mainFiles(containing: $0.uri) }) - mainFiles.subtract(events.map(\.uri)) - filesWithUpdatedDependencies.formUnion(mainFiles) - - await self.filesDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies) - } - - /// Checks if there are any files in `mainFileAssociations` where the main file - /// that we have stored has changed. - /// - /// For all of these files, re-associate the file with the new main file and - /// inform the delegate that the build settings for it might have changed. - package func mainFilesChanged() async { - var changedMainFileAssociations: Set = [] - for (file, (oldMainFile, language)) in self.watchedFiles { - let newMainFile = await self.mainFile(for: file, language: language, useCache: false) - if newMainFile != oldMainFile { - self.watchedFiles[file] = (newMainFile, language) - changedMainFileAssociations.insert(file) - } - } - - for file in changedMainFileAssociations { - guard let language = watchedFiles[file]?.language else { - continue - } - // Re-register for notifications of this file within the build server. - // This is the easiest way to make sure we are watching for build setting - // changes of the new main file and stop watching for build setting - // changes in the old main file if no other watched file depends on it. - await self.unregisterForChangeNotifications(for: file) - await self.registerForChangeNotifications(for: file, language: language) - } - - if let delegate, !changedMainFileAssociations.isEmpty { - await delegate.fileBuildSettingsChanged(changedMainFileAssociations) - } - } -} - -/// Returns `true` if the path components `selfPathComponents`, retrieved from `URL.pathComponents` are a descendent -/// of the other path components. -/// -/// This operates directly on path components instead of `URL`s because computing the path components of a URL is -/// expensive and this allows us to cache the path components. -private func isDescendant(_ selfPathComponents: [String], of otherPathComponents: [String]) -> Bool { - return selfPathComponents.dropLast().starts(with: otherPathComponents) -} - -fileprivate extension TextDocumentSourceKitOptionsResponse { - /// Adjust compiler arguments that were created for building to compiler arguments that should be used for indexing - /// or background AST builds. - /// - /// This removes compiler arguments that produce output files and adds arguments to eg. allow errors and index the - /// file. - func adjustArgsForSemanticSwiftFunctionality(fileToIndex: DocumentURI) -> TextDocumentSourceKitOptionsResponse { - // Technically, `-o` and the output file don't need to be separated by a space. Eg. `swiftc -oa file.swift` is - // valid and will write to an output file named `a`. - // We can't support that because the only way to know that `-output-file-map` is a different flag and not an option - // to write to an output file named `utput-file-map` is to know all compiler arguments of `swiftc`, which we don't. - let outputPathOption = CompilerCommandLineOption.option("o", [.singleDash], [.separatedBySpace]) - - let indexUnitOutputPathOption = - CompilerCommandLineOption.option("index-unit-output-path", [.singleDash], [.separatedBySpace]) - - let optionsToRemove: [CompilerCommandLineOption] = [ - .flag("c", [.singleDash]), - .flag("disable-cmo", [.singleDash]), - .flag("emit-dependencies", [.singleDash]), - .flag("emit-module-interface", [.singleDash]), - .flag("emit-module", [.singleDash]), - .flag("emit-objc-header", [.singleDash]), - .flag("incremental", [.singleDash]), - .flag("no-color-diagnostics", [.singleDash]), - .flag("parseable-output", [.singleDash]), - .flag("save-temps", [.singleDash]), - .flag("serialize-diagnostics", [.singleDash]), - .flag("use-frontend-parseable-output", [.singleDash]), - .flag("validate-clang-modules-once", [.singleDash]), - .flag("whole-module-optimization", [.singleDash]), - .flag("experimental-skip-all-function-bodies", frontendName: "Xfrontend", [.singleDash]), - .flag("experimental-skip-non-inlinable-function-bodies", frontendName: "Xfrontend", [.singleDash]), - .flag("experimental-skip-non-exportable-decls", frontendName: "Xfrontend", [.singleDash]), - .flag("experimental-lazy-typecheck", frontendName: "Xfrontend", [.singleDash]), - - .option("clang-build-session-file", [.singleDash], [.separatedBySpace]), - .option("emit-module-interface-path", [.singleDash], [.separatedBySpace]), - .option("emit-module-path", [.singleDash], [.separatedBySpace]), - .option("emit-objc-header-path", [.singleDash], [.separatedBySpace]), - .option("emit-package-module-interface-path", [.singleDash], [.separatedBySpace]), - .option("emit-private-module-interface-path", [.singleDash], [.separatedBySpace]), - .option("num-threads", [.singleDash], [.separatedBySpace]), - outputPathOption, - .option("output-file-map", [.singleDash], [.separatedBySpace, .separatedByEqualSign]), - ] - - var result: [String] = [] - result.reserveCapacity(compilerArguments.count) - var iterator = compilerArguments.makeIterator() - while let argument = iterator.next() { - switch optionsToRemove.firstMatch(for: argument) { - case .removeOption: - continue - case .removeOptionAndNextArgument: - _ = iterator.next() - continue - case .removeOptionAndPreviousArgument(let name): - if let previousArg = result.last, previousArg.hasSuffix("-\(name)") { - _ = result.popLast() - } - continue - case nil: - break - } - result.append(argument) - } - - result += [ - // Avoid emitting the ABI descriptor, we don't need it - "-Xfrontend", "-empty-abi-descriptor", - ] - - result += supplementalClangIndexingArgs.flatMap { ["-Xcc", $0] } - - if let outputPathIndex = compilerArguments.lastIndex(where: { outputPathOption.matches(argument: $0) != nil }), - compilerArguments.allSatisfy({ indexUnitOutputPathOption.matches(argument: $0) == nil }), - outputPathIndex + 1 < compilerArguments.count - { - // The original compiler arguments contained `-o` to specify the output file but we have stripped that away. - // Re-introduce the output path as `-index-unit-output-path` so that we have an output path for the unit file. - result += ["-index-unit-output-path", compilerArguments[outputPathIndex + 1]] - } - - var adjusted = self - adjusted.compilerArguments = result - return adjusted - } - - /// Adjust compiler arguments that were created for building to compiler arguments that should be used for indexing - /// or background AST builds. - /// - /// This removes compiler arguments that produce output files and adds arguments to eg. typecheck only. - func adjustingArgsForSemanticClangFunctionality() -> TextDocumentSourceKitOptionsResponse { - let optionsToRemove: [CompilerCommandLineOption] = [ - // Disable writing of a depfile - .flag("M", [.singleDash]), - .flag("MD", [.singleDash]), - .flag("MMD", [.singleDash]), - .flag("MG", [.singleDash]), - .flag("MM", [.singleDash]), - .flag("MV", [.singleDash]), - // Don't create phony targets - .flag("MP", [.singleDash]), - // Don't write out compilation databases - .flag("MJ", [.singleDash]), - // Don't compile - .flag("c", [.singleDash]), - - .flag("fmodules-validate-once-per-build-session", [.singleDash]), - - // Disable writing of a depfile - .option("MT", [.singleDash], [.noSpace, .separatedBySpace]), - .option("MF", [.singleDash], [.noSpace, .separatedBySpace]), - .option("MQ", [.singleDash], [.noSpace, .separatedBySpace]), - - // Don't write serialized diagnostic files - .option("serialize-diagnostics", [.singleDash, .doubleDash], [.separatedBySpace]), - - .option("fbuild-session-file", [.singleDash], [.separatedByEqualSign]), - ] - - var result: [String] = [] - result.reserveCapacity(compilerArguments.count) - var iterator = compilerArguments.makeIterator() - while let argument = iterator.next() { - switch optionsToRemove.firstMatch(for: argument) { - case .removeOption: - continue - case .removeOptionAndNextArgument: - _ = iterator.next() - continue - case .removeOptionAndPreviousArgument(let name): - if let previousArg = result.last, previousArg.hasSuffix("-\(name)") { - _ = result.popLast() - } - continue - case nil: - break - } - result.append(argument) - } - result += supplementalClangIndexingArgs - result.append( - "-fsyntax-only" - ) - - var adjusted = self - adjusted.compilerArguments = result - return adjusted - } -} - -private let supplementalClangIndexingArgs: [String] = [ - // Retain extra information for indexing - "-fretain-comments-from-system-headers", - // Pick up macro definitions during indexing - "-Xclang", "-detailed-preprocessing-record", - - // libclang uses 'raw' module-format. Match it so we can reuse the module cache and PCHs that libclang uses. - "-Xclang", "-fmodule-format=raw", - - // Be less strict - we want to continue and typecheck/index as much as possible - "-Xclang", "-fallow-pch-with-compiler-errors", - "-Xclang", "-fallow-pcm-with-compiler-errors", - "-Wno-non-modular-include-in-framework-module", - "-Wno-incomplete-umbrella", -] - -private extension OnBuildLogMessageNotification { - var lspStructure: LanguageServerProtocol.StructuredLogKind? { - guard let taskId = self.task?.id else { - return nil - } - switch structure { - case .begin(let info): - return .begin(StructuredLogBegin(title: info.title, taskID: taskId)) - case .report: - return .report(StructuredLogReport(taskID: taskId)) - case .end: - return .end(StructuredLogEnd(taskID: taskId)) - case nil: - return nil - } - } -} diff --git a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift b/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift deleted file mode 100644 index 71b78df00..000000000 --- a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -package import LanguageServerProtocol - -/// Handles build server events, such as file build settings changes. -package protocol BuildServerManagerDelegate: AnyObject, Sendable { - /// Notify the delegate that the result of `BuildServerManager.buildSettingsInferredFromMainFile` might have changed - /// for the given files. - func fileBuildSettingsChanged(_ changedFiles: Set) async - - /// Notify the delegate that the dependencies of the given files have changed - /// and that ASTs may need to be refreshed. If the given set is empty, assume - /// that all watched files are affected. - /// - /// The callee should refresh ASTs unless it is able to determine that a - /// refresh is not necessary. - func filesDependenciesUpdated(_ changedFiles: Set) async - - /// Notify the delegate that some information about the given build targets has changed and that it should recompute - /// any information based on top of it. - func buildTargetsChanged(_ changedTargets: Set?) async -} - -/// Methods with which the `BuildServerManager` can send messages to the client (aka. editor). -/// -/// This is distinct from `BuildServerManagerDelegate` because the delegate only gets set on the build server after the -/// workspace that created it has been initialized (see `BuildServerManager.setDelegate`). But the `BuildServerManager` -/// can send notifications to the client immediately. -package protocol BuildServerManagerConnectionToClient: Sendable, Connection { - /// Whether the client can handle `WorkDoneProgress` requests. - var clientSupportsWorkDoneProgress: Bool { get async } - - /// Wait until the connection to the client has been initialized. - /// - /// No messages should be sent on this connection before this returns. - func waitUntilInitialized() async - - /// Start watching for file changes with the given glob patterns. - func watchFiles(_ fileWatchers: [FileSystemWatcher]) async - - /// Log a message in the client's index log. - func logMessageToIndexLog( - message: String, - type: WindowMessageType, - structure: LanguageServerProtocol.StructuredLogKind? - ) -} diff --git a/Sources/BuildServerIntegration/BuildSettingsLogger.swift b/Sources/BuildServerIntegration/BuildSettingsLogger.swift deleted file mode 100644 index 3d13085d1..000000000 --- a/Sources/BuildServerIntegration/BuildSettingsLogger.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -package import SKLogging -import SwiftExtensions - -// MARK: - Build settings logger - -/// Shared logger that only logs build settings for a file once unless they change -package actor BuildSettingsLogger { - private var loggedSettings: [DocumentURI: FileBuildSettings] = [:] - - package func log(level: LogLevel = .default, settings: FileBuildSettings, for uri: DocumentURI) { - guard loggedSettings[uri] != settings else { - return - } - loggedSettings[uri] = settings - Self.log(level: level, settings: settings, for: uri) - } - - /// Log the given build settings for a single file - /// - /// In contrast to the instance method `log`, this will always log the build settings. The instance method only logs - /// the build settings if they have changed. - package static func log(level: LogLevel = .default, settings: FileBuildSettings, for uri: DocumentURI) { - log(level: level, settings: settings, for: [uri]) - } - - /// Log the given build settings for a list of source files that all share the same build settings. - /// - /// In contrast to the instance method `log`, this will always log the build settings. The instance method only logs - /// the build settings if they have changed. - package static func log(level: LogLevel = .default, settings: FileBuildSettings, for uris: [DocumentURI]) { - let header: String - if let uri = uris.only { - header = "Build settings for \(uri.forLogging)" - } else if let firstUri = uris.first { - header = "Build settings for \(firstUri.forLogging) and \(uris.count - 1) others" - } else { - header = "Build settings for empty list" - } - log(level: level, settings: settings, header: header) - } - - private static func log(level: LogLevel = .default, settings: FileBuildSettings, header: String) { - let log = """ - Compiler Arguments: - \(settings.compilerArguments.joined(separator: "\n")) - - Working directory: - \(settings.workingDirectory ?? "") - """ - - let chunks = splitLongMultilineMessage(message: log) - // Only print the first 100 chunks. If the argument list gets any longer, we don't want to spam the log too much. - // In practice, 100 chunks should be sufficient. - for (index, chunk) in chunks.enumerated().prefix(100) { - logger.log( - level: level, - """ - \(header) (\(index + 1)/\(chunks.count)) - \(chunk) - """ - ) - } - } -} diff --git a/Sources/BuildServerIntegration/BuildTargetIdentifierExtensions.swift b/Sources/BuildServerIntegration/BuildTargetIdentifierExtensions.swift deleted file mode 100644 index d853afb20..000000000 --- a/Sources/BuildServerIntegration/BuildTargetIdentifierExtensions.swift +++ /dev/null @@ -1,157 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import SKLogging - -extension BuildTargetIdentifier { - package static let dummy: BuildTargetIdentifier = BuildTargetIdentifier(uri: try! URI(string: "dummy://dummy")) -} - -package enum BuildDestinationIdentifier { - case host - case target - - /// A string that can be used to identify the build triple in a `BuildTargetIdentifier`. - /// - /// `BuildServerManager.canonicalBuildTargetIdentifier` picks the canonical target based on alphabetical - /// ordering. We rely on the string "destination" being ordered before "tools" so that we prefer a - /// `destination` (or "target") target over a `tools` (or "host") target. - var id: String { - switch self { - case .host: - return "tools" - case .target: - return "destination" - } - } -} - -// MARK: BuildTargetIdentifier for SwiftPM - -extension BuildTargetIdentifier { - /// - Important: *For testing only* - package static func createSwiftPM( - target: String, - destination: BuildDestinationIdentifier - ) throws -> BuildTargetIdentifier { - var components = URLComponents() - components.scheme = "swiftpm" - components.host = "target" - components.queryItems = [ - URLQueryItem(name: "target", value: target), - URLQueryItem(name: "destination", value: destination.id), - ] - - struct FailedToConvertSwiftBuildTargetToUrlError: Swift.Error, CustomStringConvertible { - var target: String - var destination: String - - var description: String { - return "Failed to generate URL for target: \(target), destination: \(destination)" - } - } - - guard let url = components.url else { - throw FailedToConvertSwiftBuildTargetToUrlError(target: target, destination: destination.id) - } - - return BuildTargetIdentifier(uri: URI(url)) - } - - static let forPackageManifest = BuildTargetIdentifier(uri: try! URI(string: "swiftpm://package-manifest")) - - var swiftpmTargetProperties: (target: String, runDestination: String) { - get throws { - struct InvalidTargetIdentifierError: Swift.Error, CustomStringConvertible { - var target: BuildTargetIdentifier - - var description: String { - return "Invalid target identifier \(target)" - } - } - guard let components = URLComponents(url: self.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false) else { - throw InvalidTargetIdentifierError(target: self) - } - let target = components.queryItems?.last(where: { $0.name == "target" })?.value - let runDestination = components.queryItems?.last(where: { $0.name == "destination" })?.value - - guard let target, let runDestination else { - throw InvalidTargetIdentifierError(target: self) - } - - return (target, runDestination) - } - } -} - -// MARK: BuildTargetIdentifier for CompileCommands - -extension BuildTargetIdentifier { - package static func createCompileCommands(compiler: String) throws -> BuildTargetIdentifier { - var components = URLComponents() - components.scheme = "compilecommands" - components.host = "target" - components.queryItems = [URLQueryItem(name: "compiler", value: compiler)] - - struct FailedToConvertSwiftBuildTargetToUrlError: Swift.Error, CustomStringConvertible { - var compiler: String - - var description: String { - return "Failed to generate URL for compiler: \(compiler)" - } - } - - guard let url = components.url else { - throw FailedToConvertSwiftBuildTargetToUrlError(compiler: compiler) - } - - return BuildTargetIdentifier(uri: URI(url)) - } - - var compileCommandsCompiler: String { - get throws { - struct InvalidTargetIdentifierError: Swift.Error, CustomStringConvertible { - var target: BuildTargetIdentifier - - var description: String { - return "Invalid target identifier \(target)" - } - } - guard let components = URLComponents(url: self.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false) else { - throw InvalidTargetIdentifierError(target: self) - } - guard components.scheme == "compilecommands", components.host == "target" else { - throw InvalidTargetIdentifierError(target: self) - } - let compiler = components.queryItems?.last(where: { $0.name == "compiler" })?.value - - guard let compiler else { - throw InvalidTargetIdentifierError(target: self) - } - - return compiler - } - } -} - -extension BuildTargetIdentifier: CustomLogStringConvertible { - package var description: String { - return uri.stringValue - } - - package var redactedDescription: String { - return uri.stringValue.hashForLogging - } -} diff --git a/Sources/BuildServerIntegration/BuiltInBuildServer.swift b/Sources/BuildServerIntegration/BuiltInBuildServer.swift deleted file mode 100644 index 814448e61..000000000 --- a/Sources/BuildServerIntegration/BuiltInBuildServer.swift +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -package import Foundation -package import LanguageServerProtocol -import SKLogging -import SKOptions -import ToolchainRegistry - -/// An error build servers can throw from `prepare` if they don't support preparation of targets. -package struct PrepareNotSupportedError: Error, CustomStringConvertible { - package init() {} - - package var description: String { "Preparation not supported" } -} - -/// Provider of FileBuildSettings and other build-related information. -package protocol BuiltInBuildServer: AnyObject, Sendable { - /// The files to watch for changes. - var fileWatchers: [FileSystemWatcher] { get async } - - /// The path to the raw index store data, if any. - var indexStorePath: URL? { get async } - - /// The path to put the index database, if any. - var indexDatabasePath: URL? { get async } - - /// Whether the build server is capable of preparing a target for indexing and determining the output paths for the - /// target, ie. whether the `prepare` method has been implemented and this build server populates the `outputPath` - /// property in the `buildTarget/sources` request. - var supportsPreparationAndOutputPaths: Bool { get } - - /// Returns all targets in the build server - func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse - - /// Returns all the source files in the given targets - func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse - - /// Called when files in the project change. - func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async - - /// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target - /// dependencies. - func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse - - /// Retrieve build settings for the given document. - /// - /// Returns `nil` if the build server can't provide build settings for this file. - func sourceKitOptions( - request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? - - /// Wait until the build graph has been loaded. - func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse -} diff --git a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift b/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift deleted file mode 100644 index a22396175..000000000 --- a/Sources/BuildServerIntegration/BuiltInBuildServerAdapter.swift +++ /dev/null @@ -1,144 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerProtocol -package import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SKOptions -import SwiftExtensions -import ToolchainRegistry - -/// The details necessary to create a `BuildServerAdapter`. -package struct BuildServerSpec { - package enum Kind { - case externalBuildServer - case jsonCompilationDatabase - case fixedCompilationDatabase - case swiftPM - case injected( - @Sendable (_ projectRoot: URL, _ connectionToSourceKitLSP: any Connection) async -> any Connection - ) - } - - package var kind: Kind - - /// The folder that best describes the root of the project that this build server handles. - package var projectRoot: URL - - /// The main path that provides the build server configuration. - package var configPath: URL - - package init(kind: BuildServerSpec.Kind, projectRoot: URL, configPath: URL) { - self.kind = kind - self.projectRoot = projectRoot - self.configPath = configPath - } -} - -/// A type that outwardly acts as a BSP build server and internally uses a `BuiltInBuildServer` to satisfy the requests. -actor BuiltInBuildServerAdapter: QueueBasedMessageHandler { - let messageHandlingHelper = QueueBasedMessageHandlerHelper( - signpostLoggingCategory: "build-server-message-handling", - createLoggingScope: false - ) - - /// The queue on which all messages from SourceKit-LSP (or more specifically `BuildServerManager`) are handled. - package let messageHandlingQueue = AsyncQueue() - - /// The underlying build server - private var underlyingBuildServer: BuiltInBuildServer - - /// The connection with which messages are sent to `BuildServerManager`. - private let connectionToSourceKitLSP: LocalConnection - - private let buildServerHooks: BuildServerHooks - - /// Create a `BuiltInBuildServerAdapter` form an existing `BuiltInBuildServer` and connection to communicate messages - /// from the build server to SourceKit-LSP. - init( - underlyingBuildServer: BuiltInBuildServer, - connectionToSourceKitLSP: LocalConnection, - buildServerHooks: BuildServerHooks - ) { - self.underlyingBuildServer = underlyingBuildServer - self.connectionToSourceKitLSP = connectionToSourceKitLSP - self.buildServerHooks = buildServerHooks - } - - deinit { - connectionToSourceKitLSP.close() - } - - private func initialize(request: InitializeBuildRequest) async -> InitializeBuildResponse { - return InitializeBuildResponse( - displayName: "\(type(of: underlyingBuildServer))", - version: "", - bspVersion: "2.2.0", - capabilities: BuildServerCapabilities(), - dataKind: .sourceKit, - data: SourceKitInitializeBuildResponseData( - indexDatabasePath: await orLog("getting index database file path") { - try await underlyingBuildServer.indexDatabasePath?.filePath - }, - indexStorePath: await orLog("getting index store file path") { - try await underlyingBuildServer.indexStorePath?.filePath - }, - outputPathsProvider: underlyingBuildServer.supportsPreparationAndOutputPaths, - prepareProvider: underlyingBuildServer.supportsPreparationAndOutputPaths, - sourceKitOptionsProvider: true, - watchers: await underlyingBuildServer.fileWatchers - ).encodeToLSPAny() - ) - } - - package func handle(notification: some NotificationType) async { - switch notification { - case is OnBuildExitNotification: - break - case is OnBuildInitializedNotification: - break - case let notification as OnWatchedFilesDidChangeNotification: - await self.underlyingBuildServer.didChangeWatchedFiles(notification: notification) - default: - logger.error("Ignoring unknown notification \(type(of: notification).method) from SourceKit-LSP") - } - } - - func handle( - request: Request, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) async { - let request = RequestAndReply(request, reply: reply) - await buildServerHooks.preHandleRequest?(request.params) - switch request { - case let request as RequestAndReply: - await request.reply { VoidResponse() } - case let request as RequestAndReply: - await request.reply { try await underlyingBuildServer.prepare(request: request.params) } - case let request as RequestAndReply: - await request.reply { try await underlyingBuildServer.buildTargetSources(request: request.params) } - case let request as RequestAndReply: - await request.reply { await self.initialize(request: request.params) } - case let request as RequestAndReply: - await request.reply { try await underlyingBuildServer.sourceKitOptions(request: request.params) } - case let request as RequestAndReply: - await request.reply { try await underlyingBuildServer.buildTargets(request: request.params) } - case let request as RequestAndReply: - await request.reply { await underlyingBuildServer.waitForBuildSystemUpdates(request: request.params) } - default: - await request.reply { throw ResponseError.methodNotFound(Request.method) } - } - } -} diff --git a/Sources/BuildServerIntegration/CMakeLists.txt b/Sources/BuildServerIntegration/CMakeLists.txt deleted file mode 100644 index 8e4d61f98..000000000 --- a/Sources/BuildServerIntegration/CMakeLists.txt +++ /dev/null @@ -1,45 +0,0 @@ - -add_library(BuildServerIntegration STATIC - BuildServerHooks.swift - BuildServerManager.swift - BuildServerManagerDelegate.swift - BuildServerMessageDependencyTracker.swift - BuildSettingsLogger.swift - BuildTargetIdentifierExtensions.swift - BuiltInBuildServer.swift - BuiltInBuildServerAdapter.swift - CMakeLists.txt - CompilationDatabase.swift - CompilerCommandLineOption.swift - DetermineBuildServer.swift - ExternalBuildServerAdapter.swift - FallbackBuildSettings.swift - FileBuildSettings.swift - FixedCompilationDatabaseBuildServer.swift - JSONCompilationDatabaseBuildServer.swift - LegacyBuildServer.swift - MainFilesProvider.swift - SplitShellCommand.swift - SwiftlyResolver.swift - SwiftPMBuildServer.swift) -set_target_properties(BuildServerIntegration PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(BuildServerIntegration PUBLIC - BuildServerProtocol - LanguageServerProtocol - LanguageServerProtocolJSONRPC - SKLogging - SKOptions - LanguageServerProtocolExtensions - SourceKitD - SwiftExtensions - ToolchainRegistry - PackageModel - TSCBasic - Build - SourceKitLSPAPI - SwiftASN1) - -target_link_libraries(BuildServerIntegration PRIVATE - SKUtilities - TSCExtensions) diff --git a/Sources/BuildServerIntegration/CompilationDatabase.swift b/Sources/BuildServerIntegration/CompilationDatabase.swift deleted file mode 100644 index 99bd1314c..000000000 --- a/Sources/BuildServerIntegration/CompilationDatabase.swift +++ /dev/null @@ -1,185 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions -import TSCExtensions - -#if os(Windows) -import WinSDK -#endif - -/// A single compilation database command. -/// -/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html -package struct CompilationDatabaseCompileCommand: Equatable, Codable { - /// The working directory for the compilation. - package var directory: String - - /// The path of the main file for the compilation, which may be relative to `directory`. - package var filename: String - - /// The compile command as a list of strings, with the program name first. - package var commandLine: [String] - - /// The name of the build output, or nil. - package var output: String? = nil - - package init(directory: String, filename: String, commandLine: [String], output: String? = nil) { - self.directory = directory - self.filename = filename - self.commandLine = commandLine - self.output = output - } - - private enum CodingKeys: String, CodingKey { - case directory - case file - case command - case arguments - case output - } - - package init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.directory = try container.decode(String.self, forKey: .directory) - self.filename = try container.decode(String.self, forKey: .file) - self.output = try container.decodeIfPresent(String.self, forKey: .output) - if let arguments = try container.decodeIfPresent([String].self, forKey: .arguments) { - self.commandLine = arguments - } else if let command = try container.decodeIfPresent(String.self, forKey: .command) { - #if os(Windows) - self.commandLine = splitWindowsCommandLine(command, initialCommandName: true) - #else - self.commandLine = splitShellEscapedCommand(command) - #endif - } else { - throw CompilationDatabaseDecodingError.missingCommandOrArguments - } - } - - package func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(directory, forKey: .directory) - try container.encode(filename, forKey: .file) - try container.encode(commandLine, forKey: .arguments) - try container.encodeIfPresent(output, forKey: .output) - } - - /// The `DocumentURI` for this file. If `filename` is relative and `directory` is - /// absolute, returns the concatenation. However, if both paths are relative, - /// it falls back to `filename`, which is more likely to be the identifier - /// that a caller will be looking for. - package var uri: DocumentURI { - if filename.isAbsolutePath || !directory.isAbsolutePath { - return DocumentURI(filePath: filename, isDirectory: false) - } else { - return DocumentURI(URL(fileURLWithPath: directory).appending(component: filename, directoryHint: .notDirectory)) - } - } -} - -/// The JSON clang-compatible compilation database. -/// -/// Example: -/// -/// ``` -/// [ -/// { -/// "directory": "/src", -/// "file": "/src/file.cpp", -/// "command": "clang++ file.cpp" -/// } -/// ] -/// ``` -/// -/// See https://clang.llvm.org/docs/JSONCompilationDatabase.html -package struct JSONCompilationDatabase: Equatable, Codable { - private var pathToCommands: [DocumentURI: [Int]] = [:] - var commands: [CompilationDatabaseCompileCommand] = [] - - package init(_ commands: [CompilationDatabaseCompileCommand] = []) { - for command in commands { - add(command) - } - } - - package init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - while !container.isAtEnd { - self.add(try container.decode(CompilationDatabaseCompileCommand.self)) - } - } - - /// Loads the compilation database located in `directory`, if any. - /// - /// - Returns: `nil` if `compile_commands.json` was not found - package init(directory: URL) throws { - let path = directory.appending(component: JSONCompilationDatabaseBuildServer.dbName) - try self.init(file: path) - } - - /// Loads the compilation database from `file` - /// - Returns: `nil` if the file does not exist - package init(file: URL) throws { - let data = try Data(contentsOf: file) - self = try JSONDecoder().decode(JSONCompilationDatabase.self, from: data) - } - - package func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - for command in commands { - try container.encode(command) - } - } - - package subscript(_ uri: DocumentURI) -> [CompilationDatabaseCompileCommand] { - if let indices = pathToCommands[uri] { - return indices.map { commands[$0] } - } - if let fileURL = try? uri.fileURL?.realpath, let indices = pathToCommands[DocumentURI(fileURL)] { - return indices.map { commands[$0] } - } - return [] - } - - private mutating func add(_ command: CompilationDatabaseCompileCommand) { - let uri = command.uri - pathToCommands[uri, default: []].append(commands.count) - - if let symlinkTarget = uri.symlinkTarget { - pathToCommands[symlinkTarget, default: []].append(commands.count) - } - - commands.append(command) - } -} - -enum CompilationDatabaseDecodingError: Error { - case missingCommandOrArguments - case fixedDatabaseDecodingError -} - -fileprivate extension String { - var isAbsolutePath: Bool { - #if os(Windows) - Array(self.utf16).withUnsafeBufferPointer { buffer in - return !PathIsRelativeW(buffer.baseAddress) - } - #else - return self.hasPrefix("/") - #endif - } -} diff --git a/Sources/BuildServerIntegration/CompilerCommandLineOption.swift b/Sources/BuildServerIntegration/CompilerCommandLineOption.swift deleted file mode 100644 index 6ead619d4..000000000 --- a/Sources/BuildServerIntegration/CompilerCommandLineOption.swift +++ /dev/null @@ -1,163 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package struct CompilerCommandLineOption { - /// Return value of `matches(argument:)`. - package enum Match: Equatable { - /// The `CompilerCommandLineOption` matched the command line argument. The next element in the command line is a - /// separate argument and should not be removed. - case removeOption - - /// The `CompilerCommandLineOption` matched the command line argument. The next element in the command line is an - /// argument to this option and should be removed as well. - case removeOptionAndNextArgument - - /// The `CompilerCommandLineOption` matched the command line argument. The previous element in the command line is - /// a prefix to this argument and should be removed if it matches `name`. - case removeOptionAndPreviousArgument(name: String) - } - - package enum DashSpelling { - case singleDash - case doubleDash - } - - package enum ArgumentStyles { - /// A command line option where arguments can be passed without a space such as `-MT/file.txt`. - case noSpace - /// A command line option where the argument is passed, separated by a space (eg. `--serialize-diagnostics /file.txt`) - case separatedBySpace - /// A command line option where the argument is passed after a `=`, eg. `-fbuild-session-file=`. - case separatedByEqualSign - } - - /// The name of the option, without any preceeding `-` or `--`. - private let name: String - - /// The name of the argument that prefixes this flag, without any preceeding `-` or `--` (eg. `Xfrontend`/`Xclang`). - private let frontendName: String? - - /// Whether the option can be spelled with one or two dashes. - private let dashSpellings: [DashSpelling] - - /// The ways that arguments can specified after the option. Empty if the option is a flag that doesn't take any - /// argument. - private let argumentStyles: [ArgumentStyles] - - package static func flag( - _ name: String, - frontendName: String? = nil, - _ dashSpellings: [DashSpelling] - ) -> CompilerCommandLineOption { - precondition(!dashSpellings.isEmpty) - return CompilerCommandLineOption( - name: name, - frontendName: frontendName, - dashSpellings: dashSpellings, - argumentStyles: [] - ) - } - - package static func option( - _ name: String, - _ dashSpellings: [DashSpelling], - _ argumentStyles: [ArgumentStyles] - ) -> CompilerCommandLineOption { - precondition(!dashSpellings.isEmpty) - precondition(!argumentStyles.isEmpty) - return CompilerCommandLineOption( - name: name, - frontendName: nil, - dashSpellings: dashSpellings, - argumentStyles: argumentStyles - ) - } - - package func matches(argument: String) -> Match? { - let match = matchesIgnoringFrontend(argument: argument) - guard let match, let frontendName else { - return match - } - - switch match { - case .removeOption: - return .removeOptionAndPreviousArgument(name: frontendName) - default: - return match - } - } - - private func matchesIgnoringFrontend(argument: String) -> Match? { - let argumentName: Substring - if argument.hasPrefix("--") { - if dashSpellings.contains(.doubleDash) { - argumentName = argument.dropFirst(2) - } else { - return nil - } - } else if argument.hasPrefix("-") { - if dashSpellings.contains(.singleDash) { - argumentName = argument.dropFirst(1) - } else { - return nil - } - } else { - return nil - } - guard argumentName.hasPrefix(self.name) else { - // Fast path in case the argument doesn't match. - return nil - } - - // Examples: - // - self.name: "emit-module", argument: "-emit-module", then textAfterArgumentName: "" - // - self.name: "o", argument: "-o", then textAfterArgumentName: "" - // - self.name: "o", argument: "-output-file-map", then textAfterArgumentName: "utput-file-map" - // - self.name: "MT", argument: "-MT/path/to/depfile", then textAfterArgumentName: "/path/to/depfile" - // - self.name: "fbuild-session-file", argument: "-fbuild-session-file=/path/to/file", then textAfterArgumentName: "=/path/to/file" - let textAfterArgumentName: Substring = argumentName.dropFirst(self.name.count) - - if argumentStyles.isEmpty { - if textAfterArgumentName.isEmpty { - return .removeOption - } - // The command line option is a flag but there is text remaining after the argument name. Thus the flag didn't - // match. Eg. self.name: "o" and argument: "-output-file-map" - return nil - } - - for argumentStyle in argumentStyles { - switch argumentStyle { - case .noSpace where !textAfterArgumentName.isEmpty: - return .removeOption - case .separatedBySpace where textAfterArgumentName.isEmpty: - return .removeOptionAndNextArgument - case .separatedByEqualSign where textAfterArgumentName.hasPrefix("="): - return .removeOption - default: - break - } - } - return nil - } -} - -extension [CompilerCommandLineOption] { - func firstMatch(for argument: String) -> CompilerCommandLineOption.Match? { - for optionToRemove in self { - if let match = optionToRemove.matches(argument: argument) { - return match - } - } - return nil - } -} diff --git a/Sources/BuildServerIntegration/DetermineBuildServer.swift b/Sources/BuildServerIntegration/DetermineBuildServer.swift deleted file mode 100644 index 9c3d06254..000000000 --- a/Sources/BuildServerIntegration/DetermineBuildServer.swift +++ /dev/null @@ -1,113 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -package import SKOptions -import SwiftExtensions -import TSCExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath - -private func searchForCompilationDatabaseConfig( - in workspaceFolder: URL, - options: SourceKitLSPOptions -) -> BuildServerSpec? { - let searchPaths = - (options.compilationDatabaseOrDefault.searchPaths ?? []).compactMap { searchPath in - orLog("Compilation DB search path") { - try RelativePath(validating: searchPath) - } - } + [ - // These default search paths match the behavior of `clangd` - try! RelativePath(validating: "."), - try! RelativePath(validating: "build"), - ] - - return - searchPaths - .lazy - .compactMap { searchPath in - let path = workspaceFolder.appending(searchPath) - - let jsonPath = path.appending(component: JSONCompilationDatabaseBuildServer.dbName) - if FileManager.default.isFile(at: jsonPath) { - return BuildServerSpec(kind: .jsonCompilationDatabase, projectRoot: workspaceFolder, configPath: jsonPath) - } - - let fixedPath = path.appending(component: FixedCompilationDatabaseBuildServer.dbName) - if FileManager.default.isFile(at: fixedPath) { - return BuildServerSpec(kind: .fixedCompilationDatabase, projectRoot: workspaceFolder, configPath: fixedPath) - } - - return nil - } - .first -} - -/// Determine which build server should be started to handle the given workspace folder and at which folder that build -/// servers's project root is (see `BuiltInBuildServer.projectRoot(for:options:)`). `onlyConsiderRoot` controls whether -/// paths outside the root should be considered (eg. configuration files in the user's home directory). -/// -/// Returns `nil` if no build server can handle this workspace folder. -package func determineBuildServer( - forWorkspaceFolder workspaceFolder: DocumentURI, - onlyConsiderRoot: Bool, - options: SourceKitLSPOptions, - hooks: BuildServerHooks -) -> BuildServerSpec? { - if let injectBuildServer = hooks.injectBuildServer { - return BuildServerSpec( - kind: .injected(injectBuildServer), - projectRoot: workspaceFolder.arbitrarySchemeURL, - configPath: workspaceFolder.arbitrarySchemeURL - ) - } - - var buildServerPreference: [WorkspaceType] = [ - .buildServer, .swiftPM, .compilationDatabase, - ] - if let defaultBuildServer = options.defaultWorkspaceType { - buildServerPreference.removeAll(where: { $0 == defaultBuildServer }) - buildServerPreference.insert(defaultBuildServer, at: 0) - } - guard let workspaceFolderUrl = workspaceFolder.fileURL else { - return nil - } - for buildServerType in buildServerPreference { - var spec: BuildServerSpec? = nil - - switch buildServerType { - case .buildServer: - spec = ExternalBuildServerAdapter.searchForConfig( - in: workspaceFolderUrl, - onlyConsiderRoot: onlyConsiderRoot, - options: options - ) - case .compilationDatabase: - spec = searchForCompilationDatabaseConfig(in: workspaceFolderUrl, options: options) - case .swiftPM: - #if !NO_SWIFTPM_DEPENDENCY - spec = SwiftPMBuildServer.searchForConfig(in: workspaceFolderUrl, options: options) - #endif - } - - if let spec { - return spec - } - } - - return nil -} diff --git a/Sources/BuildServerIntegration/ExternalBuildServerAdapter.swift b/Sources/BuildServerIntegration/ExternalBuildServerAdapter.swift deleted file mode 100644 index 3f507f8d8..000000000 --- a/Sources/BuildServerIntegration/ExternalBuildServerAdapter.swift +++ /dev/null @@ -1,322 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import LanguageServerProtocolJSONRPC -import SKLogging -import SKOptions -import SwiftExtensions -import TSCExtensions - -import func TSCBasic.getEnvSearchPaths -import var TSCBasic.localFileSystem -import func TSCBasic.lookupExecutablePath - -private func executable(_ name: String) -> String { - #if os(Windows) - guard !name.hasSuffix(".exe") else { return name } - return "\(name).exe" - #else - return name - #endif -} - -private let python3ExecutablePath: URL? = { - let pathVariable: String - #if os(Windows) - pathVariable = "Path" - #else - pathVariable = "PATH" - #endif - let searchPaths = - getEnvSearchPaths( - pathString: ProcessInfo.processInfo.environment[pathVariable], - currentWorkingDirectory: localFileSystem.currentWorkingDirectory - ) - - return lookupExecutablePath(filename: executable("python3"), searchPaths: searchPaths)?.asURL - ?? lookupExecutablePath(filename: executable("python"), searchPaths: searchPaths)?.asURL -}() - -struct ExecutableNotFoundError: Error { - let executableName: String -} - -enum BuildServerNotFoundError: Error { - case fileNotFound -} - -/// BSP configuration -/// -/// See https://build-server-protocol.github.io/docs/overview/server-discovery#the-bsp-connection-details -private struct BuildServerConfig: Codable { - /// The name of the build tool. - let name: String - - /// The version of the build tool. - let version: String - - /// The bsp version of the build tool. - let bspVersion: String - - /// A collection of languages supported by this BSP server. - let languages: [String] - - /// Command arguments runnable via server processes to start a BSP server. - let argv: [String] - - static func load(from path: URL) throws -> BuildServerConfig { - let decoder = JSONDecoder() - let fileData = try Data(contentsOf: path) - return try decoder.decode(BuildServerConfig.self, from: fileData) - } -} - -/// Launches a subprocess that is a BSP server and manages the process's lifetime. -actor ExternalBuildServerAdapter { - /// The root folder of the project. Used to resolve relative server paths. - private let projectRoot: URL - - /// The file that specifies the configuration for this build server. - private let configPath: URL - - /// The `BuildServerManager` that handles messages from the BSP server to SourceKit-LSP. - var messagesToSourceKitLSPHandler: MessageHandler - - /// The JSON-RPC connection between SourceKit-LSP and the BSP server. - private(set) var connectionToBuildServer: JSONRPCConnection? - - /// After a `build/initialize` request has been sent to the BSP server, that request, so we can replay it in case the - /// server crashes. - private var initializeRequest: InitializeBuildRequest? - - /// The process that runs the external BSP server. - private var process: Process? - - /// The date at which `clangd` was last restarted. - /// Used to delay restarting in case of a crash loop. - private var lastRestart: Date? - - static package func searchForConfig( - in workspaceFolder: URL, - onlyConsiderRoot: Bool, - options: SourceKitLSPOptions - ) -> BuildServerSpec? { - guard let configPath = getConfigPath(for: workspaceFolder, onlyConsiderRoot: onlyConsiderRoot) else { - return nil - } - - return BuildServerSpec(kind: .externalBuildServer, projectRoot: workspaceFolder, configPath: configPath) - } - - init( - projectRoot: URL, - configPath: URL, - messagesToSourceKitLSPHandler: MessageHandler - ) async throws { - self.projectRoot = projectRoot - self.configPath = configPath - self.messagesToSourceKitLSPHandler = messagesToSourceKitLSPHandler - self.connectionToBuildServer = try await self.createConnectionToBspServer() - } - - /// Change the handler that handles messages from the build server. - /// - /// The intended use of this is to intercept messages from the build server by `LegacyBuildServer`. - func changeMessageToSourceKitLSPHandler(to newHandler: MessageHandler) { - messagesToSourceKitLSPHandler = newHandler - connectionToBuildServer?.changeReceiveHandler(messagesToSourceKitLSPHandler) - } - - /// Send a notification to the build server. - func send(_ notification: some NotificationType) { - guard let connectionToBuildServer else { - logger.error("Dropping notification because BSP server has crashed: \(notification.forLogging)") - return - } - connectionToBuildServer.send(notification) - } - - /// Send a request to the build server. - func send(_ request: Request) async throws -> Request.Response { - guard let connectionToBuildServer else { - throw ResponseError.internalError("BSP server has crashed") - } - if let request = request as? InitializeBuildRequest { - if initializeRequest != nil { - logger.error("BSP server was initialized multiple times") - } - self.initializeRequest = request - } - return try await connectionToBuildServer.send(request) - } - - /// Create a new JSONRPCConnection to the build server. - private func createConnectionToBspServer() async throws -> JSONRPCConnection { - let serverConfig = try BuildServerConfig.load(from: configPath) - var serverPath = URL(fileURLWithPath: serverConfig.argv[0], relativeTo: projectRoot.ensuringCorrectTrailingSlash) - var serverArgs = Array(serverConfig.argv[1...]) - - if serverPath.pathExtension == "py" { - serverArgs = [try serverPath.filePath] + serverArgs - guard let interpreterPath = python3ExecutablePath else { - throw ExecutableNotFoundError(executableName: "python3") - } - - serverPath = interpreterPath - } - - let (connection, process) = try JSONRPCConnection.start( - executable: serverPath, - arguments: serverArgs, - name: "BSP-Server", - protocol: bspRegistry, - stderrLoggingCategory: "bsp-server-stderr", - client: messagesToSourceKitLSPHandler, - terminationHandler: { [weak self] terminationReason in - guard let self else { - return - } - if terminationReason != .exited(exitCode: 0) { - Task { - await orLog("Restarting BSP server") { - try await self.handleBspServerCrash() - } - } - } - } - ) - self.process = process - return connection - } - - private static func getConfigPath(for workspaceFolder: URL? = nil, onlyConsiderRoot: Bool = false) -> URL? { - var buildServerConfigLocations: [URL?] = [] - if let workspaceFolder = workspaceFolder { - buildServerConfigLocations.append(workspaceFolder.appending(component: ".bsp")) - } - - if !onlyConsiderRoot { - #if os(Windows) - if let localAppData = ProcessInfo.processInfo.environment["LOCALAPPDATA"] { - buildServerConfigLocations.append(URL(fileURLWithPath: localAppData).appending(component: "bsp")) - } - if let programData = ProcessInfo.processInfo.environment["PROGRAMDATA"] { - buildServerConfigLocations.append(URL(fileURLWithPath: programData).appending(component: "bsp")) - } - #else - if let xdgDataHome = ProcessInfo.processInfo.environment["XDG_DATA_HOME"] { - buildServerConfigLocations.append(URL(fileURLWithPath: xdgDataHome).appending(component: "bsp")) - } - - if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { - buildServerConfigLocations.append(libraryUrl.appending(component: "bsp")) - } - - if let xdgDataDirs = ProcessInfo.processInfo.environment["XDG_DATA_DIRS"] { - buildServerConfigLocations += xdgDataDirs.split(separator: ":").map { xdgDataDir in - URL(fileURLWithPath: String(xdgDataDir)).appending(component: "bsp") - } - } - - if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .systemDomainMask).first { - buildServerConfigLocations.append(libraryUrl.appending(component: "bsp")) - } - #endif - } - - for case let buildServerConfigLocation? in buildServerConfigLocations { - let jsonFiles = - try? FileManager.default.contentsOfDirectory(at: buildServerConfigLocation, includingPropertiesForKeys: nil) - .filter { - guard let config = try? BuildServerConfig.load(from: $0) else { - return false - } - return !Set([Language.c, .cpp, .objective_c, .objective_cpp, .swift].map(\.rawValue)) - .intersection(config.languages).isEmpty - } - - if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first { - return configFileURL - } - } - - // Pre Swift 6.1 SourceKit-LSP looked for `buildServer.json` in the project root. Maintain this search location for - // compatibility even though it's not a standard BSP search location. - if let buildServerPath = workspaceFolder?.appending(component: "buildServer.json"), - FileManager.default.isFile(at: buildServerPath) - { - return buildServerPath - } - - return nil - } - - /// Restart the BSP server after it has crashed. - private func handleBspServerCrash() async throws { - // Set `connectionToBuildServer` to `nil` to indicate that there is currently no BSP server running. - connectionToBuildServer = nil - - guard let initializeRequest else { - logger.error("BSP server crashed before it was sent an initialize request. Not restarting.") - return - } - - logger.error("The BSP server has crashed. Restarting.") - let restartDelay: Duration - if let lastClangdRestart = self.lastRestart, Date().timeIntervalSince(lastClangdRestart) < 30 { - logger.log("BSP server has been restarted in the last 30 seconds. Delaying another restart by 10 seconds.") - restartDelay = .seconds(10) - } else { - restartDelay = .zero - } - self.lastRestart = Date() - - try await Task.sleep(for: restartDelay) - - let restartedConnection = try await self.createConnectionToBspServer() - - // We assume that the server returns the same initialize response after being restarted. - // BSP does not set any state from the client to the server, so there are no other requests we need to replay - // (other than `textDocument/registerForChanges`, which is only used by the legacy BSP protocol, which didn't have - // crash recovery and doesn't need to gain it because it is deprecated). - _ = try await restartedConnection.send(initializeRequest) - restartedConnection.send(OnBuildInitializedNotification()) - self.connectionToBuildServer = restartedConnection - - // The build targets might have changed after the restart. Send a `buildTarget/didChange` notification to - // SourceKit-LSP to discard cached information. - self.messagesToSourceKitLSPHandler.handle(OnBuildTargetDidChangeNotification(changes: nil)) - } - - /// If the build server is still running after `duration`, terminate it. Otherwise a no-op. - package func terminateIfRunning(after duration: Duration) async throws { - try await process?.terminateIfRunning(after: duration) - } -} - -fileprivate extension URL { - /// If the path of this URL represents a directory, ensure that it has a trailing slash. - /// - /// This is important because if we form a file URL relative to eg. file:///tmp/a would assumes that `a` is a file - /// and use `/tmp` as the base, not `/tmp/a`. - var ensuringCorrectTrailingSlash: URL { - guard self.isFileURL else { - return self - } - // `URL(fileURLWithPath:)` checks the file system to decide whether a directory exists at the path. - return URL(fileURLWithPath: self.path) - } -} diff --git a/Sources/BuildServerIntegration/FallbackBuildSettings.swift b/Sources/BuildServerIntegration/FallbackBuildSettings.swift deleted file mode 100644 index 1fcfe3e01..000000000 --- a/Sources/BuildServerIntegration/FallbackBuildSettings.swift +++ /dev/null @@ -1,83 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -package import SKOptions -import SwiftExtensions -import TSCExtensions - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process - -/// The path to the SDK. -private let sdkpath: AbsolutePath? = { - guard Platform.current == .darwin else { return nil } - return try? AbsolutePath( - validating: Process.checkNonZeroExit(args: "/usr/bin/xcrun", "--show-sdk-path", "--sdk", "macosx") - .trimmingCharacters(in: .whitespacesAndNewlines) - ) -}() - -package func fallbackBuildSettings( - for uri: DocumentURI, - language: Language, - options: SourceKitLSPOptions.FallbackBuildSystemOptions -) -> FileBuildSettings? { - let args: [String] - switch language { - case .swift: - args = fallbackBuildSettingsSwift(for: uri, options: options) - case .c, .cpp, .objective_c, .objective_cpp: - args = fallbackBuildSettingsClang(for: uri, language: language, options: options) - default: - return nil - } - return FileBuildSettings(compilerArguments: args, workingDirectory: nil, language: language, isFallback: true) -} - -private func fallbackBuildSettingsSwift( - for uri: DocumentURI, - options: SourceKitLSPOptions.FallbackBuildSystemOptions -) -> [String] { - var args: [String] = options.swiftCompilerFlags ?? [] - if let sdkpath = AbsolutePath(validatingOrNil: options.sdk) ?? sdkpath, !args.contains("-sdk") { - args += ["-sdk", sdkpath.pathString] - } - args.append(uri.pseudoPath) - return args -} - -private func fallbackBuildSettingsClang( - for uri: DocumentURI, - language: Language, - options: SourceKitLSPOptions.FallbackBuildSystemOptions -) -> [String] { - var args: [String] = [] - switch language { - case .c: - args += options.cCompilerFlags ?? [] - case .cpp: - args += options.cxxCompilerFlags ?? [] - default: - break - } - if let sdkpath = AbsolutePath(validatingOrNil: options.sdk) ?? sdkpath, !args.contains("-isysroot") { - args += [ - "-isysroot", - sdkpath.pathString, - ] - } - args.append(uri.pseudoPath) - return args -} diff --git a/Sources/BuildServerIntegration/FileBuildSettings.swift b/Sources/BuildServerIntegration/FileBuildSettings.swift deleted file mode 100644 index fab6ef5db..000000000 --- a/Sources/BuildServerIntegration/FileBuildSettings.swift +++ /dev/null @@ -1,89 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions - -/// Build settings for a single file. -/// -/// Encapsulates all the settings needed to compile a single file, including the compiler arguments -/// and working directory. `FileBuildSettings`` are typically the result of a build server query. -package struct FileBuildSettings: Hashable, Sendable { - /// The compiler arguments to use for this file. - package var compilerArguments: [String] - - /// The working directory to resolve any relative paths in `compilerArguments`. - package var workingDirectory: String? = nil - - /// The language that the document was interpreted as, and which implies the compiler to which the build settings - /// would be passed. - package var language: Language - - /// Additional data about the build settings that was received from the BSP server, will not be interpreted by - /// SourceKit-LSP but returned to clients in the `workspace/_sourceKitOptions` LSP request. - package var data: LSPAny? - - /// Whether the build settings were computed from a real build server or whether they are synthesized fallback - /// arguments while the build server is still busy computing build settings. - package var isFallback: Bool - - package init( - compilerArguments: [String], - workingDirectory: String? = nil, - language: Language, - data: LSPAny? = nil, - isFallback: Bool = false - ) { - self.compilerArguments = compilerArguments - self.workingDirectory = workingDirectory - self.language = language - self.data = data - self.isFallback = isFallback - } - - /// Return arguments suitable for use by `newFile`. - /// - /// This patches the arguments by searching for the argument corresponding to - /// `originalFile` and replacing it. - package func patching(newFile: DocumentURI, originalFile: DocumentURI) -> FileBuildSettings { - var arguments = self.compilerArguments - // URL.lastPathComponent is only set for file URLs but we want to also infer a file extension for non-file URLs like - // untitled:file.cpp - let basename = originalFile.fileURL?.lastPathComponent ?? (originalFile.pseudoPath as NSString).lastPathComponent - if let index = arguments.lastIndex(where: { - // It's possible the arguments use relative paths while the `originalFile` given - // is an absolute/real path value. We guess based on suffixes instead of hitting - // the file system. - $0.hasSuffix(basename) && originalFile.pseudoPath.hasSuffix($0) - }) { - arguments[index] = newFile.pseudoPath - // The `-x` flag needs to be before the possible `-c
` - // argument in order for Clang to respect it. If there is a pre-existing `-x` - // flag though, Clang will honor that one instead since it comes after. - switch Language(inferredFromFileExtension: originalFile) { - case .c: arguments.insert("-xc", at: 0) - case .cpp: arguments.insert("-xc++", at: 0) - case .objective_c: arguments.insert("-xobjective-c", at: 0) - case .objective_cpp: arguments.insert("-xobjective-c++", at: 0) - default: break - } - } - return FileBuildSettings( - compilerArguments: arguments, - workingDirectory: self.workingDirectory, - language: self.language, - data: self.data, - isFallback: self.isFallback - ) - } -} diff --git a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift deleted file mode 100644 index 672111a1d..000000000 --- a/Sources/BuildServerIntegration/FixedCompilationDatabaseBuildServer.swift +++ /dev/null @@ -1,139 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -package import Foundation -package import LanguageServerProtocol -import SKLogging -import SwiftExtensions - -func lastIndexStorePathArgument(in compilerArgs: [String]) -> String? { - if let indexStorePathIndex = compilerArgs.lastIndex(of: "-index-store-path"), - indexStorePathIndex + 1 < compilerArgs.count - { - return compilerArgs[indexStorePathIndex + 1] - } - return nil -} - -/// A `BuiltInBuildServer` that provides compiler arguments from a `compile_flags.txt` file. -package actor FixedCompilationDatabaseBuildServer: BuiltInBuildServer { - package static let dbName = "compile_flags.txt" - - private let connectionToSourceKitLSP: any Connection - - package let configPath: URL - - /// The compiler arguments from the fixed compilation database. - /// - /// Note that this does not contain the path to a compiler. - var compilerArgs: [String] - - // Watch for all all changes to `compile_flags.txt` and `compile_flags.txt` instead of just the one at - // `configPath` so that we cover the following semi-common scenario: - // The user has a build that stores `compile_flags.txt` in `mybuild`. In order to pick it up, they create a - // symlink from `/compile_flags.txt` to `mybuild/compile_flags.txt`. We want to get notified - // about the change to `mybuild/compile_flags.txt` because it effectively changes the contents of - // `/compile_flags.txt`. - package let fileWatchers: [FileSystemWatcher] = [ - FileSystemWatcher(globPattern: "**/compile_flags.txt", kind: [.create, .change, .delete]) - ] - - package var indexStorePath: URL? { - guard let indexStorePath = lastIndexStorePathArgument(in: compilerArgs) else { - return nil - } - return URL(fileURLWithPath: indexStorePath, relativeTo: configPath.deletingLastPathComponent()) - } - - package var indexDatabasePath: URL? { - indexStorePath?.deletingLastPathComponent().appending(component: "IndexDatabase") - } - - package nonisolated var supportsPreparationAndOutputPaths: Bool { false } - - private static func parseCompileFlags(at configPath: URL) throws -> [String] { - let fileContents: String = try String(contentsOf: configPath, encoding: .utf8) - - var compilerArgs: [String] = [] - fileContents.enumerateLines { line, _ in - compilerArgs.append(line.trimmingCharacters(in: .whitespacesAndNewlines)) - } - return compilerArgs - } - - package init( - configPath: URL, - connectionToSourceKitLSP: any Connection - ) throws { - self.connectionToSourceKitLSP = connectionToSourceKitLSP - self.configPath = configPath - self.compilerArgs = try Self.parseCompileFlags(at: configPath) - } - - package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse { - return WorkspaceBuildTargetsResponse(targets: [ - BuildTarget( - id: .dummy, - tags: [.test], - capabilities: BuildTargetCapabilities(), - // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. - languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], - dependencies: [] - ) - ]) - } - - package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { - guard request.targets.contains(.dummy) else { - return BuildTargetSourcesResponse(items: []) - } - return BuildTargetSourcesResponse(items: [ - SourcesItem( - target: .dummy, - sources: [SourceItem(uri: URI(configPath.deletingLastPathComponent()), kind: .directory, generated: false)] - ) - ]) - } - - package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) { - if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == Self.dbName }) { - self.reloadCompilationDatabase() - } - } - - package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse { - throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method) - } - - package func sourceKitOptions( - request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? { - return TextDocumentSourceKitOptionsResponse( - compilerArguments: compilerArgs + [request.textDocument.uri.pseudoPath], - workingDirectory: try? configPath.deletingLastPathComponent().filePath - ) - } - - package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse { - return VoidResponse() - } - - /// The compilation database has been changed on disk. - /// Reload it and notify the delegate about build setting changes. - private func reloadCompilationDatabase() { - orLog("Reloading fixed compilation database") { - self.compilerArgs = try Self.parseCompileFlags(at: configPath) - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - } -} diff --git a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift b/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift deleted file mode 100644 index f6e9a619b..000000000 --- a/Sources/BuildServerIntegration/JSONCompilationDatabaseBuildServer.swift +++ /dev/null @@ -1,198 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerProtocol -package import Foundation -package import LanguageServerProtocol -import SKLogging -import SwiftExtensions -package import ToolchainRegistry - -fileprivate extension CompilationDatabaseCompileCommand { - /// The first entry in the command line identifies the compiler that should be used to compile the file and can thus - /// be used to infer the toolchain. - /// - /// Note that this compiler does not necessarily need to exist on disk. Eg. tools may just use `clang` as the compiler - /// without specifying a path. - /// - /// The absence of a compiler means we have an empty command line, which should never happen. - /// - /// If the compiler is a symlink to `swiftly`, it uses `swiftlyResolver` to find the corresponding executable in a - /// real toolchain and returns that executable. - func compiler(swiftlyResolver: SwiftlyResolver) async -> String? { - guard let compiler = commandLine.first else { - return nil - } - let swiftlyResolved = await orLog("Resolving swiftly") { - try await swiftlyResolver.resolve( - compiler: URL(fileURLWithPath: compiler), - workingDirectory: URL(fileURLWithPath: directory) - )?.filePath - } - if let swiftlyResolved { - return swiftlyResolved - } - return compiler - } -} - -/// A `BuiltInBuildServer` that provides compiler arguments from a `compile_commands.json` file. -package actor JSONCompilationDatabaseBuildServer: BuiltInBuildServer { - package static let dbName: String = "compile_commands.json" - - /// The compilation database. - var compdb: JSONCompilationDatabase { - didSet { - // Build settings have changed and thus the index store path might have changed. - // Recompute it on demand. - _indexStorePath.reset() - } - } - - private let toolchainRegistry: ToolchainRegistry - - private let connectionToSourceKitLSP: any Connection - - package let configPath: URL - - private let swiftlyResolver = SwiftlyResolver() - - // Watch for all all changes to `compile_commands.json` and `compile_flags.txt` instead of just the one at - // `configPath` so that we cover the following semi-common scenario: - // The user has a build that stores `compile_commands.json` in `mybuild`. In order to pick it up, they create a - // symlink from `/compile_commands.json` to `mybuild/compile_commands.json`. We want to get notified - // about the change to `mybuild/compile_commands.json` because it effectively changes the contents of - // `/compile_commands.json`. - package let fileWatchers: [FileSystemWatcher] = [ - FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]), - FileSystemWatcher(globPattern: "**/.swift-version", kind: [.create, .change, .delete]), - ] - - private var _indexStorePath: LazyValue = .uninitialized - package var indexStorePath: URL? { - _indexStorePath.cachedValueOrCompute { - for command in compdb.commands { - if let indexStorePath = lastIndexStorePathArgument(in: command.commandLine) { - return URL( - fileURLWithPath: indexStorePath, - relativeTo: URL(fileURLWithPath: command.directory, isDirectory: true) - ) - } - } - return nil - } - } - - package var indexDatabasePath: URL? { - indexStorePath?.deletingLastPathComponent().appending(component: "IndexDatabase") - } - - package nonisolated var supportsPreparationAndOutputPaths: Bool { false } - - package init( - configPath: URL, - toolchainRegistry: ToolchainRegistry, - connectionToSourceKitLSP: any Connection - ) throws { - self.compdb = try JSONCompilationDatabase(file: configPath) - self.toolchainRegistry = toolchainRegistry - self.connectionToSourceKitLSP = connectionToSourceKitLSP - self.configPath = configPath - } - - package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse { - let compilers = Set( - await compdb.commands.asyncCompactMap { (command) -> String? in - await command.compiler(swiftlyResolver: swiftlyResolver) - } - ).sorted { $0 < $1 } - let targets = try await compilers.asyncMap { compiler in - let toolchainUri: URI? = - if let toolchainPath = await toolchainRegistry.toolchain(withCompiler: URL(fileURLWithPath: compiler))?.path { - URI(toolchainPath) - } else { - nil - } - return BuildTarget( - id: try BuildTargetIdentifier.createCompileCommands(compiler: compiler), - tags: [.test], - capabilities: BuildTargetCapabilities(), - // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. - languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], - dependencies: [], - dataKind: .sourceKit, - data: SourceKitBuildTarget(toolchain: toolchainUri).encodeToLSPAny() - ) - } - return WorkspaceBuildTargetsResponse(targets: targets) - } - - package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { - let items = await request.targets.asyncCompactMap { (target) -> SourcesItem? in - guard let targetCompiler = orLog("Compiler for target", { try target.compileCommandsCompiler }) else { - return nil - } - let commandsWithRequestedCompilers = await compdb.commands.lazy.asyncFilter { command in - return await targetCompiler == command.compiler(swiftlyResolver: swiftlyResolver) - } - let sources = commandsWithRequestedCompilers.map { - SourceItem(uri: $0.uri, kind: .file, generated: false) - } - return SourcesItem(target: target, sources: Array(sources)) - } - - return BuildTargetSourcesResponse(items: items) - } - - package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async { - if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == Self.dbName }) { - self.reloadCompilationDatabase() - } - if notification.changes.contains(where: { $0.uri.fileURL?.lastPathComponent == ".swift-version" }) { - await swiftlyResolver.clearCache() - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - } - - package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse { - throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method) - } - - package func sourceKitOptions( - request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? { - let targetCompiler = try request.target.compileCommandsCompiler - let command = await compdb[request.textDocument.uri].asyncFilter { - return await $0.compiler(swiftlyResolver: swiftlyResolver) == targetCompiler - }.first - guard let command else { - return nil - } - return TextDocumentSourceKitOptionsResponse( - compilerArguments: Array(command.commandLine.dropFirst()), - workingDirectory: command.directory - ) - } - - package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse { - return VoidResponse() - } - - /// The compilation database has been changed on disk. - /// Reload it and notify the delegate about build setting changes. - private func reloadCompilationDatabase() { - orLog("Reloading compilation database") { - self.compdb = try JSONCompilationDatabase(file: configPath) - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - } -} diff --git a/Sources/BuildServerIntegration/LegacyBuildServer.swift b/Sources/BuildServerIntegration/LegacyBuildServer.swift deleted file mode 100644 index 2578450e7..000000000 --- a/Sources/BuildServerIntegration/LegacyBuildServer.swift +++ /dev/null @@ -1,212 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import LanguageServerProtocolJSONRPC -import SKLogging -import SKOptions -import SwiftExtensions -import ToolchainRegistry - -#if compiler(>=6.5) -#warning("We have had a two year transition period to the pull based build server. Consider removing this build server") -#endif - -/// Bridges the gap between the legacy push-based BSP settings model and the new pull based BSP settings model. -/// -/// On the one side, this type is a `BuiltInBuildServer` that offers pull-based build settings. To serve these queries, -/// it communicates with an external BSP server that uses `build/sourceKitOptionsChanged` notifications to communicate -/// build settings. -/// -/// This build server should be phased out in favor of the pull-based settings model described in -/// https://forums.swift.org/t/extending-functionality-of-build-server-protocol-with-sourcekit-lsp/74400 -actor LegacyBuildServer: MessageHandler, BuiltInBuildServer { - private var buildServer: JSONRPCConnection? - - /// The queue on which all messages that originate from the build server are - /// handled. - /// - /// These are requests and notifications sent *from* the build server, - /// not replies from the build server. - /// - /// This ensures that messages from the build server are handled in the order - /// they were received. Swift concurrency does not guarentee in-order - /// execution of tasks. - private let bspMessageHandlingQueue = AsyncQueue() - - package let projectRoot: URL - - package let configPath: URL - - var fileWatchers: [FileSystemWatcher] = [] - - let indexDatabasePath: URL? - let indexStorePath: URL? - - package let connectionToSourceKitLSP: LocalConnection - - /// The build settings that have been received from the build server. - private var buildSettings: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:] - - /// The files for which we have sent a `textDocument/registerForChanges` to the BSP server. - private var urisRegisteredForChanges: Set = [] - - init( - projectRoot: URL, - configPath: URL, - initializationData: InitializeBuildResponse, - _ externalBuildServerAdapter: ExternalBuildServerAdapter - ) async { - self.projectRoot = projectRoot - self.configPath = configPath - self.indexDatabasePath = nil - self.indexStorePath = nil - self.connectionToSourceKitLSP = LocalConnection( - receiverName: "BuildServerManager", - handler: await externalBuildServerAdapter.messagesToSourceKitLSPHandler - ) - await externalBuildServerAdapter.changeMessageToSourceKitLSPHandler(to: self) - self.buildServer = await externalBuildServerAdapter.connectionToBuildServer - } - - /// Handler for notifications received **from** the builder server, ie. - /// the build server has sent us a notification. - /// - /// We need to notify the delegate about any updated build settings. - package nonisolated func handle(_ params: some NotificationType) { - logger.info( - """ - Received notification from legacy BSP server: - \(params.forLogging) - """ - ) - bspMessageHandlingQueue.async { - if let params = params as? OnBuildTargetDidChangeNotification { - await self.handleBuildTargetsChanged(params) - } else if let params = params as? FileOptionsChangedNotification { - await self.handleFileOptionsChanged(params) - } - } - } - - /// Handler for requests received **from** the build server. - /// - /// We currently can't handle any requests sent from the build server to us. - package nonisolated func handle( - _ params: R, - id: RequestID, - reply: @escaping (LSPResult) -> Void - ) { - logger.info( - """ - Received request from legacy BSP server: - \(params.forLogging) - """ - ) - reply(.failure(ResponseError.methodNotFound(R.method))) - } - - func handleBuildTargetsChanged(_ notification: OnBuildTargetDidChangeNotification) { - connectionToSourceKitLSP.send(notification) - } - - func handleFileOptionsChanged(_ notification: FileOptionsChangedNotification) async { - let result = notification.updatedOptions - let settings = TextDocumentSourceKitOptionsResponse( - compilerArguments: result.options, - workingDirectory: result.workingDirectory - ) - await self.buildSettingsChanged(for: notification.uri, settings: settings) - } - - /// Record the new build settings for the given document and inform the delegate - /// about the changed build settings. - private func buildSettingsChanged(for document: DocumentURI, settings: TextDocumentSourceKitOptionsResponse?) async { - buildSettings[document] = settings - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - - package nonisolated var supportsPreparationAndOutputPaths: Bool { false } - - package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse { - return WorkspaceBuildTargetsResponse(targets: [ - BuildTarget( - id: .dummy, - displayName: "BuildServer", - tags: [.test], - capabilities: BuildTargetCapabilities(), - // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. - languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], - dependencies: [] - ) - ]) - } - - package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { - guard request.targets.contains(.dummy) else { - return BuildTargetSourcesResponse(items: []) - } - return BuildTargetSourcesResponse(items: [ - SourcesItem( - target: .dummy, - sources: [SourceItem(uri: DocumentURI(self.projectRoot), kind: .directory, generated: false)] - ) - ]) - } - - package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) {} - - package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse { - throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method) - } - - package func sourceKitOptions( - request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? { - // Support the pre Swift 6.1 build settings workflow where SourceKit-LSP registers for changes for a file and then - // expects updates to those build settings to get pushed to SourceKit-LSP with `FileOptionsChangedNotification`. - // We do so by registering for changes when requesting build settings for a document for the first time. We never - // unregister for changes. The expectation is that all BSP servers migrate to the `SourceKitOptionsRequest` soon, - // which renders this code path dead. - let uri = request.textDocument.uri - if urisRegisteredForChanges.insert(uri).inserted { - let request = RegisterForChanges(uri: uri, action: .register) - _ = self.buildServer?.send(request) { result in - if let error = result.failure { - logger.error("Error registering \(request.uri): \(error.forLogging)") - - Task { - // BuildServer registration failed, so tell our delegate that no build - // settings are available. - await self.buildSettingsChanged(for: request.uri, settings: nil) - } - } - } - } - - guard let buildSettings = buildSettings[uri] else { - return nil - } - - return TextDocumentSourceKitOptionsResponse( - compilerArguments: buildSettings.compilerArguments, - workingDirectory: buildSettings.workingDirectory - ) - } - - package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse { - return VoidResponse() - } -} diff --git a/Sources/BuildServerIntegration/MainFilesProvider.swift b/Sources/BuildServerIntegration/MainFilesProvider.swift deleted file mode 100644 index 48511de81..000000000 --- a/Sources/BuildServerIntegration/MainFilesProvider.swift +++ /dev/null @@ -1,28 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol - -/// A type that can provide the set of main files that include a particular file. -package protocol MainFilesProvider: Sendable { - /// Returns all the files that (transitively) include the header file at the given path. - /// - /// If `crossLanguage` is set to `true`, Swift files that import a header through a module will also be reported. - /// - /// ### Examples - /// - /// ``` - /// mainFilesContainingFile("foo.cpp") == Set(["foo.cpp"]) - /// mainFilesContainingFile("foo.h") == Set(["foo.cpp", "bar.cpp"]) - /// ``` - func mainFiles(containing uri: DocumentURI, crossLanguage: Bool) async -> Set -} diff --git a/Sources/BuildServerIntegration/SplitShellCommand.swift b/Sources/BuildServerIntegration/SplitShellCommand.swift deleted file mode 100644 index 0460cc5a5..000000000 --- a/Sources/BuildServerIntegration/SplitShellCommand.swift +++ /dev/null @@ -1,346 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -// MARK: - Unix - -private struct UnixCommandParser { - var content: Substring - var i: Substring.UTF8View.Index - var result: [String] = [] - - var ch: UInt8 { self.content.utf8[i] } - var done: Bool { self.content.endIndex == i } - - init(_ string: Substring) { - self.content = string - self.i = self.content.utf8.startIndex - } - - mutating func next() { - i = content.utf8.index(after: i) - } - - mutating func next(expect c: UInt8) { - assert(c == ch) - next() - } - - mutating func parse() -> [String] { - while !done { - switch ch { - case UInt8(ascii: " "): next() - default: parseString() - } - } - return result - } - - mutating func parseString() { - var str = "" - STRING: while !done { - switch ch { - case UInt8(ascii: " "): break STRING - case UInt8(ascii: "\""): parseDoubleQuotedString(into: &str) - case UInt8(ascii: "\'"): parseSingleQuotedString(into: &str) - default: parsePlainString(into: &str) - } - } - result.append(str) - } - - mutating func parseDoubleQuotedString(into str: inout String) { - next(expect: UInt8(ascii: "\"")) - var start = i - while !done { - switch ch { - case UInt8(ascii: "\""): - str += content[start.. ["abc", "def"] -/// abc\ def -> ["abc def"] -/// abc"\""def -> ["abc\"def"] -/// abc'\"'def -> ["abc\\"def"] -/// ``` -/// -/// See clang's `unescapeCommandLine()`. -package func splitShellEscapedCommand(_ cmd: String) -> [String] { - var parser = UnixCommandParser(cmd[...]) - return parser.parse() -} - -// MARK: - Windows - -fileprivate extension Character { - var isWhitespace: Bool { - switch self { - case " ", "\t": - return true - default: - return false - } - } - - var isWhitespaceOrNull: Bool { - return self.isWhitespace || self == "\0" - } - - func isWindowsSpecialChar(inCommandName: Bool) -> Bool { - if isWhitespace { - return true - } - if self == #"""# { - return true - } - if !inCommandName && self == #"\"# { - return true - } - return false - } -} - -private struct WindowsCommandParser { - /// The content of the entire command that shall be parsed. - private let content: String - - /// Whether we are parsing the initial command name. In this mode `\` is not treated as escaping the quote - /// character. - private var parsingCommandName: Bool - - /// An index into `content`, pointing to the character that we are currently parsing. - private var currentCharacterIndex: String.UTF8View.Index - - /// The split command line arguments. - private var result: [String] = [] - - /// The character that is currently being parsed. - /// - /// `nil` if we have reached the end of `content`. - private var currentCharacter: Character? { - guard currentCharacterIndex < content.endIndex else { - return nil - } - return self.content[currentCharacterIndex] - } - - /// The character after `currentCharacter`. - /// - /// `nil` if we have reached the end of `content`. - private var peek: Character? { - let nextIndex = content.index(after: currentCharacterIndex) - if nextIndex < content.endIndex { - return content[nextIndex] - } else { - return nil - } - } - - init(_ string: String, initialCommandName: Bool) { - self.content = string - self.currentCharacterIndex = self.content.startIndex - self.parsingCommandName = initialCommandName - } - - /// Designated entry point to split a Windows command line invocation. - mutating func parse() -> [String] { - while let currentCharacter { - if currentCharacter.isWhitespaceOrNull { - // Consume any whitespace separating arguments. - _ = consume() - } else { - result.append(parseSingleArgument()) - } - } - return result - } - - /// Consume the current character. - private mutating func consume() -> Character { - guard let character = currentCharacter else { - preconditionFailure("Nothing to consume") - } - currentCharacterIndex = content.index(after: currentCharacterIndex) - return character - } - - /// Consume the current character, asserting that it is `expectedCharacter` - private mutating func consume(expect expectedCharacter: Character) { - assert(currentCharacter == expectedCharacter) - _ = consume() - } - - /// Parses a single argument, consuming its characters and returns the parsed arguments with all escaping unfolded - /// (e.g. `\"` gets returned as `"`) - /// - /// Afterwards the parser points to the character after the argument. - mutating func parseSingleArgument() -> String { - var str = "" - while let currentCharacter { - if !currentCharacter.isWindowsSpecialChar(inCommandName: parsingCommandName) { - str.append(consume()) - continue - } - if currentCharacter.isWhitespaceOrNull { - parsingCommandName = false - return str - } else if currentCharacter == "\"" { - str += parseQuoted() - } else if currentCharacter == #"\"# { - assert(!parsingCommandName, "else we'd have treated it as a normal char") - str.append(parseBackslash()) - } else { - preconditionFailure("unexpected special character") - } - } - return str - } - - /// Assuming that we are positioned at a `"`, parse a quoted string and return the string contents without the - /// quotes. - mutating func parseQuoted() -> String { - // Discard the opening quote. Its not part of the unescaped text. - consume(expect: "\"") - - var str = "" - while let currentCharacter { - switch currentCharacter { - case "\"": - if peek == "\"" { - // Two adjacent quotes inside a quoted string are an escaped single quote. For example - // `" a "" b "` - // represents the string - // ` a " b ` - consume(expect: "\"") - consume(expect: "\"") - str += "\"" - } else { - // We have found the closing quote. Discard it and return. - consume(expect: "\"") - return str - } - case "\\" where !parsingCommandName: - str.append(parseBackslash()) - default: - str.append(consume()) - } - } - return str - } - - /// Backslashes are interpreted in a rather complicated way in the Windows-style - /// command line, because backslashes are used both to separate path and to - /// escape double quote. This method consumes runs of backslashes as well as the - /// following double quote if it's escaped. - /// - /// * If an even number of backslashes is followed by a double quote, one - /// backslash is output for every pair of backslashes, and the last double - /// quote remains unconsumed. The double quote will later be interpreted as - /// the start or end of a quoted string in the main loop outside of this - /// function. - /// - /// * If an odd number of backslashes is followed by a double quote, one - /// backslash is output for every pair of backslashes, and a double quote is - /// output for the last pair of backslash-double quote. The double quote is - /// consumed in this case. - /// - /// * Otherwise, backslashes are interpreted literally. - mutating func parseBackslash() -> String { - var str: String = "" - - let firstNonBackslashIndex = content[currentCharacterIndex...].firstIndex(where: { $0 != "\\" }) ?? content.endIndex - let numberOfBackslashes = content.distance(from: currentCharacterIndex, to: firstNonBackslashIndex) - - if firstNonBackslashIndex != content.endIndex && content[firstNonBackslashIndex] == "\"" { - str += String(repeating: "\\", count: numberOfBackslashes / 2) - if numberOfBackslashes.isMultiple(of: 2) { - // We have an even number of backslashes. Just add the escaped backslashes to `str` and return to parse the - // quote in the outer function. - currentCharacterIndex = firstNonBackslashIndex - } else { - // We have an odd number of backslashes. The last backslash escapes the quote. - str += "\"" - currentCharacterIndex = content.index(after: firstNonBackslashIndex) - } - return str - } - - // The sequence of backslashes is not followed by quotes. Interpret them literally. - str += String(repeating: "\\", count: numberOfBackslashes) - currentCharacterIndex = firstNonBackslashIndex - return str - } -} - -// Sometimes, this function will be handling a full command line including an -// executable pathname at the start. In that situation, the initial pathname -// needs different handling from the following arguments, because when -// CreateProcess or cmd.exe scans the pathname, it doesn't treat \ as -// escaping the quote character, whereas when libc scans the rest of the -// command line, it does. -package func splitWindowsCommandLine(_ cmd: String, initialCommandName: Bool) -> [String] { - var parser = WindowsCommandParser(cmd, initialCommandName: initialCommandName) - return parser.parse() -} diff --git a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift b/Sources/BuildServerIntegration/SwiftPMBuildServer.swift deleted file mode 100644 index fcf5e4d81..000000000 --- a/Sources/BuildServerIntegration/SwiftPMBuildServer.swift +++ /dev/null @@ -1,911 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#if !NO_SWIFTPM_DEPENDENCY -import Basics -@preconcurrency import Build -package import BuildServerProtocol -import Dispatch -package import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -@preconcurrency import PackageGraph -import PackageLoading -@preconcurrency import PackageModel -import SKLogging -package import SKOptions -@preconcurrency package import SPMBuildCore -import SourceControl -@preconcurrency package import SourceKitLSPAPI -import SwiftExtensions -import TSCExtensions -package import ToolchainRegistry -@preconcurrency import Workspace - -package import struct BuildServerProtocol.SourceItem -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -package import class ToolchainRegistry.Toolchain -import struct TSCBasic.FileSystemError - -private typealias AbsolutePath = Basics.AbsolutePath - -/// A build target in SwiftPM -package typealias SwiftBuildTarget = SourceKitLSPAPI.BuildTarget - -/// A build target in `BuildServerProtocol` -package typealias BuildServerTarget = BuildServerProtocol.BuildTarget - -fileprivate extension Basics.Diagnostic.Severity { - var asLogLevel: LogLevel { - switch self { - case .error, .warning: return .default - case .info: return .info - case .debug: return .debug - } - } -} - -extension BuildDestinationIdentifier { - init(_ destination: BuildDestination) { - switch destination { - case .target: self = .target - case .host: self = .host - } - } -} - -extension BuildTargetIdentifier { - fileprivate init(_ buildTarget: any SwiftBuildTarget) throws { - self = try Self.createSwiftPM( - target: buildTarget.name, - destination: BuildDestinationIdentifier(buildTarget.destination) - ) - } -} - -fileprivate extension TSCBasic.AbsolutePath { - var asURI: DocumentURI { - DocumentURI(self.asURL) - } -} - -private let preparationTaskID: AtomicUInt32 = AtomicUInt32(initialValue: 0) - -/// Swift Package Manager build server and workspace support. -/// -/// This class implements the `BuiltInBuildServe` interface to provide the build settings for a Swift -/// Package Manager (SwiftPM) package. The settings are determined by loading the Package.swift -/// manifest using `libSwiftPM` and constructing a build plan using the default (debug) parameters. -package actor SwiftPMBuildServer: BuiltInBuildServer { - package enum Error: Swift.Error { - /// Could not determine an appropriate toolchain for swiftpm to use for manifest loading. - case cannotDetermineHostToolchain - } - - // MARK: Integration with SourceKit-LSP - - /// Options that allow the user to pass extra compiler flags. - private let options: SourceKitLSPOptions - - private let testHooks: SwiftPMTestHooks - - /// The queue on which we reload the package to ensure we don't reload it multiple times concurrently, which can cause - /// issues in SwiftPM. - private let packageLoadingQueue = AsyncQueue() - - package let connectionToSourceKitLSP: any Connection - - /// Whether the `SwiftPMBuildServer` is pointed at a `.build/index-build` directory that's independent of the - /// user's build. - private var isForIndexBuild: Bool { options.backgroundIndexingOrDefault } - - // MARK: Build server options (set once and not modified) - - /// The directory containing `Package.swift`. - private let projectRoot: URL - - package let fileWatchers: [FileSystemWatcher] - - package let toolsBuildParameters: BuildParameters - package let destinationBuildParameters: BuildParameters - - private let toolchain: Toolchain - private let swiftPMWorkspace: Workspace - - private let pluginConfiguration: PluginConfiguration - private let traitConfiguration: TraitConfiguration - - /// A `ObservabilitySystem` from `SwiftPM` that logs. - private let observabilitySystem: ObservabilitySystem - - // MARK: Build server state (modified on package reload) - - /// The entry point via with we can access the `SourceKitLSPAPI` provided by SwiftPM. - private var buildDescription: SourceKitLSPAPI.BuildDescription? - - /// Maps target ids to their SwiftPM build target. - private var swiftPMTargets: [BuildTargetIdentifier: SwiftBuildTarget] = [:] - - private var targetDependencies: [BuildTargetIdentifier: Set] = [:] - - /// Regular expression that matches version-specific package manifest file names such as Package@swift-6.1.swift - private static var versionSpecificPackageManifestNameRegex: Regex<(Substring, Substring, Substring?, Substring?)> { - #/^Package@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift$/# - } - - static package func searchForConfig(in path: URL, options: SourceKitLSPOptions) -> BuildServerSpec? { - let packagePath = path.appending(component: "Package.swift") - if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false { - return BuildServerSpec(kind: .swiftPM, projectRoot: path, configPath: packagePath) - } - - return nil - } - - /// Creates a build server using the Swift Package Manager, if this workspace is a package. - /// - /// - Parameters: - /// - projectRoot: The directory containing `Package.swift` - /// - toolchainRegistry: The toolchain registry to use to provide the Swift compiler used for - /// manifest parsing and runtime support. - /// - Throws: If there is an error loading the package, or no manifest is found. - package init( - projectRoot: URL, - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions, - connectionToSourceKitLSP: any Connection, - testHooks: SwiftPMTestHooks - ) async throws { - self.projectRoot = projectRoot - self.options = options - // We could theoretically dynamically register all known files when we get back the build graph, but that seems - // more errorprone than just watching everything and then filtering when we need to (eg. in - // `SemanticIndexManager.filesDidChange`). - self.fileWatchers = [FileSystemWatcher(globPattern: "**/*", kind: [.create, .change, .delete])] - let toolchain = await toolchainRegistry.preferredToolchain(containing: [ - \.clang, \.clangd, \.sourcekitd, \.swift, \.swiftc, - ]) - guard let toolchain else { - throw Error.cannotDetermineHostToolchain - } - - self.toolchain = toolchain - self.testHooks = testHooks - self.connectionToSourceKitLSP = connectionToSourceKitLSP - - // Start an open-ended log for messages that we receive during package loading. We never end this log. - let logTaskID = TaskId(id: "swiftpm-log-\(UUID())") - connectionToSourceKitLSP.send( - OnBuildLogMessageNotification( - type: .info, - task: logTaskID, - message: "", - structure: .begin(StructuredLogBegin(title: "SwiftPM log for \(projectRoot.path)")) - ) - ) - - self.observabilitySystem = ObservabilitySystem({ scope, diagnostic in - connectionToSourceKitLSP.send( - OnBuildLogMessageNotification( - type: .info, - task: logTaskID, - message: diagnostic.description, - structure: .report(StructuredLogReport()) - ) - ) - logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") - }) - - guard let destinationToolchainBinDir = toolchain.swiftc?.deletingLastPathComponent() else { - throw Error.cannotDetermineHostToolchain - } - - let absProjectRoot = try AbsolutePath(validating: projectRoot.filePath) - let hostSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(validating: destinationToolchainBinDir.filePath)) - let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK) - - let destinationSDK = try SwiftSDK.deriveTargetSwiftSDK( - hostSwiftSDK: hostSDK, - hostTriple: hostSwiftPMToolchain.targetTriple, - customToolsets: options.swiftPMOrDefault.toolsets?.map { - try AbsolutePath(validating: $0, relativeTo: absProjectRoot) - } ?? [], - customCompileTriple: options.swiftPMOrDefault.triple.map { try Triple($0) }, - swiftSDKSelector: options.swiftPMOrDefault.swiftSDK, - store: SwiftSDKBundleStore( - swiftSDKsDirectory: localFileSystem.getSharedSwiftSDKsDirectory( - explicitDirectory: options.swiftPMOrDefault.swiftSDKsDirectory.map { - try AbsolutePath(validating: $0, relativeTo: absProjectRoot) - } - ), - hostToolchainBinDir: hostSwiftPMToolchain.swiftCompilerPath.parentDirectory, - fileSystem: localFileSystem, - observabilityScope: observabilitySystem.topScope.makeChildScope(description: "SwiftPM Bundle Store"), - outputHandler: { _ in } - ), - observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Derive Target Swift SDK"), - fileSystem: localFileSystem - ) - - let destinationSwiftPMToolchain = try UserToolchain(swiftSDK: destinationSDK) - - var location = try Workspace.Location( - forRootPackage: absProjectRoot, - fileSystem: localFileSystem - ) - - if let scratchDirectory = options.swiftPMOrDefault.scratchPath { - location.scratchDirectory = try AbsolutePath(validating: scratchDirectory, relativeTo: absProjectRoot) - } else if options.backgroundIndexingOrDefault { - location.scratchDirectory = absProjectRoot.appending(components: ".build", "index-build") - } - - var configuration = WorkspaceConfiguration.default - configuration.skipDependenciesUpdates = !options.backgroundIndexingOrDefault - - self.swiftPMWorkspace = try Workspace( - fileSystem: localFileSystem, - location: location, - configuration: configuration, - customHostToolchain: hostSwiftPMToolchain, - customManifestLoader: ManifestLoader( - toolchain: hostSwiftPMToolchain, - isManifestSandboxEnabled: !(options.swiftPMOrDefault.disableSandbox ?? false), - cacheDir: location.sharedManifestsCacheDirectory, - extraManifestFlags: options.swiftPMOrDefault.buildToolsSwiftCompilerFlags, - importRestrictions: configuration.manifestImportRestrictions - ) - ) - - let buildConfiguration: PackageModel.BuildConfiguration - switch options.swiftPMOrDefault.configuration { - case .debug, nil: - buildConfiguration = .debug - case .release: - buildConfiguration = .release - } - - let buildFlags = BuildFlags( - cCompilerFlags: options.swiftPMOrDefault.cCompilerFlags ?? [], - cxxCompilerFlags: options.swiftPMOrDefault.cxxCompilerFlags ?? [], - swiftCompilerFlags: options.swiftPMOrDefault.swiftCompilerFlags ?? [], - linkerFlags: options.swiftPMOrDefault.linkerFlags ?? [] - ) - - self.toolsBuildParameters = try BuildParameters( - destination: .host, - dataPath: location.scratchDirectory.appending( - component: hostSwiftPMToolchain.targetTriple.platformBuildPathComponent - ), - configuration: buildConfiguration, - toolchain: hostSwiftPMToolchain, - flags: buildFlags, - buildSystemKind: .native, - ) - - self.destinationBuildParameters = try BuildParameters( - destination: .target, - dataPath: location.scratchDirectory.appending( - component: destinationSwiftPMToolchain.targetTriple.platformBuildPathComponent - ), - configuration: buildConfiguration, - toolchain: destinationSwiftPMToolchain, - triple: destinationSDK.targetTriple, - flags: buildFlags, - buildSystemKind: .native, - prepareForIndexing: options.backgroundPreparationModeOrDefault.toSwiftPMPreparation - ) - - let pluginScriptRunner = DefaultPluginScriptRunner( - fileSystem: localFileSystem, - cacheDir: location.pluginWorkingDirectory.appending("cache"), - toolchain: hostSwiftPMToolchain, - extraPluginSwiftCFlags: options.swiftPMOrDefault.buildToolsSwiftCompilerFlags ?? [], - enableSandbox: !(options.swiftPMOrDefault.disableSandbox ?? false) - ) - self.pluginConfiguration = PluginConfiguration( - scriptRunner: pluginScriptRunner, - workDirectory: location.pluginWorkingDirectory, - disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false - ) - - self.traitConfiguration = TraitConfiguration( - enabledTraits: options.swiftPMOrDefault.traits.flatMap(Set.init) - ) - - packageLoadingQueue.async { - await orLog("Initial package loading") { - // Schedule an initial generation of the build graph. Once the build graph is loaded, the build server will send - // call `fileHandlingCapabilityChanged`, which allows us to move documents to a workspace with this build - // server. - try await self.reloadPackageAssumingOnPackageLoadingQueue() - } - } - } - - /// Loading the build description sometimes fails non-deterministically on Windows because it's unable to write - /// `output-file-map.json`, probably due to https://github.com/swiftlang/swift-package-manager/issues/8038. - /// If this happens, retry loading the build description up to `maxLoadAttempt` times. - private func loadBuildDescriptionWithRetryOnOutputFileMapWriteErrorOnWindows( - modulesGraph: ModulesGraph, - maxLoadAttempts: Int = 5 - ) async throws -> (description: SourceKitLSPAPI.BuildDescription, errors: String) { - // TODO: Remove this workaround once https://github.com/swiftlang/swift-package-manager/issues/8038 is fixed. - var loadAttempt = 0 - while true { - loadAttempt += 1 - do { - return try await BuildDescription.load( - destinationBuildParameters: destinationBuildParameters, - toolsBuildParameters: toolsBuildParameters, - packageGraph: modulesGraph, - pluginConfiguration: pluginConfiguration, - traitConfiguration: traitConfiguration, - disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false, - scratchDirectory: swiftPMWorkspace.location.scratchDirectory.asURL, - fileSystem: localFileSystem, - observabilityScope: observabilitySystem.topScope.makeChildScope( - description: "Create SwiftPM build description" - ) - ) - } catch { - guard SwiftExtensions.Platform.current == .windows else { - // We only retry loading the build description on Windows. The output-file-map issue does not exist on other - // platforms. - throw error - } - let isOutputFileMapWriteError: Bool - let nsError = error as NSError - if nsError.domain == NSCocoaErrorDomain, - nsError.code == CocoaError.fileWriteNoPermission.rawValue, - (nsError.userInfo["NSURL"] as? URL)?.lastPathComponent == "output-file-map.json" - { - isOutputFileMapWriteError = true - } else if let error = error as? FileSystemError, - error.kind == .invalidAccess && error.path?.basename == "output-file-map.json" - { - isOutputFileMapWriteError = true - } else { - isOutputFileMapWriteError = false - } - if isOutputFileMapWriteError, loadAttempt < maxLoadAttempts { - logger.log( - """ - Loading the build description failed to write output-file-map.json \ - (attempt \(loadAttempt)/\(maxLoadAttempts)), trying again. - \(error.forLogging) - """ - ) - continue - } - throw error - } - } - } - - /// (Re-)load the package settings by parsing the manifest and resolving all the targets and - /// dependencies. - /// - /// - Important: Must only be called on `packageLoadingQueue`. - private func reloadPackageAssumingOnPackageLoadingQueue() async throws { - let signposter = logger.makeSignposter() - let signpostID = signposter.makeSignpostID() - let state = signposter.beginInterval("Reloading package", id: signpostID, "Start reloading package") - - self.connectionToSourceKitLSP.send( - TaskStartNotification( - taskId: TaskId(id: "package-reloading"), - data: WorkDoneProgressTask(title: "SourceKit-LSP: Reloading Package").encodeToLSPAny() - ) - ) - await testHooks.reloadPackageDidStart?() - defer { - signposter.endInterval("Reloading package", state) - Task { - self.connectionToSourceKitLSP.send( - TaskFinishNotification(taskId: TaskId(id: "package-reloading"), status: .ok) - ) - await testHooks.reloadPackageDidFinish?() - } - } - - let modulesGraph = try await self.swiftPMWorkspace.loadPackageGraph( - rootInput: PackageGraphRootInput(packages: [AbsolutePath(validating: projectRoot.filePath)]), - forceResolvedVersions: !isForIndexBuild, - observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Load package graph") - ) - - signposter.emitEvent("Finished loading modules graph", id: signpostID) - - // We have a whole separate arena if we're performing background indexing. This allows us to also build and run - // plugins, without having to worry about messing up any regular build state. - let buildDescription: SourceKitLSPAPI.BuildDescription - if isForIndexBuild && !(options.swiftPMOrDefault.skipPlugins ?? false) { - let loaded = try await loadBuildDescriptionWithRetryOnOutputFileMapWriteErrorOnWindows(modulesGraph: modulesGraph) - if !loaded.errors.isEmpty { - logger.error("Loading SwiftPM description had errors: \(loaded.errors)") - } - - signposter.emitEvent("Finished generating build description", id: signpostID) - - buildDescription = loaded.description - } else { - let plan = try await BuildPlan( - destinationBuildParameters: destinationBuildParameters, - toolsBuildParameters: toolsBuildParameters, - graph: modulesGraph, - disableSandbox: options.swiftPMOrDefault.disableSandbox ?? false, - fileSystem: localFileSystem, - observabilityScope: observabilitySystem.topScope.makeChildScope(description: "Create SwiftPM build plan") - ) - - signposter.emitEvent("Finished generating build plan", id: signpostID) - - buildDescription = BuildDescription(buildPlan: plan) - } - - /// Make sure to execute any throwing statements before setting any - /// properties because otherwise we might end up in an inconsistent state - /// with only some properties modified. - - self.buildDescription = buildDescription - self.swiftPMTargets = [:] - self.targetDependencies = [:] - - buildDescription.traverseModules { buildTarget, parent in - let targetIdentifier = orLog("Getting build target identifier") { try BuildTargetIdentifier(buildTarget) } - guard let targetIdentifier else { - return - } - if let parent, - let parentIdentifier = orLog("Getting parent build target identifier", { try BuildTargetIdentifier(parent) }) - { - self.targetDependencies[parentIdentifier, default: []].insert(targetIdentifier) - } - swiftPMTargets[targetIdentifier] = buildTarget - } - - signposter.emitEvent("Finished traversing modules", id: signpostID) - - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - - package nonisolated var supportsPreparationAndOutputPaths: Bool { options.backgroundIndexingOrDefault } - - package var buildPath: URL { - return destinationBuildParameters.buildPath.asURL - } - - package var indexStorePath: URL? { - if destinationBuildParameters.indexStoreMode == .off { - return nil - } - return destinationBuildParameters.indexStore.asURL - } - - package var indexDatabasePath: URL? { - return buildPath.appending(components: "index", "db") - } - - private func indexUnitOutputPath(forSwiftFile uri: DocumentURI) -> String { - return uri.pseudoPath + ".o" - } - - /// Return the compiler arguments for the given source file within a target, making any necessary adjustments to - /// account for differences in the SwiftPM versions being linked into SwiftPM and being installed in the toolchain. - private func compilerArguments(for file: DocumentURI, in buildTarget: any SwiftBuildTarget) async throws -> [String] { - guard let fileURL = file.fileURL else { - struct NonFileURIError: Swift.Error, CustomStringConvertible { - let uri: DocumentURI - var description: String { - "Trying to get build settings for non-file URI: \(uri)" - } - } - - throw NonFileURIError(uri: file) - } - #if compiler(>=6.4) - #warning( - "Once we can guarantee that the toolchain can index multiple Swift files in a single invocation, we no longer need to set -index-unit-output-path since it's always set using an -output-file-map" - ) - #endif - var compilerArguments = try buildTarget.compileArguments(for: fileURL) - if buildTarget.compiler == .swift { - compilerArguments += [ - // Fake an output path so that we get a different unit file for every Swift file we background index - "-index-unit-output-path", indexUnitOutputPath(forSwiftFile: file), - ] - } - return compilerArguments - } - - package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse { - var targets = self.swiftPMTargets.map { (targetId, target) in - var tags: [BuildTargetTag] = [] - if target.isTestTarget { - tags.append(.test) - } - if !target.isPartOfRootPackage { - tags.append(.dependency) - } - return BuildTarget( - id: targetId, - displayName: target.name, - tags: tags, - capabilities: BuildTargetCapabilities(), - // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. - languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], - dependencies: self.targetDependencies[targetId, default: []].sorted { $0.uri.stringValue < $1.uri.stringValue }, - dataKind: .sourceKit, - data: SourceKitBuildTarget(toolchain: URI(toolchain.path)).encodeToLSPAny() - ) - } - targets.append( - BuildTarget( - id: .forPackageManifest, - displayName: "Package.swift", - tags: [.notBuildable], - capabilities: BuildTargetCapabilities(), - languageIds: [.swift], - dependencies: [] - ) - ) - return WorkspaceBuildTargetsResponse(targets: targets) - } - - package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { - var result: [SourcesItem] = [] - // TODO: Query The SwiftPM build server for the document's language and add it to SourceItem.data - // (https://github.com/swiftlang/sourcekit-lsp/issues/1267) - for target in request.targets { - if target == .forPackageManifest { - let versionSpecificManifests = try? FileManager.default.contentsOfDirectory( - at: projectRoot, - includingPropertiesForKeys: nil - ).compactMap { (url) -> SourceItem? in - guard (try? Self.versionSpecificPackageManifestNameRegex.wholeMatch(in: url.lastPathComponent)) != nil else { - return nil - } - return SourceItem( - uri: DocumentURI(url), - kind: .file, - generated: false - ) - } - let packageManifest = SourceItem( - uri: DocumentURI(projectRoot.appending(component: "Package.swift")), - kind: .file, - generated: false - ) - result.append( - SourcesItem( - target: target, - sources: [packageManifest] + (versionSpecificManifests ?? []) - ) - ) - } - guard let swiftPMTarget = self.swiftPMTargets[target] else { - continue - } - var sources = swiftPMTarget.sources.map { sourceItem in - let outputPath: String? = - if let outputFile = sourceItem.outputFile { - orLog("Getting file path of output file") { try outputFile.filePath } - } else if swiftPMTarget.compiler == .swift { - indexUnitOutputPath(forSwiftFile: DocumentURI(sourceItem.sourceFile)) - } else { - nil - } - return SourceItem( - uri: DocumentURI(sourceItem.sourceFile), - kind: .file, - generated: false, - dataKind: .sourceKit, - data: SourceKitSourceItemData(outputPath: outputPath).encodeToLSPAny() - ) - } - sources += swiftPMTarget.headers.map { - SourceItem( - uri: DocumentURI($0), - kind: .file, - generated: false, - dataKind: .sourceKit, - data: SourceKitSourceItemData(kind: .header).encodeToLSPAny() - ) - } - sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others) - .map { (url: URL) -> SourceItem in - var data: SourceKitSourceItemData? = nil - if url.isDirectory, url.pathExtension == "docc" { - data = SourceKitSourceItemData(kind: .doccCatalog) - } - return SourceItem( - uri: DocumentURI(url), - kind: url.isDirectory ? .directory : .file, - generated: false, - dataKind: data != nil ? .sourceKit : nil, - data: data?.encodeToLSPAny() - ) - } - result.append(SourcesItem(target: target, sources: sources)) - } - return BuildTargetSourcesResponse(items: result) - } - - package func sourceKitOptions( - request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? { - guard let url = request.textDocument.uri.fileURL, let path = try? AbsolutePath(validating: url.filePath) else { - // We can't determine build settings for non-file URIs. - return nil - } - - if request.target == .forPackageManifest { - return try settings(forPackageManifest: path) - } - - guard let swiftPMTarget = self.swiftPMTargets[request.target] else { - logger.error("Did not find target \(request.target.forLogging)") - return nil - } - - if !swiftPMTarget.sources.lazy.map({ DocumentURI($0.sourceFile) }).contains(request.textDocument.uri), - let substituteFile = swiftPMTarget.sources.map(\.sourceFile).sorted(by: { $0.description < $1.description }).first - { - logger.info("Getting compiler arguments for \(url) using substitute file \(substituteFile)") - // If `url` is not part of the target's source, it's most likely a header file. Fake compiler arguments for it - // from a substitute file within the target. - // Even if the file is not a header, this should give reasonable results: Say, there was a new `.cpp` file in a - // target and for some reason the `SwiftPMBuildServer` doesn’t know about it. Then we would infer the target based - // on the file's location on disk and generate compiler arguments for it by picking a source file in that target, - // getting its compiler arguments and then patching up the compiler arguments by replacing the substitute file - // with the `.cpp` file. - let buildSettings = FileBuildSettings( - compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget), - workingDirectory: try projectRoot.filePath, - language: request.language - ).patching(newFile: DocumentURI(try path.asURL.realpath), originalFile: DocumentURI(substituteFile)) - return TextDocumentSourceKitOptionsResponse( - compilerArguments: buildSettings.compilerArguments, - workingDirectory: buildSettings.workingDirectory - ) - } - - return TextDocumentSourceKitOptionsResponse( - compilerArguments: try await compilerArguments(for: request.textDocument.uri, in: swiftPMTarget), - workingDirectory: try projectRoot.filePath - ) - } - - package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse { - await self.packageLoadingQueue.async {}.valuePropagatingCancellation - return VoidResponse() - } - - package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse { - // TODO: Support preparation of multiple targets at once. (https://github.com/swiftlang/sourcekit-lsp/issues/1262) - for target in request.targets { - await orLog("Preparing") { try await prepare(singleTarget: target) } - } - return VoidResponse() - } - - private func prepare(singleTarget target: BuildTargetIdentifier) async throws { - if target == .forPackageManifest { - // Nothing to prepare for package manifests. - return - } - - guard let swift = toolchain.swift else { - logger.error( - "Not preparing because toolchain at \(self.toolchain.identifier) does not contain a Swift compiler" - ) - return - } - logger.debug("Preparing '\(target.forLogging)' using \(self.toolchain.identifier)") - var arguments = [ - try swift.filePath, "build", - "--package-path", try projectRoot.filePath, - "--scratch-path", self.swiftPMWorkspace.location.scratchDirectory.pathString, - "--disable-index-store", - "--target", try target.swiftpmTargetProperties.target, - ] - if options.swiftPMOrDefault.disableSandbox ?? false { - arguments += ["--disable-sandbox"] - } - if let configuration = options.swiftPMOrDefault.configuration { - arguments += ["-c", configuration.rawValue] - } - if let triple = options.swiftPMOrDefault.triple { - arguments += ["--triple", triple] - } - if let swiftSDKsDirectory = options.swiftPMOrDefault.swiftSDKsDirectory { - arguments += ["--swift-sdks-path", swiftSDKsDirectory] - } - if let swiftSDK = options.swiftPMOrDefault.swiftSDK { - arguments += ["--swift-sdk", swiftSDK] - } - if let traits = options.swiftPMOrDefault.traits { - arguments += ["--traits", traits.joined(separator: ",")] - } - arguments += options.swiftPMOrDefault.cCompilerFlags?.flatMap { ["-Xcc", $0] } ?? [] - arguments += options.swiftPMOrDefault.cxxCompilerFlags?.flatMap { ["-Xcxx", $0] } ?? [] - arguments += options.swiftPMOrDefault.swiftCompilerFlags?.flatMap { ["-Xswiftc", $0] } ?? [] - arguments += options.swiftPMOrDefault.linkerFlags?.flatMap { ["-Xlinker", $0] } ?? [] - arguments += options.swiftPMOrDefault.buildToolsSwiftCompilerFlags?.flatMap { ["-Xbuild-tools-swiftc", $0] } ?? [] - switch options.backgroundPreparationModeOrDefault { - case .build: break - case .noLazy: arguments += ["--experimental-prepare-for-indexing", "--experimental-prepare-for-indexing-no-lazy"] - case .enabled: arguments.append("--experimental-prepare-for-indexing") - } - if Task.isCancelled { - return - } - let start = ContinuousClock.now - - let taskID: TaskId = TaskId(id: "preparation-\(preparationTaskID.fetchAndIncrement())") - connectionToSourceKitLSP.send( - BuildServerProtocol.OnBuildLogMessageNotification( - type: .info, - task: taskID, - message: "\(arguments.joined(separator: " "))", - structure: .begin( - StructuredLogBegin(title: "Preparing \(self.swiftPMTargets[target]?.name ?? target.uri.stringValue)") - ) - ) - ) - let stdoutHandler = PipeAsStringHandler { message in - self.connectionToSourceKitLSP.send( - BuildServerProtocol.OnBuildLogMessageNotification( - type: .info, - task: taskID, - message: message, - structure: .report(StructuredLogReport()) - ) - ) - } - let stderrHandler = PipeAsStringHandler { message in - self.connectionToSourceKitLSP.send( - BuildServerProtocol.OnBuildLogMessageNotification( - type: .info, - task: taskID, - message: message, - structure: .report(StructuredLogReport()) - ) - ) - } - - let result = try await Process.run( - arguments: arguments, - workingDirectory: nil, - outputRedirection: .stream( - stdout: { @Sendable bytes in stdoutHandler.handleDataFromPipe(Data(bytes)) }, - stderr: { @Sendable bytes in stderrHandler.handleDataFromPipe(Data(bytes)) } - ) - ) - let exitStatus = result.exitStatus.exhaustivelySwitchable - self.connectionToSourceKitLSP.send( - BuildServerProtocol.OnBuildLogMessageNotification( - type: exitStatus.isSuccess ? .info : .error, - task: taskID, - message: "Finished with \(exitStatus.description) in \(start.duration(to: .now))", - structure: .end(StructuredLogEnd()) - ) - ) - switch exitStatus { - case .terminated(code: 0): - break - case .terminated(let code): - // This most likely happens if there are compilation errors in the source file. This is nothing to worry about. - let stdout = (try? String(bytes: result.output.get(), encoding: .utf8)) ?? "" - let stderr = (try? String(bytes: result.stderrOutput.get(), encoding: .utf8)) ?? "" - logger.debug( - """ - Preparation of target \(target.forLogging) terminated with non-zero exit code \(code) - Stderr: - \(stderr) - Stdout: - \(stdout) - """ - ) - case .signalled(let signal): - if !Task.isCancelled { - // The indexing job finished with a signal. Could be because the compiler crashed. - // Ignore signal exit codes if this task has been cancelled because the compiler exits with SIGINT if it gets - // interrupted. - logger.error("Preparation of target \(target.forLogging) signaled \(signal)") - } - case .abnormal(let exception): - if !Task.isCancelled { - logger.error("Preparation of target \(target.forLogging) exited abnormally \(exception)") - } - } - } - - private func isPackageManifestOrPackageResolved(_ url: URL) -> Bool { - guard url.lastPathComponent.contains("Package") else { - // Fast check to early exit for files that don't like a package manifest or Package.resolved - return false - } - guard - url.lastPathComponent == "Package.resolved" || url.lastPathComponent == "Package.swift" - || (try? Self.versionSpecificPackageManifestNameRegex.wholeMatch(in: url.lastPathComponent)) != nil - else { - return false - } - return url.deletingLastPathComponent() == self.projectRoot - } - - /// An event is relevant if it modifies a file that matches one of the file rules used by the SwiftPM workspace. - private func fileEventShouldTriggerPackageReload(event: FileEvent) -> Bool { - guard let fileURL = event.uri.fileURL else { - return false - } - if isPackageManifestOrPackageResolved(fileURL) { - return true - } - switch event.type { - case .created, .deleted: - guard let buildDescription else { - return false - } - - return buildDescription.fileAffectsSwiftOrClangBuildSettings(fileURL) - case .changed: - // Only modified package manifests should trigger a package reload and that's handled above. - return false - default: // Unknown file change type - return false - } - } - - package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async { - if let packageReloadTriggerEvent = notification.changes.first(where: { - self.fileEventShouldTriggerPackageReload(event: $0) - }) { - logger.log("Reloading package because \(packageReloadTriggerEvent.uri.forLogging) changed") - await packageLoadingQueue.async { - await orLog("Reloading package") { - try await self.reloadPackageAssumingOnPackageLoadingQueue() - } - }.valuePropagatingCancellation - } - } - - /// Retrieve settings for a package manifest (Package.swift). - private func settings(forPackageManifest path: AbsolutePath) throws -> TextDocumentSourceKitOptionsResponse? { - let compilerArgs = try swiftPMWorkspace.interpreterFlags(for: path) + [path.pathString] - return TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArgs) - } -} - -fileprivate extension URL { - var isDirectory: Bool { - (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true - } -} - -fileprivate extension SourceKitLSPOptions.BackgroundPreparationMode { - var toSwiftPMPreparation: BuildParameters.PrepareForIndexingMode { - switch self { - case .build: - return .off - case .noLazy: - return .noLazy - case .enabled: - return .on - } - } -} - -#endif diff --git a/Sources/BuildServerIntegration/SwiftlyResolver.swift b/Sources/BuildServerIntegration/SwiftlyResolver.swift deleted file mode 100644 index 52372a5d1..000000000 --- a/Sources/BuildServerIntegration/SwiftlyResolver.swift +++ /dev/null @@ -1,74 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKUtilities -import SwiftExtensions -import TSCExtensions - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process - -/// Given a path to a compiler, which might be a symlink to `swiftly`, this type determines the compiler executable in -/// an actual toolchain. It also caches the results. The client needs to invalidate the cache if the path that swiftly -/// might resolve to has changed, eg. because `.swift-version` has been updated. -actor SwiftlyResolver { - private struct CacheKey: Hashable { - let compiler: URL - let workingDirectory: URL? - } - - private var cache: LRUCache> = LRUCache(capacity: 100) - - /// Check if `compiler` is a symlink to `swiftly`. If so, find the executable in the toolchain that swiftly resolves - /// to within the given working directory and return the URL of the corresponding compiler in that toolchain. - /// If `compiler` does not resolve to `swiftly`, return `nil`. - func resolve(compiler: URL, workingDirectory: URL?) async throws -> URL? { - let cacheKey = CacheKey(compiler: compiler, workingDirectory: workingDirectory) - if let cached = cache[cacheKey] { - return try cached.get() - } - let computed: Result - do { - computed = .success( - try await resolveSwiftlyTrampolineImpl(compiler: compiler, workingDirectory: workingDirectory) - ) - } catch { - computed = .failure(error) - } - cache[cacheKey] = computed - return try computed.get() - } - - private func resolveSwiftlyTrampolineImpl(compiler: URL, workingDirectory: URL?) async throws -> URL? { - let realpath = try compiler.realpath - guard realpath.lastPathComponent == "swiftly" else { - return nil - } - let swiftlyResult = try await Process.run( - arguments: [realpath.filePath, "use", "-p"], - workingDirectory: try AbsolutePath(validatingOrNil: workingDirectory?.filePath) - ) - let swiftlyToolchain = URL( - fileURLWithPath: try swiftlyResult.utf8Output().trimmingCharacters(in: .whitespacesAndNewlines) - ) - let resolvedCompiler = swiftlyToolchain.appending(components: "usr", "bin", compiler.lastPathComponent) - if FileManager.default.fileExists(at: resolvedCompiler) { - return resolvedCompiler - } - return nil - } - - func clearCache() { - cache.removeAll() - } -} diff --git a/Sources/BuildServerProtocol/CMakeLists.txt b/Sources/BuildServerProtocol/CMakeLists.txt index dba8de99c..39b326107 100644 --- a/Sources/BuildServerProtocol/CMakeLists.txt +++ b/Sources/BuildServerProtocol/CMakeLists.txt @@ -28,3 +28,5 @@ set_target_properties(BuildServerProtocol PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_link_libraries(BuildServerProtocol PRIVATE LanguageServerProtocol) + +set_property(GLOBAL APPEND PROPERTY SWIFTTOOLSPROTOCOLS_EXPORTS BuildServerProtocol) diff --git a/Sources/BuildServerProtocol/Messages.swift b/Sources/BuildServerProtocol/Messages.swift index 5a597488d..930b8120d 100644 --- a/Sources/BuildServerProtocol/Messages.swift +++ b/Sources/BuildServerProtocol/Messages.swift @@ -37,4 +37,8 @@ private let notificationTypes: [NotificationType.Type] = [ TaskStartNotification.self, ] -public let bspRegistry = MessageRegistry(requests: requestTypes, notifications: notificationTypes) +extension MessageRegistry { + public var bspProtocol: MessageRegistry { + MessageRegistry(requests: requestTypes, notifications: notificationTypes) + } +} diff --git a/Sources/CAtomics/CMakeLists.txt b/Sources/CAtomics/CMakeLists.txt deleted file mode 100644 index 9ed253763..000000000 --- a/Sources/CAtomics/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_library(CAtomics INTERFACE) -target_include_directories(CAtomics INTERFACE "include") diff --git a/Sources/CAtomics/include/module.modulemap b/Sources/CAtomics/include/module.modulemap deleted file mode 100644 index 0e9d1d3ed..000000000 --- a/Sources/CAtomics/include/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CAtomics { - header "CAtomics.h" - export * -} diff --git a/Sources/CCompletionScoring/CCompletionScoring.c b/Sources/CCompletionScoring/CCompletionScoring.c deleted file mode 100644 index b5221ced3..000000000 --- a/Sources/CCompletionScoring/CCompletionScoring.c +++ /dev/null @@ -1,12 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - diff --git a/Sources/CCompletionScoring/CMakeLists.txt b/Sources/CCompletionScoring/CMakeLists.txt deleted file mode 100644 index 18437046e..000000000 --- a/Sources/CCompletionScoring/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_library(CCompletionScoring INTERFACE) -target_include_directories(CCompletionScoring INTERFACE "include") diff --git a/Sources/CCompletionScoring/include/CCompletionScoring.h b/Sources/CCompletionScoring/include/CCompletionScoring.h deleted file mode 100644 index 6f6d60ba0..000000000 --- a/Sources/CCompletionScoring/include/CCompletionScoring.h +++ /dev/null @@ -1,43 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifndef SOURCEKITLSP_CCOMPLETIONSCORING_H -#define SOURCEKITLSP_CCOMPLETIONSCORING_H - -#define _GNU_SOURCE -#include - -static inline void *sourcekitlsp_memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) { - #if defined(_WIN32) && !defined(__CYGWIN__) - // memmem is not available on Windows - if (!haystack || haystack_len == 0) { - return NULL; - } - if (!needle || needle_len == 0) { - return NULL; - } - if (needle_len > haystack_len) { - return NULL; - } - - for (size_t offset = 0; offset <= haystack_len - needle_len; ++offset) { - if (memcmp(haystack + offset, needle, needle_len) == 0) { - return (void *)haystack + offset; - } - } - return NULL; - #else - return memmem(haystack, haystack_len, needle, needle_len); - #endif -} - -#endif // SOURCEKITLSP_CCOMPLETIONSCORING_H diff --git a/Sources/CCompletionScoring/include/module.modulemap b/Sources/CCompletionScoring/include/module.modulemap deleted file mode 100644 index 533a86eb3..000000000 --- a/Sources/CCompletionScoring/include/module.modulemap +++ /dev/null @@ -1,4 +0,0 @@ -module CCompletionScoring { - header "CCompletionScoring.h" - export * -} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 8460311a1..3ae3a5450 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -1,29 +1,9 @@ -add_compile_options("$<$:SHELL:-package-name sourcekit_lsp>") +add_compile_options("$<$:SHELL:-package-name swift_language_server_protocol>") add_compile_options("$<$:SHELL:-DRESILIENT_LIBRARIES>") add_compile_options("$<$:SHELL:-swift-version 6>") -add_subdirectory(BuildServerIntegration) add_subdirectory(BuildServerProtocol) -add_subdirectory(CAtomics) -add_subdirectory(CCompletionScoring) -add_subdirectory(ClangLanguageService) -add_subdirectory(CompletionScoring) -add_subdirectory(Csourcekitd) -add_subdirectory(Diagnose) -add_subdirectory(InProcessClient) +add_subdirectory(ToolsProtocolsCAtomics) add_subdirectory(LanguageServerProtocol) -add_subdirectory(LanguageServerProtocolExtensions) -add_subdirectory(LanguageServerProtocolJSONRPC) -add_subdirectory(SemanticIndex) +add_subdirectory(LanguageServerProtocolTransport) add_subdirectory(SKLogging) -add_subdirectory(SKOptions) -add_subdirectory(SKUtilities) -add_subdirectory(sourcekit-lsp) -add_subdirectory(SourceKitD) -add_subdirectory(SourceKitLSP) add_subdirectory(SwiftExtensions) -add_subdirectory(SwiftLanguageService) -add_subdirectory(SwiftSourceKitClientPlugin) -add_subdirectory(SwiftSourceKitPlugin) -add_subdirectory(SwiftSourceKitPluginCommon) -add_subdirectory(ToolchainRegistry) -add_subdirectory(TSCExtensions) diff --git a/Sources/CSKTestSupport/CSKTestSupport.c b/Sources/CSKTestSupport/CSKTestSupport.c deleted file mode 100644 index 386526ba9..000000000 --- a/Sources/CSKTestSupport/CSKTestSupport.c +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifdef __linux__ -// For testing, override __cxa_atexit to prevent registration of static -// destructors due to https://github.com/swiftlang/swift/issues/55112. -int __cxa_atexit(void (*f) (void *), void *arg, void *dso_handle) { - return 0; -} -#endif diff --git a/Sources/CSKTestSupport/include/module.modulemap b/Sources/CSKTestSupport/include/module.modulemap deleted file mode 100644 index 5694be53d..000000000 --- a/Sources/CSKTestSupport/include/module.modulemap +++ /dev/null @@ -1,3 +0,0 @@ -module CSKTestSupport { - export * -} diff --git a/Sources/ClangLanguageService/CMakeLists.txt b/Sources/ClangLanguageService/CMakeLists.txt deleted file mode 100644 index 8ea42c567..000000000 --- a/Sources/ClangLanguageService/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -add_library(ClangLanguageService STATIC - ClangLanguageService.swift - SemanticTokenTranslator.swift -) - -set_target_properties(ClangLanguageService PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY} -) - -target_link_libraries(ClangLanguageService PUBLIC - LanguageServerProtocol - SKOptions - SourceKitLSP - SwiftExtensions - ToolchainRegistry - SwiftSyntax::SwiftSyntax -) - -target_link_libraries(ClangLanguageService PRIVATE - BuildServerIntegration - LanguageServerProtocolExtensions - LanguageServerProtocolJSONRPC - SKLogging - TSCExtensions -) diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift deleted file mode 100644 index e8946851d..000000000 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ /dev/null @@ -1,744 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import Foundation -import IndexStoreDB -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import LanguageServerProtocolJSONRPC -import SKLogging -package import SKOptions -package import SourceKitLSP -package import SwiftExtensions -package import SwiftSyntax -import TSCExtensions -package import ToolchainRegistry - -#if os(Windows) -import WinSDK -#endif - -/// A thin wrapper over a connection to a clangd server providing build setting handling. -/// -/// In addition, it also intercepts notifications and replies from clangd in order to do things -/// like withholding diagnostics when fallback build settings are being used. -/// -/// ``ClangLanguageServerShim`` conforms to ``MessageHandler`` to receive -/// requests and notifications **from** clangd, not from the editor, and it will -/// forward these requests and notifications to the editor. -package actor ClangLanguageService: LanguageService, MessageHandler { - /// The queue on which all messages that originate from clangd are handled. - /// - /// These are requests and notifications sent *from* clangd, not replies from - /// clangd. - /// - /// Since we are blindly forwarding requests from clangd to the editor, we - /// cannot allow concurrent requests. This should be fine since the number of - /// requests and notifications sent from clangd to the client is quite small. - package let clangdMessageHandlingQueue = AsyncQueue() - - /// The ``SourceKitLSPServer`` instance that created this `ClangLanguageService`. - /// - /// Used to send requests and notifications to the editor. - private weak var sourceKitLSPServer: SourceKitLSPServer? - - /// The connection to the clangd LSP. `nil` until `startClangdProcesss` has been called. - var clangd: Connection! - - /// Capabilities of the clangd LSP, if received. - var capabilities: ServerCapabilities? = nil - - /// Path to the clang binary. - let clangPath: URL? - - /// Path to the `clangd` binary. - let clangdPath: URL - - let options: SourceKitLSPOptions - - /// The current state of the `clangd` language server. - /// Changing the property automatically notified the state change handlers. - private var state: LanguageServerState { - didSet { - for handler in stateChangeHandlers { - handler(oldValue, state) - } - } - } - - private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = [] - - /// The date at which `clangd` was last restarted. - /// Used to delay restarting in case of a crash loop. - private var lastClangdRestart: Date? - - /// Whether or not a restart of `clangd` has been scheduled. - /// Used to make sure we are not restarting `clangd` twice. - private var clangRestartScheduled = false - - /// The `InitializeRequest` with which `clangd` was originally initialized. - /// Stored so we can replay the initialization when clangd crashes. - private var initializeRequest: InitializeRequest? - - /// The workspace this `ClangLanguageServer` was opened for. - /// - /// `clangd` doesn't have support for multi-root workspaces, so we need to start a separate `clangd` instance for every workspace root. - private let workspace: WeakWorkspace - - /// The documents that have been opened and which language they have been - /// opened with. - private var openDocuments: [DocumentURI: LanguageServerProtocol.Language] = [:] - - /// Type to map `clangd`'s semantic token legend to SourceKit-LSP's. - private var semanticTokensTranslator: SemanticTokensLegendTranslator? = nil - - /// While `clangd` is running, its `Process` object. - private var clangdProcess: Process? - - package static var builtInCommands: [String] { [] } - - /// Creates a language server for the given client referencing the clang binary specified in `toolchain`. - /// Returns `nil` if `clangd` can't be found. - package init( - sourceKitLSPServer: SourceKitLSPServer, - toolchain: Toolchain, - options: SourceKitLSPOptions, - hooks: Hooks, - workspace: Workspace - ) async throws { - guard let clangdPath = toolchain.clangd else { - throw ResponseError.unknown( - "Cannot create SwiftLanguage service because \(toolchain.identifier) does not contain clangd" - ) - } - self.clangPath = toolchain.clang - self.clangdPath = clangdPath - self.options = options - self.workspace = WeakWorkspace(workspace) - self.state = .connected - self.sourceKitLSPServer = sourceKitLSPServer - try startClangdProcess() - } - - private func buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> ClangBuildSettings? { - guard let workspace = workspace.value, let language = openDocuments[document] else { - return nil - } - guard - let settings = await workspace.buildServerManager.buildSettingsInferredFromMainFile( - for: document, - language: language, - fallbackAfterTimeout: fallbackAfterTimeout - ) - else { - return nil - } - return ClangBuildSettings(settings, clangPath: clangPath) - } - - package nonisolated func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool { - // We launch different clangd instance for each workspace because clangd doesn't have multi-root workspace support. - return workspace === self.workspace.value && self.clangdPath == toolchain.clangd - } - - package func addStateChangeHandler(handler: @escaping (LanguageServerState, LanguageServerState) -> Void) { - self.stateChangeHandlers.append(handler) - } - - /// Called after the `clangd` process exits. - /// - /// Restarts `clangd` if it has crashed. - /// - /// - Parameter terminationStatus: The exit code of `clangd`. - private func handleClangdTermination(terminationReason: JSONRPCConnection.TerminationReason) { - self.clangdProcess = nil - if terminationReason != .exited(exitCode: 0) { - self.state = .connectionInterrupted - logger.info("clangd crashed. Restarting it.") - self.restartClangd() - } - } - - /// Start the `clangd` process, either on creation of the `ClangLanguageService` or after `clangd` has crashed. - private func startClangdProcess() throws { - // Since we are starting a new clangd process, reset the list of open document - openDocuments = [:] - - let (connectionToClangd, process) = try JSONRPCConnection.start( - executable: clangdPath, - arguments: [ - "-compile_args_from=lsp", // Provide compiler args programmatically. - "-background-index=false", // Disable clangd indexing, we use the build - "-index=false", // system index store instead. - ] + (options.clangdOptions ?? []), - name: "clangd", - protocol: MessageRegistry.lspProtocol, - stderrLoggingCategory: "clangd-stderr", - client: self, - terminationHandler: { [weak self] terminationReason in - guard let self = self else { return } - Task { - await self.handleClangdTermination(terminationReason: terminationReason) - } - - } - ) - self.clangd = connectionToClangd - - self.clangdProcess = process - } - - /// Restart `clangd` after it has crashed. - /// Delays restarting of `clangd` in case there is a crash loop. - private func restartClangd() { - precondition(self.state == .connectionInterrupted) - - precondition(self.clangRestartScheduled == false) - self.clangRestartScheduled = true - - guard let initializeRequest = self.initializeRequest else { - logger.error("clangd crashed before it was sent an InitializeRequest.") - return - } - - let restartDelay: Duration - if let lastClangdRestart = self.lastClangdRestart, Date().timeIntervalSince(lastClangdRestart) < 30 { - logger.log("clangd has already been restarted in the last 30 seconds. Delaying another restart by 10 seconds.") - restartDelay = .seconds(10) - } else { - restartDelay = .zero - } - self.lastClangdRestart = Date() - - Task { - try await Task.sleep(for: restartDelay) - self.clangRestartScheduled = false - do { - try self.startClangdProcess() - // We assume that clangd will return the same capabilities after restarting. - // Theoretically they could have changed and we would need to inform SourceKitLSPServer about them. - // But since SourceKitLSPServer more or less ignores them right now anyway, this should be fine for now. - _ = try await self.initialize(initializeRequest) - await self.clientInitialized(InitializedNotification()) - if let sourceKitLSPServer { - await sourceKitLSPServer.reopenDocuments(for: self) - } else { - logger.fault("Cannot reopen documents because SourceKitLSPServer is no longer alive") - } - self.state = .connected - } catch { - logger.fault("Failed to restart clangd after a crash.") - } - } - } - - /// Handler for notifications received **from** clangd, ie. **clangd** is - /// sending a notification that's intended for the editor. - /// - /// We should either handle it ourselves or forward it to the editor. - package nonisolated func handle(_ params: some NotificationType) { - logger.info( - """ - Received notification from clangd: - \(params.forLogging) - """ - ) - clangdMessageHandlingQueue.async { - switch params { - case let publishDiags as PublishDiagnosticsNotification: - await self.publishDiagnostics(publishDiags) - default: - // We don't know how to handle any other notifications and ignore them. - logger.error("Ignoring unknown notification \(type(of: params))") - break - } - } - } - - /// Handler for requests received **from** clangd, ie. **clangd** is - /// sending a notification that's intended for the editor. - /// - /// We should either handle it ourselves or forward it to the client. - package nonisolated func handle( - _ params: R, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) { - logger.info( - """ - Received request from clangd: - \(params.forLogging) - """ - ) - clangdMessageHandlingQueue.async { - guard let sourceKitLSPServer = await self.sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the language - // server. Nothing left to do. - reply(.failure(.unknown("Connection to the editor closed"))) - return - } - - do { - let result = try await sourceKitLSPServer.sendRequestToClient(params) - reply(.success(result)) - } catch { - reply(.failure(ResponseError(error))) - } - } - } - - /// Forward the given request to `clangd`. - func forwardRequestToClangd(_ request: R) async throws -> R.Response { - let timeoutHandle = TimeoutHandle() - do { - return try await withTimeout(options.semanticServiceRestartTimeoutOrDefault, handle: timeoutHandle) { - await self.sourceKitLSPServer?.hooks.preForwardRequestToClangd?(request) - return try await self.clangd.send(request) - } - } catch let error as TimeoutError where error.handle == timeoutHandle { - logger.fault( - "Did not receive reply from clangd after \(self.options.semanticServiceRestartTimeoutOrDefault, privacy: .public). Terminating and restarting clangd." - ) - await self.crash() - throw error - } - } - - package func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? { - return nil - } - - package func crash() async { - clangdProcess?.terminateImmediately() - } -} - -// MARK: - LanguageServer - -extension ClangLanguageService { - - /// Intercept clangd's `PublishDiagnosticsNotification` to withhold it if we're using fallback - /// build settings. - func publishDiagnostics(_ notification: PublishDiagnosticsNotification) async { - // Technically, the publish diagnostics notification could still originate - // from when we opened the file with fallback build settings and we could - // have received real build settings since, which haven't been acknowledged - // by clangd yet. - // - // Since there is no way to tell which build settings clangd used to generate - // the diagnostics, there's no good way to resolve this race. For now, this - // should be good enough since the time in which the race may occur is pretty - // short and we expect clangd to send us new diagnostics with the updated - // non-fallback settings very shortly after, which will override the - // incorrect result, making it very temporary. - // TODO: We want to know the build settings that are currently transmitted to clangd, not whichever ones we would - // get next. (https://github.com/swiftlang/sourcekit-lsp/issues/1761) - let buildSettings = await self.buildSettings(for: notification.uri, fallbackAfterTimeout: true) - guard let sourceKitLSPServer else { - logger.fault("Cannot publish diagnostics because SourceKitLSPServer has been destroyed") - return - } - if buildSettings?.isFallback ?? true { - // Fallback: send empty publish notification instead. - sourceKitLSPServer.sendNotificationToClient( - PublishDiagnosticsNotification( - uri: notification.uri, - version: notification.version, - diagnostics: [] - ) - ) - } else { - sourceKitLSPServer.sendNotificationToClient(notification) - } - } - -} - -// MARK: - LanguageService - -extension ClangLanguageService { - - package func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult { - // Store the initialize request so we can replay it in case clangd crashes - self.initializeRequest = initialize - - let result = try await clangd.send(initialize) - self.capabilities = result.capabilities - if let legend = result.capabilities.semanticTokensProvider?.legend { - self.semanticTokensTranslator = SemanticTokensLegendTranslator( - clangdLegend: legend, - sourceKitLSPLegend: SemanticTokensLegend.sourceKitLSPLegend - ) - } - return result - } - - package func clientInitialized(_ initialized: InitializedNotification) async { - clangd.send(initialized) - } - - package func shutdown() async { - _ = await orLog("Shutting down clangd") { - guard let clangd else { return } - // Give clangd 2 seconds to shut down by itself. If it doesn't shut down within that time, terminate it. - try await withTimeout(.seconds(2)) { - _ = try await clangd.send(ShutdownRequest()) - clangd.send(ExitNotification()) - } - } - await orLog("Terminating clangd") { - // Give clangd 1 second to exit after receiving the `exit` notification. If it doesn't exit within that time, - // terminate it. - try await clangdProcess?.terminateIfRunning(after: .seconds(1)) - } - } - - // MARK: - Text synchronization - - package func openDocument(_ notification: DidOpenTextDocumentNotification, snapshot: DocumentSnapshot) async { - openDocuments[notification.textDocument.uri] = notification.textDocument.language - // Send clangd the build settings for the new file. We need to do this before - // sending the open notification, so that the initial diagnostics already - // have build settings. - await documentUpdatedBuildSettings(notification.textDocument.uri) - clangd.send(notification) - } - - package func closeDocument(_ notification: DidCloseTextDocumentNotification) { - openDocuments[notification.textDocument.uri] = nil - clangd.send(notification) - } - - package func reopenDocument(_ notification: ReopenTextDocumentNotification) {} - - package func changeDocument( - _ notification: DidChangeTextDocumentNotification, - preEditSnapshot: DocumentSnapshot, - postEditSnapshot: DocumentSnapshot, - edits: [SourceEdit] - ) { - clangd.send(notification) - } - - package func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async { - - } - - package func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async { - clangd.send(notification) - } - - // MARK: - Build Server Integration - - package func documentUpdatedBuildSettings(_ uri: DocumentURI) async { - guard let url = uri.fileURL else { - logger.error("Received updated build settings for non-file URI '\(uri.forLogging)'. Ignoring the update.") - return - } - let clangBuildSettings = await self.buildSettings(for: uri, fallbackAfterTimeout: false) - - // The compile command changed, send over the new one. - if let compileCommand = clangBuildSettings?.compileCommand, let pathString = try? url.filePath { - let notification = DidChangeConfigurationNotification( - settings: .clangd(ClangWorkspaceSettings(compilationDatabaseChanges: [pathString: compileCommand])) - ) - clangd.send(notification) - } else { - logger.error("No longer have build settings for \(url.description) but can't send null build settings to clangd") - } - } - - package func documentDependenciesUpdated(_ uris: Set) async { - for uri in uris { - // In order to tell clangd to reload an AST, we send it an empty `didChangeTextDocument` - // with `forceRebuild` set in case any missing header files have been added. - // This works well for us as the moment since clangd ignores the document version. - let notification = DidChangeTextDocumentNotification( - textDocument: VersionedTextDocumentIdentifier(uri, version: 0), - contentChanges: [], - forceRebuild: true - ) - clangd.send(notification) - } - } - - // MARK: - Text Document - - package func definition(_ req: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { - // We handle it to provide jump-to-header support for #import/#include. - return try await self.forwardRequestToClangd(req) - } - - package func declaration(_ req: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { - return try await forwardRequestToClangd(req) - } - - package func completion(_ req: CompletionRequest) async throws -> CompletionList { - return try await forwardRequestToClangd(req) - } - - package func completionItemResolve(_ req: CompletionItemResolveRequest) async throws -> CompletionItem { - return try await forwardRequestToClangd(req) - } - - package func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? { - return try await forwardRequestToClangd(req) - } - - package func hover(_ req: HoverRequest) async throws -> HoverResponse? { - return try await forwardRequestToClangd(req) - } - - package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { - guard let language = openDocuments[req.textDocument.uri] else { - throw ResponseError.requestFailed("Documentation preview is not available for clang files") - } - throw ResponseError.requestFailed("Documentation preview is not available for \(language.description) files") - } - - package func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] { - return try await forwardRequestToClangd(req) - } - - package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { - return try await forwardRequestToClangd(req) - } - - package func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? { - return try await forwardRequestToClangd(req) - } - - package func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] { - guard self.capabilities?.colorProvider?.isSupported ?? false else { - return [] - } - return try await forwardRequestToClangd(req) - } - - package func documentSemanticTokens( - _ req: DocumentSemanticTokensRequest - ) async throws -> DocumentSemanticTokensResponse? { - guard var response = try await forwardRequestToClangd(req) else { - return nil - } - if let semanticTokensTranslator { - response.data = semanticTokensTranslator.translate(response.data) - } - return response - } - - package func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest - ) async throws -> DocumentSemanticTokensDeltaResponse? { - guard var response = try await forwardRequestToClangd(req) else { - return nil - } - if let semanticTokensTranslator { - switch response { - case .tokens(var tokens): - tokens.data = semanticTokensTranslator.translate(tokens.data) - response = .tokens(tokens) - case .delta(var delta): - delta.edits = delta.edits.map { - var edit = $0 - if let data = edit.data { - edit.data = semanticTokensTranslator.translate(data) - } - return edit - } - response = .delta(delta) - } - } - return response - } - - package func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest - ) async throws -> DocumentSemanticTokensResponse? { - guard var response = try await forwardRequestToClangd(req) else { - return nil - } - if let semanticTokensTranslator { - response.data = semanticTokensTranslator.translate(response.data) - } - return response - } - - package func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] { - guard self.capabilities?.colorProvider?.isSupported ?? false else { - return [] - } - return try await forwardRequestToClangd(req) - } - - package func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? { - return try await forwardRequestToClangd(req) - } - - package func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? { - return try await forwardRequestToClangd(req) - } - - package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { - return try await forwardRequestToClangd(req) - } - - package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { - return try await forwardRequestToClangd(req) - } - - package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { - return try await forwardRequestToClangd(req) - } - - package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport { - return try await forwardRequestToClangd(req) - } - - package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] { - return try await forwardRequestToClangd(req) ?? [] - } - - package func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? { - guard self.capabilities?.foldingRangeProvider?.isSupported ?? false else { - return nil - } - return try await forwardRequestToClangd(req) - } - - package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { - return try await forwardRequestToClangd(request) - } - - // MARK: - Other - - package func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { - return try await forwardRequestToClangd(req) - } - - package func rename(_ renameRequest: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { - async let edits = forwardRequestToClangd(renameRequest) - let symbolInfoRequest = SymbolInfoRequest( - textDocument: renameRequest.textDocument, - position: renameRequest.position - ) - let symbolDetail = try await forwardRequestToClangd(symbolInfoRequest).only - return (try await edits ?? WorkspaceEdit(), symbolDetail?.usr) - } - - package func syntacticDocumentTests( - for uri: DocumentURI, - in workspace: Workspace - ) async throws -> [AnnotatedTestItem]? { - return nil - } - - package static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] { - return [] - } - - package func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName oldCrossLanguageName: CrossLanguageName, - newName newCrossLanguageName: CrossLanguageName - ) async throws -> [TextEdit] { - let positions = [ - snapshot.uri: renameLocations.compactMap { snapshot.position(of: $0) } - ] - guard - let oldName = oldCrossLanguageName.clangName, - let newName = newCrossLanguageName.clangName - else { - throw ResponseError.unknown( - "Failed to rename \(snapshot.uri.forLogging) because the clang name for rename is unknown" - ) - } - let request = IndexedRenameRequest( - textDocument: TextDocumentIdentifier(snapshot.uri), - oldName: oldName, - newName: newName, - positions: positions - ) - do { - let edits = try await forwardRequestToClangd(request) - return edits?.changes?[snapshot.uri] ?? [] - } catch { - logger.error("Failed to get indexed rename edits: \(error.forLogging)") - return [] - } - } - - package func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { - guard let prepareRename = try await forwardRequestToClangd(request) else { - return nil - } - let symbolInfo = try await forwardRequestToClangd( - SymbolInfoRequest(textDocument: request.textDocument, position: request.position) - ) - return (prepareRename, symbolInfo.only?.usr) - } - - package func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] { - // When renaming a clang function name, we don't need to rename any references to the arguments. - return [] - } -} - -/// Clang build settings derived from a `FileBuildSettingsChange`. -private struct ClangBuildSettings: Equatable { - /// The compiler arguments, including the program name, argv[0]. - package let compilerArgs: [String] - - /// The working directory for the invocation. - package let workingDirectory: String - - /// Whether the compiler arguments are considered fallback - we withhold diagnostics for - /// fallback arguments and represent the file state differently. - package let isFallback: Bool - - package init(_ settings: FileBuildSettings, clangPath: URL?) { - var arguments = [(try? clangPath?.filePath) ?? "clang"] + settings.compilerArguments - if arguments.contains("-fmodules") { - // Clangd is not built with support for the 'obj' format. - arguments.append(contentsOf: [ - "-Xclang", "-fmodule-format=raw", - ]) - } - if let workingDirectory = settings.workingDirectory { - // TODO: This is a workaround for clangd not respecting the compilation - // database's "directory" field for relative -fmodules-cache-path. - // Remove once rdar://63984913 is fixed - arguments.append(contentsOf: [ - "-working-directory", workingDirectory, - ]) - } - - self.compilerArgs = arguments - self.workingDirectory = settings.workingDirectory ?? "" - self.isFallback = settings.isFallback - } - - package var compileCommand: ClangCompileCommand { - return ClangCompileCommand( - compilationCommand: self.compilerArgs, - workingDirectory: self.workingDirectory - ) - } -} diff --git a/Sources/ClangLanguageService/SemanticTokenTranslator.swift b/Sources/ClangLanguageService/SemanticTokenTranslator.swift deleted file mode 100644 index 80aa5efa9..000000000 --- a/Sources/ClangLanguageService/SemanticTokenTranslator.swift +++ /dev/null @@ -1,156 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging - -/// `clangd` might use a different semantic token legend than SourceKit-LSP. -/// -/// This type allows translation the semantic tokens from `clangd` into the token legend that is used by SourceKit-LSP. -struct SemanticTokensLegendTranslator { - private enum Translation { - /// The token type or modifier from clangd does not exist in SourceKit-LSP - case doesNotExistInSourceKitLSP - - /// The token type or modifier exists in SourceKit-LSP but it uses a different index. We need to translate the - /// clangd index to this SourceKit-LSP index. - case translation(UInt32) - } - - /// For all token types whose representation in clang differs from the representation in SourceKit-LSP, maps the - /// index of that token type in clangd’s token type legend to the corresponding representation in SourceKit-LSP. - private let tokenTypeTranslations: [UInt32: Translation] - - /// For all token modifiers whose representation in clang differs from the representation in SourceKit-LSP, maps the - /// index of that token modifier in clangd’s token type legend to the corresponding representation in SourceKit-LSP. - private let tokenModifierTranslations: [UInt32: Translation] - - /// A bitmask that has all bits set to 1 that are used for clangd token modifiers which have a different - /// representation in SourceKit-LSP. If a token modifier does not have any bits set in common with this bitmask, no - /// token mapping needs to be performed. - private let tokenModifierTranslationBitmask: UInt32 - - /// For token types in clangd that do not exist in SourceKit-LSP's token legend, we need to map their token types to - /// some valid SourceKit-LSP token type. Use the token type with this index. - private let tokenTypeFallbackIndex: UInt32 - - init(clangdLegend: SemanticTokensLegend, sourceKitLSPLegend: SemanticTokensLegend) { - var tokenTypeTranslations: [UInt32: Translation] = [:] - for (index, tokenType) in clangdLegend.tokenTypes.enumerated() { - switch sourceKitLSPLegend.tokenTypes.firstIndex(of: tokenType) { - case index: - break - case nil: - logger.error("Token type '\(tokenType, privacy: .public)' from clangd does not exist in SourceKit-LSP's legend") - tokenTypeTranslations[UInt32(index)] = .doesNotExistInSourceKitLSP - case let sourceKitLSPIndex?: - tokenTypeTranslations[UInt32(index)] = .translation(UInt32(sourceKitLSPIndex)) - } - } - self.tokenTypeTranslations = tokenTypeTranslations - - var tokenModifierTranslations: [UInt32: Translation] = [:] - for (index, tokenModifier) in clangdLegend.tokenModifiers.enumerated() { - switch sourceKitLSPLegend.tokenModifiers.firstIndex(of: tokenModifier) { - case index: - break - case nil: - logger.error( - "Token modifier '\(tokenModifier, privacy: .public)' from clangd does not exist in SourceKit-LSP's legend" - ) - tokenModifierTranslations[UInt32(index)] = .doesNotExistInSourceKitLSP - case let sourceKitLSPIndex?: - tokenModifierTranslations[UInt32(index)] = .translation(UInt32(sourceKitLSPIndex)) - } - } - struct TranslationsLogObject: CustomLogStringConvertible { - let translations: [UInt32: Translation] - - var description: String { redactedDescription } - - var redactedDescription: String { - translations.sorted { $0.key < $1.key }.map { entry in - switch entry.value { - case .translation(let translation): - return "\(entry.key): \(translation)" - case .doesNotExistInSourceKitLSP: - return "\(entry.key): does not exist" - } - }.joined(separator: "\n") - } - } - logger.logFullObjectInMultipleLogMessages( - level: .info, - header: "clangd token type translations", - TranslationsLogObject(translations: tokenTypeTranslations) - ) - logger.logFullObjectInMultipleLogMessages( - level: .info, - header: "clangd token modifier translations", - TranslationsLogObject(translations: tokenModifierTranslations) - ) - self.tokenModifierTranslations = tokenModifierTranslations - - var tokenModifierTranslationBitmask: UInt32 = 0 - for translatedIndex in tokenModifierTranslations.keys { - tokenModifierTranslationBitmask.setBitToOne(at: Int(translatedIndex)) - } - self.tokenModifierTranslationBitmask = tokenModifierTranslationBitmask - - self.tokenTypeFallbackIndex = UInt32( - sourceKitLSPLegend.tokenTypes.firstIndex(of: SemanticTokenTypes.unknown.name) ?? 0 - ) - } - - func translate(_ data: [UInt32]) -> [UInt32] { - var data = data - // Translate token types, which are at offset n + 3. - for i in stride(from: 3, to: data.count, by: 5) { - switch tokenTypeTranslations[data[i]] { - case .doesNotExistInSourceKitLSP: data[i] = tokenTypeFallbackIndex - case .translation(let translatedIndex): data[i] = translatedIndex - case nil: break - } - } - - // Translate token modifiers, which are at offset n + 4 - for i in stride(from: 4, to: data.count, by: 5) { - guard data[i] & tokenModifierTranslationBitmask != 0 else { - // Fast path: There is nothing to translate - continue - } - var translatedModifiersBitmask: UInt32 = 0 - for (clangdModifier, sourceKitLSPModifier) in tokenModifierTranslations { - guard data[i].hasBitSet(at: Int(clangdModifier)) else { - continue - } - switch sourceKitLSPModifier { - case .doesNotExistInSourceKitLSP: break - case .translation(let sourceKitLSPIndex): translatedModifiersBitmask.setBitToOne(at: Int(sourceKitLSPIndex)) - } - } - data[i] = data[i] & ~tokenModifierTranslationBitmask | translatedModifiersBitmask - } - - return data - } -} - -fileprivate extension UInt32 { - mutating func hasBitSet(at index: Int) -> Bool { - return self & (1 << index) != 0 - } - - mutating func setBitToOne(at index: Int) { - self |= 1 << index - } -} diff --git a/Sources/CompletionScoring/CMakeLists.txt b/Sources/CompletionScoring/CMakeLists.txt deleted file mode 100644 index 1356e009b..000000000 --- a/Sources/CompletionScoring/CMakeLists.txt +++ /dev/null @@ -1,51 +0,0 @@ -set(sources - Semantics/SemanticClassification.swift - Semantics/CompletionScore.swift - Semantics/Components - Semantics/Components/TypeCompatibility.swift - Semantics/Components/StructuralProximity.swift - Semantics/Components/Flair.swift - Semantics/Components/CompletionKind.swift - Semantics/Components/Popularity.swift - Semantics/Components/ScopeProximity.swift - Semantics/Components/Availability.swift - Semantics/Components/SynchronicityCompatibility.swift - Semantics/Components/PopularityIndex.swift - Semantics/Components/ModuleProximity.swift - Semantics/Components/PopularityTable.swift - Utilities/UnsafeStackAllocator.swift - Utilities/Serialization - Utilities/Serialization/BinaryCodable.swift - Utilities/Serialization/BinaryEncoder.swift - Utilities/Serialization/BinaryDecoder.swift - Utilities/Serialization/Conformances - Utilities/Serialization/Conformances/Dictionary+BinaryCodable.swift - Utilities/Serialization/Conformances/OptionSet+BinaryCodable.swift - Utilities/Serialization/Conformances/Optional+BinaryCodable.swift - Utilities/Serialization/Conformances/Scalars+BinaryCodable.swift - Utilities/Serialization/Conformances/String+BinaryCodable.swift - Utilities/Serialization/Conformances/Array+BinaryCodable.swift - Utilities/SwiftExtensions.swift - Utilities/SelectTopK.swift - Utilities/UnsafeArray.swift - Text/CandidateBatch.swift - Text/MatchCollator.Match.swift - Text/UTF8Byte.swift - Text/MatchCollator.swift - Text/InfluencingIdentifiers.swift - Text/MatchCollator.Selection.swift - Text/ScoredMatchSelector.swift - Text/RejectionFilter.swift - Text/Pattern.swift) - -add_library(CompletionScoring STATIC ${sources}) -set_target_properties(CompletionScoring PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(CompletionScoring PUBLIC - CCompletionScoring) - -add_library(CompletionScoringForPlugin STATIC ${sources}) -set_target_properties(CompletionScoringForPlugin PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(CompletionScoringForPlugin PUBLIC - CCompletionScoring) diff --git a/Sources/CompletionScoring/Semantics/CompletionScore.swift b/Sources/CompletionScoring/Semantics/CompletionScore.swift deleted file mode 100644 index 8affba6e8..000000000 --- a/Sources/CompletionScoring/Semantics/CompletionScore.swift +++ /dev/null @@ -1,75 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// Represents a composite score formed from a semantic and textual components. -/// -/// The textual component forms the bulk of the score typically having a value in the 10's to 100's. -/// You can think of the semantic component as a bonus to the text score. -/// It usually has a value between 0 and 2, and is used as a multiplier. -package struct CompletionScore: Comparable { - package var semanticComponent: Double - package var textComponent: Double - - package init(textComponent: Double, semanticComponent: Double) { - self.semanticComponent = semanticComponent - self.textComponent = textComponent - } - - package init(textComponent: Double, semanticClassification: SemanticClassification) { - self.semanticComponent = semanticClassification.score - self.textComponent = textComponent - } - - package var value: Double { - semanticComponent * textComponent - } - - package static func < (_ lhs: Self, _ rhs: Self) -> Bool { - lhs.value < rhs.value - } -} - -// MARK: - Deprecated - -extension CompletionScore { - /// There is no natural order to these arguments, so they're alphabetical. - @available( - *, - deprecated, - renamed: - "SemanticClassification(completionKind:deprecationStatus:flair:moduleProximity:popularity:scopeProximity:structuralProximity:synchronicityCompatibility:typeCompatibility:)" - ) - package static func semanticScore( - completionKind: CompletionKind, - deprecationStatus: DeprecationStatus, - flair: Flair, - moduleProximity: ModuleProximity, - popularity: Popularity, - scopeProximity: ScopeProximity, - structuralProximity: StructuralProximity, - synchronicityCompatibility: SynchronicityCompatibility, - typeCompatibility: TypeCompatibility - ) -> Double { - SemanticClassification( - availability: deprecationStatus, - completionKind: completionKind, - flair: flair, - moduleProximity: moduleProximity, - popularity: popularity, - scopeProximity: scopeProximity, - structuralProximity: structuralProximity, - synchronicityCompatibility: synchronicityCompatibility, - typeCompatibility: typeCompatibility - ).score - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/Availability.swift b/Sources/CompletionScoring/Semantics/Components/Availability.swift deleted file mode 100644 index 8a45de790..000000000 --- a/Sources/CompletionScoring/Semantics/Components/Availability.swift +++ /dev/null @@ -1,75 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum Availability: Equatable { - /// Example: Either not tagged, or explicit availability is compatible with current build context - case available - - /// Example: Explicitly unavailable in current build context - ie, only for another platform. - case unavailable - - /// Example: deprecated in the future - case softDeprecated - - /// Example: deprecated in the present, or past - case deprecated - - /// Completion provider doesn't know if the method is deprecated or not - case unknown - - /// Example: keyword - case inapplicable - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension Availability: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .available - case 1: return .unavailable - case 2: return .softDeprecated - case 3: return .deprecated - case 4: return .unknown - case 5: return .inapplicable - case 6: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - let value: UInt8 - switch self { - case .available: value = 0 - case .unavailable: value = 1 - case .softDeprecated: value = 2 - case .deprecated: value = 3 - case .unknown: value = 4 - case .inapplicable: value = 5 - case .unspecified: value = 6 - } - encoder.write(value) - } -} - -@available(*, deprecated, renamed: "Availability") -package typealias DeprecationStatus = Availability - -extension Availability { - @available(*, deprecated, renamed: "Availability.available") - package static let none = DeprecationStatus.available -} diff --git a/Sources/CompletionScoring/Semantics/Components/CompletionKind.swift b/Sources/CompletionScoring/Semantics/Components/CompletionKind.swift deleted file mode 100644 index 105fff56a..000000000 --- a/Sources/CompletionScoring/Semantics/Components/CompletionKind.swift +++ /dev/null @@ -1,92 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum CompletionKind: Equatable { - /// Example: try, throw, where - case keyword - - /// Example: A case in an enumeration - case enumCase - - /// Example: A local, argument, property within a type - case variable - - /// Example: A function at global scope, or within some other type. - case function - - /// Example: An init method - case initializer - - /// Example: in `append(|)`, suggesting `contentsOf:` - case argumentLabels - - /// Example: `String`, `Int`, generic parameter, typealias, associatedtype - case type - - /// Example: A `guard let` template for a local variable. - case template - - /// Example: Foundation, AppKit, UIKit, SwiftUI - case module - - /// Example: Something not listed here, consider adding a new case - case other - - /// Example: Completion provider can't even tell what it's offering up. - case unknown - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension CompletionKind: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .keyword - case 1: return .enumCase - case 2: return .variable - case 3: return .function - case 4: return .initializer - case 5: return .argumentLabels - case 6: return .type - case 7: return .template - case 8: return .other - case 9: return .unknown - case 10: return .unspecified - case 11: return .module - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - let value: UInt8 - switch self { - case .keyword: value = 0 - case .enumCase: value = 1 - case .variable: value = 2 - case .function: value = 3 - case .initializer: value = 4 - case .argumentLabels: value = 5 - case .type: value = 6 - case .template: value = 7 - case .other: value = 8 - case .unknown: value = 9 - case .unspecified: value = 10 - case .module: value = 11 - } - encoder.write(value) - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/Flair.swift b/Sources/CompletionScoring/Semantics/Components/Flair.swift deleted file mode 100644 index 0a36d31d0..000000000 --- a/Sources/CompletionScoring/Semantics/Components/Flair.swift +++ /dev/null @@ -1,139 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package struct Flair: OptionSet { - package var rawValue: Int64 - - package init(rawValue: Int64) { - self.rawValue = rawValue - } - - package init(_ rawValue: Int64) { - self.rawValue = rawValue - } - - /// Xcode prior to version 13, grouped many high priority completions under the tag 'expression specific'. - /// To aid in mapping those into the new API, redefine this case as a catch-all 'high priority' expression. - /// Please do not use it for new cases, instead, add a case to this enum to model the significance of the completion. - package static let oldExpressionSpecific_pleaseAddSpecificCaseToThisEnum = Flair(1 << 0) - - /// E.g. `override func foo() { super.foo() ...` - package static let chainedCallToSuper = Flair(1 << 1) - - /// E.g. `bar.baz` in `foo.bar.baz`. - package static let chainedMember = Flair(1 << 2) - - /// E.g. using class, struct, protocol, public, private, or fileprivate at file level. - internal static let _situationallyLikely = Flair(1 << 3) - - /// E.g. using `class` inside of a function body, or a protocol in an expression. - internal static let _situationallyUnlikely = Flair(1 << 4) - - /// E.g. referencing a type at top level position in a non script/main.swift file. - internal static let _situationallyInvalid = Flair(1 << 5) - - /// E.g. an instance method/property in SwiftUI ViewBuilder with implicit-self context. - package static let swiftUIModifierOnSelfWhileBuildingSelf = Flair(1 << 6) - - /// E.g. `frame()`, since you almost certainly want to pass an argument to it. - package static let swiftUIUnlikelyViewMember = Flair(1 << 7) - - /// E.g. using struct, enum, protocol, class, public, private, or fileprivate at file level. - package static let commonKeywordAtCurrentPosition = Flair(1 << 8) - - /// E.g. nesting class in a function. - package static let rareKeywordAtCurrentPosition = Flair(1 << 9) - - /// E.g. using a protocol by name in an expression in a non-type position. `let x = 3 + Comparable…` - package static let rareTypeAtCurrentPosition = Flair(1 << 10) - - /// E.g. referencing a type, function, etc… at top level position in a non script/main.swift file. - package static let expressionAtNonScriptOrMainFileScope = Flair(1 << 11) - - /// E.g. `printContents`, which is almost never what you want when typing `print`. - package static let rareMemberWithCommonName = Flair(1 << 12) - - @available( - *, - deprecated, - message: """ - This is an escape hatch for scenarios we haven't thought of. \ - When using, file a bug report to name the new situational oddity so that we can directly model it. - """ - ) - package static let situationallyLikely = _situationallyLikely - - @available( - *, - deprecated, - message: """ - This is an escape hatch for scenarios we haven't thought of. \ - When using, file a bug report to name the new situational oddity so that we can directly model it. - """ - ) - package static let situationallyUnlikely = _situationallyUnlikely - - @available( - *, - deprecated, - message: """ - This is an escape hatch for scenarios we haven't thought of. \ - When using, file a bug report to name the new situational oddity so that we can directly model it. - """ - ) - package static let situationallyInvalid = _situationallyInvalid -} - -extension Flair: CustomDebugStringConvertible { - private static let namedValues: [(Flair, String)] = [ - ( - .oldExpressionSpecific_pleaseAddSpecificCaseToThisEnum, - "oldExpressionSpecific_pleaseAddSpecificCaseToThisEnum" - ), - (.chainedCallToSuper, "chainedCallToSuper"), - (.chainedMember, "chainedMember"), - (._situationallyLikely, "_situationallyLikely"), - (._situationallyUnlikely, "_situationallyUnlikely"), - (._situationallyInvalid, "_situationallyInvalid"), - (.swiftUIModifierOnSelfWhileBuildingSelf, "swiftUIModifierOnSelfWhileBuildingSelf"), - (.swiftUIUnlikelyViewMember, "swiftUIUnlikelyViewMember"), - (.commonKeywordAtCurrentPosition, "commonKeywordAtCurrentPosition"), - (.rareKeywordAtCurrentPosition, "rareKeywordAtCurrentPosition"), - (.rareTypeAtCurrentPosition, "rareTypeAtCurrentPosition"), - (.expressionAtNonScriptOrMainFileScope, "expressionAtNonScriptOrMainFileScope"), - (.rareMemberWithCommonName, "rareMemberWithCommonName"), - ] - - package var debugDescription: String { - var descriptions = [String]() - for (flair, name) in Self.namedValues { - if self.contains(flair) { - descriptions.append(name) - } - } - if descriptions.isEmpty { - return "none" - } else { - return descriptions.joined(separator: ",") - } - } -} - -extension Flair: OptionSetBinaryCodable {} - -extension Flair { - package var factor: Double { - return self.scoreComponent - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/ModuleProximity.swift b/Sources/CompletionScoring/Semantics/Components/ModuleProximity.swift deleted file mode 100644 index 5da20ac74..000000000 --- a/Sources/CompletionScoring/Semantics/Components/ModuleProximity.swift +++ /dev/null @@ -1,71 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum ModuleProximity: Equatable { - /// Example: Distance 0 is this module. if you have only "import AppKit", AppKit would be 1, Foundation and - /// CoreGraphics would be 2. - case imported(distance: Int) - - /// Example: Referencing NSDocument (AppKit) from a tool only using Foundation. An import to (AppKit) would have to - /// be added. - case importable - - /// Example: Keywords - case inapplicable - - // Completion provider doesn't understand modules - case unknown - - /// Example: Circular dependency, wrong platform - case invalid - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified - - package static let same = ModuleProximity.imported(distance: 0) -} - -extension ModuleProximity: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .imported(distance: try Int(&decoder)) - case 1: return .importable - case 2: return .inapplicable - case 3: return .unknown - case 4: return .invalid - case 5: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - switch self { - case .imported(let distance): - encoder.writeByte(0) - encoder.write(distance) - case .importable: - encoder.writeByte(1) - case .inapplicable: - encoder.writeByte(2) - case .unknown: - encoder.writeByte(3) - case .invalid: - encoder.writeByte(4) - case .unspecified: - encoder.writeByte(5) - } - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/Popularity.swift b/Sources/CompletionScoring/Semantics/Components/Popularity.swift deleted file mode 100644 index 67af9215a..000000000 --- a/Sources/CompletionScoring/Semantics/Components/Popularity.swift +++ /dev/null @@ -1,125 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package struct PopularityScoreComponent: Equatable, Comparable { - var value: Double - static let unspecified = PopularityScoreComponent(value: unspecifiedScore) - static let none = PopularityScoreComponent(value: 1.0) - - package static func < (_ lhs: Self, _ rhs: Self) -> Bool { - lhs.value < rhs.value - } -} - -extension PopularityScoreComponent: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - value = try Double(&decoder) - } - - package func encode(_ encoder: inout BinaryEncoder) { - encoder.write(value) - } -} - -package struct Popularity: Equatable, Comparable { - package var scoreComponent: Double { - symbolComponent * moduleComponent - } - - package var symbolComponent: Double - - package var moduleComponent: Double - - // TODO: remove once `PopularityTable` is removed. - package static let unspecified = Popularity(scoreComponent: unspecifiedScore) - package static let none = Popularity(scoreComponent: 1.0) - - package enum Category { - /// Used by `PopularityIndex`, where the popularities don't have much context. - case index - - /// Used when a client has a lot context. Allowing for a more precise popularity boost. - case predictive - - private static let predictiveMin = 1.10 - private static let predictiveMax = 1.30 - - var minimum: Double { - switch self { - case .index: - return 1.02 - case .predictive: - return Self.predictiveMin - } - } - - var maximum: Double { - switch self { - case .index: - return 1.10 - case .predictive: - return Self.predictiveMax - } - } - } - - @available(*, deprecated) - package init(probability: Double) { - self.init(probability: probability, category: .index) - } - - /// - Parameter probability: a value in range `0...1` - package init(probability: Double, category: Category) { - let score = Self.scoreComponent(probability: probability, category: category) - self.init(scoreComponent: score) - } - - /// Takes value in range `0...1`, - /// and converts to a value that can be used for multiplying with other score components. - static func scoreComponent(probability: Double, category: Category) -> Double { - let min = category.minimum - let max = category.maximum - if min > max { - assertionFailure("min \(min) > max \(max)") - return 1.0 - } - return (probability * (max - min)) + min - } - - package init(scoreComponent: Double) { - self.symbolComponent = scoreComponent - self.moduleComponent = 1.0 - } - - internal init(symbolComponent: Double, moduleComponent: Double) { - self.symbolComponent = symbolComponent - self.moduleComponent = moduleComponent - } - - package static func < (_ lhs: Self, _ rhs: Self) -> Bool { - lhs.scoreComponent < rhs.scoreComponent - } -} - -extension Popularity: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - symbolComponent = try Double(&decoder) - moduleComponent = try Double(&decoder) - } - - package func encode(_ encoder: inout BinaryEncoder) { - encoder.write(symbolComponent) - encoder.write(moduleComponent) - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/PopularityIndex.swift b/Sources/CompletionScoring/Semantics/Components/PopularityIndex.swift deleted file mode 100644 index cb4e45078..000000000 --- a/Sources/CompletionScoring/Semantics/Components/PopularityIndex.swift +++ /dev/null @@ -1,221 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A `PopularityIndex` is constructed from symbol reference frequencies and uses that data to bestow -/// `Popularity` bonuses on completions. -package struct PopularityIndex { - - /// The namespace of a symbol. - /// - /// Examples - /// * `Swift.Array.append(:)` would be `Scope(container: "Array", module: "Swift")` - /// * `Swift.Array` would be `Scope(container: nil, module: "Swift")`. - /// - /// This library imposes no constraints on formatting `container`. It's entirely up to the client to - /// decide how precise to be, and how to spell values. They could use `[String]`, `Array` - /// or `Array`. It only matters that they refer to types consistently. They're also free to model - /// inner types with strings like `List.Node`. - package struct Scope: Hashable { - package var container: String? - package var module: String - - package init(container: String?, module: String) { - self.module = module - self.container = container - } - } - - /// A name within a scope. - /// - /// Examples - /// * `Swift.Array.append(:)` would be: - /// * `Symbol(name: "append(:)", scope: Scope(container: "Array", module: "Swift"))` - /// * `Swift.Array` would be: - /// * `Symbol(name: "Array", scope: Scope(container: nil, module: "Swift"))` - /// - /// This library imposes no constraints on formatting `name`. It's entirely up to the client to use - /// consistent values. For example, they could independently track overloads by including types - /// in function names, or they could combine all related methods by tracking only function base - /// names. - package struct Symbol: Hashable { - package var name: String - package var scope: Scope - - package init(name: String, scope: Scope) { - self.name = name - self.scope = scope - } - } - - package private(set) var symbolPopularity: [Symbol: PopularityScoreComponent] = [:] - package private(set) var modulePopularity: [String: PopularityScoreComponent] = [:] - - private var knownScopes = Set() - - /// Clients can use this to find a relevant `Scope`. - /// To contruct a `Symbol` to pass to `popularity(of:)`. - package func isKnownScope(_ scope: Scope) -> Bool { - return knownScopes.contains(scope) - } - - /// - Parameters: - /// - `symbolReferencePercentages`: Symbol reference percentages per scope. - /// For example, if the data that produced the symbol reference percentags had 1 call to `Array.append(:)`, - /// 3 calls to `Array.count`, and 1 call to `String.append(:)` the table would be: - /// ``` - /// [ - /// "Swift.Array" : [ - /// "append(:)" : 0.25, - /// "count" : 0.75 - /// ], - /// "Swift.String" : [ - /// "append(:)" : 1.0 - /// ] - /// ] - /// ``` - /// - `notoriousSymbols`: Symbols from this list will get a significant penalty. - /// - `popularModules`: Symbols from these modules will get a slight bonus. - /// - `notoriousModules`: symbols from these modules will get a significant penalty. - package init( - symbolReferencePercentages: [Scope: [String: Double]], - notoriousSymbols: [Symbol], - popularModules: [String], - notoriousModules: [String] - ) { - knownScopes = Set(symbolReferencePercentages.keys) - - raisePopularities(symbolReferencePercentages: symbolReferencePercentages) - raisePopularities(popularModules: popularModules) - - // Even if data shows that it's popular, if we manually penalized it, always do that. - lowerPopularities(notoriousModules: notoriousModules) - lowerPopularities(notoriousSymbols: notoriousSymbols) - } - - fileprivate init() {} - - private mutating func raisePopularities(symbolReferencePercentages: [Scope: [String: Double]]) { - for (scope, namedReferencePercentages) in symbolReferencePercentages { - if let maxReferencePercentage = namedReferencePercentages.lazy.map(\.value).max() { - for (completion, referencePercentage) in namedReferencePercentages { - let symbol = Symbol(name: completion, scope: scope) - let normalizedScore = referencePercentage / maxReferencePercentage // 0...1 - let flattenedScore = pow(normalizedScore, 0.25) // Don't make it so much of a winner takes all - symbolPopularity.raise( - symbol, - toAtLeast: Popularity.scoreComponent(probability: flattenedScore, category: .index) - ) - } - } - } - } - - private mutating func lowerPopularities(notoriousSymbols: [Symbol]) { - symbolPopularity.lower(notoriousSymbols, toAtMost: Availability.deprecated.scoreComponent) - } - - private mutating func lowerPopularities(notoriousModules: [String]) { - modulePopularity.lower(notoriousModules, toAtMost: Availability.deprecated.scoreComponent) - } - - private mutating func raisePopularities(popularModules: [String]) { - modulePopularity.raise(popularModules, toAtLeast: Popularity.scoreComponent(probability: 0.0, category: .index)) - } - - package func popularity(of symbol: Symbol) -> Popularity { - let symbolPopularity = symbolPopularity[symbol] ?? .none - let modulePopularity = modulePopularity[symbol.scope.module] ?? .none - return Popularity(symbolComponent: symbolPopularity.value, moduleComponent: modulePopularity.value) - } -} - -fileprivate extension Dictionary where Value == PopularityScoreComponent { - mutating func raise(_ key: Key, toAtLeast minimum: Double) { - let leastPopular = PopularityScoreComponent(value: -Double.infinity) - if self[key, default: leastPopular].value < minimum { - self[key] = PopularityScoreComponent(value: minimum) - } - } - - mutating func lower(_ key: Key, toAtMost maximum: Double) { - let mostPopular = PopularityScoreComponent(value: Double.infinity) - if self[key, default: mostPopular].value > maximum { - self[key] = PopularityScoreComponent(value: maximum) - } - } - - mutating func raise(_ keys: [Key], toAtLeast minimum: Double) { - for key in keys { - raise(key, toAtLeast: minimum) - } - } - - mutating func lower(_ keys: [Key], toAtMost maximum: Double) { - for key in keys { - lower(key, toAtMost: maximum) - } - } -} - -/// Implement coding with BinaryCodable without singing up for package conformance -extension PopularityIndex { - package enum SerializationVersion: Int { - case initial - } - - private struct SerializableSymbol: Hashable, BinaryCodable { - var symbol: Symbol - - init(symbol: Symbol) { - self.symbol = symbol - } - - init(_ decoder: inout BinaryDecoder) throws { - let name = try String(&decoder) - let container = try String?(&decoder) - let module = try String(&decoder) - symbol = Symbol(name: name, scope: Scope(container: container, module: module)) - } - - func encode(_ encoder: inout BinaryEncoder) { - encoder.write(symbol.name) - encoder.write(symbol.scope.container) - encoder.write(symbol.scope.module) - } - } - - package func serialize(version: SerializationVersion) -> [UInt8] { - BinaryEncoder.encode(contentVersion: version.rawValue) { encoder in - encoder.write(symbolPopularity.mapKeys(overwritingDuplicates: .affirmative, SerializableSymbol.init)) - encoder.write(modulePopularity) - } - } - - package static func deserialize(data serialization: [UInt8]) throws -> Self { - try BinaryDecoder.decode(bytes: serialization) { decoder in - switch SerializationVersion(rawValue: decoder.contentVersion) { - case .initial: - var index = Self() - index.symbolPopularity = try [SerializableSymbol: PopularityScoreComponent](&decoder).mapKeys( - overwritingDuplicates: .affirmative, - \.symbol - ) - index.modulePopularity = try [String: PopularityScoreComponent](&decoder) - return index - case .none: - throw GenericError("Unknown \(String(describing: self)) serialization format") - } - } - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/PopularityTable.swift b/Sources/CompletionScoring/Semantics/Components/PopularityTable.swift deleted file mode 100644 index ff4533824..000000000 --- a/Sources/CompletionScoring/Semantics/Components/PopularityTable.swift +++ /dev/null @@ -1,192 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -// TODO: deprecate -//@available(*, deprecated, message: "Use PopularityIndex instead.") -package struct PopularityTable { - /// Represents a list of symbols form a module, with a value for each symbol representing what % of references to this module were to that symbol. - package struct ModuleSymbolReferenceTable { - var symbolReferencePercentages: [String: Double] - var notoriousSymbols: [String] - package init(symbolReferencePercentages: [String: Double], notoriousSymbols: [String] = []) { - self.symbolReferencePercentages = symbolReferencePercentages - self.notoriousSymbols = notoriousSymbols - } - } - - /// package so `PopularityTable` can be serialized into an XPC object to be sent to the SourceKit plugin - package private(set) var symbolPopularity: [String: Popularity] = [:] - package private(set) var modulePopularity: [String: Popularity] = [:] - - /// Only to be used in the SourceKit plugin when deserializing a `PopularityTable` from an XPC object. - package init(symbolPopularity: [String: Popularity], modulePopularity: [String: Popularity]) { - self.symbolPopularity = symbolPopularity - self.modulePopularity = modulePopularity - } - - /// Initialize with popular symbol usage statistics, a list of recent completions, and module notoriety. - /// - Parameters: - /// - moduleSymbolReferenceTables: The symbol reference statistics should include the popular symbols for each - /// module. - /// - recentCompletions: A list of recent completions, with repetitions, with the earliest completions at the head - /// of the list. - /// - popularModules: Symbols from these modules will get a slight bonus. - /// - notoriousModules: symbols from these modules will get a significant penalty. - package init( - moduleSymbolReferenceTables: [ModuleSymbolReferenceTable], - recentCompletions: [String], - popularModules: [String], - notoriousModules: [String] - ) { - recordPopularSymbolsBonuses(modules: moduleSymbolReferenceTables) - recordNotoriousSymbolsBonuses(modules: moduleSymbolReferenceTables) - recordSymbolRecencyBonuses(recentCompletions: recentCompletions) - recordPopularModules(popularModules: popularModules) - recordNotoriousModules(notoriousModules: notoriousModules) - } - - /// Takes value from 0...1 - private func scoreComponent(normalizedPopularity: Double) -> Double { - let maxPopularityBonus = 1.10 - let minPopularityBonus = 1.02 - return (normalizedPopularity * (maxPopularityBonus - minPopularityBonus)) + minPopularityBonus - } - - private mutating func recordPopularSymbolsBonuses(modules: [ModuleSymbolReferenceTable]) { - for module in modules { - if let maxReferencePercentage = module.symbolReferencePercentages.map(\.value).max() { - for (symbol, referencePercentage) in module.symbolReferencePercentages { - let normalizedScore = referencePercentage / maxReferencePercentage // 0...1 - let flattenedScore = pow(normalizedScore, 0.25) // Don't make it so much of a winner takes all - symbolPopularity.record( - scoreComponent: scoreComponent(normalizedPopularity: flattenedScore), - for: symbol - ) - } - } - } - } - - /// Record recency so that repeated use also impacts score. Completing `NSString` 100 times, then completing - /// `NSStream` once should not make `NSStream` the top result. - private mutating func recordSymbolRecencyBonuses(recentCompletions: [String]) { - var pointsPerFilterText: [String: Int] = [:] - let count = recentCompletions.count - var totalPoints = 0 - for (position, filterText) in recentCompletions.enumerated() { - let points = count - position - pointsPerFilterText[filterText, default: 0] += points - totalPoints += points - } - for (filterText, points) in pointsPerFilterText { - let bonus = scoreComponent(normalizedPopularity: Double(points) / Double(totalPoints)) - symbolPopularity.record(scoreComponent: bonus, for: filterText) - } - } - - internal mutating func recordPopularModules(popularModules: [String]) { - let scoreComponent = scoreComponent(normalizedPopularity: 0.0) - for module in popularModules { - modulePopularity.record(scoreComponent: scoreComponent, for: module) - } - } - - internal mutating func recordNotoriousModules(notoriousModules: [String]) { - for module in notoriousModules { - modulePopularity.record(scoreComponent: Availability.deprecated.scoreComponent, for: module) - } - } - - internal mutating func record(notoriousSymbols: [String]) { - for symbol in notoriousSymbols { - symbolPopularity.record(scoreComponent: Availability.deprecated.scoreComponent, for: symbol) - } - } - - private mutating func recordNotoriousSymbolsBonuses(modules: [ModuleSymbolReferenceTable]) { - for module in modules { - record(notoriousSymbols: module.notoriousSymbols) - } - } - - private func popularity(symbol: String) -> Popularity { - return symbolPopularity[symbol] ?? .none - } - - private func popularity(module: String) -> Popularity { - return modulePopularity[module] ?? .none - } - - package func popularity(symbol: String?, module: String?) -> Popularity { - let symbolPopularity = symbol.map { popularity(symbol: $0) } ?? .none - let modulePopularity = module.map { popularity(module: $0) } ?? .none - return Popularity( - symbolComponent: symbolPopularity.scoreComponent, - moduleComponent: modulePopularity.scoreComponent - ) - } -} - -extension [String: Popularity] { - fileprivate mutating func record(scoreComponent: Double, for key: String) { - let leastPopular = Popularity(scoreComponent: -Double.infinity) - if self[key, default: leastPopular].scoreComponent < scoreComponent { - self[key] = Popularity(scoreComponent: scoreComponent) - } - } -} - -// TODO: deprecate -//@available(*, deprecated, message: "Use PopularityIndex instead.") -extension PopularityTable { - package init(popularSymbols: [String] = [], recentSymbols: [String] = [], notoriousSymbols: [String] = []) { - add(popularSymbols: popularSymbols) - recordSymbolRecencyBonuses(recentCompletions: recentSymbols) - record(notoriousSymbols: notoriousSymbols) - } - - package mutating func add(popularSymbols: [String]) { - for (index, symbol) in popularSymbols.enumerated() { - let popularity = (1.0 - (Double(index) / Double(popularSymbols.count + 1))) // 1.0...0.0 - let scoreComponent = scoreComponent(normalizedPopularity: popularity) - symbolPopularity.record(scoreComponent: scoreComponent, for: symbol) - } - } -} - -// TODO: deprecate -//@available(*, deprecated, message: "Use PopularityIndex instead.") -extension PopularityTable { - @available( - *, - renamed: "ModuleSymbolReferenceTable", - message: "Popularity is now for modules in addition to symbols. This was renamed to be more precise." - ) - package typealias ModulePopularityTable = ModuleSymbolReferenceTable - - @available(*, deprecated, message: "Pass a module name with popularity(symbol:module:)") - package func popularity(for symbol: String) -> Popularity { - popularity(symbol: symbol, module: nil) - } - - @available(*, deprecated, message: "Pass popularModules: and notoriousModules:") - package init(modules: [ModuleSymbolReferenceTable], recentCompletions: [String]) { - self.init( - moduleSymbolReferenceTables: modules, - recentCompletions: recentCompletions, - popularModules: [], - notoriousModules: [] - ) - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/ScopeProximity.swift b/Sources/CompletionScoring/Semantics/Components/ScopeProximity.swift deleted file mode 100644 index 24dca845d..000000000 --- a/Sources/CompletionScoring/Semantics/Components/ScopeProximity.swift +++ /dev/null @@ -1,81 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum ScopeProximity: Equatable { - /// Example: Within the context of a function, a local definition. Could be a variable, or an inner function, type, - /// etc... - case local - - /// Example: An argument to a function, including generic parameters - case argument - - /// Example: Within the context of a class, struct, etc..., possibly nested in a function, references to definitions - /// at the container level. - case container - - /// Example: Within the context of a class, struct, etc..., possibly nested in a function, references to definitions - /// from an inherited class or protocol - case inheritedContainer - - /// Example: Referring to a type in an outer container, for example, within the iterator for a Sequence, referring to - /// Element - case outerContainer - - /// Example: Global variables, free functions, top level types - case global - - /// Example: Keywords - case inapplicable - - /// Provider doesn't know the relation between the completion and the context - case unknown - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension ScopeProximity: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .local - case 1: return .argument - case 2: return .container - case 3: return .inheritedContainer - case 4: return .outerContainer - case 5: return .global - case 6: return .inapplicable - case 7: return .unknown - case 8: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - let value: UInt8 - switch self { - case .local: value = 0 - case .argument: value = 1 - case .container: value = 2 - case .inheritedContainer: value = 3 - case .outerContainer: value = 4 - case .global: value = 5 - case .inapplicable: value = 6 - case .unknown: value = 7 - case .unspecified: value = 8 - } - encoder.write(value) - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/StructuralProximity.swift b/Sources/CompletionScoring/Semantics/Components/StructuralProximity.swift deleted file mode 100644 index 7c56f6184..000000000 --- a/Sources/CompletionScoring/Semantics/Components/StructuralProximity.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum StructuralProximity: Equatable { - /// Example: Definition is in Project/Framework/UI/View.swift, usage site is Project/Framework/Model/View.swift, - /// so hops == 2, up one, and into a sibling. Hops is edit distance where the operations are 'delete, add', not replace. - case project(fileSystemHops: Int?) - - /// Example: Source of completion is from NSObject.h in the SDK - case sdk - - /// Example: Keyword - case inapplicable - - /// Example: Provider doesn't keep track of where definitions come from - case unknown - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension StructuralProximity: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .project(fileSystemHops: try Int?(&decoder)) - case 1: return .sdk - case 2: return .inapplicable - case 3: return .unknown - case 4: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - switch self { - case .project(let hops): - encoder.writeByte(0) - encoder.write(hops) - case .sdk: encoder.writeByte(1) - case .inapplicable: encoder.writeByte(2) - case .unknown: encoder.writeByte(3) - case .unspecified: encoder.writeByte(4) - } - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/SynchronicityCompatibility.swift b/Sources/CompletionScoring/Semantics/Components/SynchronicityCompatibility.swift deleted file mode 100644 index f51158f72..000000000 --- a/Sources/CompletionScoring/Semantics/Components/SynchronicityCompatibility.swift +++ /dev/null @@ -1,62 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum SynchronicityCompatibility: Equatable { - /// Example: sync->sync, async->sync, async->await async - case compatible - - /// Example: async->async without await - case convertible - - /// Example: sync->async - case incompatible - - /// Example: Accessing a type, using a keyword - case inapplicable - - /// Example: Not confident about either the context, or the target - case unknown - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension SynchronicityCompatibility: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .compatible - case 1: return .convertible - case 2: return .incompatible - case 3: return .inapplicable - case 4: return .unknown - case 5: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - let value: UInt8 - switch self { - case .compatible: value = 0 - case .convertible: value = 1 - case .incompatible: value = 2 - case .inapplicable: value = 3 - case .unknown: value = 4 - case .unspecified: value = 5 - } - encoder.write(value) - } -} diff --git a/Sources/CompletionScoring/Semantics/Components/TypeCompatibility.swift b/Sources/CompletionScoring/Semantics/Components/TypeCompatibility.swift deleted file mode 100644 index 3b69805e2..000000000 --- a/Sources/CompletionScoring/Semantics/Components/TypeCompatibility.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package enum TypeCompatibility: Equatable { - /// Examples: - /// - `String` is compatible with `String` - /// - `String` is compatible with `String?` - /// - `TextField` is compatible with `View`. - case compatible - - /// Example: `String` is unrelated to `Int` - case unrelated - - /// Example: `void` is invalid for `String` - case invalid - - /// Example: doesn't have a type: a keyword like 'try', or 'while' - case inapplicable - - /// Example: Failed to type check the expression context - case unknown - - /// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value - case unspecified -} - -extension TypeCompatibility { - /// This used to penalize producing an `Int` when `Int?` was expected, which isn't correct. - /// We no longer ask providers to make this distinction - @available(*, deprecated, renamed: "compatible") - package static let same = Self.compatible - - /// This used to penalize producing an `Int` when `Int?` was expected, which isn't correct. - /// We no longer ask providers to make this distinction - @available(*, deprecated, renamed: "compatible") - package static let implicitlyConvertible = Self.compatible -} - -extension TypeCompatibility: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - self = try decoder.decodeEnumByte { decoder, n in - switch n { - case 0: return .compatible - case 1: return .unrelated - case 2: return .invalid - case 3: return .inapplicable - case 4: return .unknown - case 5: return .unspecified - default: return nil - } - } - } - - package func encode(_ encoder: inout BinaryEncoder) { - let value: UInt8 - switch self { - case .compatible: value = 0 - case .unrelated: value = 1 - case .invalid: value = 2 - case .inapplicable: value = 3 - case .unknown: value = 4 - case .unspecified: value = 5 - } - encoder.write(value) - } -} diff --git a/Sources/CompletionScoring/Semantics/SemanticClassification.swift b/Sources/CompletionScoring/Semantics/SemanticClassification.swift deleted file mode 100644 index 5656717d3..000000000 --- a/Sources/CompletionScoring/Semantics/SemanticClassification.swift +++ /dev/null @@ -1,362 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package struct SemanticClassification: Equatable { - package var completionKind: CompletionKind - package var popularity: Popularity - package var moduleProximity: ModuleProximity - package var scopeProximity: ScopeProximity - package var structuralProximity: StructuralProximity - package var typeCompatibility: TypeCompatibility - package var synchronicityCompatibility: SynchronicityCompatibility - package var availability: Availability - package var flair: Flair - - /// - Note: There is no natural order to these arguments, so they're alphabetical. - package init( - availability: Availability, - completionKind: CompletionKind, - flair: Flair, - moduleProximity: ModuleProximity, - popularity: Popularity, - scopeProximity: ScopeProximity, - structuralProximity: StructuralProximity, - synchronicityCompatibility: SynchronicityCompatibility, - typeCompatibility: TypeCompatibility - ) { - self.availability = availability - self.completionKind = completionKind - self.flair = flair - self.moduleProximity = moduleProximity - self.popularity = popularity - self.scopeProximity = scopeProximity - self.structuralProximity = structuralProximity - self.synchronicityCompatibility = synchronicityCompatibility - self.typeCompatibility = typeCompatibility - } - - package var score: Double { - let score = - availability.scoreComponent - * completionKind.scoreComponent - * flair.scoreComponent - * moduleProximity.scoreComponent - * popularity.scoreComponent - * scopeProximity.scoreComponent - * structuralProximity.scoreComponent - * synchronicityCompatibility.scoreComponent - * typeCompatibility.scoreComponent - * globalVariablesPenalty - - return score - } - - private var globalVariablesPenalty: Double { - // Global types and functions are fine, global variables and c enum cases in the global space are not. - if (scopeProximity == .global) && ((completionKind == .variable) || (completionKind == .enumCase)) { - return 0.75 - } - return 1.0 - } - - package struct ComponentDebugDescription { - package let name: String - package let instance: String - package let scoreComponent: Double - } - - private var scoreComponents: [CompletionScoreComponent] { - return [ - availability, - completionKind, - flair, - RawCompletionScoreComponent( - name: "symbolPopularity", - instance: "\(popularity.symbolComponent)", - scoreComponent: popularity.symbolComponent - ), - RawCompletionScoreComponent( - name: "modulePopularity", - instance: "\(popularity.moduleComponent)", - scoreComponent: popularity.moduleComponent - ), - moduleProximity, - scopeProximity, - structuralProximity, - synchronicityCompatibility, - typeCompatibility, - RawCompletionScoreComponent( - name: "globalVariablesPenalty", - instance: "\(globalVariablesPenalty != 1.0)", - scoreComponent: globalVariablesPenalty - ), - ] - } - - package var componentsDebugDescription: [ComponentDebugDescription] { - return scoreComponents.map { $0.componentDebugDescription } - } -} - -extension SemanticClassification: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - availability = try Availability(&decoder) - completionKind = try CompletionKind(&decoder) - flair = try Flair(&decoder) - moduleProximity = try ModuleProximity(&decoder) - popularity = try Popularity(&decoder) - scopeProximity = try ScopeProximity(&decoder) - structuralProximity = try StructuralProximity(&decoder) - synchronicityCompatibility = try SynchronicityCompatibility(&decoder) - typeCompatibility = try TypeCompatibility(&decoder) - } - - package func encode(_ encoder: inout BinaryEncoder) { - encoder.write(availability) - encoder.write(completionKind) - encoder.write(flair) - encoder.write(moduleProximity) - encoder.write(popularity) - encoder.write(scopeProximity) - encoder.write(structuralProximity) - encoder.write(synchronicityCompatibility) - encoder.write(typeCompatibility) - } -} - -/// Published serialization methods -extension SemanticClassification { - package func byteRepresentation() -> [UInt8] { - binaryCodedRepresentation(contentVersion: 0) - } - - package init(byteRepresentation: [UInt8]) throws { - try self.init(binaryCodedRepresentation: byteRepresentation) - } - - package static func byteRepresentation(classifications: [Self]) -> [UInt8] { - classifications.binaryCodedRepresentation(contentVersion: 0) - } - - package static func classifications(byteRepresentations: [UInt8]) throws -> [Self] { - try [Self].init(binaryCodedRepresentation: byteRepresentations) - } -} - -/// Used for debugging. -private protocol CompletionScoreComponent { - var name: String { get } - - var instance: String { get } - - /// Return a value in 0...2. - /// - /// Think of values between 0 and 1 as penalties, 1 as neutral, and values from 1 and 2 as bonuses. - var scoreComponent: Double { get } -} - -extension CompletionScoreComponent { - var componentDebugDescription: SemanticClassification.ComponentDebugDescription { - return .init(name: name, instance: instance, scoreComponent: scoreComponent) - } -} - -/// Used for components that don't have a dedicated model. -private struct RawCompletionScoreComponent: CompletionScoreComponent { - let name: String - let instance: String - let scoreComponent: Double -} - -internal let unknownScore = 0.750 -internal let inapplicableScore = 1.000 -internal let unspecifiedScore = 1.000 - -private let localVariableScore = CompletionKind.variable.scoreComponent * ScopeProximity.local.scoreComponent -private let globalTypeScore = CompletionKind.type.scoreComponent * ScopeProximity.global.scoreComponent -internal let localVariableToGlobalTypeScoreRatio = localVariableScore / globalTypeScore - -extension CompletionKind: CompletionScoreComponent { - fileprivate var name: String { "CompletionKind" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .keyword: return 1.000 - case .enumCase: return 1.100 - case .variable: return 1.075 - case .initializer: return 1.020 - case .argumentLabels: return 2.000 - case .function: return 1.025 - case .type: return 1.025 - case .template: return 1.100 - case .module: return 0.925 - case .other: return 1.000 - - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension Flair: CompletionScoreComponent { - fileprivate var name: String { "Flair" } - fileprivate var instance: String { "\(self.debugDescription)" } - internal var scoreComponent: Double { - var total = 1.0 - if self.contains(.oldExpressionSpecific_pleaseAddSpecificCaseToThisEnum) { - total *= 1.5 - } - if self.contains(.chainedCallToSuper) { - total *= 1.5 - } - if self.contains(.chainedMember) { - total *= 0.3 - } - if self.contains(.swiftUIModifierOnSelfWhileBuildingSelf) { - total *= 0.3 - } - if self.contains(.swiftUIUnlikelyViewMember) { - total *= 0.125 - } - if self.contains(.commonKeywordAtCurrentPosition) { - total *= 1.25 - } - if self.contains(.rareKeywordAtCurrentPosition) { - total *= 0.75 - } - if self.contains(.rareTypeAtCurrentPosition) { - total *= 0.75 - } - if self.contains(.expressionAtNonScriptOrMainFileScope) { - total *= 0.125 - } - if self.contains(.rareMemberWithCommonName) { - total *= 0.75 - } - if self.contains(._situationallyLikely) { - total *= 1.25 - } - if self.contains(._situationallyUnlikely) { - total *= 0.75 - } - if self.contains(._situationallyInvalid) { - total *= 0.125 - } - return total - } -} - -extension ModuleProximity: CompletionScoreComponent { - fileprivate var name: String { "ModuleProximity" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .imported(0): return 1.0500 - case .imported(1): return 1.0250 - case .imported(_): return 1.0125 - case .importable: return 0.5000 - case .invalid: return 0.2500 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension ScopeProximity: CompletionScoreComponent { - fileprivate var name: String { "ScopeProximity" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .local: return 1.500 - case .argument: return 1.450 - case .container: return 1.350 - case .inheritedContainer: return 1.325 - case .outerContainer: return 1.325 - case .global: return 0.950 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension StructuralProximity: CompletionScoreComponent { - fileprivate var name: String { "StructuralProximity" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .project(fileSystemHops: 0): return 1.010 - case .project(fileSystemHops: 1): return 1.005 - case .project(fileSystemHops: _): return 1.000 - case .sdk: return 0.995 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension SynchronicityCompatibility: CompletionScoreComponent { - fileprivate var name: String { "SynchronicityCompatibility" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .compatible: return 1.00 - case .convertible: return 0.90 - case .incompatible: return 0.50 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension TypeCompatibility: CompletionScoreComponent { - fileprivate var name: String { "TypeCompatibility" } - fileprivate var instance: String { "\(self)" } - fileprivate var scoreComponent: Double { - switch self { - case .compatible: return 1.300 - case .unrelated: return 0.900 - case .invalid: return 0.300 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} - -extension Availability: CompletionScoreComponent { - fileprivate var name: String { "Availability" } - fileprivate var instance: String { "\(self)" } - internal var scoreComponent: Double { - switch self { - case .available: return 1.00 - case .unavailable: return 0.40 - case .softDeprecated, - .deprecated: - return 0.50 - - case .inapplicable: return inapplicableScore - case .unspecified: return unspecifiedScore - case .unknown: return unknownScore - } - } -} diff --git a/Sources/CompletionScoring/Text/CandidateBatch.swift b/Sources/CompletionScoring/Text/CandidateBatch.swift deleted file mode 100644 index e23a9d4df..000000000 --- a/Sources/CompletionScoring/Text/CandidateBatch.swift +++ /dev/null @@ -1,584 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A list of possible code completion results that need to be culled and sorted based on a ``Pattern``. -package struct CandidateBatch: Sendable { - package typealias UTF8Bytes = Pattern.UTF8Bytes - package typealias ContentType = Candidate.ContentType - - /// Clients can access this via `CandidateBatch.withUnsafeStorage()` so that they can have read-access without the - /// overhead of runtime exclusitivy checks that happen when you read through a reference type. - package struct UnsafeStorage { - var bytes: UnsafeArray - var candidateByteOffsets: UnsafeArray - var filters: UnsafeArray - var contentTypes: UnsafeArray - - private init( - bytes: UnsafeArray, - candidateByteOffsets: UnsafeArray, - filters: UnsafeArray, - contentTypes: UnsafeArray - ) { - self.bytes = bytes - self.candidateByteOffsets = candidateByteOffsets - self.filters = filters - self.contentTypes = contentTypes - } - - static func allocate(candidateCapacity: Int, byteCapacity: Int) -> Self { - var candidateByteOffsets = UnsafeArray.allocate(initialCapacity: candidateCapacity + 1) - candidateByteOffsets.append(0) // Always contains the 'endIndex' - return Self( - bytes: UnsafeArray.allocate(initialCapacity: byteCapacity), - candidateByteOffsets: candidateByteOffsets, - filters: UnsafeArray.allocate(initialCapacity: candidateCapacity), - contentTypes: UnsafeArray.allocate(initialCapacity: candidateCapacity) - ) - } - - mutating func deallocate() { - bytes.deallocate() - candidateByteOffsets.deallocate() - filters.deallocate() - contentTypes.deallocate() - } - - func allocateCopy() -> Self { - return Self( - bytes: bytes.allocateCopy(preservingCapacity: true), - candidateByteOffsets: candidateByteOffsets.allocateCopy(preservingCapacity: true), - filters: filters.allocateCopy(preservingCapacity: true), - contentTypes: contentTypes.allocateCopy(preservingCapacity: true) - ) - } - - @inline(__always) - func bytes(at index: Int) -> UTF8Bytes { - let position = candidateByteOffsets[index] - let nextPosition = candidateByteOffsets[index + 1] - return UnsafeBufferPointer(start: bytes.elements.advanced(by: position), count: nextPosition - position) - } - - @inline(__always) - func candidateContent(at index: Int) -> (UTF8Bytes, ContentType) { - let position = candidateByteOffsets[index] - let nextPosition = candidateByteOffsets[index + 1] - let bytes = UnsafeBufferPointer( - start: bytes.elements.advanced(by: position), - count: nextPosition - position - ) - let contentType = contentTypes[index] - return (bytes, contentType) - } - - var count: Int { - filters.count - } - - package var indices: Range { - return 0.. Candidate { - Candidate(bytes: bytes(at: index), contentType: contentTypes[index], rejectionFilter: filters[index]) - } - - /// Don't add a method that returns a candidate, the candidates have unsafe pointers back into the batch, and - /// must not outlive it. - @inline(__always) - func enumerate(body: (Candidate) throws -> Void) rethrows { - for idx in 0.., body: (Int, Candidate) throws -> Void) rethrows { - precondition(range.lowerBound >= 0) - precondition(range.upperBound <= count) - for idx in range { - try body(idx, candidate(at: idx)) - } - } - - subscript(stringAt index: Int) -> String { - // Started as a valid string, so UTF8 must be valid, if this ever fails (would have to be something like a - // string with unvalidated content), we should fix it on the input side, not here. - return String(bytes: bytes(at: index), encoding: .utf8).unwrap(orFail: "Invalid UTF8 Sequence") - } - - mutating func append(_ candidate: String, contentType: ContentType) { - candidate.withUncachedUTF8Bytes { bytes in - append(candidateBytes: bytes, contentType: contentType, rejectionFilter: RejectionFilter(bytes: bytes)) - } - } - - mutating func append(_ candidate: Candidate) { - append( - candidateBytes: candidate.bytes, - contentType: candidate.contentType, - rejectionFilter: candidate.rejectionFilter - ) - } - - mutating func append(_ bytes: UTF8Bytes, contentType: ContentType) { - append(Candidate(bytes: bytes, contentType: contentType, rejectionFilter: .init(bytes: bytes))) - } - - mutating func append( - candidateBytes: some Collection, - contentType: ContentType, - rejectionFilter: RejectionFilter - ) { - bytes.append(contentsOf: candidateBytes) - filters.append(rejectionFilter) - contentTypes.append(contentType) - candidateByteOffsets.append(bytes.count) - } - - mutating func append(contentsOf candidates: [String], contentType: ContentType) { - filters.reserve(minimumAdditionalCapacity: candidates.count) - contentTypes.reserve(minimumAdditionalCapacity: candidates.count) - candidateByteOffsets.reserve(minimumAdditionalCapacity: candidates.count) - for text in candidates { - append(text, contentType: contentType) - } - } - - mutating func append(contentsOf candidates: [UTF8Bytes], contentType: ContentType) { - filters.reserve(minimumAdditionalCapacity: candidates.count) - contentTypes.reserve(minimumAdditionalCapacity: candidates.count) - candidateByteOffsets.reserve(minimumAdditionalCapacity: candidates.count) - for text in candidates { - append(text, contentType: contentType) - } - } - } - - private final class StorageBox { - /// Column oriented data for better cache performance. - var storage: UnsafeStorage - - private init(storage: UnsafeStorage) { - self.storage = storage - } - - init(candidateCapacity: Int, byteCapacity: Int) { - storage = UnsafeStorage.allocate(candidateCapacity: candidateCapacity, byteCapacity: byteCapacity) - } - - func copy() -> Self { - Self(storage: storage.allocateCopy()) - } - - deinit { - storage.deallocate() - } - } - - // `nonisolated(unsafe)` is fine because this `CandidateBatch` is the only struct with access to the `StorageBox`. - // All mutating access go through `mutate`, which copies `StorageBox` if `CandidateBatch` is not uniquely - // referenced. - nonisolated(unsafe) private var __storageBox_useAccessor: StorageBox - private var readonlyStorage: UnsafeStorage { - __storageBox_useAccessor.storage - } - - package init(byteCapacity: Int) { - self.init(candidateCapacity: byteCapacity / 16, byteCapacity: byteCapacity) - } - - private init(candidateCapacity: Int, byteCapacity: Int) { - __storageBox_useAccessor = StorageBox(candidateCapacity: candidateCapacity, byteCapacity: byteCapacity) - } - - package init() { - self.init(candidateCapacity: 0, byteCapacity: 0) - } - - package init(candidates: [String], contentType: ContentType) { - let byteCapacity = candidates.reduce(into: 0) { sum, string in - sum += string.utf8.count - } - self.init(candidateCapacity: candidates.count, byteCapacity: byteCapacity) - append(contentsOf: candidates, contentType: contentType) - } - - package init(candidates: [UTF8Bytes], contentType: ContentType) { - let byteCapacity = candidates.reduce(into: 0) { sum, candidate in - sum += candidate.count - } - self.init(candidateCapacity: candidates.count, byteCapacity: byteCapacity) - append(contentsOf: candidates, contentType: contentType) - } - - package func enumerate(body: (Candidate) throws -> Void) rethrows { - try readonlyStorage.enumerate(body: body) - } - - package func enumerate(body: (Int, Candidate) throws -> Void) rethrows { - try readonlyStorage.enumerate(0.., body: (Int, Candidate) throws -> Void) rethrows { - try readonlyStorage.enumerate(range, body: body) - } - - package func withAccessToCandidate(at idx: Int, body: (Candidate) throws -> R) rethrows -> R { - try withUnsafeStorage { storage in - try body(storage.candidate(at: idx)) - } - } - - package func withAccessToBytes(at idx: Int, body: (UTF8Bytes) throws -> R) rethrows -> R { - try withUnsafeStorage { storage in - try body(storage.bytes(at: idx)) - } - } - - package func withUnsafeStorage(_ body: (UnsafeStorage) throws -> R) rethrows -> R { - try withExtendedLifetime(__storageBox_useAccessor) { - try body(__storageBox_useAccessor.storage) - } - } - - static func withUnsafeStorages(_ batches: [Self], _ body: (UnsafeBufferPointer) -> R) -> R { - withExtendedLifetime(batches) { - withUnsafeTemporaryAllocation(of: UnsafeStorage.self, capacity: batches.count) { storages in - for (index, batch) in batches.enumerated() { - storages.initialize(index: index, to: batch.readonlyStorage) - } - let result = body(UnsafeBufferPointer(storages)) - storages.deinitializeAll() - return result - } - } - } - - package subscript(stringAt index: Int) -> String { - readonlyStorage[stringAt: index] - } - - package var count: Int { - return readonlyStorage.count - } - - package var indices: Range { - return readonlyStorage.indices - } - - var hasContent: Bool { - count > 0 - } - - private mutating func mutate(body: (inout UnsafeStorage) -> Void) { - if !isKnownUniquelyReferenced(&__storageBox_useAccessor) { - __storageBox_useAccessor = __storageBox_useAccessor.copy() - } - body(&__storageBox_useAccessor.storage) - } - - package mutating func append(_ candidate: String, contentType: ContentType) { - mutate { storage in - storage.append(candidate, contentType: contentType) - } - } - - package mutating func append(_ candidate: Candidate) { - mutate { storage in - storage.append(candidate) - } - } - - package mutating func append(_ candidate: UTF8Bytes, contentType: ContentType) { - mutate { storage in - storage.append(candidate, contentType: contentType) - } - } - - package mutating func append(contentsOf candidates: [String], contentType: ContentType) { - mutate { storage in - storage.append(contentsOf: candidates, contentType: contentType) - } - } - - package mutating func append(contentsOf candidates: [UTF8Bytes], contentType: ContentType) { - mutate { storage in - storage.append(contentsOf: candidates, contentType: contentType) - } - } - - package func filter(keepWhere predicate: (Int, Candidate) -> Bool) -> CandidateBatch { - var copy = CandidateBatch() - enumerate { index, candidate in - if predicate(index, candidate) { - copy.append(candidate) - } - } - return copy - } -} - -extension Pattern { - package struct CandidateBatchesMatch: Equatable, CustomStringConvertible { - package var batchIndex: Int - package var candidateIndex: Int - package var textScore: Double - - package init(batchIndex: Int, candidateIndex: Int, textScore: Double) { - self.batchIndex = batchIndex - self.candidateIndex = candidateIndex - self.textScore = textScore - } - - package var description: String { - "CandidateBatchesMatch(batch: \(batchIndex), candidateIndex: \(candidateIndex), score: \(textScore))" - } - } - - /// Represents work to be done by each thread when parallelizing scoring. The work is divided ahead of time to maximize memory locality. - /// Represents work to be done by each thread when parallelizing scoring. - /// The work is divided ahead of time to maximize memory locality. - /// - /// If we have 3 threads, A, B, and C, and 9 things to match, we want to assign the work like AAABBBCCC, not ABCABCABC, to maximize memory locality. - /// So if we had 2 candidate batches of 8 candidates each, and 3 threads, the new code divides them like [AAAAABBB][BBCCCCCC] - /// We expect parallelism in the ballpark of 4-64 threads, and the number of candidates to be in the 1,000-100,000 range. - /// So the remainder of having 1 thread process a few extra candidates doesn't matter. - /// - /// - /// Equatable for testing. - package struct ScoringWorkload: Equatable { - package struct CandidateBatchSlice: Equatable { - var batchIndex: Int - var candidateRange: Range - - package init(batchIndex: Int, candidateRange: Range) { - self.batchIndex = batchIndex - self.candidateRange = candidateRange - } - } - /// When scoring and matching and storing the results in a shared buffer, this is the base output index for this - /// thread workload. - var outputStartIndex: Int - var slices: [CandidateBatchSlice] = [] - - package init(outputStartIndex: Int, slices: [CandidateBatchSlice] = []) { - self.outputStartIndex = outputStartIndex - self.slices = slices - } - - package static func workloads( - for batches: [CandidateBatch], - parallelism threads: Int - ) - -> [ScoringWorkload] - { // Internal for testing. - let crossBatchCandidateCount = totalCandidates(batches: batches) - let budgetPerScoringWorkload = crossBatchCandidateCount / threads - let budgetPerScoringWorkloadRemainder = crossBatchCandidateCount - (budgetPerScoringWorkload * threads) - - var batchIndex = 0 - var candidateIndexInBatch = 0 - var workloads: [ScoringWorkload] = [] - var globalOutputIndex = 0 - for workloadIndex in 0.. Int { - batches.reduce(into: 0) { sum, batch in - sum += batch.count - } - } - - /// Find all of the matches across `batches` and score them, returning the scored results. - /// - /// This is a first part of selecting matches. Later the matches will be combined with matches from other providers, - /// where we'll pick the best matches and sort them with `selectBestMatches(from:textProvider:)` - package func scoredMatches(across batches: [CandidateBatch], precision: Precision) -> [CandidateBatchesMatch] { - compactScratchArea(capacity: Self.totalCandidates(batches: batches)) { matchesScratchArea in - let scoringWorkloads = ScoringWorkload.workloads( - for: batches, - parallelism: ProcessInfo.processInfo.activeProcessorCount - ) - // `nonisolated(unsafe)` is fine because every iteration accesses a distinct index of the buffer. - nonisolated(unsafe) let matchesScratchArea = matchesScratchArea - scoringWorkloads.concurrentForEach { threadWorkload in - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - var outputIndex = threadWorkload.outputStartIndex - for slice in threadWorkload.slices { - batches[slice.batchIndex].enumerate(slice.candidateRange) { candidateIndex, candidate in - assert(matchesScratchArea[outputIndex] == nil) - if let score = self.matchAndScore( - candidate: candidate, - precision: precision, - allocator: &allocator - ) { - matchesScratchArea[outputIndex] = CandidateBatchesMatch( - batchIndex: slice.batchIndex, - candidateIndex: candidateIndex, - textScore: score.value - ) - } - outputIndex += 1 - } - } - } - } - } - } - - package struct CandidateBatchMatch: Equatable { - package var candidateIndex: Int - package var textScore: Double - - package init(candidateIndex: Int, textScore: Double) { - self.candidateIndex = candidateIndex - self.textScore = textScore - } - } - - package func scoredMatches(in batch: CandidateBatch, precision: Precision) -> [CandidateBatchMatch] { - scoredMatches(across: [batch], precision: precision).map { multiMatch in - CandidateBatchMatch(candidateIndex: multiMatch.candidateIndex, textScore: multiMatch.textScore) - } - } -} - -/// A single potential code completion result that can be scored against a ``Pattern``. -package struct Candidate { - package enum ContentType: Equatable { - /// A symbol found by code completion. - case codeCompletionSymbol - /// The name of a file in the project. - case fileName - /// A symbol defined in the project, which can be found by eg. the workspace symbols request. - case projectSymbol - case unknown - } - - package let bytes: Pattern.UTF8Bytes - package let contentType: ContentType - let rejectionFilter: RejectionFilter - - package static func withAccessToCandidate( - for text: String, - contentType: ContentType, - body: (Candidate) throws -> R - ) - rethrows -> R - { - var text = text - return try text.withUTF8 { bytes in - return try body(.init(bytes: bytes, contentType: contentType, rejectionFilter: .init(bytes: bytes))) - } - } - - /// For debugging - internal var text: String { - String(bytes: bytes, encoding: .utf8).unwrap(orFail: "UTF8 was prevalidated.") - } -} - -// Creates a buffer of `capacity` elements of type `T?`, each initially set to nil. -/// -/// After running `initialize`, returns all elements that were set to non-`nil` values. -private func compactScratchArea(capacity: Int, initialize: (UnsafeMutablePointer) -> Void) -> [T] { - let scratchArea = UnsafeMutablePointer.allocate(capacity: capacity) - scratchArea.initialize(repeating: nil, count: capacity) - defer { - scratchArea.deinitialize(count: capacity) // Should be a no-op - scratchArea.deallocate() - } - initialize(scratchArea) - return UnsafeMutableBufferPointer(start: scratchArea, count: capacity).compacted() -} - -extension Candidate: CustomStringConvertible { - package var description: String { - return String(bytes: bytes, encoding: .utf8) ?? "(Invalid UTF8 Sequence)" - } -} - -extension Candidate { - @available(*, deprecated, message: "Pass an explicit content type") - package static func withAccessToCandidate(for text: String, body: (Candidate) throws -> R) rethrows -> R { - try withAccessToCandidate(for: text, contentType: .codeCompletionSymbol, body: body) - } -} - -extension CandidateBatch: Equatable { - package static func == (_ lhs: Self, _ rhs: Self) -> Bool { - (lhs.count == rhs.count) - && lhs.indices.allSatisfy { index in - lhs.withAccessToCandidate(at: index) { lhs in - rhs.withAccessToCandidate(at: index) { rhs in - return equateBytes(lhs.bytes, rhs.bytes) - && lhs.contentType == rhs.contentType - && lhs.rejectionFilter == rhs.rejectionFilter - } - } - } - } -} - -extension CandidateBatch { - @available(*, deprecated, message: "Pass an explicit content type") - package init(candidates: [String] = []) { - self.init(candidates: candidates, contentType: .codeCompletionSymbol) - } - - @available(*, deprecated, message: "Pass an explicit content type") - package mutating func append(_ candidate: String) { - append(candidate, contentType: .codeCompletionSymbol) - } - @available(*, deprecated, message: "Pass an explicit content type") - package mutating func append(_ candidate: UTF8Bytes) { - append(candidate, contentType: .codeCompletionSymbol) - } - @available(*, deprecated, message: "Pass an explicit content type") - package mutating func append(contentsOf candidates: [String]) { - append(contentsOf: candidates, contentType: .codeCompletionSymbol) - } -} diff --git a/Sources/CompletionScoring/Text/InfluencingIdentifiers.swift b/Sources/CompletionScoring/Text/InfluencingIdentifiers.swift deleted file mode 100644 index 6e3098c26..000000000 --- a/Sources/CompletionScoring/Text/InfluencingIdentifiers.swift +++ /dev/null @@ -1,167 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -private typealias UTF8Bytes = Pattern.UTF8Bytes - -package struct InfluencingIdentifiers: Sendable { - // `nonisolated(unsafe)` is fine because the underlying buffer is not modified until `deallocate` is called and the - // struct must not be used anymore after `deallocate` was called. - private nonisolated(unsafe) let identifiers: UnsafeBufferPointer - - private init(identifiers: UnsafeBufferPointer) { - self.identifiers = identifiers - } - - private static func allocate(copyingTokenizedIdentifiers possiblyEmptyTokenizedIdentifiers: [[String]]) -> Self { - let tokenizedIdentifiers = possiblyEmptyTokenizedIdentifiers.filter { possiblyEmptyTokenizedIdentifier in - possiblyEmptyTokenizedIdentifier.count > 0 - } - let allocatedIdentifiers: [Identifier] = tokenizedIdentifiers.enumerated().map { - identifierIndex, - tokenizedIdentifier in - // First is 1, last is 0.9375, scale is linear. Only a small preference for the first word. Right now when - // we have two words, it's for cases like an argument label and internal name predicting the argument type - // or a variable name and its type predicting it's value. This scoring shows a slight affinity for the name. - let scoreScale = - (identifierIndex == 0) - ? 1 : 1 - (0.0625 * (Double(identifierIndex) / Double(tokenizedIdentifiers.count - 1))) - return Identifier.allocate(copyingTokenizedIdentifier: tokenizedIdentifier, scoreScale: scoreScale) - } - return InfluencingIdentifiers(identifiers: UnsafeBufferPointer.allocate(copyOf: allocatedIdentifiers)) - } - - private func deallocate() { - for identifier in identifiers { - identifier.deallocate() - } - identifiers.deallocate() - } - - /// Invoke `body` with an instance of `InfluencingIdentifiers` that refers to memory only valid during the scope of `body`. - /// This pattern is used so that this code has no referencing counting overhead. Using types like Array to represent the - /// tokens during scoring results in referencing counting costing ~30% of the work. To avoid that, we use unsafe - /// buffer pointers, and then this method to constrain lifetimes. - /// - Parameter identifiers: The influencing identifiers in most to least influencing order. - package static func withUnsafeInfluencingTokenizedIdentifiers( - _ tokenizedIdentifiers: [[String]], - body: (Self) throws -> R - ) rethrows -> R { - let allocatedIdentifiers = allocate(copyingTokenizedIdentifiers: tokenizedIdentifiers) - defer { allocatedIdentifiers.deallocate() } - return try body(allocatedIdentifiers) - } - - var hasContent: Bool { - identifiers.hasContent - } - - private func match(token: Token, candidate: Candidate, candidateTokenization: Pattern.Tokenization) -> Bool { - candidateTokenization.anySatisfy { candidateTokenRange in - if token.bytes.count == candidateTokenRange.count { - let candidateToken = UnsafeBufferPointer(rebasing: candidate.bytes[candidateTokenRange]) - let leadingByteMatches = token.bytes[0].lowercasedUTF8Byte == candidateToken[0].lowercasedUTF8Byte - return leadingByteMatches && equateBytes(token.bytes.afterFirst(), candidateToken.afterFirst()) - } - return false - } - } - - /// Returns a value between 0...1, where 0 indicates `Candidate` was not textually related to the identifiers, and 1.0 - /// indicates the candidate was strongly related to the identifiers. - /// - /// Currently, this is implemented by tokenizing the candidate and the identifiers, and then seeing if any of the tokens - /// match. If each identifier has one or more tokens in the candidate, return 1.0. If no tokens from the identifiers appear - /// in the candidate, return 0.0. - package func score(candidate: Candidate, allocator: inout UnsafeStackAllocator) -> Double { - var candidateTokenization: Pattern.Tokenization? = nil - defer { candidateTokenization?.deallocate(allocator: &allocator) } - var score = 0.0 - for identifier in identifiers { - // TODO: We could turn this loop inside out to walk the candidate tokens first, and skip the ones that are shorter - // than the shortest token, or keep bit for each length we have, and skip almost all of them. - let matchedTokenCount = identifier.tokens.countOf { token in - if RejectionFilter.match(pattern: token.rejectionFilter, candidate: candidate.rejectionFilter) == .maybe { - let candidateTokenization = candidateTokenization.lazyInitialize { - Pattern.Tokenization.allocate( - mixedcaseBytes: candidate.bytes, - contentType: candidate.contentType, - allocator: &allocator - ) - } - return match(token: token, candidate: candidate, candidateTokenization: candidateTokenization) - } - return false - } - score = max(score, identifier.score(matchedTokenCount: matchedTokenCount)) - } - return score - } -} - -fileprivate extension InfluencingIdentifiers { - struct Identifier { - let tokens: UnsafeBufferPointer - private let scoreScale: Double - - private init(tokens: UnsafeBufferPointer, scoreScale: Double) { - self.tokens = tokens - self.scoreScale = scoreScale - } - - func deallocate() { - for token in tokens { - token.deallocate() - } - tokens.deallocate() - } - - static func allocate(copyingTokenizedIdentifier tokenizedIdentifier: [String], scoreScale: Double) -> Self { - return Identifier( - tokens: UnsafeBufferPointer.allocate(copyOf: tokenizedIdentifier.map(Token.allocate)), - scoreScale: scoreScale - ) - } - - /// Returns a value between 0...1 - func score(matchedTokenCount: Int) -> Double { - if matchedTokenCount == 0 { - return 0 - } else if tokens.count == 1 { // We matched them all, make it obvious we won't divide by 0. - return 1 * scoreScale - } else { - let p = Double(matchedTokenCount - 1) / Double(tokens.count - 1) - return (0.75 + (p * 0.25)) * scoreScale - } - } - } -} - -fileprivate extension InfluencingIdentifiers { - struct Token { - let bytes: UTF8Bytes - let rejectionFilter: RejectionFilter - private init(bytes: UTF8Bytes) { - self.bytes = bytes - self.rejectionFilter = RejectionFilter(bytes: bytes) - } - - static func allocate(_ text: String) -> Self { - Token(bytes: UnsafeBufferPointer.allocate(copyOf: text.utf8)) - } - - func deallocate() { - bytes.deallocate() - } - } -} diff --git a/Sources/CompletionScoring/Text/MatchCollator.Match.swift b/Sources/CompletionScoring/Text/MatchCollator.Match.swift deleted file mode 100644 index c75b2a8ef..000000000 --- a/Sources/CompletionScoring/Text/MatchCollator.Match.swift +++ /dev/null @@ -1,48 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -extension MatchCollator { - package struct Match { - /// For client use, has no meaning to CompletionScoring, useful when mapping Match instances back to - /// higher level constructs. - package var identifier: Int - - package var batchIndex: Int - - package var candidateIndex: Int - - /// Items with the same (groupID, batchID) sort together. Initially used to locate types with their initializers. - package var groupID: Int? - - package var score: CompletionScore - - package init(batchIndex: Int, candidateIndex: Int, groupID: Int?, score: CompletionScore) { - self.init( - identifier: 0, - batchIndex: batchIndex, - candidateIndex: candidateIndex, - groupID: groupID, - score: score - ) - } - - package init(identifier: Int, batchIndex: Int, candidateIndex: Int, groupID: Int?, score: CompletionScore) { - self.identifier = identifier - self.batchIndex = batchIndex - self.candidateIndex = candidateIndex - self.groupID = groupID - self.score = score - } - } -} diff --git a/Sources/CompletionScoring/Text/MatchCollator.Selection.swift b/Sources/CompletionScoring/Text/MatchCollator.Selection.swift deleted file mode 100644 index 5b0c64370..000000000 --- a/Sources/CompletionScoring/Text/MatchCollator.Selection.swift +++ /dev/null @@ -1,22 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -extension MatchCollator { - /// The result of best match selection. - package struct Selection { - /// The precision used during matching, which varies based on the number of candidates and the input pattern length. - package var precision: Pattern.Precision - package var matches: [Match] - } -} diff --git a/Sources/CompletionScoring/Text/MatchCollator.swift b/Sources/CompletionScoring/Text/MatchCollator.swift deleted file mode 100644 index 07e6df1a3..000000000 --- a/Sources/CompletionScoring/Text/MatchCollator.swift +++ /dev/null @@ -1,547 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// Aggregates functionality to support the `MatchCollator.selectBestMatches(for:from:in:)` function, which sorts and -/// selects the best matches from a list, applying the `.thorough` scoring function while being conscience of it's expense. -package struct MatchCollator { - private var originalMatches: UnsafeBufferPointer - private var rescoredMatches: UnsafeMutableBufferPointer - private let batches: UnsafeBufferPointer - private let groupScores: UnsafeMutableBufferPointer - private let influencers: InfluencingIdentifiers - private let patternUTF8Length: Int - private let tieBreaker: UnsafePointer - private let maximumNumberOfItemsForExpensiveSelection: Int - package static let defaultMaximumNumberOfItemsForExpensiveSelection = 100 - - private init( - originalMatches: UnsafeBufferPointer, - rescoredMatches: UnsafeMutableBufferPointer, - batches: UnsafeBufferPointer, - groupScores: UnsafeMutableBufferPointer, - influencers: InfluencingIdentifiers, - patternUTF8Length: Int, - orderingTiesBy tieBreaker: UnsafePointer, - maximumNumberOfItemsForExpensiveSelection: Int - ) { - for match in originalMatches { - precondition(batches.indices.contains(match.batchIndex)) - precondition(batches[match.batchIndex].indices.contains(match.candidateIndex)) - } - self.originalMatches = originalMatches - self.rescoredMatches = rescoredMatches - self.batches = batches - self.groupScores = groupScores - self.influencers = influencers - self.patternUTF8Length = patternUTF8Length - self.tieBreaker = tieBreaker - self.maximumNumberOfItemsForExpensiveSelection = maximumNumberOfItemsForExpensiveSelection - } - - private static func withUnsafeMatchCollator( - matches originalMatches: [Match], - batches: [CandidateBatch], - influencingTokenizedIdentifiers: [[String]], - patternUTF8Length: Int, - orderingTiesBy tieBreakerBody: (_ lhs: Match, _ rhs: Match) -> Bool, - maximumNumberOfItemsForExpensiveSelection: Int, - body: (inout MatchCollator) -> R - ) -> R { - let rescoredMatches = UnsafeMutableBufferPointer.allocate(capacity: originalMatches.count) - defer { rescoredMatches.deinitializeAllAndDeallocate() } - let groupScores = UnsafeMutableBufferPointer.allocate(capacity: originalMatches.count) - defer { groupScores.deinitializeAllAndDeallocate() } - for (matchIndex, originalMatch) in originalMatches.enumerated() { - rescoredMatches.initialize( - index: matchIndex, - to: RescoredMatch( - originalMatchIndex: matchIndex, - textIndex: TextIndex(batch: originalMatch.batchIndex, candidate: originalMatch.candidateIndex), - denseGroupID: nil, - individualScore: originalMatch.score, - groupScore: -Double.infinity, - falseStarts: 0 - ) - ) - } - assignDenseGroupId(to: rescoredMatches, from: originalMatches, batchCount: batches.count) - return withoutActuallyEscaping(tieBreakerBody) { tieBreakerBody in - var tieBreaker = TieBreaker(tieBreakerBody) - return withExtendedLifetime(tieBreaker) { - InfluencingIdentifiers.withUnsafeInfluencingTokenizedIdentifiers(influencingTokenizedIdentifiers) { - influencers in - originalMatches.withUnsafeBufferPointer { originalMatches in - CandidateBatch.withUnsafeStorages(batches) { batchStorages in - var collator = Self( - originalMatches: originalMatches, - rescoredMatches: rescoredMatches, - batches: batchStorages, - groupScores: groupScores, - influencers: influencers, - patternUTF8Length: patternUTF8Length, - orderingTiesBy: &tieBreaker, - maximumNumberOfItemsForExpensiveSelection: maximumNumberOfItemsForExpensiveSelection - ) - return body(&collator) - } - } - } - } - } - } - - /// This allows us to only take the dictionary hit one time, so that we don't have to do repeated dictionary lookups - /// as we lookup groupIDs and map them to group scores. - private static func assignDenseGroupId( - to rescoredMatches: UnsafeMutableBufferPointer, - from originalMatches: [Match], - batchCount: Int - ) { - typealias SparseGroupID = Int - typealias DenseGroupID = Int - let initialDictionaryCapacity = (batchCount > 0) ? originalMatches.count / batchCount : 0 - var batchAssignments: [[SparseGroupID: DenseGroupID]] = Array( - repeating: Dictionary(capacity: initialDictionaryCapacity), - count: batchCount - ) - var nextDenseID = 0 - for (matchIndex, match) in originalMatches.enumerated() { - if let sparseID = match.groupID { - rescoredMatches[matchIndex].denseGroupID = batchAssignments[match.batchIndex][sparseID].lazyInitialize { - let denseID = nextDenseID - nextDenseID += 1 - return denseID - } - } - } - } - - private mutating func selectBestFastScoredMatchesForThoroughScoring() { - if rescoredMatches.count > maximumNumberOfItemsForExpensiveSelection { - rescoredMatches.selectTopKAndTruncate(maximumNumberOfItemsForExpensiveSelection) { lhs, rhs in - (lhs.groupScore >? rhs.groupScore) ?? (lhs.individualScore > rhs.individualScore) - } - } - } - - private func refreshGroupScores() { - // We call this the first time without initializing the Double values. - groupScores.setAll(to: -.infinity) - for match in rescoredMatches { - if let denseGroupID = match.denseGroupID { - groupScores[denseGroupID] = max(groupScores[denseGroupID], match.individualScore.value) - } - } - for (index, match) in rescoredMatches.enumerated() { - if let denseGroupID = match.denseGroupID { - rescoredMatches[index].groupScore = groupScores[denseGroupID] - } else { - rescoredMatches[index].groupScore = rescoredMatches[index].individualScore.value - } - } - } - - private func unsafeBytes(at textIndex: TextIndex) -> CandidateBatch.UTF8Bytes { - return batches[textIndex.batch].bytes(at: textIndex.candidate) - } - - mutating func thoroughlyRescore(pattern: Pattern) { - // `nonisolated(unsafe)` is fine because every iteration accesses a different index of `batches`. - nonisolated(unsafe) let batches = batches - // `nonisolated(unsafe)` is fine because every iteration accesses a disjunct set of indices of `rescoredMatches`. - nonisolated(unsafe) let rescoredMatches = rescoredMatches - let pattern = pattern - rescoredMatches.slicedConcurrentForEachSliceRange { sliceRange in - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - for matchIndex in sliceRange { - let textIndex = rescoredMatches[matchIndex].textIndex - let (candidateBytes, candidateContentType) = batches[textIndex.batch] - .candidateContent(at: textIndex.candidate) - let textScore = pattern.score( - candidate: candidateBytes, - contentType: candidateContentType, - precision: .thorough, - allocator: &allocator - ) - rescoredMatches[matchIndex].individualScore.textComponent = textScore.value - rescoredMatches[matchIndex].falseStarts = textScore.falseStarts - } - } - } - } - - /// Generated and validated by `MatchCollatorTests.testMinimumTextCutoff()` - package static let bestRejectedTextScoreByPatternLength: [Double] = [ - 0.0, - 0.0, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - 2.900400881379344, - ] - - private var cutoffRatio: Double { - // | - // 0.67 | ____________________ - // | / - // |/ - // +------------------------ - // 4 - let fullCutoffRatio = (2.0 / 3.0) - let weight = min(max(Double(patternUTF8Length), 1.0) / 4.0, 1.0) - return fullCutoffRatio * weight - } - - private static let maxInfluenceBonus = 0.10 - - private static let maxFalseStarts = 2 - - private var bestRejectedTextScore: Double { - let bestRejectedTextScoreByPatternLength = Self.bestRejectedTextScoreByPatternLength - let inBounds = bestRejectedTextScoreByPatternLength.indices.contains(patternUTF8Length) - return - (inBounds - ? bestRejectedTextScoreByPatternLength[patternUTF8Length] : bestRejectedTextScoreByPatternLength.last) - ?? 0 - } - - private mutating func selectBestThoroughlyScoredMatches() { - if let bestThoroughlyScoredMatch = rescoredMatches.max(by: \.individualScore) { - let topMatchFalseStarts = bestThoroughlyScoredMatch.falseStarts - let compositeCutoff = self.cutoffRatio * bestThoroughlyScoredMatch.individualScore.value - let semanticCutoffForTokenFalseStartsExemption = - bestThoroughlyScoredMatch.individualScore.semanticComponent / 3.0 - let bestRejectedTextScore = bestRejectedTextScore - let maxAllowedFalseStarts = Self.maxFalseStarts - rescoredMatches.removeAndTruncateWhere { candidate in - let overcomesTextCutoff = candidate.individualScore.textComponent > bestRejectedTextScore - let overcomesFalseStartCutoff = candidate.falseStarts <= maxAllowedFalseStarts - let acceptedByCompositeScore = candidate.individualScore.value >= compositeCutoff - let acceptedByTokenFalseStarts = - (candidate.falseStarts <= topMatchFalseStarts) - && (candidate.individualScore.semanticComponent >= semanticCutoffForTokenFalseStartsExemption) - let keep = - overcomesTextCutoff && overcomesFalseStartCutoff - && (acceptedByCompositeScore || acceptedByTokenFalseStarts) - return !keep - } - } - } - - private mutating func selectBestFastScoredMatches() { - if let bestSemanticScore = rescoredMatches.max(of: { candidate in candidate.individualScore.semanticComponent }) { - let minimumSemanticScore = bestSemanticScore * cutoffRatio - rescoredMatches.removeAndTruncateWhere { candidate in - candidate.individualScore.semanticComponent < minimumSemanticScore - } - } - } - - private mutating func applyInfluence() { - let influencers = self.influencers - let maxInfluenceBonus = Self.maxInfluenceBonus - if influencers.hasContent && (maxInfluenceBonus != 0.0) { - // `nonisolated(unsafe)` is fine because every iteration accesses a disjoint set of indices in `rescoredMatches`. - nonisolated(unsafe) let rescoredMatches = rescoredMatches - // `nonisolated(unsafe)` is fine because `batches` is not modified - nonisolated(unsafe) let batches = batches - rescoredMatches.slicedConcurrentForEachSliceRange { sliceRange in - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - for matchIndex in sliceRange { - let textIndex = rescoredMatches[matchIndex].textIndex - let candidate = batches[textIndex.batch].candidate(at: textIndex.candidate) - let percentOfInfluenceBonus = influencers.score(candidate: candidate, allocator: &allocator) - let textCoefficient = (percentOfInfluenceBonus * maxInfluenceBonus) + 1.0 - rescoredMatches[matchIndex].individualScore.textComponent *= textCoefficient - } - } - } - refreshGroupScores() - } - } - - private func lessThan(_ lhs: RescoredMatch, _ rhs: RescoredMatch) -> Bool { - if let definitiveGroupScoreComparison = lhs.groupScore >? rhs.groupScore { - return definitiveGroupScoreComparison - // Only compare `individualScore` within the same group, or among items that have no group. - // Otherwise when the group score ties, we would interleave the members of the tying groups. - } else if lhs.denseGroupID == rhs.denseGroupID, - let definitiveIndividualScoreComparison = lhs.individualScore.value >? rhs.individualScore.value - { - return definitiveIndividualScoreComparison - } else { - let lhsBytes = unsafeBytes(at: lhs.textIndex) - let rhsBytes = unsafeBytes(at: rhs.textIndex) - switch compareBytes(lhsBytes, rhsBytes) { - case .ascending: - return true - case .descending: - return false - case .same: - if rescoredMatches.count <= maximumNumberOfItemsForExpensiveSelection { - let lhsOriginal = originalMatches[lhs.originalMatchIndex] - let rhsOriginal = originalMatches[rhs.originalMatchIndex] - if tieBreaker.pointee.lessThan(lhsOriginal, rhsOriginal) { - return true - } else if tieBreaker.pointee.lessThan(rhsOriginal, lhsOriginal) { - return false - } - } - return (lhs.originalMatchIndex < rhs.originalMatchIndex) - } - } - } - - private mutating func sort() { - rescoredMatches.sort(by: lessThan) - } - - private mutating func selectBestMatches(pattern: Pattern) -> Selection { - refreshGroupScores() - let precision: Pattern.Precision - if pattern.typedEnoughForThoroughScoring - || (rescoredMatches.count <= maximumNumberOfItemsForExpensiveSelection) - { - selectBestFastScoredMatchesForThoroughScoring() - thoroughlyRescore(pattern: pattern) - refreshGroupScores() - selectBestThoroughlyScoredMatches() - precision = .thorough - } else { - selectBestFastScoredMatches() - precision = .fast - } - applyInfluence() - sort() - return Selection( - precision: precision, - matches: rescoredMatches.map { match in - originalMatches[match.originalMatchIndex] - } - ) - } - - /// Uses heuristics to cull matches, and then apply the expensive `.thorough` scoring function. - /// - /// Returns the results stably ordered by score, then text. - package static func selectBestMatches( - _ matches: [Match], - from batches: [CandidateBatch], - for pattern: Pattern, - influencingTokenizedIdentifiers: [[String]], - orderingTiesBy tieBreaker: (_ lhs: Match, _ rhs: Match) -> Bool, - maximumNumberOfItemsForExpensiveSelection: Int - ) -> Selection { - withUnsafeMatchCollator( - matches: matches, - batches: batches, - influencingTokenizedIdentifiers: influencingTokenizedIdentifiers, - patternUTF8Length: pattern.patternUTF8Length, - orderingTiesBy: tieBreaker, - maximumNumberOfItemsForExpensiveSelection: maximumNumberOfItemsForExpensiveSelection - ) { collator in - collator.selectBestMatches(pattern: pattern) - } - } - - /// Short for `selectBestMatches(_:from:for:influencingTokenizedIdentifiers:orderingTiesBy:).matches` - package static func selectBestMatches( - for pattern: Pattern, - from matches: [Match], - in batches: [CandidateBatch], - influencingTokenizedIdentifiers: [[String]], - orderingTiesBy tieBreaker: (_ lhs: Match, _ rhs: Match) -> Bool, - maximumNumberOfItemsForExpensiveSelection: Int = Self.defaultMaximumNumberOfItemsForExpensiveSelection - ) -> [Match] { - return selectBestMatches( - matches, - from: batches, - for: pattern, - influencingTokenizedIdentifiers: influencingTokenizedIdentifiers, - orderingTiesBy: tieBreaker, - maximumNumberOfItemsForExpensiveSelection: maximumNumberOfItemsForExpensiveSelection - ).matches - } - - /// Split identifiers into constituent subwords. For example "documentDownload" becomes ["document", "Download"] - /// - Parameters: - /// - identifiers: Strings from the program source, like "documentDownload" - /// - filterLowSignalTokens: When true, removes common tokens that would falsely signal influence, like "from". - /// - Returns: A value suitable for use with the `influencingTokenizedIdentifiers:` parameter of `selectBestMatches(…)`. - package static func tokenize( - influencingTokenizedIdentifiers identifiers: [String], - filterLowSignalTokens: Bool - ) -> [[String]] { - identifiers.map { identifier in - tokenize(influencingTokenizedIdentifier: identifier, filterLowSignalTokens: filterLowSignalTokens) - } - } - - /// Only package so that we can performance test this - package static func performanceTest_influenceScores( - for batches: [CandidateBatch], - influencingTokenizedIdentifiers: [[String]], - iterations: Int - ) -> Double { - let matches = batches.enumerated().flatMap { batchIndex, batch in - (0.. [String] - { - var tokens: [String] = [] - identifier.withUncachedUTF8Bytes { identifierBytes in - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - var tokenization = Pattern.Tokenization.allocate( - mixedcaseBytes: identifierBytes, - contentType: .codeCompletionSymbol, - allocator: &allocator - ); defer { tokenization.deallocate(allocator: &allocator) } - tokenization.enumerate { tokenRange in - if let token = String(bytes: identifierBytes[tokenRange], encoding: .utf8) { - tokens.append(token) - } - } - } - } - if filterLowSignalTokens { - let minimumLength = 4 // Shorter tokens appear too much to be useful: (in, on, a, the…) - let ignoredTokens: Set = ["from", "with"] - tokens.removeAll { token in - return (token.count < minimumLength) || ignoredTokens.contains(token.lowercased()) - } - } - return tokens - } -} - -extension MatchCollator { - fileprivate struct TextIndex { - var batch: Int - var candidate: Int - } -} - -extension MatchCollator { - fileprivate struct RescoredMatch { - var originalMatchIndex: Int - var textIndex: TextIndex - var denseGroupID: Int? - var individualScore: CompletionScore - var groupScore: Double - var falseStarts: Int - } -} - -extension MatchCollator { - /// A wrapper to allow taking an unsafe pointer to a closure. - fileprivate final class TieBreaker { - var lessThan: (_ lhs: Match, _ rhs: Match) -> Bool - - init(_ lessThan: @escaping (_ lhs: Match, _ rhs: Match) -> Bool) { - self.lessThan = lessThan - } - } -} - -extension MatchCollator.RescoredMatch: CustomStringConvertible { - var description: String { - func format(_ value: Double) -> String { - String(format: "%0.3f", value) - } - return - """ - RescoredMatch(\ - idx: \(originalMatchIndex), \ - gid: \(denseGroupID?.description ?? "_"), \ - score.t: \(format(individualScore.textComponent)), \ - score.s: \(format(individualScore.semanticComponent)), \ - groupScore: \(format(groupScore)), \ - falseStarts: \(falseStarts)\ - ) - """ - } -} - -fileprivate extension Pattern { - var typedEnoughForThoroughScoring: Bool { - patternUTF8Length >= MatchCollator.minimumPatternLengthToAlwaysRescoreWithThoroughPrecision - } -} - -// Deprecated Entry Points -extension MatchCollator { - @available(*, deprecated, renamed: "selectBestMatches(for:from:in:influencingTokenizedIdentifiers:orderingTiesBy:)") - package static func selectBestMatches( - for pattern: Pattern, - from matches: [Match], - in batches: [CandidateBatch], - influencingTokenizedIdentifiers: [[String]] - ) -> [Match] { - selectBestMatches( - for: pattern, - from: matches, - in: batches, - influencingTokenizedIdentifiers: influencingTokenizedIdentifiers, - orderingTiesBy: { _, _ in false } - ) - } - - @available( - *, - deprecated, - message: - "Use the MatchCollator.Selection.precision value returned from selectBestMatches(...) to choose between fast and thorough matched text ranges." - ) - package static var bestMatchesThoroughScanningMinimumPatternLength: Int { - minimumPatternLengthToAlwaysRescoreWithThoroughPrecision - } -} - -extension MatchCollator { - package static let minimumPatternLengthToAlwaysRescoreWithThoroughPrecision = 2 -} diff --git a/Sources/CompletionScoring/Text/Pattern.swift b/Sources/CompletionScoring/Text/Pattern.swift deleted file mode 100644 index 65016889e..000000000 --- a/Sources/CompletionScoring/Text/Pattern.swift +++ /dev/null @@ -1,1277 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// The pattern that the user has typed and which should be used to cull and score results. -package final class Pattern: Sendable { - package typealias ContentType = Candidate.ContentType - package typealias UTF8Bytes = UnsafeBufferPointer - package typealias UTF8ByteRange = Range - package enum Precision { - case fast - case thorough - } - - package let text: String - - /// These all have pattern in the name to avoid confusion with their candidate counterparts in functions operating on both. - // `nonisolated(unsafe)` is fine because the underlying buffer is never modified. - private nonisolated(unsafe) let patternMixedcaseBytes: UTF8Bytes - private nonisolated(unsafe) let patternLowercaseBytes: UTF8Bytes - private let patternRejectionFilter: RejectionFilter - /// For each byte in `text`, a rejection filter that contains all the characters occurring after or at that offset. - /// - /// This way when we have already matched the first 4 bytes, we can check which characters occur from byte 5 onwards - /// and check that they appear in the candidate's remaining text. - private let patternSuccessiveRejectionFilters: [RejectionFilter] - private let patternHasMixedcase: Bool - internal var patternUTF8Length: Int { patternMixedcaseBytes.count } - - package init(text: String) { - self.text = text - let mixedcaseBytes = Array(text.utf8) - let lowercaseBytes = mixedcaseBytes.map(\.lowercasedUTF8Byte) - self.patternMixedcaseBytes = UnsafeBufferPointer.allocate(copyOf: mixedcaseBytes) - self.patternLowercaseBytes = UnsafeBufferPointer.allocate(copyOf: lowercaseBytes) - self.patternRejectionFilter = .init(lowercaseBytes: lowercaseBytes) - self.patternHasMixedcase = lowercaseBytes != mixedcaseBytes - self.patternSuccessiveRejectionFilters = UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - return lowercaseBytes.withUnsafeBufferPointer { lowercaseBytes in - var rejectionFilters = Self.allocateSuccessiveRejectionFilters( - lowercaseBytes: lowercaseBytes, - allocator: &allocator - ) - defer { allocator.deallocate(&rejectionFilters) } - return Array(rejectionFilters) - } - } - } - - deinit { - patternMixedcaseBytes.deallocate() - patternLowercaseBytes.deallocate() - } - - /// Perform an insensitive greedy match and return the location where the greedy match stared if it succeeded. - /// If the match was not successful, return `nil`. - /// - /// Future searches used during scoring can use this location to jump past all of the initial bytes that don't match - /// anything. An empty pattern matches with location 0. - private func matchLocation(candidate: Candidate) -> Int? { - if RejectionFilter.match(pattern: patternRejectionFilter, candidate: candidate.rejectionFilter) == .maybe { - let cBytes = candidate.bytes - let pCount = patternLowercaseBytes.count - let cCount = cBytes.count - var pRemaining = pCount - var cRemaining = cCount - if (pRemaining > 0) && (cRemaining > 0) && (pRemaining <= cRemaining) { - let pFirst = patternLowercaseBytes[0] - let cStart = cBytes.firstIndex { cByte in - cByte.lowercasedUTF8Byte == pFirst - } - if let cStart = cStart { - cRemaining -= cStart - cRemaining -= 1 - pRemaining -= 1 - // While we're not at the end and the remaining pattern is as short or shorter than the remaining candidate. - while (pRemaining > 0) && (cRemaining > 0) && (pRemaining <= cRemaining) { - let cIdx = cCount - cRemaining - let pIdx = pCount - pRemaining - if cBytes[cIdx].lowercasedUTF8Byte == patternLowercaseBytes[pIdx] { - pRemaining -= 1 - } - cRemaining -= 1 - } - return (pRemaining == 0) ? cStart : nil - } - } - return pCount == 0 ? 0 : nil - } - return nil - } - - package func matches(candidate: Candidate) -> Bool { - matchLocation(candidate: candidate) != nil - } - - package func score(candidate: Candidate, precision: Precision) -> Double { - score(candidate: candidate.bytes, contentType: candidate.contentType, precision: precision) - } - - package func score( - candidate candidateMixedcaseBytes: UTF8Bytes, - contentType: ContentType, - precision: Precision - ) -> Double { - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - score( - candidate: candidateMixedcaseBytes, - contentType: contentType, - precision: precision, - allocator: &allocator - ) - .value - } - } - - package struct TextScore: Comparable { - package var value: Double - package var falseStarts: Int - - static let worstPossibleScore = TextScore(value: -Double.infinity, falseStarts: Int.max) - - package init(value: Double, falseStarts: Int) { - self.value = value - self.falseStarts = falseStarts - } - - package static func < (_ lhs: Self, _ rhs: Self) -> Bool { - return (lhs.value rhs.falseStarts) - } - - fileprivate static let emptyMatchScore = TextScore(value: 1.0, falseStarts: 0) - fileprivate static let noMatchScore = TextScore(value: 0.0, falseStarts: 0) - } - - internal func matchAndScore( - candidate: Candidate, - precision: Precision, - allocator: inout UnsafeStackAllocator - ) -> TextScore? { - return matchLocation(candidate: candidate).map { firstMatchingLowercaseByteIndex in - if patternLowercaseBytes.hasContent { - var indexedCandidate = IndexedCandidate.allocate( - referencing: candidate.bytes, - patternByteCount: patternLowercaseBytes.count, - firstMatchingLowercaseByteIndex: firstMatchingLowercaseByteIndex, - contentType: candidate.contentType, - allocator: &allocator - ) - defer { indexedCandidate.deallocate(allocator: &allocator) } - return score( - candidate: &indexedCandidate, - precision: precision, - captureMatchingRanges: false, - allocator: &allocator - ) - } else { - return .emptyMatchScore - } - } - } - - package func score( - candidate candidateMixedcaseBytes: UTF8Bytes, - contentType: ContentType, - precision: Precision, - allocator: inout UnsafeStackAllocator - ) -> TextScore { - if patternLowercaseBytes.hasContent { - var candidate = IndexedCandidate.allocate( - referencing: candidateMixedcaseBytes, - patternByteCount: patternLowercaseBytes.count, - firstMatchingLowercaseByteIndex: nil, - contentType: contentType, - allocator: &allocator - ) - defer { candidate.deallocate(allocator: &allocator) } - return score( - candidate: &candidate, - precision: precision, - captureMatchingRanges: false, - allocator: &allocator - ) - } else { - return .emptyMatchScore - } - } - - package func score( - candidate candidateMixedcaseBytes: UTF8Bytes, - contentType: ContentType, - precision: Precision, - captureMatchingRanges: Bool, - ranges: inout [UTF8ByteRange] - ) -> Double { - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - if patternLowercaseBytes.hasContent { - var candidate = IndexedCandidate.allocate( - referencing: candidateMixedcaseBytes, - patternByteCount: patternLowercaseBytes.count, - firstMatchingLowercaseByteIndex: nil, - contentType: contentType, - allocator: &allocator - ) - defer { candidate.deallocate(allocator: &allocator) } - let score = score( - candidate: &candidate, - precision: precision, - captureMatchingRanges: captureMatchingRanges, - allocator: &allocator - ) - if captureMatchingRanges { - ranges = Array(candidate.matchedRanges) - } - return score.value - } else { - return TextScore.emptyMatchScore.value - } - } - } - - private func score( - candidate: inout IndexedCandidate, - precision: Precision, - captureMatchingRanges: Bool, - allocator: inout UnsafeStackAllocator - ) -> TextScore { - switch precision { - case .fast: - let matchStyle = populateMatchingRangesForFastScoring(&candidate) - return singleScore(candidate: candidate, precision: precision, matchStyle: matchStyle) - case .thorough: - let budget = 5000 - return bestScore( - candidate: &candidate, - budget: budget, - captureMatchingRanges: captureMatchingRanges, - allocator: &allocator - ) - } - } - - private func eligibleForAcronymMatch(candidate: IndexedCandidate) -> Bool { - return (patternLowercaseBytes.count >= 3) - && candidate.contentType.isEligibleForAcronymMatch - && candidate.tokenization.hasNonUppercaseNonDelimiterBytes - } - - private enum MatchStyle: CaseIterable { - case lowercaseContinuous - case mixedcaseContinuous - case mixedcaseGreedy - case lowercaseGreedy - case acronym - } - - /// Looks for matches like `tamic` against `[t]ranslates[A]utoresizing[M]ask[I]nto[C]onstraints.` - /// Or `MOC` against `NS[M]anaged[O]bject[C]ontext`. - /// Accomplishes this by doing a greedy match over the acronym characters, which are the first characters of each token. - private func populateMatchingRangesForAcronymMatch(_ candidate: inout IndexedCandidate) -> Bool { - let tokenization = candidate.tokenization - if eligibleForAcronymMatch(candidate: candidate) && tokenization.tokens.hasContent { - let candidateLowercaseBytes = candidate.lowercaseBytes - let allowFullMatchesBeginningWithTokenIndex = - candidate.contentType.acronymMatchAllowsMultiCharacterMatchesAfterBaseName - ? candidate.tokenization.firstNonBaseNameTokenIndex : candidate.tokenization.tokenCount - var tidx = 0 - let tcnt = tokenization.tokens.count - var cidx = 0 - let ccnt = candidateLowercaseBytes.count - var pidx = 0 - let pcnt = patternLowercaseBytes.count - - var matches = true - while (cidx < ccnt) && (pidx < pcnt) && (tidx < tcnt) && matches { - let token = tokenization.tokens[tidx] - var tcidx = 0 - let tccnt = (token.allUppercase || (tidx >= allowFullMatchesBeginningWithTokenIndex)) ? token.length : 1 - let initialCidx = cidx - while (tcidx < tccnt) && (cidx < ccnt) && (pidx < pcnt) { - if candidateLowercaseBytes[cidx] == patternLowercaseBytes[pidx] { - tcidx += 1 - cidx += 1 - pidx += 1 - } else { - break - } - } - matches = - (tcidx > 0) // Matched any characters in this token - // Allow skipping the first token if it's all uppercase - || ((tidx == 0) && token.allUppercase) - // Allow skipping single character delemiters - || ((token.length == 1) && candidateLowercaseBytes[cidx].isDelimiter) - - if tcidx > 0 { - candidate.matchedRanges.append(initialCidx.., - startOffset: Int, - candidateBytes: UTF8Bytes, - patternBytes: UTF8Bytes - ) -> Bool { - if let contiguousMatch = candidateBytes.rangeOf(bytes: patternBytes, startOffset: startOffset) { - ranges.append(contiguousMatch) - return true - } - return false - } - - /// Returns `true` if the pattern matches the candidate according to the rules of the `matchStyle`. - /// When successful, the matched ranges will be added to `ranges`. Otherwise `ranges` will be cleared. - private func populateMatchingRanges(_ candidate: inout IndexedCandidate, matchStyle: MatchStyle) -> Bool { - let startOffset = candidate.firstMatchingLowercaseByteIndex ?? 0 - switch matchStyle { - case .lowercaseContinuous: - return Self.populateMatchingContinuousRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.lowercaseBytes, - patternBytes: patternLowercaseBytes - ) - case .mixedcaseContinuous: - return Self.populateMatchingContinuousRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.mixedcaseBytes, - patternBytes: patternMixedcaseBytes - ) - case .acronym: - return populateMatchingRangesForAcronymMatch(&candidate) - case .mixedcaseGreedy: - return Self.populateGreedyMatchingRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.mixedcaseBytes, - patternBytes: patternMixedcaseBytes - ) - case .lowercaseGreedy: - return Self.populateGreedyMatchingRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.lowercaseBytes, - patternBytes: patternLowercaseBytes - ) - } - } - - /// Tries a fixed set of strategies in most to least desirable order for matching the candidate to the pattern. - /// - /// Returns the first strategy that matches. - /// Generally this match will produce the highest score of the considered strategies, but this isn't known, and a - /// higher scoring match could be found by the `.thorough` search. - /// For example, the highest priority strategy is a case matched contiguous sequence. If you search for "name" in - /// "filenames(name:)", this code will select the first one, but the second occurrence will score higher since it's a - /// complete token. - /// The fast scoring just needs to get the result into the second round though where `.thorough` will find the better - /// match. - private func populateMatchingRangesForFastScoring(_ candidate: inout IndexedCandidate) -> MatchStyle? { - let startOffset = candidate.firstMatchingLowercaseByteIndex ?? 0 - if Self.populateMatchingContinuousRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.lowercaseBytes, - patternBytes: patternLowercaseBytes - ) { - return .lowercaseContinuous - } else if populateMatchingRangesForAcronymMatch(&candidate) { - return .acronym - } else if Self.populateGreedyMatchingRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.mixedcaseBytes, - patternBytes: patternMixedcaseBytes - ) { - return .mixedcaseGreedy - } else if Self.populateGreedyMatchingRanges( - &candidate.matchedRanges, - startOffset: startOffset, - candidateBytes: candidate.lowercaseBytes, - patternBytes: patternLowercaseBytes - ) { - return .lowercaseGreedy - } - return nil - } - - /// Tries to match `patternBytes` against `candidateBytes` by greedily matching the first occurrence of a character - /// it finds, ignoring tokenization. For example, "filename" matches both "file(named:)" and "decoyfileadecoynamedecoy". - /// - /// If a successful match was found, returns `true` and populates `ranges` with the matched ranges. - /// Otherwise `ranges` is cleared since it's used as scratch space. - private static func populateGreedyMatchingRanges( - _ ranges: inout UnsafeStackArray, - startOffset: Int, - candidateBytes: UTF8Bytes, - patternBytes: UTF8Bytes - ) -> Bool { - var cidx = startOffset - var pidx = 0 - var currentlyMatching = false - while (cidx < candidateBytes.count) && (pidx < patternBytes.count) { - if candidateBytes[cidx] == patternBytes[pidx] { - pidx += 1 - if !currentlyMatching { - currentlyMatching = true - ranges.append(cidx ..+ 0) - } - ranges[ranges.count - 1].extend(upperBoundBy: 1) - } else { - currentlyMatching = false - } - cidx += 1 - } - let matched = pidx == patternBytes.count - if !matched { - ranges.removeAll() - } - return matched - } - - private func singleScore(candidate: IndexedCandidate, precision: Precision, matchStyle: MatchStyle?) -> TextScore { - func ratio(_ lhs: Int, _ rhs: Int) -> Double { - return Double(lhs) / Double(rhs) - } - - var patternCharactersRemaining = patternMixedcaseBytes.count - let matchedRanges = candidate.matchedRanges - if let firstMatchedRange = matchedRanges.first { - let candidateMixedcaseBytes = candidate.mixedcaseBytes - let tokenization = candidate.tokenization - let prefixMatchBonusValue = candidate.contentType.prefixMatchBonus - let contentAfterBasenameIsTrivial = candidate.contentType.contentAfterBasenameIsTrivial - let leadingCaseMatchableCount = - contentAfterBasenameIsTrivial ? tokenization.baseNameLength : candidateMixedcaseBytes.count - var score = 0.0 - var falseStarts = 0 - var uppercaseMatches = 0 - var uppercaseMismatches = 0 - var anyCaseMatches = 0 - var isPrefixUppercaseMatch = false - do { - var pidx = 0 - for matchedRange in matchedRanges { - for cidx in matchedRange { - let candidateCharacter = candidateMixedcaseBytes[cidx] - if cidx < leadingCaseMatchableCount { - if candidateCharacter == patternMixedcaseBytes[pidx] { - /// Check for case match. - uppercaseMatches += candidateCharacter.isUppercase ? 1 : 0 - isPrefixUppercaseMatch = - isPrefixUppercaseMatch || (candidateCharacter.isUppercase && (cidx == 0)) - anyCaseMatches += 1 - } else { - uppercaseMismatches += 1 - } - } - pidx += 1 - } - } - } - - var badShortMatches = 0 - var incompletelyMatchedTokens = 0 - var allRunsStartOnWordStartOrUppercaseLetter = true - - for range in matchedRanges { - var position = range.lowerBound - var remainingCharacters = range.length - var matchedTokenPrefix = false - repeat { - let tokenIndex = tokenization.byteTokenAddresses[position].tokenIndex - let tokenLength = tokenization.tokens[tokenIndex].length - let positionInToken = tokenization.byteTokenAddresses[position].indexInToken - let tokenCharactersRemaining = (tokenLength - positionInToken) - let coveredCharacters = - (remainingCharacters > tokenCharactersRemaining) - ? tokenCharactersRemaining : remainingCharacters - let coveredWholeToken = (coveredCharacters == tokenLength) - incompletelyMatchedTokens += coveredWholeToken ? 0 : 1 - let laterMatchesExist = (coveredCharacters < patternCharactersRemaining) - - let incompleteMatch = (!coveredWholeToken && laterMatchesExist) - if incompleteMatch || (positionInToken != 0) { - falseStarts += 1 - } - - if incompleteMatch && (coveredCharacters <= 2) { - badShortMatches += 1 - } - if positionInToken == 0 { - matchedTokenPrefix = true - } else if !candidateMixedcaseBytes[position].isUppercase { - allRunsStartOnWordStartOrUppercaseLetter = false - } - - patternCharactersRemaining -= coveredCharacters - remainingCharacters -= coveredCharacters - position += coveredCharacters - } while remainingCharacters > 0 - if (range.length > 1) || matchedTokenPrefix { - score += pow(Double(range.length), 1.5) - } - } - // This is for cases like an autogenerated member-wise initializer of a huge struct matching everything. - // If they only matched within the arguments, and it's a huge symbol, it's a false start. - if (firstMatchedRange.lowerBound > tokenization.baseNameLength) && (candidateMixedcaseBytes.count > 256) { - falseStarts += 1 - score *= 0.75 - } - - if matchStyle == .acronym { - badShortMatches = 0 - falseStarts = 0 - } - - if matchedRanges.only?.length == candidateMixedcaseBytes.count { - score *= candidate.contentType.fullMatchBonus - } else if matchedRanges.only == 0.. 1) && (matchStyle != .acronym) { - score /= 2 - } - } - return TextScore(value: score, falseStarts: falseStarts) - } else { - return .noMatchScore - } - } - - private static func allocateSuccessiveRejectionFilters( - lowercaseBytes: UTF8Bytes, - allocator: inout UnsafeStackAllocator - ) -> UnsafeStackArray { - var filters = allocator.allocateUnsafeArray(of: RejectionFilter.self, maximumCapacity: lowercaseBytes.count) - filters.initializeWithContainedGarbage() - var idx = lowercaseBytes.count - 1 - var accumulated = RejectionFilter.empty - while idx >= 0 { - accumulated.formUnion(lowercaseByte: lowercaseBytes[idx]) - filters[idx] = accumulated - idx -= 1 - } - return filters - } -} - -extension Pattern { - package struct ByteTokenAddress { - package var tokenIndex = 0 - package var indexInToken = 0 - } - - /// A token is a single conceptual name piece of an identifier, usually separated by camel case or underscores. - /// - /// For example `doFancyStuff` is divided into 3 tokens: `do`, `Fancy`, and `Stuff`. - package struct Token { - var storage: UInt - init(length: Int, allUppercase: Bool) { - let bit: UInt = (allUppercase ? 1 : 0) << (Int.bitWidth - 1) - storage = UInt(bitPattern: length) | bit - } - - package var length: Int { - Int(bitPattern: UInt(storage & ~(1 << (Int.bitWidth - 1)))) - } - - package var allUppercase: Bool { - (storage & (1 << (Int.bitWidth - 1))) != 0 - } - } - - package struct Tokenization { - package private(set) var baseNameLength: Int - package private(set) var hasNonUppercaseNonDelimiterBytes: Bool - package private(set) var tokens: UnsafeStackArray - package private(set) var byteTokenAddresses: UnsafeStackArray - - var tokenCount: Int { - tokens.count - } - - var byteCount: Int { - byteTokenAddresses.count - } - - private enum CharacterClass: Equatable { - case uppercase - case delimiter - case other - } - - // `nonisolated(unsafe)` is fine because the underlying buffer is never mutated or deallocated. - private nonisolated(unsafe) static let characterClasses: UnsafePointer = { - let array: [CharacterClass] = (0...255).map { (character: UInt8) in - if character.isDelimiter { - return .delimiter - } else if character.isUppercase { - return .uppercase - } else { - return .other - } - } - return UnsafeBufferPointer.allocate(copyOf: array).baseAddress! - }() - - // name -> [name] - // myName -> [my][Name] - // File.h -> [File][.][h] - // NSOpenGLView -> [NS][Open][GL][View] - // NSURL -> [NSURL] - private init(mixedcaseBytes: UTF8Bytes, contentType: ContentType, allocator: inout UnsafeStackAllocator) { - let byteCount = mixedcaseBytes.count - let maxTokenCount = byteCount - let baseNameMatchesLast = (contentType.baseNameAffinity == .last) - let baseNameSeparator = contentType.baseNameSeparator - byteTokenAddresses = allocator.allocateUnsafeArray(of: ByteTokenAddress.self, maximumCapacity: byteCount) - tokens = allocator.allocateUnsafeArray(of: Token.self, maximumCapacity: maxTokenCount) - baseNameLength = -1 - var baseNameLength: Int? = nil - if byteCount > 1 { - let mixedcaseBytes = mixedcaseBytes.baseAddress! - let characterClasses = Self.characterClasses - let endMixedCaseBytes = mixedcaseBytes + byteCount - func characterClass(at pointer: UnsafePointer) -> CharacterClass { - if pointer != endMixedCaseBytes { - return characterClasses[Int(pointer.pointee)] - } else { - return .delimiter - } - } - var previous = characterClass(at: mixedcaseBytes) - var current = characterClass(at: mixedcaseBytes + 1) - var token = (index: 0, length: 1, isAllUppercase: (previous == .uppercase)) - hasNonUppercaseNonDelimiterBytes = (previous == .other) - tokens.initializeWithContainedGarbage() - byteTokenAddresses.initializeWithContainedGarbage() - var nextByteTokenAddress = byteTokenAddresses.base - nextByteTokenAddress.pointee = .init(tokenIndex: 0, indexInToken: 0) - var nextBytePointer = mixedcaseBytes + 1 - while nextBytePointer != endMixedCaseBytes { - nextBytePointer += 1 - let next = characterClass(at: nextBytePointer) - let currentIsUppercase = (current == .uppercase) - let tokenizeBeforeCurrentCharacter = - (currentIsUppercase && ((previous == .other) || (next == .other))) - || (current == .delimiter) - || (previous == .delimiter) - - if tokenizeBeforeCurrentCharacter { - let anyOtherCase = !(token.isAllUppercase || (previous == .delimiter)) - hasNonUppercaseNonDelimiterBytes = hasNonUppercaseNonDelimiterBytes || anyOtherCase - tokens[token.index] = .init(length: token.length, allUppercase: token.isAllUppercase) - token.isAllUppercase = true - token.length = 0 - token.index += 1 - let lookBack = nextBytePointer - 2 - if lookBack.pointee == baseNameSeparator { - if baseNameLength == nil || baseNameMatchesLast { - baseNameLength = mixedcaseBytes.distance(to: lookBack) - } - } - } - token.isAllUppercase = token.isAllUppercase && currentIsUppercase - nextByteTokenAddress += 1 - nextByteTokenAddress.pointee.tokenIndex = token.index - nextByteTokenAddress.pointee.indexInToken = token.length - token.length += 1 - previous = current - current = next - } - let anyOtherCase = !(token.isAllUppercase || (previous == .delimiter)) - hasNonUppercaseNonDelimiterBytes = hasNonUppercaseNonDelimiterBytes || anyOtherCase - tokens[token.index] = .init(length: token.length, allUppercase: token.isAllUppercase) - tokens.truncateLeavingGarbage(to: token.index + 1) - } else if byteCount == 1 { - let characterClass = Self.characterClasses[Int(mixedcaseBytes[0])] - tokens.append(.init(length: 1, allUppercase: characterClass == .uppercase)) - byteTokenAddresses.append(.init(tokenIndex: 0, indexInToken: 0)) - hasNonUppercaseNonDelimiterBytes = (characterClass == .other) - } else { - hasNonUppercaseNonDelimiterBytes = false - } - self.baseNameLength = baseNameLength ?? byteCount - } - - func enumerate(body: (Range) -> Void) { - var position = 0 - for token in tokens { - body(position ..+ token.length) - position += token.length - } - } - - func anySatisfy(predicate: (Range) -> Bool) -> Bool { - var position = 0 - for token in tokens { - if predicate(position ..+ token.length) { - return true - } - position += token.length - } - return false - } - - package mutating func deallocate(allocator: inout UnsafeStackAllocator) { - allocator.deallocate(&tokens) - allocator.deallocate(&byteTokenAddresses) - } - - package static func allocate( - mixedcaseBytes: UTF8Bytes, - contentType: ContentType, - allocator: inout UnsafeStackAllocator - ) - -> Tokenization - { - Tokenization(mixedcaseBytes: mixedcaseBytes, contentType: contentType, allocator: &allocator) - } - - var firstNonBaseNameTokenIndex: Int { - (byteCount == baseNameLength) ? tokenCount : byteTokenAddresses[baseNameLength].tokenIndex - } - } -} - -extension Pattern.UTF8Bytes { - fileprivate func allocateLowercaseBytes(allocator: inout UnsafeStackAllocator) -> Self { - let lowercaseBytes = allocator.allocateBuffer(of: UTF8Byte.self, count: count) - for index in indices { - lowercaseBytes[index] = self[index].lowercasedUTF8Byte - } - return UnsafeBufferPointer(lowercaseBytes) - } -} - -extension Pattern { - fileprivate struct IndexedCandidate { - var mixedcaseBytes: UTF8Bytes - var lowercaseBytes: UTF8Bytes - var contentType: ContentType - var tokenization: Tokenization - var matchedRanges: UnsafeStackArray - var firstMatchingLowercaseByteIndex: Int? - - static func allocate( - referencing mixedcaseBytes: UTF8Bytes, - patternByteCount: Int, - firstMatchingLowercaseByteIndex: Int?, - contentType: ContentType, - allocator: inout UnsafeStackAllocator - ) -> Self { - let lowercaseBytes = mixedcaseBytes.allocateLowercaseBytes(allocator: &allocator) - let tokenization = Tokenization.allocate( - mixedcaseBytes: mixedcaseBytes, - contentType: contentType, - allocator: &allocator - ) - let matchedRanges = allocator.allocateUnsafeArray(of: UTF8ByteRange.self, maximumCapacity: patternByteCount) - return Self( - mixedcaseBytes: mixedcaseBytes, - lowercaseBytes: lowercaseBytes, - contentType: contentType, - tokenization: tokenization, - matchedRanges: matchedRanges, - firstMatchingLowercaseByteIndex: firstMatchingLowercaseByteIndex - ) - } - - mutating func deallocate(allocator: inout UnsafeStackAllocator) { - allocator.deallocate(&matchedRanges) - tokenization.deallocate(allocator: &allocator) - allocator.deallocate(&lowercaseBytes) - } - - /// Create a new stack array such that for every offset `i` in this pattern `result[i]` points to the start offset - /// of the next token. If there are no more valid start locations `result[i]` points to one character after the end - /// of the candidate. - /// Valid start locations are the start of the next token that begins with a byte that's in the pattern. - /// - /// Examples: - /// ------------------------------------------------------------------------------------------------------- - /// Candidate | Pattern | Result - /// ------------------------------------------------------------------------------------------------------- - /// "doSomeWork" | "SWork" | `[2, 2, 6, 6, 6, 6, 10, 10, 10, 10]` - /// "fn(one:two:three:four:)" | "tf" | `[7,7,7,7,7,7,7,11,11,11,11,17,17,17,17,17,17,23,23,23,23,23,23]` - /// - fileprivate func allocateNextSearchStarts( - allocator: inout UnsafeStackAllocator, - patternRejectionFilter: RejectionFilter - ) -> UnsafeStackArray { - var nextSearchStarts = allocator.allocateUnsafeArray(of: Int.self, maximumCapacity: mixedcaseBytes.count) - var nextStart = mixedcaseBytes.count - let byteTokenAddresses = tokenization.byteTokenAddresses - nextSearchStarts.initializeWithContainedGarbage() - for cidx in mixedcaseBytes.indices.reversed() { - nextSearchStarts[cidx] = nextStart - let isTokenStart = byteTokenAddresses[cidx].indexInToken == 0 - if isTokenStart && patternRejectionFilter.contains(candidateByte: mixedcaseBytes[cidx]) == .maybe { - nextStart = cidx - } - } - return nextSearchStarts - } - - var looksLikeAType: Bool { - (tokenization.hasNonUppercaseNonDelimiterBytes && tokenization.baseNameLength == mixedcaseBytes.count) - } - } -} - -// MARK: - Best Score Search - - -extension Pattern { - private struct Location { - var pattern: Int - var candidate: Int - - func nextByMatching() -> Self { - Location(pattern: pattern + 1, candidate: candidate + 1) - } - - func nextBySkipping(validStartingLocations: UnsafeStackArray) -> Self { - Location(pattern: pattern, candidate: validStartingLocations[candidate]) - } - - static let start = Self(pattern: 0, candidate: 0) - } - - private enum RestoredRange { - case restore(UTF8ByteRange) - case unwind - case none - - func restore(ranges: inout UnsafeStackArray) { - switch self { - case .restore(let lastRange): - ranges[ranges.count - 1] = lastRange - case .unwind: - ranges.removeLast() - case .none: - break - } - } - } - - private struct Step { - var location: Location - var restoredRange: RestoredRange - } - - private struct Context { - var pattern: UTF8Bytes - var candidate: UTF8Bytes - var location = Location.start - - var patternBytesRemaining: Int { - pattern.count - location.pattern - } - - var candidateBytesRemaining: Int { - candidate.count - location.candidate - } - - var enoughCandidateBytesRemain: Bool { - patternBytesRemaining <= candidateBytesRemaining - } - - var isCompleteMatch: Bool { - return patternBytesRemaining == 0 - } - - var isCharacterMatch: Bool { - pattern[location.pattern] == candidate[location.candidate] - } - } - - private struct Buffers { - var steps: UnsafeStackArray - var bestRangeSnapshot: UnsafeStackArray - var validMatchStartingLocations: UnsafeStackArray - /// For each byte the candidate's text, a rejection filter that contains all the characters occurring after or - /// at that offset. - /// - /// This way when we have already matched the first 4 bytes, we can check which characters occur from byte 5 - /// onwards and check that they appear in the candidate's remaining text. - var candidateSuccessiveRejectionFilters: UnsafeStackArray - - static func allocate( - patternLowercaseBytes: UTF8Bytes, - candidate: IndexedCandidate, - patternRejectionFilter: RejectionFilter, - allocator: inout UnsafeStackAllocator - ) -> Self { - let steps = allocator.allocateUnsafeArray(of: Step.self, maximumCapacity: patternLowercaseBytes.count + 1) - let bestRangeSnapshot = allocator.allocateUnsafeArray( - of: UTF8ByteRange.self, - maximumCapacity: patternLowercaseBytes.count - ) - let validMatchStartingLocations = candidate.allocateNextSearchStarts( - allocator: &allocator, - patternRejectionFilter: patternRejectionFilter - ) - let candidateSuccessiveRejectionFilters = Pattern.allocateSuccessiveRejectionFilters( - lowercaseBytes: candidate.lowercaseBytes, - allocator: &allocator - ) - return Buffers( - steps: steps, - bestRangeSnapshot: bestRangeSnapshot, - validMatchStartingLocations: validMatchStartingLocations, - candidateSuccessiveRejectionFilters: candidateSuccessiveRejectionFilters - ) - } - - mutating func deallocate(allocator: inout UnsafeStackAllocator) { - allocator.deallocate(&candidateSuccessiveRejectionFilters) - allocator.deallocate(&validMatchStartingLocations) - allocator.deallocate(&bestRangeSnapshot) - allocator.deallocate(&steps) - } - } - - private struct ExecutionLimit { - private(set) var remainingCycles: Int - mutating func permitCycle() -> Bool { - if remainingCycles == 0 { - return false - } else { - remainingCycles -= 1 - return true - } - } - } - - /// Exhaustively searches for ways to match the pattern to the candidate, scoring each one, and then returning the best score. - /// If `captureMatchingRanges` is `true`, and a match is found, writes the matching ranges out to `matchedRangesStorage`. - fileprivate func bestScore( - candidate: inout IndexedCandidate, - budget maxSteps: Int, - captureMatchingRanges: Bool, - allocator: inout UnsafeStackAllocator - ) -> TextScore { - var context = Context(pattern: patternLowercaseBytes, candidate: candidate.lowercaseBytes) - var bestScore: TextScore? = nil - // Find the best score by visiting all possible scorings. - // So given the pattern "down" and candidate "documentDocument", enumerate these matches: - // - [do]cumentDo[wn]load - // - [d]ocumentD[own]load - // - document[Down]load - // Score each one, and choose the best. - // - // While recursion would be easier, use a manual stack because in practice we run out of stack space. - // - // Shortcuts: - // * Stop if there are more character remaining to be matched than candidates to match. - // * Keep a list of successive reject filters for the candidate and pattern, so that if the remaining pattern - // characters are known not to exist in the rest of the candidate, we can stop early. - // * Each time we step forward after a failed match in a candidate, move up to the next token, not next character. - // * Impose a budget of several thousand iterations so that we back off if things are going to hit the exponential worst case - if patternMixedcaseBytes.hasContent && context.enoughCandidateBytesRemain { - var buffers = Buffers.allocate( - patternLowercaseBytes: patternLowercaseBytes, - candidate: candidate, - patternRejectionFilter: patternRejectionFilter, - allocator: &allocator - ) - defer { buffers.deallocate(allocator: &allocator) } - var executionLimit = ExecutionLimit(remainingCycles: maxSteps) - buffers.steps.push(Step(location: .start, restoredRange: .none)) - while executionLimit.permitCycle(), let step = buffers.steps.popLast() { - context.location = step.location - step.restoredRange.restore(ranges: &candidate.matchedRanges) - - if context.isCompleteMatch { - let newScore = singleScore(candidate: candidate, precision: .thorough, matchStyle: nil) - accumulate( - score: newScore, - into: &bestScore, - matchedRanges: candidate.matchedRanges, - captureMatchingRanges: captureMatchingRanges, - matchedRangesStorage: &buffers.bestRangeSnapshot - ) - } else if context.enoughCandidateBytesRemain { - if RejectionFilter.match( - pattern: patternSuccessiveRejectionFilters[context.location.pattern], - candidate: buffers.candidateSuccessiveRejectionFilters[context.location.candidate] - ) == .maybe { - if context.isCharacterMatch { - let extending = candidate.matchedRanges.last?.upperBound == context.location.candidate - let restoredRange: RestoredRange - if extending { - let lastIndex = candidate.matchedRanges.count - 1 - restoredRange = .restore(candidate.matchedRanges[lastIndex]) - candidate.matchedRanges[lastIndex].extend(upperBoundBy: 1) - } else { - restoredRange = .unwind - candidate.matchedRanges.append(context.location.candidate ..+ 1) - } - buffers.steps.push( - Step( - location: context.location.nextBySkipping( - validStartingLocations: buffers.validMatchStartingLocations - ), - restoredRange: restoredRange - ) - ) - buffers.steps.push(Step(location: context.location.nextByMatching(), restoredRange: .none)) - } else { - buffers.steps.push( - Step( - location: context.location.nextBySkipping( - validStartingLocations: buffers.validMatchStartingLocations - ), - restoredRange: .none - ) - ) - } - } - } - } - - // We need to consider the special cases the `.fast` search scans for so that a `.fast` score can't be better than - // a .`thorough` score. - // For example, they could match 30 character that weren't on a token boundary, which we would have skipped above. - // Or we could have ran out of time searching and missed a contiguous match. - for matchStyle in MatchStyle.allCases { - candidate.matchedRanges.removeAll() - if populateMatchingRanges(&candidate, matchStyle: matchStyle) { - let newScore = singleScore(candidate: candidate, precision: .thorough, matchStyle: matchStyle) - accumulate( - score: newScore, - into: &bestScore, - matchedRanges: candidate.matchedRanges, - captureMatchingRanges: captureMatchingRanges, - matchedRangesStorage: &buffers.bestRangeSnapshot - ) - } - } - candidate.matchedRanges.removeAll() - candidate.matchedRanges.append(contentsOf: buffers.bestRangeSnapshot) - } - return bestScore ?? .noMatchScore - } - - private func accumulate( - score newScore: TextScore, - into bestScore: inout TextScore?, - matchedRanges: UnsafeStackArray, - captureMatchingRanges: Bool, - matchedRangesStorage bestMatchedRangesStorage: inout UnsafeStackArray - ) { - if newScore > (bestScore ?? .worstPossibleScore) { - bestScore = newScore - if captureMatchingRanges { - bestMatchedRangesStorage.removeAll() - bestMatchedRangesStorage.append(contentsOf: matchedRanges) - } - } - } -} - -extension Pattern { - package func test_searchStart(candidate: Candidate, contentType: ContentType) -> [Int] { - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - var indexedCandidate = IndexedCandidate.allocate( - referencing: candidate.bytes, - patternByteCount: patternLowercaseBytes.count, - firstMatchingLowercaseByteIndex: nil, - contentType: contentType, - allocator: &allocator - ) - defer { indexedCandidate.deallocate(allocator: &allocator) } - var nextSearchStarts = indexedCandidate.allocateNextSearchStarts( - allocator: &allocator, - patternRejectionFilter: patternRejectionFilter - ) - defer { allocator.deallocate(&nextSearchStarts) } - return Array(nextSearchStarts) - } - } - - package func testPerformance_tokenizing(batch: CandidateBatch, contentType: ContentType) -> Int { - UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - var all = 0 - batch.enumerate { candidate in - var indexedCandidate = IndexedCandidate.allocate( - referencing: candidate.bytes, - patternByteCount: patternLowercaseBytes.count, - firstMatchingLowercaseByteIndex: nil, - contentType: contentType, - allocator: &allocator - ) - defer { indexedCandidate.deallocate(allocator: &allocator) } - all &= indexedCandidate.tokenization.tokenCount - } - return all - } - } -} - -extension Candidate.ContentType { - fileprivate var prefixMatchBonus: Double { - switch self { - case .codeCompletionSymbol: return 2.00 - case .fileName, .projectSymbol: return 1.05 - case .unknown: return 2.00 - } - } - - fileprivate var fullMatchBonus: Double { - switch self { - case .codeCompletionSymbol: return 1.00 - case .fileName, .projectSymbol: return 1.50 - case .unknown: return 1.00 - } - } - - fileprivate var fullBaseNameMatchBonus: Double { - switch self { - case .codeCompletionSymbol: return 1.00 - case .fileName, .projectSymbol: return 1.50 - case .unknown: return 1.00 - } - } - - fileprivate var baseNameAffinity: BaseNameAffinity { - switch self { - case .codeCompletionSymbol, .projectSymbol: return .first - case .fileName: return .last - case .unknown: return .last - } - } - - fileprivate var baseNameSeparator: UTF8Byte { - switch self { - case .codeCompletionSymbol, .projectSymbol: return .cLeftParentheses - case .fileName: return .cPeriod - case .unknown: return 0 - } - } - - fileprivate var isEligibleForAcronymMatch: Bool { - switch self { - case .codeCompletionSymbol, .fileName, .projectSymbol: return true - case .unknown: return false - } - } - - fileprivate var acronymMatchAllowsMultiCharacterMatchesAfterBaseName: Bool { - switch self { - // You can't do a acronym match into function arguments - case .codeCompletionSymbol, .projectSymbol, .unknown: return false - case .fileName: return true - } - } - - fileprivate var acronymMatchMustBeInBaseName: Bool { - switch self { - // You can't do a acronym match into function arguments - case .codeCompletionSymbol, .projectSymbol: return true - case .fileName, .unknown: return false - } - } - - fileprivate var contentAfterBasenameIsTrivial: Bool { - switch self { - case .codeCompletionSymbol, .projectSymbol: return false - case .fileName: return true - case .unknown: return false - } - } - - fileprivate var isEligibleForTypeNameOverLocalVariableModifier: Bool { - switch self { - case .codeCompletionSymbol: return true - case .fileName, .projectSymbol, .unknown: return false - } - } - - fileprivate enum BaseNameAffinity { - case first - case last - } -} - -extension Pattern { - @available(*, deprecated, message: "Pass a contentType") - package func score( - candidate: UTF8Bytes, - precision: Precision, - captureMatchingRanges: Bool, - ranges: inout [UTF8ByteRange] - ) -> Double { - score( - candidate: candidate, - contentType: .codeCompletionSymbol, - precision: precision, - captureMatchingRanges: captureMatchingRanges, - ranges: &ranges - ) - } - - @available(*, deprecated, message: "Pass a contentType") - package func score(candidate: UTF8Bytes, precision: Precision) -> Double { - score(candidate: candidate, contentType: .codeCompletionSymbol, precision: precision) - } -} diff --git a/Sources/CompletionScoring/Text/RejectionFilter.swift b/Sources/CompletionScoring/Text/RejectionFilter.swift deleted file mode 100644 index ee08aceff..000000000 --- a/Sources/CompletionScoring/Text/RejectionFilter.swift +++ /dev/null @@ -1,135 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A `RejectionFilter` can quickly rule out two byte strings matching with a simple bitwise and. -/// It's the first, most aggressive, and cheapest filter used in matching. -/// -/// -- The Mask -- -/// It's 32 bits. Conceptually it uses 26 bits to represent `a...z`. If the letter corresponding -/// to a byte was present in the candidate, it's bit is set in the filter. -/// -/// The filter is case insensitive, so making a filter from "abc" produces the same filter as one -/// produced from "AbC". -/// -/// Computing ranges in the alphabet is a bottle neck, and a filter will work as long as it's case -/// insensitive for ascii. So instead of mapping `a...z` to `0...25` we map the lower 5 bits of the -/// byte to (0...31), and then shift 1 by that amount to map all bytes to a bit in a way that is -/// both case preserving, and very cheap to compute. -/// -/// -- The Filter -- -/// For a pattern to match a candidate, all bytes in the pattern must be in the candidate in the -/// same order - ascii case insensitive. If you have a mask for the pattern and candidate, then -/// if any bits from the pattern mask aren't in the candidate mask, they can't possibly satisfy -/// the stricter matching criteria. -/// -/// If every bit in the pattern mask is also in the candidate mask it might match the candidate. -/// Examples of cases where it still wouldn't match: -/// * Character occurs 2 times in pattern, but 1 time in candidate -/// * Character in pattern are in different order from candidate -/// * Multiple distinct pattern characters mapped to the same bit, and only one of them was -/// in the candidate. For example, both '8' and 'x' map to the same bit. -package struct RejectionFilter { - package enum Match { - case no - case maybe - } - - private var mask: UInt32 = 0 - static var empty: Self { - .init(mask: 0) - } - - private func maskBit(byte: UTF8Byte) -> UInt32 { - // This mapping relies on the fact that the ascii values for a...z and A...Z - // are equivalent modulo 32, and since that's a power of 2, we can extract - // a bunch of information about that with shifts and masks. - // The comments below refer to each of these groups of 32 values as "bands" - - // The last 5 bits of the byte fulfill the following properties: - // - Every character in a...z has a unique value - // - The value of an uppercase character is the same as the corresponding lowercase character - let lowerFiveBits = UInt32(1) << UInt32(byte & 0b0001_1111) - - // We want to minimize aliasing between a-z values with other values with the same lower 5 bits. - // Start with their 6th bit, which will be zero for many of the non-alpha bands. - let hasAlphaBandBit = UInt32(byte & 0b0100_0000) >> 6 - - // Multiply their lower five bits by that value to map them to either themselves, or 0. - // This eliminates aliasing between 'z' and ':', 'h' and '(', and ')' and 'i', which commonly - // occur in filter text. - let mask = lowerFiveBits * hasAlphaBandBit - - // Ensure that every byte sets at least one bit, by always setting 0b01. - // That bit is never set for a-z because a is the second character in its band. - // - // Technically we don't need this, but without it you get surprising but not wrong results like - // all characters outside of the alpha bands return `.maybe` for matching either the empty string, - // or characters inside the alpha bands. - return mask | 0b01 - } - - package init(bytes: Bytes) where Bytes.Element == UTF8Byte { - for byte in bytes { - mask = mask | maskBit(byte: byte) - } - } - - package init(lowercaseBytes: Bytes) where Bytes.Element == UTF8Byte { - for byte in lowercaseBytes { - mask = mask | maskBit(byte: byte) - } - } - - private init(mask: UInt32) { - self.mask = mask - } - - package init(lowercaseByte: UTF8Byte) { - mask = maskBit(byte: lowercaseByte) - } - - package init(string: String) { - self.init(bytes: string.utf8) - } - - package static func match(pattern: RejectionFilter, candidate: RejectionFilter) -> Match { - return (pattern.mask & candidate.mask) == pattern.mask ? .maybe : .no - } - - func contains(candidateByte: UTF8Byte) -> Match { - let candidateMask = maskBit(byte: candidateByte) - return (mask & candidateMask) == candidateMask ? .maybe : .no - } - - mutating func formUnion(_ rhs: Self) { - mask = mask | rhs.mask - } - - mutating func formUnion(lowercaseByte: UTF8Byte) { - mask = mask | maskBit(byte: lowercaseByte) - } -} - -extension RejectionFilter: CustomStringConvertible { - package var description: String { - let base = String(mask, radix: 2) - return "0b" + String(repeating: "0", count: mask.bitWidth - base.count) + base - } -} - -extension RejectionFilter: Equatable { - package static func == (lhs: RejectionFilter, rhs: RejectionFilter) -> Bool { - lhs.mask == rhs.mask - } -} diff --git a/Sources/CompletionScoring/Text/ScoredMatchSelector.swift b/Sources/CompletionScoring/Text/ScoredMatchSelector.swift deleted file mode 100644 index 426650695..000000000 --- a/Sources/CompletionScoring/Text/ScoredMatchSelector.swift +++ /dev/null @@ -1,119 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// Use `ScoredMatchSelector` to find the matching indexes for a `Pattern` from an array of `CandidateBatch` structures. -/// It's a reference type to allow sharing state between calls to improve performance, and has an internal lock, -/// so only enter it from one thread at a time. -/// -/// It's primary performance improvement is that it creates and initializes all of the scratch buffers ahead of time, so that they can be -/// amortized across all matching calls. -package class ScoredMatchSelector { - typealias CandidateBatchSlice = Pattern.ScoringWorkload.CandidateBatchSlice - typealias CandidateBatchesMatch = Pattern.CandidateBatchesMatch - typealias ScoringWorkload = Pattern.ScoringWorkload - - private let threadWorkloads: [ThreadWorkload] - private let queue: DispatchQueue - package init(batches: [CandidateBatch]) { - let scoringWorkloads = ScoringWorkload.workloads( - for: batches, - parallelism: ProcessInfo.processInfo.activeProcessorCount - ) - threadWorkloads = scoringWorkloads.map { scoringWorkload in - ThreadWorkload(allBatches: batches, slices: scoringWorkload.slices) - } - queue = DispatchQueue(label: "ScoredMatchSelector") - } - - /// Find all of the matches across `batches` and score them, returning the scored results. This is a first part of selecting matches. Later the matches will be combined with matches from other providers, where we'll pick the best matches and sort them with `selectBestMatches(from:textProvider:)` - package func scoredMatches(pattern: Pattern, precision: Pattern.Precision) -> [Pattern.CandidateBatchesMatch] { - /// The whole point is share the allocation space across many re-uses, but if the client calls into us concurrently, we shouldn't just crash. An assert would also be acceptable. - return queue.sync { - // `nonisolated(unsafe)` is fine because every concurrent iteration accesses a different element. - nonisolated(unsafe) let threadWorkloads = threadWorkloads - DispatchQueue.concurrentPerform(iterations: threadWorkloads.count) { index in - threadWorkloads[index].updateOutput(pattern: pattern, precision: precision) - } - let totalMatchCount = threadWorkloads.sum { threadWorkload in - threadWorkload.matchCount - } - return Array(unsafeUninitializedCapacity: totalMatchCount) { aggregate, initializedCount in - if var writePosition = aggregate.baseAddress { - for threadWorkload in threadWorkloads { - threadWorkload.moveResults(to: &writePosition) - } - } else { - precondition(totalMatchCount == 0) - } - initializedCount = totalMatchCount - } - } - } -} - -extension ScoredMatchSelector { - fileprivate final class ThreadWorkload { - - fileprivate private(set) var matchCount = 0 - private let output: UnsafeMutablePointer - private let slices: [CandidateBatchSlice] - private let allBatches: [CandidateBatch] - - init(allBatches: [CandidateBatch], slices: [CandidateBatchSlice]) { - let candidateCount = slices.sum { slice in - slice.candidateRange.count - } - self.output = UnsafeMutablePointer.allocate(capacity: candidateCount) - self.slices = slices - self.allBatches = allBatches - } - - deinit { - precondition(matchCount == 0) // Missing call to moveResults? - output.deallocate() - } - - fileprivate func updateOutput(pattern: Pattern, precision: Pattern.Precision) { - precondition(matchCount == 0) // Missing call to moveResults? - self.matchCount = UnsafeStackAllocator.withUnsafeStackAllocator { allocator in - var matchCount = 0 - for slice in slices { - let batch = allBatches[slice.batchIndex] - batch.enumerate(slice.candidateRange) { candidateIndex, candidate in - if let score = pattern.matchAndScore( - candidate: candidate, - precision: precision, - allocator: &allocator - ) { - let match = CandidateBatchesMatch( - batchIndex: slice.batchIndex, - candidateIndex: candidateIndex, - textScore: score.value - ) - output.advanced(by: matchCount).initialize(to: match) - matchCount += 1 - } - } - } - return matchCount - } - } - - fileprivate func moveResults(to aggregate: inout UnsafeMutablePointer) { - aggregate.moveInitialize(from: output, count: matchCount) - aggregate = aggregate.advanced(by: matchCount) - matchCount = 0 - } - } -} diff --git a/Sources/CompletionScoring/Text/UTF8Byte.swift b/Sources/CompletionScoring/Text/UTF8Byte.swift deleted file mode 100644 index d91b5aded..000000000 --- a/Sources/CompletionScoring/Text/UTF8Byte.swift +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package typealias UTF8Byte = UInt8 -package extension UTF8Byte { - init(_ character: Character) throws { - self = try character.utf8.only.unwrap(orThrow: "More than one byte: \(character)") - } -} - -package func UTF8ByteValue(_ character: Character) -> UTF8Byte? { - character.utf8.only -} - -package extension UTF8Byte { - static let uppercaseAZ: ClosedRange = (65...90) - static let lowercaseAZ: ClosedRange = (97...122) - - static let cSpace: Self = 32 // ' ' - static let cPlus: Self = 43 // '+' - static let cMinus: Self = 45 // '-' - static let cColon: Self = 58 // ':' - static let cPeriod: Self = 46 // '.' - static let cLeftParentheses: Self = 40 // '(' - static let cRightParentheses: Self = 41 // ')' - static let cUnderscore: Self = 95 // '_' - - var isLowercase: Bool { - return Self.lowercaseAZ.contains(self) - } - - var isUppercase: Bool { - return Self.uppercaseAZ.contains(self) - } - - var lowercasedUTF8Byte: UInt8 { - return isUppercase ? (self - Self.uppercaseAZ.lowerBound) + Self.lowercaseAZ.lowerBound : self - } - - var uppercasedUTF8Byte: UInt8 { - return isLowercase ? (self - Self.lowercaseAZ.lowerBound) + Self.uppercaseAZ.lowerBound : self - } - - var isDelimiter: Bool { - return (self == .cSpace) - || (self == .cPlus) - || (self == .cMinus) - || (self == .cColon) - || (self == .cPeriod) - || (self == .cUnderscore) - || (self == .cLeftParentheses) - || (self == .cRightParentheses) - } -} diff --git a/Sources/CompletionScoring/Utilities/SelectTopK.swift b/Sources/CompletionScoring/Utilities/SelectTopK.swift deleted file mode 100644 index cfa6929d0..000000000 --- a/Sources/CompletionScoring/Utilities/SelectTopK.swift +++ /dev/null @@ -1,134 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -extension UnsafeMutableBufferPointer { - /// Find the top `k` elements as ordered by `lessThan`, and move them to the front of the receiver. - /// The order of elements at positions `k.. Bool) { - if k < count { - withoutActuallyEscaping(lessThan) { lessThan in - var sorter = HeapSorter(self, orderedBy: lessThan) - sorter.sortToBack(maxSteps: k) - let slide = count - k - for frontIdx in 0.. { - var heapStorage: UnsafeMutablePointer - var heapCount: Int - var inOrder: (Element, Element) -> Bool - - init(_ storage: UnsafeMutableBufferPointer, orderedBy lessThan: @escaping (Element, Element) -> Bool) { - self.heapCount = storage.count - self.heapStorage = storage.baseAddress! - - self.inOrder = { lhs, rhs in // Make a `<=` out of `<`, we don't need to push down when equal - return lessThan(lhs, rhs) || !lessThan(rhs, lhs) - } - if heapCount > 0 { - let lastIndex = heapLastIndex - let lastItemOnSecondLevelFromBottom = lastIndex.parent - for index in (0...lastItemOnSecondLevelFromBottom.value).reversed() { - pushParentDownIfNeeded(at: HeapIndex(index)) - } - } - } - - struct HeapIndex: Comparable { - var value: Int - - init(_ value: Int) { - self.value = value - } - - var parent: Self { .init((value - 1) / 2) } - var leftChild: Self { .init((value * 2) + 1) } - var rightChild: Self { .init((value * 2) + 2) } - - static func < (lhs: Self, rhs: Self) -> Bool { - lhs.value < rhs.value - } - } - - var heapStartIndex: HeapIndex { .init(0) } - var heapLastIndex: HeapIndex { .init(heapCount - 1) } - var heapEndIndex: HeapIndex { .init(heapCount) } - - subscript(heapIndex heapIndex: HeapIndex) -> Element { - get { - heapStorage[heapIndex.value] - } - set { - heapStorage[heapIndex.value] = newValue - } - } - - func heapSwap(_ a: HeapIndex, _ b: HeapIndex) { - let t = heapStorage[a.value] - heapStorage[a.value] = heapStorage[b.value] - heapStorage[b.value] = t - } - - mutating func sortToBack(maxSteps: Int) { - precondition(maxSteps < heapCount) - for _ in 0.. 0 { - try check(heapStartIndex) - } - } -} diff --git a/Sources/CompletionScoring/Utilities/Serialization/BinaryCodable.swift b/Sources/CompletionScoring/Utilities/Serialization/BinaryCodable.swift deleted file mode 100644 index 12329979d..000000000 --- a/Sources/CompletionScoring/Utilities/Serialization/BinaryCodable.swift +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// BinaryCodable, along with BinaryEncoder, and BinaryDecoder enable streaming values into a byte array representation. -/// To support BinaryCodable, implement the encode method, and write all of your fields into the coder using -/// `encoder.write()`. In your `init(BinaryDecoder)` method, decode those same fields using `init(BinaryDecoder)`. -/// -/// A typical conformance is as simple as: -/// ``` -/// struct Size: BinaryCodable { -/// var width: Double -/// var height: Double -/// -/// func encode(_ encoder: inout BinaryEncoder) { -/// encoder.write(width) -/// encoder.write(height) -/// } -/// -/// init(_ decoder: inout BinaryDecoder) throws { -/// width = try Double(&decoder) -/// height = try Double(&decoder) -/// } -/// } -/// ``` -/// -/// The encoding is very minimal. There is no metadata in the stream, and decode purely has meaning based on what order -/// clients decode values, and which types they use. If your encoder encodes a bool and two ints, your decoder must -/// decode a bool and two ints, otherwise the next structure to be decoded would read what ever you didn't decode, -/// rather than what it encoded. -package protocol BinaryCodable { - - /// Initialize self using values previously writen in `encode(_:)`. All values written by `encode(_:)` must be read - /// by `init(_:)`, in the same order, using the same types. Otherwise the next structure to decode will read the - /// last value you didn't read rather than the first value it wrote. - init(_ decoder: inout BinaryDecoder) throws - - /// Recursively encode content using `encoder.write(_:)` - func encode(_ encoder: inout BinaryEncoder) -} - -extension BinaryCodable { - /// Convenience method to encode a structure to a byte array - package func binaryCodedRepresentation(contentVersion: Int) -> [UInt8] { - BinaryEncoder.encode(contentVersion: contentVersion) { encoder in - encoder.write(self) - } - } - - /// Convenience method to decode a structure from a byte array - package init(binaryCodedRepresentation: [UInt8]) throws { - self = try BinaryDecoder.decode(bytes: binaryCodedRepresentation) { decoder in - try Self(&decoder) - } - } -} diff --git a/Sources/CompletionScoring/Utilities/Serialization/BinaryDecoder.swift b/Sources/CompletionScoring/Utilities/Serialization/BinaryDecoder.swift deleted file mode 100644 index c68524394..000000000 --- a/Sources/CompletionScoring/Utilities/Serialization/BinaryDecoder.swift +++ /dev/null @@ -1,92 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package struct BinaryDecoder { - private let stream: [UInt8] - private var position = 0 - - static let maximumUnderstoodStreamVersion = BinaryEncoder.maximumUnderstoodStreamVersion - private(set) var contentVersion: Int - - private init(stream: [UInt8]) throws { - self.stream = stream - self.contentVersion = 0 - - let streamVersion = try Int(&self) - if streamVersion > Self.maximumUnderstoodStreamVersion { - throw GenericError("Stream version is too new: \(streamVersion)") - } - self.contentVersion = try Int(&self) - } - - /// Top level function to begin decoding. - /// - Parameters: - /// - body: a closure accepting a `BinaryDecoder` that you can make `init(_:)` calls against to decode the - /// archive. - /// - Returns: The value (if any) returned by the body block. - static func decode(bytes: [UInt8], _ body: (inout Self) throws -> R) throws -> R { - var decoder = try BinaryDecoder(stream: bytes) - let decoded = try body(&decoder) - if decoder.position != decoder.stream.count { - // 99% of the time, the client didn't line up their reads and writes, and just decoded garbage. It's more important to catch this than to allow it for some hypothetical use case. - throw GenericError("Unaligned decode") - } - return decoded - } - - private var bytesRemaining: Int { - stream.count - position - } - - // Return the next `byteCount` bytes from the archvie, and advance the read location. - // Throws if there aren't enough bytes in the archive. - mutating func readRawBytes(count byteCount: Int) throws -> ArraySlice { - if bytesRemaining >= byteCount && byteCount >= 0 { - let slice = stream[position ..+ byteCount] - position += byteCount - return slice - } else { - throw GenericError("Stream has \(bytesRemaining) bytes renamining, requires \(byteCount)") - } - } - - // Return the next byte from the archvie, and advance the read location. Throws if there aren't any more bytes in - // the archive. - mutating func readByte() throws -> UInt8 { - let slice = try readRawBytes(count: 1) - return slice[slice.startIndex] - } - - // Read the next bytes from the archive into the memory holding `V`. Useful for decoding primitive values like - // `UInt32`. All architecture specific constraints like endianness, or sizing, are the responsibility of the caller. - mutating func read(rawBytesInto result: inout V) throws { - try withUnsafeMutableBytes(of: &result) { valueBytes in - let slice = try readRawBytes(count: valueBytes.count) - for (offset, byte) in slice.enumerated() { - valueBytes[offset] = byte - } - } - } - - // A convenience method for decoding an enum, and throwing a common error for an unknown case. The body block can - // decode additional additional payload for data associated with each enum case. - mutating func decodeEnumByte(body: (inout BinaryDecoder, UInt8) throws -> E?) throws -> E { - let numericRepresentation = try readByte() - if let decoded = try body(&self, numericRepresentation) { - return decoded - } else { - throw GenericError("Invalid encoding of \(E.self): \(numericRepresentation)") - } - } -} diff --git a/Sources/CompletionScoring/Utilities/Serialization/BinaryEncoder.swift b/Sources/CompletionScoring/Utilities/Serialization/BinaryEncoder.swift deleted file mode 100644 index 7fa78cc49..000000000 --- a/Sources/CompletionScoring/Utilities/Serialization/BinaryEncoder.swift +++ /dev/null @@ -1,59 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -package struct BinaryEncoder { - private var stream: [UInt8] = [] - static let maximumUnderstoodStreamVersion = 0 - let contentVersion: Int - - private init(contentVersion: Int) { - self.contentVersion = contentVersion - write(Self.maximumUnderstoodStreamVersion) - write(contentVersion) - } - - /// Top level function to begin encoding. - /// - Parameters: - /// - contentVersion: A version number for the content of the whole archive. - /// - body: a closure accepting a `BinaryEncoder` that you can make `write(_:)` calls against to populate the - /// archive. - /// - Returns: a byte array that can be used with `BinaryDecoder` - static func encode(contentVersion: Int, _ body: (inout Self) -> Void) -> [UInt8] { - var encoder = BinaryEncoder(contentVersion: contentVersion) - body(&encoder) - return encoder.stream - } - - /// Write the literal bytes of `value` into the archive. The client is responsible for any endian or architecture - /// sizing considerations. - mutating func write(rawBytesOf value: V) { - withUnsafeBytes(of: value) { valueBytes in - write(rawBytes: valueBytes) - } - } - - /// Write `rawBytes` into the archive. You might use this to encode the contents of a bitmap, or a UTF8 sequence. - mutating func write(rawBytes: C) where C.Element == UInt8 { - stream.append(contentsOf: rawBytes) - } - - mutating func writeByte(_ value: UInt8) { - write(value) - } - - /// Recursively encode `value` and all of it's contents. - mutating func write(_ value: V) { - value.encode(&self) - } -} diff --git a/Sources/CompletionScoring/Utilities/Serialization/Conformances/Array+BinaryCodable.swift b/Sources/CompletionScoring/Utilities/Serialization/Conformances/Array+BinaryCodable.swift deleted file mode 100644 index 966ee2d7d..000000000 --- a/Sources/CompletionScoring/Utilities/Serialization/Conformances/Array+BinaryCodable.swift +++ /dev/null @@ -1,30 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -extension Array: BinaryCodable where Element: BinaryCodable { - package init(_ decoder: inout BinaryDecoder) throws { - let count = try Int(&decoder) - self.init(capacity: count) - for _ in 0..() -> [T] where Element == T? { - compactMap { $0 } - } -} - -extension Sequence where Element: Numeric { - package func sum() -> Element { - reduce(0, +) - } -} - -extension Sequence { - func sum(of valueExtractor: (Element) -> I) -> I { - reduce(into: 0) { partialResult, element in - partialResult += valueExtractor(element) - } - } -} - -extension Sequence { - func countOf(predicate: (Element) throws -> Bool) rethrows -> Int { - var count = 0 - for element in self { - if try predicate(element) { - count += 1 - } - } - return count - } -} - -struct GenericError: Error, LocalizedError { - var message: String - init(_ message: String) { - self.message = message - } -} - -extension Optional { - package func unwrap(orThrow message: String) throws -> Wrapped { - if let result = self { - return result - } - throw GenericError(message) - } - - package func unwrap(orFail message: String) -> Wrapped { - if let result = self { - return result - } - preconditionFailure(message) - } - - package mutating func lazyInitialize(initializer: () -> Wrapped) -> Wrapped { - if let wrapped = self { - return wrapped - } else { - let wrapped = initializer() - self = wrapped - return wrapped - } - } -} - -extension UnsafeBufferPointer { - init(to element: inout Element) { - self = withUnsafePointer(to: &element) { pointer in - UnsafeBufferPointer(start: pointer, count: 1) - } - } -} - -extension UnsafeBufferPointer { - package static func allocate(copyOf original: some Collection) -> Self { - return Self(UnsafeMutableBufferPointer.allocate(copyOf: original)) - } -} - -extension UnsafeMutablePointer { - mutating func resize(fromCount oldCount: Int, toCount newCount: Int) { - let replacement = UnsafeMutablePointer.allocate(capacity: newCount) - let copiedCount = min(oldCount, newCount) - replacement.moveInitialize(from: self, count: copiedCount) - let abandondedCount = oldCount - copiedCount - self.advanced(by: copiedCount).deinitialize(count: abandondedCount) - deallocate() - self = replacement - } - - func initialize(from collection: some Collection) { - let buffer = UnsafeMutableBufferPointer(start: self, count: collection.count) - _ = buffer.initialize(from: collection) - } -} - -extension UnsafeMutableBufferPointer { - package static func allocate(copyOf original: some Collection) -> Self { - let copy = UnsafeMutableBufferPointer.allocate(capacity: original.count) - _ = copy.initialize(from: original) - return copy - } - - package func initialize(index: Int, to value: Element) { - self.baseAddress!.advanced(by: index).initialize(to: value) - } - - func deinitializeAll() { - baseAddress!.deinitialize(count: count) - } - - package func deinitializeAllAndDeallocate() { - deinitializeAll() - deallocate() - } - - mutating func truncateAndDeinitializeTail(maxLength: Int) { - if maxLength < count { - self.baseAddress!.advanced(by: maxLength).deinitialize(count: count - maxLength) - self = UnsafeMutableBufferPointer(start: baseAddress, count: maxLength) - } - } - - mutating func removeAndTruncateWhere(_ predicate: (Element) -> Bool) { - var writeIndex = 0 - for readIndex in indices { - if !predicate(self[readIndex]) { - if writeIndex != readIndex { - swapAt(writeIndex, readIndex) - } - writeIndex += 1 - } - } - truncateAndDeinitializeTail(maxLength: writeIndex) - } - - func setAll(to value: Element) { - for index in indices { - self[index] = value - } - } -} - -infix operator ? : ComparisonPrecedence - -extension Comparable { - /// Useful for chained comparison, for example on a person, sorting by last, first, age: - /// ``` - /// static func <(_ lhs: Self, _ rhs: Self) -> Bool { - /// return lhs.last Bool? { - // Assume that `<` is most likely, and avoid a redundant `==`. - if lhs < rhs { - return true - } else if lhs == rhs { - return nil - } else { - return false - } - } - - /// See ? (_ lhs: Self, _ rhs: Self) -> Bool? { - // Assume that `>` is most likely, and avoid a redundant `==`. - if lhs > rhs { - return true - } else if lhs == rhs { - return nil - } else { - return false - } - } -} - -infix operator ..+ : RangeFormationPrecedence - -package func ..+ (lhs: Bound, rhs: Bound) -> Range { - lhs..<(lhs + rhs) -} - -extension RandomAccessCollection { - fileprivate func withMapScratchArea(body: (UnsafeMutablePointer) -> R) -> R { - let scratchArea = UnsafeMutablePointer.allocate(capacity: count) - defer { - scratchArea.deinitialize(count: count) - /// Should be a no-op - scratchArea.deallocate() - } - return body(scratchArea) - } - - package func concurrentCompactMap(_ f: @Sendable (Element) -> T?) -> [T] where Self: Sendable, Index: Sendable { - return withMapScratchArea { (results: UnsafeMutablePointer) -> [T] in - // `nonisolated(unsafe)` is fine because we write to different offsets within the buffer on every concurrent - // iteration. - nonisolated(unsafe) let results = results - DispatchQueue.concurrentPerform(iterations: count) { iterationIndex in - let collectionIndex = self.index(self.startIndex, offsetBy: iterationIndex) - results.advanced(by: iterationIndex).initialize(to: f(self[collectionIndex])) - } - return UnsafeBufferPointer(start: results, count: count).compacted() - } - } - - package func concurrentMap(_ f: @Sendable (Element) -> T) -> [T] where Self: Sendable, Index: Sendable { - return withMapScratchArea { (results: UnsafeMutablePointer) -> [T] in - // `nonisolated(unsafe)` is fine because we write to different offsets within the buffer on every concurrent - // iteration. - nonisolated(unsafe) let results = results - DispatchQueue.concurrentPerform(iterations: count) { iterationIndex in - let collectionIndex = self.index(self.startIndex, offsetBy: iterationIndex) - results.advanced(by: iterationIndex).initialize(to: f(self[collectionIndex])) - } - return Array(UnsafeBufferPointer(start: results, count: count)) - } - } - - package func max(by accessor: (Element) -> some Comparable) -> Element? { - self.max { lhs, rhs in - accessor(lhs) < accessor(rhs) - } - } - - package func min(by accessor: (Element) -> some Comparable) -> Element? { - self.min { lhs, rhs in - accessor(lhs) < accessor(rhs) - } - } - - package func max(of accessor: (Element) -> T) -> T? { - let extreme = self.max { lhs, rhs in - accessor(lhs) < accessor(rhs) - } - return extreme.map(accessor) - } - - package func min(of accessor: (Element) -> T) -> T? { - let extreme = self.min { lhs, rhs in - accessor(lhs) < accessor(rhs) - } - return extreme.map(accessor) - } -} - -protocol ContiguousZeroBasedIndexedCollection: Collection where Index == Int { - var indices: Range { get } -} - -extension ContiguousZeroBasedIndexedCollection { - func slicedConcurrentForEachSliceRange(body: @Sendable (Range) -> Void) where Self: SendableMetatype { - // We want to use `DispatchQueue.concurrentPerform`, but we want to be called only a few times. So that we - // can amortize per-callback work. We also want to oversubscribe so that we can efficiently use - // heterogeneous CPUs. If we had 4 efficiency cores, and 4 performance cores, and we dispatched 8 work items - // we'd finish 4 quickly, and then either migrate the work, or leave the performance cores idle. Scheduling - // extra jobs should let the performance cores pull a disproportionate amount of work items. More fine - // granularity also helps if the work items aren't all the same difficulty, for the same reason. - - // Defensive against `activeProcessorCount` failing - let sliceCount = Swift.min(Swift.max(ProcessInfo.processInfo.activeProcessorCount * 32, 1), count) - let count = self.count - DispatchQueue.concurrentPerform(iterations: sliceCount) { sliceIndex in - precondition(sliceCount >= 1) - /// Remainder will be distributed across leading slices, so slicing an array with count 5 into 3 slices will give you - /// slices of size [2, 2, 1]. - let equalPortion = count / sliceCount - let remainder = count - (equalPortion * sliceCount) - let getsRemainder = sliceIndex < remainder - let length = equalPortion + (getsRemainder ? 1 : 0) - let previousSlicesGettingRemainder = Swift.min(sliceIndex, remainder) - let start = (sliceIndex * equalPortion) + previousSlicesGettingRemainder - body(start ..+ length) - } - } -} - -extension Array: ContiguousZeroBasedIndexedCollection {} -extension UnsafeMutableBufferPointer: ContiguousZeroBasedIndexedCollection {} - -extension Array { - /// A concurrent map that allows amortizing per-thread work. For example, if you need a scratch buffer - /// to complete the mapping, but could use the same scratch buffer for every iteration on the same thread - /// you can use this function instead of `concurrentMap`. This method also often helps amortize reference - /// counting since there are less callbacks - /// - /// - Important: The callback must write to all values in `destination[0..( - writer: @Sendable (ArraySlice, _ destination: UnsafeMutablePointer) -> Void - ) -> [T] where Self: Sendable { - return [T](unsafeUninitializedCapacity: count) { buffer, initializedCount in - if let bufferBase = buffer.baseAddress { - // `nonisolated(unsafe)` is fine because every concurrent iteration accesses a disjunct slice of `buffer`. - nonisolated(unsafe) let bufferBase = bufferBase - slicedConcurrentForEachSliceRange { sliceRange in - writer(self[sliceRange], bufferBase.advanced(by: sliceRange.startIndex)) - } - } else { - precondition(isEmpty) - } - initializedCount = count - } - } - - /// Concurrent for-each on self, but slice based to allow the body to amortize work across callbacks - func slicedConcurrentForEach(body: @Sendable (ArraySlice) -> Void) where Self: Sendable { - slicedConcurrentForEachSliceRange { sliceRange in - body(self[sliceRange]) - } - } - - func concurrentForEach(body: @Sendable (Element) -> Void) where Self: Sendable { - DispatchQueue.concurrentPerform(iterations: count) { index in - body(self[index]) - } - } - - init(capacity: Int) { - self = Self() - reserveCapacity(capacity) - } - - package init(count: Int, generator: () -> Element) { - self = (0..(overwritingDuplicates: Affirmative, _ map: (Key) -> K) -> [K: Value] { - var result = [K: Value](capacity: count) - for (key, value) in self { - result[map(key)] = value - } - return result - } -} - -enum Affirmative { - case affirmative -} - -package enum ComparisonOrder: Equatable { - case ascending - case same - case descending - - init(_ value: Int) { - if value < 0 { - self = .ascending - } else if value == 0 { - self = .same - } else { - self = .descending - } - } -} - -extension UnsafeBufferPointer { - func afterFirst() -> Self { - precondition(hasContent) - return UnsafeBufferPointer(start: baseAddress! + 1, count: count - 1) - } - - package static func withSingleElementBuffer( - of element: Element, - body: (Self) throws -> R - ) rethrows -> R { - var element = element - let typedBufferPointer = Self(to: &element) - return try body(typedBufferPointer) - } -} - -extension UnsafeBufferPointer { - package func rangeOf(bytes needle: UnsafeBufferPointer, startOffset: Int = 0) -> Range? { - guard count > 0, let baseAddress else { - return nil - } - guard needle.count > 0, let needleBaseAddress = needle.baseAddress else { - return nil - } - guard - let match = sourcekitlsp_memmem(baseAddress + startOffset, count - startOffset, needleBaseAddress, needle.count) - else { - return nil - } - let start = baseAddress.distance(to: match.assumingMemoryBound(to: UInt8.self)) - return start ..+ needle.count - } - - func rangeOf(bytes needle: [UInt8]) -> Range? { - needle.withUnsafeBufferPointer { bytes in - rangeOf(bytes: bytes) - } - } -} - -func equateBytes(_ lhs: UnsafeBufferPointer, _ rhs: UnsafeBufferPointer) -> Bool { - compareBytes(lhs, rhs) == .same -} - -package func compareBytes( - _ lhs: UnsafeBufferPointer, - _ rhs: UnsafeBufferPointer -) -> ComparisonOrder { - compareBytes(UnsafeRawBufferPointer(lhs), UnsafeRawBufferPointer(rhs)) -} - -func compareBytes(_ lhs: UnsafeRawBufferPointer, _ rhs: UnsafeRawBufferPointer) -> ComparisonOrder { - let result = Int(memcmp(lhs.baseAddress!, rhs.baseAddress!, min(lhs.count, rhs.count))) - return (result != 0) ? ComparisonOrder(result) : ComparisonOrder(lhs.count - rhs.count) -} - -extension String { - /// Non mutating version of withUTF8. withUTF8 is mutating to make the string contiguous, so that future calls will - /// be cheaper. - /// Useful when you're operating on an argument, and have no way to avoid this copy dance. - package func withUncachedUTF8Bytes( - _ body: ( - UnsafeBufferPointer - ) throws -> R - ) rethrows -> R { - var copy = self - return try copy.withUTF8(body) - } -} diff --git a/Sources/CompletionScoring/Utilities/UnsafeArray.swift b/Sources/CompletionScoring/Utilities/UnsafeArray.swift deleted file mode 100644 index 17bf3f290..000000000 --- a/Sources/CompletionScoring/Utilities/UnsafeArray.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A manually allocated and deallocated array with automatic growth on insert. -/// -/// - Warning: this type is Unsafe. Writing to an allocated instance must be exclusive to one client. -/// Multiple readers are OK, as long as deallocation is coordinated. Writing must be exclusive because -/// appends can cause realloc, leaving dangling pointers in the copies held by other clients. Appends -/// also would not update the counts in other clients. -internal struct UnsafeArray { - private(set) var count = 0 - private(set) var capacity: Int - private(set) var elements: UnsafeMutablePointer - - private init(elements: UnsafeMutablePointer, capacity: Int) { - self.capacity = capacity - self.elements = elements - } - - /// Must be deallocated with `deallocate()`. Will grow beyond `initialCapacity` as elements are added. - static func allocate(initialCapacity: Int) -> Self { - Self(elements: UnsafeMutablePointer.allocate(capacity: initialCapacity), capacity: initialCapacity) - } - - mutating func deallocate() { - elements.deinitialize(count: count) - elements.deallocate() - count = 0 - capacity = 0 - } - - /// Must be deallocated with `deallocate()`. - func allocateCopy(preservingCapacity: Bool) -> Self { - var copy = UnsafeArray.allocate(initialCapacity: preservingCapacity ? capacity : count) - copy.elements.initialize(from: elements, count: count) - copy.count = count - return copy - } - - private mutating func resize(newCapacity: Int) { - assert(newCapacity >= count) - elements.resize(fromCount: count, toCount: newCapacity) - capacity = newCapacity - } - - mutating func reserve(minimumAdditionalCapacity: Int) { - let availableAdditionalCapacity = (capacity - count) - if availableAdditionalCapacity < minimumAdditionalCapacity { - resize(newCapacity: max(capacity * 2, capacity + minimumAdditionalCapacity)) - } - } - - mutating func append(_ element: Element) { - reserve(minimumAdditionalCapacity: 1) - elements[count] = element - count += 1 - } - - mutating func append(contentsOf collection: some Collection) { - reserve(minimumAdditionalCapacity: collection.count) - elements.advanced(by: count).initialize(from: collection) - count += collection.count - } - - private func assertBounds(_ index: Int) { - assert(index >= 0) - assert(index < count) - } - - subscript(_ index: Int) -> Element { - get { - assertBounds(index) - return elements[index] - } - set { - assertBounds(index) - elements[index] = newValue - } - } -} diff --git a/Sources/CompletionScoring/Utilities/UnsafeStackAllocator.swift b/Sources/CompletionScoring/Utilities/UnsafeStackAllocator.swift deleted file mode 100644 index e7a2c870e..000000000 --- a/Sources/CompletionScoring/Utilities/UnsafeStackAllocator.swift +++ /dev/null @@ -1,245 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -private struct Bytes8 { var storage: (UInt64) = (0) } -private struct Bytes16 { var storage: (Bytes8, Bytes8) = (.init(), .init()) } -private struct Bytes32 { var storage: (Bytes16, Bytes16) = (.init(), .init()) } -private struct Bytes64 { var storage: (Bytes32, Bytes32) = (.init(), .init()) } -private struct Bytes128 { var storage: (Bytes64, Bytes64) = (.init(), .init()) } -private struct Bytes256 { var storage: (Bytes128, Bytes128) = (.init(), .init()) } -private struct Bytes512 { var storage: (Bytes256, Bytes256) = (.init(), .init()) } -private struct Bytes1024 { var storage: (Bytes512, Bytes512) = (.init(), .init()) } -private struct Bytes2048 { var storage: (Bytes1024, Bytes1024) = (.init(), .init()) } -private struct Bytes4096 { var storage: (Bytes2048, Bytes2048) = (.init(), .init()) } -private struct Bytes8192 { var storage: (Bytes4096, Bytes4096) = (.init(), .init()) } - -package struct UnsafeStackAllocator { - private typealias Storage = Bytes8192 - private var storage = Storage() - private static let pageSize = 64 - private static let storageCapacity = MemoryLayout.size - private var pagesAllocated = 0 - private var pagesAvailable: Int { - pagesCapacity - pagesAllocated - } - - private init() { - } - - package static func withUnsafeStackAllocator(body: (inout Self) throws -> R) rethrows -> R { - var allocator = Self() - defer { assert(allocator.pagesAllocated == 0) } - return try body(&allocator) - } - - private let pagesCapacity = Self.storageCapacity / Self.pageSize - - private func pages(for type: Element.Type, maximumCapacity: Int) -> Int { - let bytesNeeded = MemoryLayout.stride * maximumCapacity - return ((bytesNeeded - 1) / Self.pageSize) + 1 - } - - private mutating func allocate( - of type: Element.Type, - maximumCapacity: Int - ) -> UnsafeMutablePointer { - // Avoid dealing with alignment for now. - assert(MemoryLayout.alignment <= MemoryLayout.alignment) - // Avoid dealing with alignment for now. - assert(MemoryLayout.alignment <= Self.pageSize) - let pagesNeeded = pages(for: type, maximumCapacity: maximumCapacity) - if pagesNeeded < pagesAvailable { - return withUnsafeMutableBytes(of: &storage) { arena in - let start = arena.baseAddress!.advanced(by: pagesAllocated * Self.pageSize).bindMemory( - to: Element.self, - capacity: maximumCapacity - ) - pagesAllocated += pagesNeeded - return start - } - } else { - return UnsafeMutablePointer.allocate(capacity: maximumCapacity) - } - } - - mutating func allocateBuffer(of type: Element.Type, count: Int) -> UnsafeMutableBufferPointer { - return UnsafeMutableBufferPointer(start: allocate(of: type, maximumCapacity: count), count: count) - } - - mutating func allocateUnsafeArray( - of type: Element.Type, - maximumCapacity: Int - ) -> UnsafeStackArray { - UnsafeStackArray(base: allocate(of: type, maximumCapacity: maximumCapacity), capacity: maximumCapacity) - } - - mutating private func deallocate(_ base: UnsafePointer, capacity: Int) { - let arrayStart = UnsafeRawPointer(base) - let arrayPages = pages(for: Element.self, maximumCapacity: capacity) - withUnsafeBytes(of: &storage) { arena in - let arenaStart = UnsafeRawPointer(arena.baseAddress)! - let arenaEnd = arenaStart.advanced(by: Self.storageCapacity) - if (arrayStart >= arenaStart) && (arrayStart < arenaEnd) { - let projectedArrayStart = arenaStart.advanced(by: (pagesAllocated - arrayPages) * Self.pageSize) - assert(projectedArrayStart == arrayStart, "deallocate(...) must be called in FIFO order.") - pagesAllocated -= arrayPages - } else { - arrayStart.deallocate() - } - } - } - - /// - Note: `buffer.count` must the be the same as from the original allocation. - /// - Note: deiniting buffer contents is caller's responsibility. - mutating func deallocate(_ buffer: inout UnsafeBufferPointer) { - if let baseAddress = buffer.baseAddress { - deallocate(baseAddress, capacity: buffer.count) - buffer = UnsafeBufferPointer(start: nil, count: 0) - } - } - - /// - Note: `buffer.count` must the be the same as from the original allocation. - /// - Note: deiniting buffer contents is caller's responsibility. - mutating func deallocate(_ buffer: inout UnsafeMutableBufferPointer) { - if let baseAddress = buffer.baseAddress { - deallocate(baseAddress, capacity: buffer.count) - buffer = UnsafeMutableBufferPointer(start: nil, count: 0) - } - } - - mutating func deallocate(_ array: inout UnsafeStackArray) { - array.prepareToDeallocate() - deallocate(array.base, capacity: array.capacity) - array = UnsafeStackArray(base: array.base, capacity: 0) - } - - package mutating func withStackArray( - of elementType: Element.Type, - maximumCapacity: Int, - body: (inout UnsafeStackArray) throws -> R - ) rethrows -> R { - var stackArray = allocateUnsafeArray(of: elementType, maximumCapacity: maximumCapacity) - defer { deallocate(&stackArray) } - return try body(&stackArray) - } -} - -package struct UnsafeStackArray { - private(set) var base: UnsafeMutablePointer - fileprivate let capacity: Int - package private(set) var count = 0 - - fileprivate init(base: UnsafeMutablePointer, capacity: Int) { - self.base = base - self.capacity = capacity - } - - fileprivate mutating func prepareToDeallocate() { - removeAll() // Contained elements may need de-init - } - - // Assume the memory is initialized with whatever is there. Only safe on trivial types. - mutating func initializeWithContainedGarbage() { - count = capacity - } - - package mutating func fill(with element: Element) { - while count < capacity { - append(element) - } - } - - package mutating func removeAll() { - base.deinitialize(count: count) - count = 0 - } - - mutating func append(contentsOf sequence: some Sequence) { - for element in sequence { - append(element) - } - } - - package mutating func append(_ element: Element) { - assert(count < capacity) - (base + count).initialize(to: element) - count += 1 - } - - mutating func push(_ element: Element) { - append(element) - } - - package mutating func removeLast() { - assert(count > 0) - (base + count - 1).deinitialize(count: 1) - count -= 1 - } - - package subscript(_ index: Int) -> Element { - get { - assert(index < count) - return (base + index).pointee - } - set { - assert(index < count) - (base + index).pointee = newValue - } - } - - package mutating func truncate(to countLimit: Int) { - assert(countLimit >= 0) - if count > countLimit { - (base + countLimit).deinitialize(count: count - countLimit) - count = countLimit - } - } - - mutating func truncateLeavingGarbage(to countLimit: Int) { - assert(countLimit >= 0) - count = countLimit - } - - var contiguousStorage: UnsafeBufferPointer { - UnsafeBufferPointer(start: base, count: count) - } - - func contiguousStorage(count viewCount: Int) -> UnsafeBufferPointer { - assert(viewCount <= count) - return UnsafeBufferPointer(start: base, count: viewCount) - } -} - -extension UnsafeStackArray: RandomAccessCollection { - package var startIndex: Int { - 0 - } - - package var endIndex: Int { - count - } -} - -extension UnsafeStackArray: MutableCollection { -} - -extension UnsafeStackArray { - package mutating func popLast() -> Element? { - let last = last - if hasContent { - removeLast() - } - return last - } -} diff --git a/Sources/CompletionScoringForPlugin b/Sources/CompletionScoringForPlugin deleted file mode 120000 index 097f08392..000000000 --- a/Sources/CompletionScoringForPlugin +++ /dev/null @@ -1 +0,0 @@ -CompletionScoring/ \ No newline at end of file diff --git a/Sources/CompletionScoringTestSupport/INPUTS/CommonFunctionTerms.json b/Sources/CompletionScoringTestSupport/INPUTS/CommonFunctionTerms.json deleted file mode 100644 index 88ec39d2d..000000000 --- a/Sources/CompletionScoringTestSupport/INPUTS/CommonFunctionTerms.json +++ /dev/null @@ -1,994 +0,0 @@ -[ -{ "word" : "Should", "count" : 14}, -{ "word" : "Suggested", "count" : 14}, -{ "word" : "Trigger", "count" : 14}, -{ "word" : "any", "count" : 14}, -{ "word" : "calculate", "count" : 14}, -{ "word" : "constructor", "count" : 14}, -{ "word" : "direct", "count" : 14}, -{ "word" : "dummy", "count" : 14}, -{ "word" : "empty", "count" : 14}, -{ "word" : "error", "count" : 14}, -{ "word" : "last", "count" : 14}, -{ "word" : "length", "count" : 14}, -{ "word" : "metadata", "count" : 14}, -{ "word" : "obj", "count" : 14}, -{ "word" : "objcpp", "count" : 14}, -{ "word" : "pasteboard", "count" : 14}, -{ "word" : "rename", "count" : 14}, -{ "word" : "save", "count" : 14}, -{ "word" : "selector", "count" : 14}, -{ "word" : "service", "count" : 14}, -{ "word" : "title", "count" : 14}, -{ "word" : "when", "count" : 14}, -{ "word" : "Body", "count" : 15}, -{ "word" : "CALayer", "count" : 15}, -{ "word" : "CGColor", "count" : 15}, -{ "word" : "Cases", "count" : 15}, -{ "word" : "Check", "count" : 15}, -{ "word" : "Clicked", "count" : 15}, -{ "word" : "Coordinator", "count" : 15}, -{ "word" : "Date", "count" : 15}, -{ "word" : "Deleted", "count" : 15}, -{ "word" : "Dict", "count" : 15}, -{ "word" : "Heading", "count" : 15}, -{ "word" : "Message", "count" : 15}, -{ "word" : "Metadata", "count" : 15}, -{ "word" : "More", "count" : 15}, -{ "word" : "NSImage", "count" : 15}, -{ "word" : "NSPoint", "count" : 15}, -{ "word" : "NSValidated", "count" : 15}, -{ "word" : "Parent", "count" : 15}, -{ "word" : "Popover", "count" : 15}, -{ "word" : "Received", "count" : 15}, -{ "word" : "Sequence", "count" : 15}, -{ "word" : "UIContext", "count" : 15}, -{ "word" : "UIEvent", "count" : 15}, -{ "word" : "active", "count" : 15}, -{ "word" : "async", "count" : 15}, -{ "word" : "closest", "count" : 15}, -{ "word" : "disable", "count" : 15}, -{ "word" : "extension", "count" : 15}, -{ "word" : "framework", "count" : 15}, -{ "word" : "nif", "count" : 15}, -{ "word" : "parameters", "count" : 15}, -{ "word" : "prepare", "count" : 15}, -{ "word" : "previous", "count" : 15}, -{ "word" : "run", "count" : 15}, -{ "word" : "sorted", "count" : 15}, -{ "word" : "specification", "count" : 15}, -{ "word" : "suggestion", "count" : 15}, -{ "word" : "top", "count" : 15}, -{ "word" : "unregister", "count" : 15}, -{ "word" : "Activity", "count" : 16}, -{ "word" : "Balance", "count" : 16}, -{ "word" : "Bounds", "count" : 16}, -{ "word" : "Byte", "count" : 16}, -{ "word" : "Child", "count" : 16}, -{ "word" : "Converter", "count" : 16}, -{ "word" : "DVTFile", "count" : 16}, -{ "word" : "Dynamic", "count" : 16}, -{ "word" : "Indentation", "count" : 16}, -{ "word" : "Jump", "count" : 16}, -{ "word" : "Local", "count" : 16}, -{ "word" : "NSColor", "count" : 16}, -{ "word" : "Notifications", "count" : 16}, -{ "word" : "Operations", "count" : 16}, -{ "word" : "Out", "count" : 16}, -{ "word" : "Register", "count" : 16}, -{ "word" : "Replacing", "count" : 16}, -{ "word" : "Selecting", "count" : 16}, -{ "word" : "Shown", "count" : 16}, -{ "word" : "Storage", "count" : 16}, -{ "word" : "Strategy", "count" : 16}, -{ "word" : "Struct", "count" : 16}, -{ "word" : "That", "count" : 16}, -{ "word" : "UIScene", "count" : 16}, -{ "word" : "Updates", "count" : 16}, -{ "word" : "Values", "count" : 16}, -{ "word" : "Without", "count" : 16}, -{ "word" : "args", "count" : 16}, -{ "word" : "attributes", "count" : 16}, -{ "word" : "children", "count" : 16}, -{ "word" : "contents", "count" : 16}, -{ "word" : "decode", "count" : 16}, -{ "word" : "dismiss", "count" : 16}, -{ "word" : "dynamic", "count" : 16}, -{ "word" : "external", "count" : 16}, -{ "word" : "extract", "count" : 16}, -{ "word" : "generate", "count" : 16}, -{ "word" : "inner", "count" : 16}, -{ "word" : "local", "count" : 16}, -{ "word" : "minimap", "count" : 16}, -{ "word" : "original", "count" : 16}, -{ "word" : "protocol", "count" : 16}, -{ "word" : "relative", "count" : 16}, -{ "word" : "sdk", "count" : 16}, -{ "word" : "Auto", "count" : 17}, -{ "word" : "Closing", "count" : 17}, -{ "word" : "Dark", "count" : 17}, -{ "word" : "Delegate", "count" : 17}, -{ "word" : "Dropdown", "count" : 17}, -{ "word" : "Endings", "count" : 17}, -{ "word" : "Fix", "count" : 17}, -{ "word" : "Graphic", "count" : 17}, -{ "word" : "Header", "count" : 17}, -{ "word" : "Inset", "count" : 17}, -{ "word" : "Inside", "count" : 17}, -{ "word" : "Known", "count" : 17}, -{ "word" : "Last", "count" : 17}, -{ "word" : "Macros", "count" : 17}, -{ "word" : "Marked", "count" : 17}, -{ "word" : "Moving", "count" : 17}, -{ "word" : "NSCollection", "count" : 17}, -{ "word" : "NSWindow", "count" : 17}, -{ "word" : "Pattern", "count" : 17}, -{ "word" : "Rendition", "count" : 17}, -{ "word" : "Same", "count" : 17}, -{ "word" : "Spell", "count" : 17}, -{ "word" : "UIDrop", "count" : 17}, -{ "word" : "Vertical", "count" : 17}, -{ "word" : "aspect", "count" : 17}, -{ "word" : "bark", "count" : 17}, -{ "word" : "configure", "count" : 17}, -{ "word" : "containing", "count" : 17}, -{ "word" : "dict", "count" : 17}, -{ "word" : "dictionary", "count" : 17}, -{ "word" : "editing", "count" : 17}, -{ "word" : "free", "count" : 17}, -{ "word" : "mode", "count" : 17}, -{ "word" : "module", "count" : 17}, -{ "word" : "operation", "count" : 17}, -{ "word" : "translation", "count" : 17}, -{ "word" : "Bound", "count" : 18}, -{ "word" : "Candidate", "count" : 18}, -{ "word" : "Centered", "count" : 18}, -{ "word" : "Crash", "count" : 18}, -{ "word" : "Delimiter", "count" : 18}, -{ "word" : "MARK", "count" : 18}, -{ "word" : "NSError", "count" : 18}, -{ "word" : "Named", "count" : 18}, -{ "word" : "Param", "count" : 18}, -{ "word" : "Parameters", "count" : 18}, -{ "word" : "Pop", "count" : 18}, -{ "word" : "Prefix", "count" : 18}, -{ "word" : "Re", "count" : 18}, -{ "word" : "Scale", "count" : 18}, -{ "word" : "This", "count" : 18}, -{ "word" : "UITouch", "count" : 18}, -{ "word" : "be", "count" : 18}, -{ "word" : "convert", "count" : 18}, -{ "word" : "ctx", "count" : 18}, -{ "word" : "extend", "count" : 18}, -{ "word" : "f", "count" : 18}, -{ "word" : "float", "count" : 18}, -{ "word" : "guard", "count" : 18}, -{ "word" : "keyword", "count" : 18}, -{ "word" : "kinds", "count" : 18}, -{ "word" : "not", "count" : 18}, -{ "word" : "queue", "count" : 18}, -{ "word" : "schedule", "count" : 18}, -{ "word" : "section", "count" : 18}, -{ "word" : "tear", "count" : 18}, -{ "word" : "visible", "count" : 18}, -{ "word" : "CGContext", "count" : 19}, -{ "word" : "CName", "count" : 19}, -{ "word" : "Categorized", "count" : 19}, -{ "word" : "Cell", "count" : 19}, -{ "word" : "Checking", "count" : 19}, -{ "word" : "Deleting", "count" : 19}, -{ "word" : "Foo", "count" : 19}, -{ "word" : "Highlights", "count" : 19}, -{ "word" : "It", "count" : 19}, -{ "word" : "List", "count" : 19}, -{ "word" : "Modifier", "count" : 19}, -{ "word" : "Movement", "count" : 19}, -{ "word" : "Output", "count" : 19}, -{ "word" : "Queue", "count" : 19}, -{ "word" : "Reference", "count" : 19}, -{ "word" : "Return", "count" : 19}, -{ "word" : "Show", "count" : 19}, -{ "word" : "Sub", "count" : 19}, -{ "word" : "Wrap", "count" : 19}, -{ "word" : "an", "count" : 19}, -{ "word" : "arguments", "count" : 19}, -{ "word" : "candidate", "count" : 19}, -{ "word" : "clear", "count" : 19}, -{ "word" : "copy", "count" : 19}, -{ "word" : "disposition", "count" : 19}, -{ "word" : "drag", "count" : 19}, -{ "word" : "elements", "count" : 19}, -{ "word" : "enable", "count" : 19}, -{ "word" : "enum", "count" : 19}, -{ "word" : "expression", "count" : 19}, -{ "word" : "groups", "count" : 19}, -{ "word" : "icon", "count" : 19}, -{ "word" : "journal", "count" : 19}, -{ "word" : "log", "count" : 19}, -{ "word" : "margin", "count" : 19}, -{ "word" : "read", "count" : 19}, -{ "word" : "replacement", "count" : 19}, -{ "word" : "system", "count" : 19}, -{ "word" : "Bottom", "count" : 20}, -{ "word" : "Disposition", "count" : 20}, -{ "word" : "Enum", "count" : 20}, -{ "word" : "Hasher", "count" : 20}, -{ "word" : "Help", "count" : 20}, -{ "word" : "Level", "count" : 20}, -{ "word" : "NSOutline", "count" : 20}, -{ "word" : "Nested", "count" : 20}, -{ "word" : "Part", "count" : 20}, -{ "word" : "Preference", "count" : 20}, -{ "word" : "Protocol", "count" : 20}, -{ "word" : "Remove", "count" : 20}, -{ "word" : "Returns", "count" : 20}, -{ "word" : "Section", "count" : 20}, -{ "word" : "Substring", "count" : 20}, -{ "word" : "Translation", "count" : 20}, -{ "word" : "Version", "count" : 20}, -{ "word" : "aux", "count" : 20}, -{ "word" : "best", "count" : 20}, -{ "word" : "callback", "count" : 20}, -{ "word" : "callers", "count" : 20}, -{ "word" : "child", "count" : 20}, -{ "word" : "contains", "count" : 20}, -{ "word" : "g", "count" : 20}, -{ "word" : "hasher", "count" : 20}, -{ "word" : "height", "count" : 20}, -{ "word" : "initializer", "count" : 20}, -{ "word" : "leading", "count" : 20}, -{ "word" : "placeholder", "count" : 20}, -{ "word" : "that", "count" : 20}, -{ "word" : "Are", "count" : 21}, -{ "word" : "Availability", "count" : 21}, -{ "word" : "Blank", "count" : 21}, -{ "word" : "Brace", "count" : 21}, -{ "word" : "Changes", "count" : 21}, -{ "word" : "Defaults", "count" : 21}, -{ "word" : "Editable", "count" : 21}, -{ "word" : "Has", "count" : 21}, -{ "word" : "Incremental", "count" : 21}, -{ "word" : "Mark", "count" : 21}, -{ "word" : "NSPasteboard", "count" : 21}, -{ "word" : "One", "count" : 21}, -{ "word" : "Pad", "count" : 21}, -{ "word" : "Page", "count" : 21}, -{ "word" : "Region", "count" : 21}, -{ "word" : "Secondary", "count" : 21}, -{ "word" : "Suggestions", "count" : 21}, -{ "word" : "Switch", "count" : 21}, -{ "word" : "Trailing", "count" : 21}, -{ "word" : "Transient", "count" : 21}, -{ "word" : "UIDrag", "count" : 21}, -{ "word" : "UITable", "count" : 21}, -{ "word" : "animate", "count" : 21}, -{ "word" : "control", "count" : 21}, -{ "word" : "declaration", "count" : 21}, -{ "word" : "expand", "count" : 21}, -{ "word" : "global", "count" : 21}, -{ "word" : "has", "count" : 21}, -{ "word" : "hash", "count" : 21}, -{ "word" : "jump", "count" : 21}, -{ "word" : "numeric", "count" : 21}, -{ "word" : "project", "count" : 21}, -{ "word" : "scene", "count" : 21}, -{ "word" : "uninstall", "count" : 21}, -{ "word" : "Alignment", "count" : 22}, -{ "word" : "Debug", "count" : 22}, -{ "word" : "Feature", "count" : 22}, -{ "word" : "Fold", "count" : 22}, -{ "word" : "History", "count" : 22}, -{ "word" : "Leading", "count" : 22}, -{ "word" : "Match", "count" : 22}, -{ "word" : "Member", "count" : 22}, -{ "word" : "Project", "count" : 22}, -{ "word" : "Reason", "count" : 22}, -{ "word" : "Role", "count" : 22}, -{ "word" : "Search", "count" : 22}, -{ "word" : "Valid", "count" : 22}, -{ "word" : "autoclosure", "count" : 22}, -{ "word" : "call", "count" : 22}, -{ "word" : "cancel", "count" : 22}, -{ "word" : "count", "count" : 22}, -{ "word" : "e", "count" : 22}, -{ "word" : "hide", "count" : 22}, -{ "word" : "navigator", "count" : 22}, -{ "word" : "provider", "count" : 22}, -{ "word" : "reason", "count" : 22}, -{ "word" : "transform", "count" : 22}, -{ "word" : "Argument", "count" : 23}, -{ "word" : "Bytes", "count" : 23}, -{ "word" : "Char", "count" : 23}, -{ "word" : "IDEWorkspace", "count" : 23}, -{ "word" : "Layers", "count" : 23}, -{ "word" : "Open", "count" : 23}, -{ "word" : "Setting", "count" : 23}, -{ "word" : "Title", "count" : 23}, -{ "word" : "Views", "count" : 23}, -{ "word" : "Visualization", "count" : 23}, -{ "word" : "all", "count" : 23}, -{ "word" : "attributed", "count" : 23}, -{ "word" : "baz", "count" : 23}, -{ "word" : "bottom", "count" : 23}, -{ "word" : "closure", "count" : 23}, -{ "word" : "else", "count" : 23}, -{ "word" : "frame", "count" : 23}, -{ "word" : "pre", "count" : 23}, -{ "word" : "references", "count" : 23}, -{ "word" : "setup", "count" : 23}, -{ "word" : "shift", "count" : 23}, -{ "word" : "width", "count" : 23}, -{ "word" : "Custom", "count" : 24}, -{ "word" : "Definition", "count" : 24}, -{ "word" : "Delimiters", "count" : 24}, -{ "word" : "Description", "count" : 24}, -{ "word" : "Files", "count" : 24}, -{ "word" : "Generation", "count" : 24}, -{ "word" : "Internal", "count" : 24}, -{ "word" : "Note", "count" : 24}, -{ "word" : "Points", "count" : 24}, -{ "word" : "Style", "count" : 24}, -{ "word" : "Tab", "count" : 24}, -{ "word" : "Tests", "count" : 24}, -{ "word" : "Time", "count" : 24}, -{ "word" : "Timestamp", "count" : 24}, -{ "word" : "apply", "count" : 24}, -{ "word" : "handler", "count" : 24}, -{ "word" : "install", "count" : 24}, -{ "word" : "suggested", "count" : 24}, -{ "word" : "theme", "count" : 24}, -{ "word" : "verify", "count" : 24}, -{ "word" : "Background", "count" : 25}, -{ "word" : "Coverage", "count" : 25}, -{ "word" : "Doesnt", "count" : 25}, -{ "word" : "Framework", "count" : 25}, -{ "word" : "Matches", "count" : 25}, -{ "word" : "Matching", "count" : 25}, -{ "word" : "Preprocessor", "count" : 25}, -{ "word" : "by", "count" : 25}, -{ "word" : "category", "count" : 25}, -{ "word" : "column", "count" : 25}, -{ "word" : "instance", "count" : 25}, -{ "word" : "operator", "count" : 25}, -{ "word" : "property", "count" : 25}, -{ "word" : "relayout", "count" : 25}, -{ "word" : "Area", "count" : 26}, -{ "word" : "Begin", "count" : 26}, -{ "word" : "Callback", "count" : 26}, -{ "word" : "Characters", "count" : 26}, -{ "word" : "Hashable", "count" : 26}, -{ "word" : "Label", "count" : 26}, -{ "word" : "NSTable", "count" : 26}, -{ "word" : "Needs", "count" : 26}, -{ "word" : "Rects", "count" : 26}, -{ "word" : "Spacing", "count" : 26}, -{ "word" : "Static", "count" : 26}, -{ "word" : "Typing", "count" : 26}, -{ "word" : "U", "count" : 26}, -{ "word" : "Updated", "count" : 26}, -{ "word" : "When", "count" : 26}, -{ "word" : "character", "count" : 26}, -{ "word" : "col", "count" : 26}, -{ "word" : "defining", "count" : 26}, -{ "word" : "direction", "count" : 26}, -{ "word" : "interaction", "count" : 26}, -{ "word" : "markup", "count" : 26}, -{ "word" : "post", "count" : 26}, -{ "word" : "row", "count" : 26}, -{ "word" : "target", "count" : 26}, -{ "word" : "we", "count" : 26}, -{ "word" : "where", "count" : 26}, -{ "word" : "word", "count" : 26}, -{ "word" : "As", "count" : 27}, -{ "word" : "Assistant", "count" : 27}, -{ "word" : "Height", "count" : 27}, -{ "word" : "Landmarks", "count" : 27}, -{ "word" : "NSAccessibility", "count" : 27}, -{ "word" : "Names", "count" : 27}, -{ "word" : "Wrapping", "count" : 27}, -{ "word" : "enumerate", "count" : 27}, -{ "word" : "group", "count" : 27}, -{ "word" : "init", "count" : 27}, -{ "word" : "number", "count" : 27}, -{ "word" : "query", "count" : 27}, -{ "word" : "structured", "count" : 27}, -{ "word" : "translator", "count" : 27}, -{ "word" : "After", "count" : 28}, -{ "word" : "Equivalent", "count" : 28}, -{ "word" : "Finish", "count" : 28}, -{ "word" : "Formatter", "count" : 28}, -{ "word" : "Global", "count" : 28}, -{ "word" : "Insets", "count" : 28}, -{ "word" : "Pass", "count" : 28}, -{ "word" : "Replacement", "count" : 28}, -{ "word" : "Translator", "count" : 28}, -{ "word" : "block", "count" : 28}, -{ "word" : "dragging", "count" : 28}, -{ "word" : "matching", "count" : 28}, -{ "word" : "meow", "count" : 28}, -{ "word" : "message", "count" : 28}, -{ "word" : "response", "count" : 28}, -{ "word" : "send", "count" : 28}, -{ "word" : "touches", "count" : 28}, -{ "word" : "workspace", "count" : 28}, -{ "word" : "A", "count" : 29}, -{ "word" : "CAAction", "count" : 29}, -{ "word" : "Commands", "count" : 29}, -{ "word" : "Drop", "count" : 29}, -{ "word" : "Import", "count" : 29}, -{ "word" : "Literal", "count" : 29}, -{ "word" : "Space", "count" : 29}, -{ "word" : "Throws", "count" : 29}, -{ "word" : "command", "count" : 29}, -{ "word" : "lines", "count" : 29}, -{ "word" : "mouse", "count" : 29}, -{ "word" : "nreturn", "count" : 29}, -{ "word" : "options", "count" : 29}, -{ "word" : "selected", "count" : 29}, -{ "word" : "this", "count" : 29}, -{ "word" : "Auxiliary", "count" : 30}, -{ "word" : "Declarator", "count" : 30}, -{ "word" : "Effect", "count" : 30}, -{ "word" : "Empty", "count" : 30}, -{ "word" : "Encoding", "count" : 30}, -{ "word" : "Icon", "count" : 30}, -{ "word" : "Macro", "count" : 30}, -{ "word" : "NSString", "count" : 30}, -{ "word" : "Responder", "count" : 30}, -{ "word" : "Unkeyed", "count" : 30}, -{ "word" : "adjust", "count" : 30}, -{ "word" : "completions", "count" : 30}, -{ "word" : "formatting", "count" : 30}, -{ "word" : "inserted", "count" : 30}, -{ "word" : "into", "count" : 30}, -{ "word" : "search", "count" : 30}, -{ "word" : "utility", "count" : 30}, -{ "word" : "x", "count" : 30}, -{ "word" : "Close", "count" : 31}, -{ "word" : "Groups", "count" : 31}, -{ "word" : "Interval", "count" : 31}, -{ "word" : "Multi", "count" : 31}, -{ "word" : "Navigator", "count" : 31}, -{ "word" : "Property", "count" : 31}, -{ "word" : "Split", "count" : 31}, -{ "word" : "Wrapped", "count" : 31}, -{ "word" : "char", "count" : 31}, -{ "word" : "delete", "count" : 31}, -{ "word" : "my", "count" : 31}, -{ "word" : "prefix", "count" : 31}, -{ "word" : "quick", "count" : 31}, -{ "word" : "s", "count" : 31}, -{ "word" : "user", "count" : 31}, -{ "word" : "Edge", "count" : 32}, -{ "word" : "Modify", "count" : 32}, -{ "word" : "Statement", "count" : 32}, -{ "word" : "UIGesture", "count" : 32}, -{ "word" : "close", "count" : 32}, -{ "word" : "draw", "count" : 32}, -{ "word" : "formatter", "count" : 32}, -{ "word" : "import", "count" : 32}, -{ "word" : "uid", "count" : 32}, -{ "word" : "utf", "count" : 32}, -{ "word" : "C", "count" : 33}, -{ "word" : "CMBlock", "count" : 33}, -{ "word" : "Comments", "count" : 33}, -{ "word" : "Filter", "count" : 33}, -{ "word" : "Journal", "count" : 33}, -{ "word" : "Overrides", "count" : 33}, -{ "word" : "Parameter", "count" : 33}, -{ "word" : "Scalar", "count" : 33}, -{ "word" : "Substitution", "count" : 33}, -{ "word" : "application", "count" : 33}, -{ "word" : "base", "count" : 33}, -{ "word" : "current", "count" : 33}, -{ "word" : "documentation", "count" : 33}, -{ "word" : "first", "count" : 33}, -{ "word" : "functionality", "count" : 33}, -{ "word" : "highlight", "count" : 33}, -{ "word" : "id", "count" : 33}, -{ "word" : "results", "count" : 33}, -{ "word" : "some", "count" : 33}, -{ "word" : "Continuation", "count" : 34}, -{ "word" : "Diagnostics", "count" : 34}, -{ "word" : "Non", "count" : 34}, -{ "word" : "Refactoring", "count" : 34}, -{ "word" : "Viewport", "count" : 34}, -{ "word" : "adjusted", "count" : 34}, -{ "word" : "append", "count" : 34}, -{ "word" : "begin", "count" : 34}, -{ "word" : "node", "count" : 34}, -{ "word" : "observer", "count" : 34}, -{ "word" : "old", "count" : 34}, -{ "word" : "Category", "count" : 35}, -{ "word" : "Dragging", "count" : 35}, -{ "word" : "NSDocument", "count" : 35}, -{ "word" : "it", "count" : 35}, -{ "word" : "observed", "count" : 35}, -{ "word" : "or", "count" : 35}, -{ "word" : "stop", "count" : 35}, -{ "word" : "syntaxcolor", "count" : 35}, -{ "word" : "Copy", "count" : 36}, -{ "word" : "Formatting", "count" : 36}, -{ "word" : "IDEIndex", "count" : 36}, -{ "word" : "Lozenge", "count" : 36}, -{ "word" : "Placement", "count" : 36}, -{ "word" : "R", "count" : 36}, -{ "word" : "Row", "count" : 36}, -{ "word" : "Scoring", "count" : 36}, -{ "word" : "Settings", "count" : 36}, -{ "word" : "Syntax", "count" : 36}, -{ "word" : "Unicode", "count" : 36}, -{ "word" : "accessor", "count" : 36}, -{ "word" : "gutter", "count" : 36}, -{ "word" : "named", "count" : 36}, -{ "word" : "object", "count" : 36}, -{ "word" : "quickly", "count" : 36}, -{ "word" : "replace", "count" : 36}, -{ "word" : "Arg", "count" : 37}, -{ "word" : "Configuration", "count" : 37}, -{ "word" : "Map", "count" : 37}, -{ "word" : "Relative", "count" : 37}, -{ "word" : "Tokens", "count" : 37}, -{ "word" : "characters", "count" : 37}, -{ "word" : "container", "count" : 37}, -{ "word" : "label", "count" : 37}, -{ "word" : "path", "count" : 37}, -{ "word" : "ranges", "count" : 37}, -{ "word" : "Arrow", "count" : 38}, -{ "word" : "Batch", "count" : 38}, -{ "word" : "Children", "count" : 38}, -{ "word" : "IDESource", "count" : 38}, -{ "word" : "Self", "count" : 38}, -{ "word" : "Service", "count" : 38}, -{ "word" : "gesture", "count" : 38}, -{ "word" : "process", "count" : 38}, -{ "word" : "reset", "count" : 38}, -{ "word" : "toggle", "count" : 38}, -{ "word" : "var", "count" : 38}, -{ "word" : "Inserting", "count" : 39}, -{ "word" : "Keystroke", "count" : 39}, -{ "word" : "Markup", "count" : 39}, -{ "word" : "New", "count" : 39}, -{ "word" : "Transaction", "count" : 39}, -{ "word" : "buffer", "count" : 39}, -{ "word" : "double", "count" : 39}, -{ "word" : "encode", "count" : 39}, -{ "word" : "using", "count" : 39}, -{ "word" : "APINotes", "count" : 40}, -{ "word" : "Field", "count" : 40}, -{ "word" : "NSDragging", "count" : 40}, -{ "word" : "Right", "count" : 40}, -{ "word" : "map", "count" : 40}, -{ "word" : "scope", "count" : 40}, -{ "word" : "Mutable", "count" : 41}, -{ "word" : "Optional", "count" : 41}, -{ "word" : "Overlay", "count" : 41}, -{ "word" : "UTF", "count" : 41}, -{ "word" : "invalidate", "count" : 41}, -{ "word" : "session", "count" : 41}, -{ "word" : "Args", "count" : 42}, -{ "word" : "CType", "count" : 42}, -{ "word" : "Call", "count" : 42}, -{ "word" : "Dictionary", "count" : 42}, -{ "word" : "Load", "count" : 42}, -{ "word" : "Raw", "count" : 42}, -{ "word" : "Shift", "count" : 42}, -{ "word" : "result", "count" : 42}, -{ "word" : "size", "count" : 42}, -{ "word" : "Aux", "count" : 43}, -{ "word" : "Case", "count" : 43}, -{ "word" : "Gesture", "count" : 43}, -{ "word" : "Insertion", "count" : 43}, -{ "word" : "Or", "count" : 43}, -{ "word" : "and", "count" : 43}, -{ "word" : "i", "count" : 43}, -{ "word" : "modifier", "count" : 43}, -{ "word" : "offset", "count" : 43}, -{ "word" : "r", "count" : 43}, -{ "word" : "rect", "count" : 43}, -{ "word" : "Elements", "count" : 44}, -{ "word" : "Invalidate", "count" : 44}, -{ "word" : "NSView", "count" : 44}, -{ "word" : "Whitespace", "count" : 44}, -{ "word" : "filter", "count" : 44}, -{ "word" : "items", "count" : 44}, -{ "word" : "outline", "count" : 44}, -{ "word" : "types", "count" : 44}, -{ "word" : "Annotations", "count" : 45}, -{ "word" : "Float", "count" : 45}, -{ "word" : "NSMutable", "count" : 45}, -{ "word" : "Needed", "count" : 45}, -{ "word" : "change", "count" : 45}, -{ "word" : "create", "count" : 45}, -{ "word" : "present", "count" : 45}, -{ "word" : "Active", "count" : 46}, -{ "word" : "Click", "count" : 46}, -{ "word" : "Descriptor", "count" : 46}, -{ "word" : "Request", "count" : 46}, -{ "word" : "arg", "count" : 46}, -{ "word" : "c", "count" : 46}, -{ "word" : "hello", "count" : 46}, -{ "word" : "landmarks", "count" : 46}, -{ "word" : "level", "count" : 46}, -{ "word" : "validate", "count" : 46}, -{ "word" : "Assert", "count" : 47}, -{ "word" : "Parse", "count" : 47}, -{ "word" : "Replace", "count" : 47}, -{ "word" : "Snippet", "count" : 47}, -{ "word" : "Start", "count" : 47}, -{ "word" : "The", "count" : 47}, -{ "word" : "UIText", "count" : 47}, -{ "word" : "default", "count" : 47}, -{ "word" : "Direction", "count" : 48}, -{ "word" : "IDs", "count" : 48}, -{ "word" : "Keyword", "count" : 48}, -{ "word" : "animated", "count" : 48}, -{ "word" : "str", "count" : 48}, -{ "word" : "xcode", "count" : 48}, -{ "word" : "Add", "count" : 49}, -{ "word" : "Control", "count" : 49}, -{ "word" : "Forward", "count" : 49}, -{ "word" : "Handler", "count" : 49}, -{ "word" : "Offset", "count" : 49}, -{ "word" : "Unsafe", "count" : 49}, -{ "word" : "as", "count" : 49}, -{ "word" : "indent", "count" : 49}, -{ "word" : "raw", "count" : 49}, -{ "word" : "Backward", "count" : 50}, -{ "word" : "Contents", "count" : 50}, -{ "word" : "Mode", "count" : 50}, -{ "word" : "Undo", "count" : 50}, -{ "word" : "array", "count" : 50}, -{ "word" : "bar", "count" : 50}, -{ "word" : "Comment", "count" : 51}, -{ "word" : "No", "count" : 51}, -{ "word" : "next", "count" : 51}, -{ "word" : "Collection", "count" : 52}, -{ "word" : "Beginning", "count" : 53}, -{ "word" : "Default", "count" : 53}, -{ "word" : "Error", "count" : 53}, -{ "word" : "NSAttributed", "count" : 53}, -{ "word" : "NSRect", "count" : 53}, -{ "word" : "Rename", "count" : 53}, -{ "word" : "collection", "count" : 53}, -{ "word" : "register", "count" : 53}, -{ "word" : "Attributes", "count" : 54}, -{ "word" : "Container", "count" : 54}, -{ "word" : "Image", "count" : 54}, -{ "word" : "Margin", "count" : 54}, -{ "word" : "Process", "count" : 54}, -{ "word" : "annotation", "count" : 54}, -{ "word" : "discardable", "count" : 54}, -{ "word" : "parameter", "count" : 54}, -{ "word" : "request", "count" : 54}, -{ "word" : "Log", "count" : 55}, -{ "word" : "Scoped", "count" : 55}, -{ "word" : "on", "count" : 55}, -{ "word" : "variant", "count" : 55}, -{ "word" : "CGSize", "count" : 56}, -{ "word" : "Perform", "count" : 56}, -{ "word" : "body", "count" : 56}, -{ "word" : "end", "count" : 56}, -{ "word" : "ids", "count" : 56}, -{ "word" : "By", "count" : 57}, -{ "word" : "Previous", "count" : 57}, -{ "word" : "Visible", "count" : 57}, -{ "word" : "can", "count" : 57}, -{ "word" : "menu", "count" : 57}, -{ "word" : "other", "count" : 57}, -{ "word" : "state", "count" : 57}, -{ "word" : "First", "count" : 58}, -{ "word" : "Interface", "count" : 58}, -{ "word" : "Left", "count" : 58}, -{ "word" : "Multiple", "count" : 58}, -{ "word" : "NSMenu", "count" : 58}, -{ "word" : "User", "count" : 58}, -{ "word" : "include", "count" : 58}, -{ "word" : "window", "count" : 58}, -{ "word" : "Count", "count" : 59}, -{ "word" : "Width", "count" : 59}, -{ "word" : "Cursor", "count" : 60}, -{ "word" : "Paragraph", "count" : 60}, -{ "word" : "font", "count" : 60}, -{ "word" : "landmark", "count" : 60}, -{ "word" : "parse", "count" : 60}, -{ "word" : "ref", "count" : 60}, -{ "word" : "Array", "count" : 61}, -{ "word" : "Interaction", "count" : 61}, -{ "word" : "Selected", "count" : 61}, -{ "word" : "parent", "count" : 61}, -{ "word" : "value", "count" : 61}, -{ "word" : "Next", "count" : 62}, -{ "word" : "Pointer", "count" : 62}, -{ "word" : "reference", "count" : 62}, -{ "word" : "System", "count" : 63}, -{ "word" : "b", "count" : 63}, -{ "word" : "enclosing", "count" : 63}, -{ "word" : "make", "count" : 63}, -{ "word" : "NSKey", "count" : 64}, -{ "word" : "code", "count" : 64}, -{ "word" : "notification", "count" : 64}, -{ "word" : "Accessibility", "count" : 65}, -{ "word" : "Documentation", "count" : 65}, -{ "word" : "Expression", "count" : 65}, -{ "word" : "Flags", "count" : 65}, -{ "word" : "Is", "count" : 65}, -{ "word" : "scroll", "count" : 65}, -{ "word" : "syntax", "count" : 65}, -{ "word" : "If", "count" : 66}, -{ "word" : "Number", "count" : 66}, -{ "word" : "Structured", "count" : 66}, -{ "word" : "action", "count" : 66}, -{ "word" : "symbol", "count" : 66}, -{ "word" : "Current", "count" : 67}, -{ "word" : "kind", "count" : 68}, -{ "word" : "Class", "count" : 69}, -{ "word" : "DVTText", "count" : 69}, -{ "word" : "Status", "count" : 69}, -{ "word" : "Completions", "count" : 70}, -{ "word" : "Group", "count" : 70}, -{ "word" : "Selector", "count" : 70}, -{ "word" : "Something", "count" : 70}, -{ "word" : "insert", "count" : 70}, -{ "word" : "Double", "count" : 71}, -{ "word" : "Frame", "count" : 71}, -{ "word" : "Placeholder", "count" : 71}, -{ "word" : "find", "count" : 71}, -{ "word" : "new", "count" : 71}, -{ "word" : "table", "count" : 71}, -{ "word" : "Update", "count" : 72}, -{ "word" : "Accessory", "count" : 73}, -{ "word" : "Diagnostic", "count" : 73}, -{ "word" : "Objc", "count" : 73}, -{ "word" : "Session", "count" : 73}, -{ "word" : "Mouse", "count" : 74}, -{ "word" : "Types", "count" : 74}, -{ "word" : "color", "count" : 74}, -{ "word" : "structure", "count" : 74}, -{ "word" : "Performance", "count" : 75}, -{ "word" : "Panel", "count" : 76}, -{ "word" : "Recognizer", "count" : 76}, -{ "word" : "Results", "count" : 76}, -{ "word" : "mutating", "count" : 76}, -{ "word" : "Drag", "count" : 77}, -{ "word" : "Button", "count" : 78}, -{ "word" : "DVTSource", "count" : 78}, -{ "word" : "start", "count" : 78}, -{ "word" : "Observing", "count" : 80}, -{ "word" : "Column", "count" : 81}, -{ "word" : "Display", "count" : 81}, -{ "word" : "Keypath", "count" : 81}, -{ "word" : "Single", "count" : 81}, -{ "word" : "Consumer", "count" : 82}, -{ "word" : "display", "count" : 82}, -{ "word" : "do", "count" : 82}, -{ "word" : "functions", "count" : 82}, -{ "word" : "Pane", "count" : 83}, -{ "word" : "Quickly", "count" : 83}, -{ "word" : "Scroll", "count" : 83}, -{ "word" : "Down", "count" : 84}, -{ "word" : "Size", "count" : 84}, -{ "word" : "element", "count" : 84}, -{ "word" : "Scope", "count" : 85}, -{ "word" : "Lines", "count" : 86}, -{ "word" : "Module", "count" : 86}, -{ "word" : "Query", "count" : 86}, -{ "word" : "accessibility", "count" : 86}, -{ "word" : "Indent", "count" : 87}, -{ "word" : "Will", "count" : 87}, -{ "word" : "show", "count" : 87}, -{ "word" : "Highlight", "count" : 88}, -{ "word" : "Decl", "count" : 89}, -{ "word" : "Character", "count" : 90}, -{ "word" : "Extension", "count" : 90}, -{ "word" : "On", "count" : 90}, -{ "word" : "Override", "count" : 91}, -{ "word" : "Rect", "count" : 91}, -{ "word" : "Theme", "count" : 91}, -{ "word" : "lhs", "count" : 91}, -{ "word" : "Cpp", "count" : 92}, -{ "word" : "Paste", "count" : 92}, -{ "word" : "move", "count" : 92}, -{ "word" : "Word", "count" : 93}, -{ "word" : "Items", "count" : 94}, -{ "word" : "rhs", "count" : 94}, -{ "word" : "language", "count" : 95}, -{ "word" : "location", "count" : 96}, -{ "word" : "Attribute", "count" : 98}, -{ "word" : "UID", "count" : 98}, -{ "word" : "Composite", "count" : 99}, -{ "word" : "Info", "count" : 99}, -{ "word" : "Observer", "count" : 99}, -{ "word" : "Test", "count" : 99}, -{ "word" : "Target", "count" : 100}, -{ "word" : "point", "count" : 100}, -{ "word" : "NSEvent", "count" : 101}, -{ "word" : "Point", "count" : 101}, -{ "word" : "url", "count" : 101}, -{ "word" : "Buffer", "count" : 102}, -{ "word" : "UInt", "count" : 102}, -{ "word" : "Notification", "count" : 103}, -{ "word" : "Quick", "count" : 103}, -{ "word" : "editor", "count" : 103}, -{ "word" : "get", "count" : 104}, -{ "word" : "End", "count" : 105}, -{ "word" : "case", "count" : 106}, -{ "word" : "navigation", "count" : 106}, -{ "word" : "Operation", "count" : 107}, -{ "word" : "Editing", "count" : 108}, -{ "word" : "Insert", "count" : 110}, -{ "word" : "document", "count" : 110}, -{ "word" : "select", "count" : 111}, -{ "word" : "Move", "count" : 112}, -{ "word" : "selection", "count" : 112}, -{ "word" : "will", "count" : 113}, -{ "word" : "FALSE", "count" : 115}, -{ "word" : "index", "count" : 115}, -{ "word" : "Suggestion", "count" : 116}, -{ "word" : "Vi", "count" : 116}, -{ "word" : "XCTAssert", "count" : 117}, -{ "word" : "of", "count" : 117}, -{ "word" : "IBAction", "count" : 119}, -{ "word" : "identifier", "count" : 119}, -{ "word" : "token", "count" : 119}, -{ "word" : "int", "count" : 120}, -{ "word" : "All", "count" : 121}, -{ "word" : "Declaration", "count" : 121}, -{ "word" : "sourcekit", "count" : 121}, -{ "word" : "should", "count" : 122}, -{ "word" : "Obj", "count" : 123}, -{ "word" : "Select", "count" : 123}, -{ "word" : "Window", "count" : 123}, -{ "word" : "Equal", "count" : 124}, -{ "word" : "remove", "count" : 124}, -{ "word" : "t", "count" : 128}, -{ "word" : "decl", "count" : 129}, -{ "word" : "optional", "count" : 130}, -{ "word" : "handle", "count" : 132}, -{ "word" : "T", "count" : 133}, -{ "word" : "Bindings", "count" : 134}, -{ "word" : "CGRect", "count" : 134}, -{ "word" : "Gutter", "count" : 134}, -{ "word" : "internal", "count" : 134}, -{ "word" : "Kind", "count" : 135}, -{ "word" : "Structure", "count" : 136}, -{ "word" : "observe", "count" : 136}, -{ "word" : "Provider", "count" : 138}, -{ "word" : "Ranges", "count" : 140}, -{ "word" : "Method", "count" : 141}, -{ "word" : "Up", "count" : 141}, -{ "word" : "inout", "count" : 141}, -{ "word" : "Bar", "count" : 142}, -{ "word" : "Function", "count" : 143}, -{ "word" : "from", "count" : 147}, -{ "word" : "Delete", "count" : 148}, -{ "word" : "From", "count" : 149}, -{ "word" : "method", "count" : 149}, -{ "word" : "Location", "count" : 150}, -{ "word" : "if", "count" : 152}, -{ "word" : "Object", "count" : 154}, -{ "word" : "Minimap", "count" : 156}, -{ "word" : "Change", "count" : 157}, -{ "word" : "Block", "count" : 160}, -{ "word" : "ID", "count" : 160}, -{ "word" : "the", "count" : 170}, -{ "word" : "Changed", "count" : 172}, -{ "word" : "Options", "count" : 174}, -{ "word" : "add", "count" : 174}, -{ "word" : "Action", "count" : 176}, -{ "word" : "Menu", "count" : 176}, -{ "word" : "Layer", "count" : 177}, -{ "word" : "Value", "count" : 179}, -{ "word" : "Response", "count" : 180}, -{ "word" : "return", "count" : 181}, -{ "word" : "file", "count" : 182}, -{ "word" : "let", "count" : 182}, -{ "word" : "text", "count" : 182}, -{ "word" : "CGPoint", "count" : 185}, -{ "word" : "NSRange", "count" : 185}, -{ "word" : "completion", "count" : 186}, -{ "word" : "content", "count" : 186}, -{ "word" : "Landmark", "count" : 190}, -{ "word" : "escaping", "count" : 192}, -{ "word" : "TRUE", "count" : 194}, -{ "word" : "Set", "count" : 196}, -{ "word" : "State", "count" : 198}, -{ "word" : "Void", "count" : 198}, -{ "word" : "Node", "count" : 199}, -{ "word" : "Edit", "count" : 201}, -{ "word" : "Path", "count" : 205}, -{ "word" : "class", "count" : 210}, -{ "word" : "Func", "count" : 212}, -{ "word" : "key", "count" : 213}, -{ "word" : "Def", "count" : 216}, -{ "word" : "event", "count" : 221}, -{ "word" : "Color", "count" : 223}, -{ "word" : "set", "count" : 224}, -{ "word" : "Kit", "count" : 225}, -{ "word" : "view", "count" : 225}, -{ "word" : "Language", "count" : 227}, -{ "word" : "lang", "count" : 229}, -{ "word" : "perform", "count" : 233}, -{ "word" : "And", "count" : 236}, -{ "word" : "Navigation", "count" : 238}, -{ "word" : "swift", "count" : 239}, -{ "word" : "type", "count" : 241}, -{ "word" : "Font", "count" : 242}, -{ "word" : "Find", "count" : 247}, -{ "word" : "Of", "count" : 252}, -{ "word" : "to", "count" : 252}, -{ "word" : "is", "count" : 257}, -{ "word" : "At", "count" : 259}, -{ "word" : "File", "count" : 259}, -{ "word" : "nil", "count" : 266}, -{ "word" : "Controller", "count" : 267}, -{ "word" : "CGFloat", "count" : 268}, -{ "word" : "fileprivate", "count" : 268}, -{ "word" : "Event", "count" : 272}, -{ "word" : "Did", "count" : 275}, -{ "word" : "Index", "count" : 275}, -{ "word" : "Token", "count" : 276}, -{ "word" : "Annotation", "count" : 280}, -{ "word" : "Document", "count" : 286}, -{ "word" : "Result", "count" : 299}, -{ "word" : "Completion", "count" : 301}, -{ "word" : "did", "count" : 306}, -{ "word" : "item", "count" : 313}, -{ "word" : "layout", "count" : 314}, -{ "word" : "name", "count" : 317}, -{ "word" : "update", "count" : 334}, -{ "word" : "Element", "count" : 340}, -{ "word" : "context", "count" : 344}, -{ "word" : "at", "count" : 346}, -{ "word" : "With", "count" : 365}, -{ "word" : "Content", "count" : 373}, -{ "word" : "a", "count" : 373}, -{ "word" : "URL", "count" : 380}, -{ "word" : "Key", "count" : 383}, -{ "word" : "Name", "count" : 387}, -{ "word" : "string", "count" : 391}, -{ "word" : "Swift", "count" : 400}, -{ "word" : "In", "count" : 417}, -{ "word" : "SMSource", "count" : 429}, -{ "word" : "Manager", "count" : 435}, -{ "word" : "Identifier", "count" : 437}, -{ "word" : "with", "count" : 438}, -{ "word" : "objc", "count" : 440}, -{ "word" : "position", "count" : 440}, -{ "word" : "Code", "count" : 466}, -{ "word" : "Context", "count" : 480}, -{ "word" : "Layout", "count" : 483}, -{ "word" : "foo", "count" : 483}, -{ "word" : "Data", "count" : 495}, -{ "word" : "Command", "count" : 508}, -{ "word" : "To", "count" : 518}, -{ "word" : "Selection", "count" : 534}, -{ "word" : "in", "count" : 542}, -{ "word" : "Text", "count" : 546}, -{ "word" : "open", "count" : 564}, -{ "word" : "Model", "count" : 576}, -{ "word" : "data", "count" : 581}, -{ "word" : "Cache", "count" : 590}, -{ "word" : "range", "count" : 602}, -{ "word" : "sender", "count" : 632}, -{ "word" : "source", "count" : 635}, -{ "word" : "Symbol", "count" : 736}, -{ "word" : "line", "count" : 773}, -{ "word" : "Item", "count" : 812}, -{ "word" : "function", "count" : 824}, -{ "word" : "Position", "count" : 846}, -{ "word" : "for", "count" : 879}, -{ "word" : "Any", "count" : 905}, -{ "word" : "For", "count" : 928}, -{ "word" : "n", "count" : 953}, -{ "word" : "Type", "count" : 1063}, -{ "word" : "Line", "count" : 1122}, -{ "word" : "Int", "count" : 1282}, -{ "word" : "Bool", "count" : 1459}, -{ "word" : "View", "count" : 1555}, -{ "word" : "String", "count" : 1753}, -{ "word" : "test", "count" : 1917}, -{ "word" : "Range", "count" : 2311}, -{ "word" : "Editor", "count" : 3726}, -{ "word" : "Source", "count" : 4647}, -] diff --git a/Sources/CompletionScoringTestSupport/INPUTS/RandomSeed.plist b/Sources/CompletionScoringTestSupport/INPUTS/RandomSeed.plist deleted file mode 100644 index 4bb648ec5..000000000 --- a/Sources/CompletionScoringTestSupport/INPUTS/RandomSeed.plist +++ /dev/null @@ -1,1030 +0,0 @@ - - - - - 10649861844062857673 - 9060052091246972257 - 12942090708244661392 - 9933494248155016441 - 10828989690894343768 - 12788493555305265074 - 488913797917615037 - 17822819483841388785 - 16041325000510672802 - 17190357727871864693 - 2604045150856744782 - 3838047026243215059 - 16401602770669964302 - 17107743994455227755 - 15434291936037106882 - 4225037278253001179 - 2226084105488369337 - 4687545976950943822 - 18300466681609490739 - 1980023098261872498 - 17670050748207816560 - 12736629629229369041 - 17534768455463174174 - 8898732241997181651 - 11931833952676904910 - 4180390725280484058 - 17205636073021493376 - 8276422979551858588 - 11016946354289893893 - 12308495956251452848 - 7263476329554197015 - 16648149319419784874 - 6910896050161240625 - 4537394396171061871 - 14330771505108052901 - 2648492956127761911 - 4573649173901336171 - 14309751716159813221 - 5292378636516124469 - 4275765076679543001 - 16800899818181450055 - 18046600854094793356 - 3328010366260167308 - 18321147642883125694 - 6369084976403485091 - 588331132772749814 - 11797866875121243596 - 11370890767156732586 - 7719936951740554188 - 3556339505485058626 - 17945163430954770531 - 16384587621745106164 - 14177709071124149204 - 16389318410635712785 - 12629884384090305288 - 3952432847401018371 - 8887921331038563277 - 9224845742834590146 - 7047212806667268568 - 5387596695274976479 - 1131203715221761975 - 2995057563514828948 - 558281077740462941 - 5367201933815075486 - 18027555175583703203 - 3815931685421562694 - 15880990512495071458 - 11896350560114355651 - 15814670821480969269 - 10935248045238249312 - 14181477726458506552 - 13315947572172874533 - 2470569774214883886 - 2798813742852898826 - 17614405024579320401 - 6523750826029322289 - 6756800893765504828 - 9818274192037245886 - 5117070528299740587 - 13814322025010542269 - 10356863184679782778 - 14975879377646477909 - 2350268543890682019 - 6631595411582422424 - 13277607298226234479 - 10385880706748317705 - 3660035258487782819 - 6008288383785847474 - 246601306717851796 - 848618438465728844 - 2070517310903822636 - 7432124920080869709 - 687381512677760101 - 12015022105020395012 - 3475230368836172952 - 16455552951368856545 - 15441048807568448572 - 8546565033941286823 - 16604558369994954083 - 11511725767193317464 - 10700027132809754525 - 10125005576366217941 - 14173153666902620086 - 18435111687658672088 - 6081171684353748341 - 7117220862167305877 - 8715329109917373981 - 14576066639047453787 - 6819838309247178119 - 5315591605791168058 - 4183658152695964138 - 15850723641679793030 - 3260394523996062592 - 14708104201405533466 - 16750871475452877735 - 1501301636767725184 - 4175247676013603295 - 9365772035441220932 - 2493974198448035110 - 16370832881004661625 - 7232607699507705655 - 7089957862226922504 - 8889564998214887059 - 13250930397927823913 - 7603648579649087770 - 8806788810289802956 - 10485507589128383963 - 8796912621312452297 - 7944218858783999674 - 13592161546327668189 - 15835726225464933071 - 15756432662032529541 - 12073430065772685012 - 6952632564689999453 - 11249630342032732808 - 2509867731429781047 - 16904639689230170389 - 17612059305728605918 - 13851406172513444310 - 5483410043989408105 - 6938766405332021603 - 1510949420482395497 - 2996629394982073843 - 10288087186209705197 - 15778600019175082509 - 1497188676751912940 - 12456166283648243832 - 4328215192563713093 - 16063362166699614075 - 224440550801654658 - 467173145809158449 - 6051582188300983325 - 1521644966635562847 - 5349583029163228262 - 8803258351493233214 - 6207477265726665160 - 12490477206949640997 - 12173050753365171247 - 16627447209970552133 - 4899007879465179257 - 1032877983252151766 - 14800195005248586699 - 103714118666334821 - 6331463130237455963 - 8023292035116829472 - 12743427328631916460 - 10893743866060136956 - 11784908726336611686 - 9272180451441530572 - 5226640388588021031 - 5282421459110945544 - 4603209530857826057 - 3405454570302181247 - 2652317086563278175 - 16267939348686284725 - 18037428696829987574 - 15592816560914422821 - 8812378943602908405 - 14025012160250685737 - 15162339649807406865 - 15215894536809080237 - 10167694656226448375 - 14702025245317465605 - 9372459083077415041 - 818358333489715854 - 14117643407301722750 - 8879804017142196824 - 112533076176097978 - 1511090644119764320 - 8225431350377428647 - 6199691980930380308 - 6828841551981237016 - 17750823230854363753 - 2467076291276307182 - 12215749135069455309 - 5638571524478586382 - 15678666311027847249 - 4143399646329005882 - 16113772543508599165 - 8209040153748446089 - 15112564987413698859 - 6172570544118441475 - 10322741172264022672 - 803053488013042119 - 4595786559820044176 - 15471468821072007798 - 11236864219566698735 - 17624017015062092641 - 14399242371297004032 - 11896041473242596087 - 11618110437649791830 - 14247925393773410211 - 17756227468854180793 - 16771115508141070992 - 2234447464881468255 - 9617656292538057974 - 5370920711691898768 - 2424105781649600271 - 1492710966453179177 - 136463354103358456 - 770480694893157328 - 14250317640649090542 - 8448646640351200030 - 1161853657331519536 - 4401159506571376371 - 6591894851150470146 - 5922434182959282354 - 6217920520259148784 - 15347344960336749413 - 13504388544471169084 - 13541491650812014632 - 1192070502729533005 - 16359984411140186755 - 12842056301276820267 - 5290884053919160288 - 1955099608780971257 - 3106135892764740981 - 1085016533782390853 - 17608170015828607165 - 13156774632397276808 - 14073268027679629308 - 3715854401229574121 - 13116947759532507420 - 2876934206571122475 - 14076682054552377635 - 12243305646559136372 - 12223706178285816638 - 14979559968528001213 - 12335417939297159304 - 2936252503089252940 - 3747382846822585987 - 15095295351041827739 - 5591514828918870630 - 7683111237086780725 - 17580409963527887307 - 3464617359382551874 - 10756855216724740053 - 15632332098803710275 - 17710282918878009389 - 10723606802310966690 - 7757433379777905208 - 8493023912912638493 - 16878616212254342421 - 16309339687315968312 - 5312406456728738908 - 6326506451277319174 - 8667727879590816023 - 1494069411588951712 - 17297971643510650192 - 16917457951535409155 - 10664288556384772179 - 14708376270460122483 - 6181317130280679048 - 870273264114765336 - 5525322326233390453 - 17397676358668710084 - 15577340719173711292 - 14960804116168411205 - 4187771741316334282 - 6445682767178346728 - 7330689919879062999 - 2190542648006954640 - 751295493049147875 - 14946585476217475215 - 4359629194490935816 - 1717022239925159845 - 8096410117681086795 - 13478634282918720960 - 16681040550139062010 - 4278750200559248307 - 15200667783814360428 - 5476254718955324267 - 13455357500767250649 - 10131480236589848261 - 12311081712143503650 - 2451208355928096722 - 3404070383063701936 - 1922936335652107237 - 8237728127754705548 - 12098220285188531801 - 5937916602119842503 - 16175212903699578327 - 12021790035614654024 - 12640112880082548543 - 9988825613639928377 - 222609074448911387 - 1463521475844014988 - 4814437674650683769 - 16663544195197391006 - 6776010347659523905 - 10762042953762752045 - 5466415224686378976 - 14096361420610155366 - 15984576656958250391 - 1400516108101495466 - 5363503825487985388 - 1150631902970999264 - 10767412659093418454 - 18322557570588838336 - 16881614450553938560 - 3338835856882095887 - 11006002028040042948 - 17818905097866076030 - 12738552382738970462 - 17563926324335897738 - 4202646357611763900 - 15610565280136198449 - 10838252318642422779 - 5751812652980752916 - 7707893267971477521 - 3438132572842590888 - 4398435217304672327 - 16731071122206732352 - 14177732933684507045 - 11253425265272496890 - 10504550207674021349 - 559444543901764141 - 11935728146105165543 - 12065888561217960564 - 6715879368372938878 - 7776591968702898575 - 2777892297442503589 - 8393596575808532785 - 10827174201281750732 - 10132611551939750451 - 4013040436104214123 - 9728946780972020798 - 11717397340205329286 - 14855633993462500175 - 8160551898753346851 - 14084807108588385469 - 1567279422163562706 - 11283114109075124863 - 15702454923950617920 - 12874674570724606944 - 15807685566348425479 - 3600542442602250880 - 13860794123126236651 - 18127658097486831628 - 17406633693057974802 - 13089463694395076394 - 994516662000214978 - 3948482519103438457 - 15637130163941710188 - 13258085859046266385 - 13770398394010134258 - 14731955952766459464 - 13812131625811308456 - 7990208979722858489 - 5555587803554688408 - 7912791757010083995 - 1356424559030639541 - 338226593499300764 - 7399798178166915827 - 7949892765528318736 - 8327946347768554799 - 7131733815895456735 - 10561337352372529567 - 9886755048693748436 - 12342420998458234222 - 18272185012221482515 - 2450975714124769815 - 15240240319819211743 - 1276547427350682222 - 11425318910829596551 - 113278865471929396 - 1085778659240049187 - 6576126420121055333 - 10994288847555394400 - 5735659066908006737 - 8776603742348551719 - 2782442786661054022 - 7547596557460541818 - 14971259749014482802 - 8247866334026378787 - 3159320118598665492 - 3153516205698038954 - 13242801524675246308 - 910573127268711400 - 14517389832786916398 - 313860173542898909 - 8422667745160081352 - 13672451047528317787 - 12249368813504890142 - 10103471637433797622 - 13266998533234084192 - 15464861287619024271 - 11486649941824635081 - 9796419365933784848 - 9312270706782964358 - 18285230460419430173 - 11081838169631481396 - 16261865581520769875 - 2071108473059438072 - 9962909113398157430 - 17437177570347930858 - 10076384205656142997 - 7404421492872539392 - 11992094620567828368 - 12947127439459936277 - 15755912943951857513 - 12710691931457120204 - 3965665105313955005 - 10321083169710718356 - 12859317438659787132 - 7827191266905579959 - 8841516927274164065 - 16904399358292801416 - 3103350280716949907 - 4010630207948740390 - 2637559525125777296 - 14963256201043248293 - 14693644106751253929 - 2057594095369183668 - 3690842488290627212 - 9506382959032264239 - 11974663545756131010 - 8323295925319611964 - 14519244833595374159 - 3847204941706060264 - 5536830860483730687 - 12896208588653174561 - 17013735695687392678 - 8346350983856995145 - 17820067124124915269 - 12123170406567551327 - 10124657217191790186 - 634502160289894313 - 7550978471003164232 - 4094776230922722828 - 17188475887859731498 - 7594742709221962116 - 1039590662170898718 - 1271331939192015400 - 11408338179751728497 - 1607474487644489413 - 3635015549777668420 - 14899862472375305307 - 15363547124080264 - 15886171035138107522 - 15453352049913274317 - 11694693379283692291 - 2132069305255467439 - 16870569243420438869 - 18234717839127666423 - 18285965981560479753 - 18111270940626227162 - 14104907628149322697 - 15158769583813602260 - 4900158007465310288 - 16059245221521486098 - 8835079774250545313 - 1959590564828004787 - 15456698581616030214 - 15055913423841403882 - 1655448446099140352 - 4108768096812565019 - 9724548332660252576 - 12373231212148260977 - 15424235784251394897 - 4531799232001851247 - 15984062460192617134 - 11272935587233366998 - 8853864046153834919 - 7146639226724470809 - 2984718311776160456 - 15070396665303514562 - 15367401321295747433 - 12387077815185644578 - 9657244021094071412 - 4461836674847507969 - 4180167270242856127 - 18241195421625251300 - 4105066547526874364 - 12022413181605983622 - 16103562972557811098 - 6659040731699663714 - 10870725420173034865 - 1212681754951069008 - 18025860074680620716 - 5906131488358401977 - 71957734665699424 - 11567540804976982784 - 2140481550254395548 - 9944458069371005838 - 11030757435188357389 - 12990280364598539881 - 3565028159946373568 - 1334051976087420715 - 9006290438966883915 - 965838808964037972 - 4164717558437863614 - 14341871877963814151 - 3485082034215152475 - 12546231594237180069 - 10587484162454639860 - 13614240923783466076 - 3504259420033640996 - 1167824288424891076 - 5546092811808623604 - 937208460419065008 - 1925902397820921919 - 17534477297797308709 - 7090952359397365826 - 16982113483193862176 - 4294602117745642388 - 4131795503687367484 - 1691133646233315032 - 3921624376008763574 - 14334692677173758151 - 14958388812399917866 - 5414021284167547309 - 53111008964741545 - 15460842510158605442 - 18186696881224426947 - 4787514778396839041 - 11884379039943607693 - 7626776604732729889 - 12268202217349925844 - 17230974352098363483 - 16020272298836028903 - 4327732613709196145 - 3573282460319585686 - 6947068702518677381 - 7761123522498093357 - 16568055532312358864 - 8425074468755982712 - 2148470338733320074 - 12246256392907011670 - 1963158579973938344 - 14502857040825842122 - 15515812806640818639 - 10485826424841515738 - 5681452801018167217 - 982745659416036093 - 11735153083848110714 - 6818870286431341260 - 17961429884980870492 - 18029995955319183851 - 7605878799974366109 - 6604944111527816208 - 14781747067457983180 - 1247318105229658450 - 9127209326713543172 - 1506714000539518770 - 3075274491283773496 - 16076599434080024881 - 7642153328316556712 - 11857141806684495137 - 16118183247047629884 - 9526085861565373767 - 8687867772019296640 - 3233333886204900284 - 18167925477273144501 - 2772784027860017033 - 17030353793293721352 - 6978860470841283656 - 1597597641706181670 - 10477988387528564444 - 4929379273223932205 - 16051022509003727042 - 16289045876882967052 - 10032359947712426942 - 7276616997079239490 - 613431939712187006 - 17272077609007089864 - 3319440480636294292 - 14688147951886244835 - 9650287268626680316 - 14327878174987249796 - 16752637269458948196 - 11501126535203194976 - 2317659836353362247 - 17704851477043430102 - 504409360458771772 - 7933019655221080648 - 8511011861368929016 - 9777971681473367131 - 11959568219357422200 - 17611114855351085553 - 5196608542564207124 - 15869889390963813253 - 4743008911590199952 - 4657725142705044680 - 16847406279944882428 - 1236122570182666922 - 4972486929448268837 - 7782177498389426561 - 13383350700086680433 - 6892227676341131892 - 18393780754913436199 - 15366525749877629247 - 6737961354939730160 - 16183958835289748403 - 11960416625298078954 - 423579361091429368 - 1618860279892545620 - 17471930208520045762 - 15348486898381944494 - 15649921972451800223 - 17454920248989374713 - 368641911983013333 - 1471190651490855721 - 4337963864587114984 - 243526101784923158 - 9751678209577508995 - 16846034479237475904 - 9680194725841310016 - 7550399273655542013 - 8285415011050684165 - 14233293819653694114 - 16535070254851058073 - 13617704796229472564 - 9766478673887995247 - 17124001308840712861 - 251690693260949897 - 3314718624207989813 - 5281985406429705176 - 12597020231134382987 - 9782905438451074513 - 17355935286801936580 - 16017265418145351544 - 12747300411223579434 - 711321098380648732 - 4863398326887814242 - 9210599065733202346 - 16573571549449943316 - 7249556861078243366 - 1910858652205186599 - 5250569912794257866 - 1039347345860769292 - 7199861398663786870 - 2011759111183301182 - 7740338971964919436 - 4317321564741068438 - 2074938441103773191 - 4419379448455177799 - 667245781545830005 - 2540729041474268608 - 4208158592511586861 - 14063912840669258147 - 17719062053917364410 - 17312004689762000220 - 5232089336482481146 - 6604861290263336223 - 123590523944275513 - 12406009365553186504 - 2469889520880869100 - 3858879346150415915 - 16579466621405269693 - 17983576308245389640 - 16262453522113220072 - 1693882361212735052 - 16587246742152088064 - 2508682579764432850 - 4832330462517197196 - 16278756989208823310 - 758246325560768291 - 5306018193130314356 - 11422740843545529284 - 5128690802144714991 - 10924050433941265694 - 1632469577207175962 - 15571890758353614494 - 11113091412206882631 - 6622826861857398684 - 13433758488897905996 - 1133813309861010178 - 13876281389188668628 - 1496844208448379722 - 14035655260784191760 - 7632503394576793159 - 1996405771784781732 - 12273423142638581190 - 6447652678883147921 - 13640817613751710072 - 1888485066898739132 - 5093501448291383046 - 3952069885294190767 - 12940205664340693565 - 7908501217890455204 - 7520597156587673184 - 767767061119854846 - 11898339525032033768 - 12123850033636734886 - 8765200496337861713 - 1679221168964040643 - 13159931486887302320 - 17545841896314162371 - 18407555266226246062 - 7085618836503982785 - 13508631139981686746 - 4776686543043414990 - 10257766090624152320 - 15478360341159100954 - 10866814433432653400 - 1266181251575719938 - 9874832045439102043 - 12670121444072556821 - 14415048846072178379 - 8576790772504445299 - 10398215560057959490 - 13793955191481823072 - 7705926180284189770 - 764675236710303754 - 13230695744109747833 - 17117739322459402141 - 14251152644700291132 - 17925924700753351551 - 8014403157810821343 - 7663656250015373447 - 15986729484496988211 - 16993995513943202776 - 8163314895115788322 - 13893840939053554287 - 10385068827404185981 - 5193058723474298989 - 14597908901451293948 - 10460419535139862480 - 10967483515691179220 - 17460876096361245793 - 10814764302177548819 - 2624558864665368841 - 466468485564559014 - 14252181633448897528 - 12904479801311122937 - 4036418914598247369 - 5025738328865459682 - 17804665164863226244 - 5615667923431706137 - 13088986081549949361 - 12877985488322567902 - 1146903302020974453 - 4860964478930959825 - 12085875644594601176 - 13116092602275838627 - 8321274857970877952 - 9385482918563989479 - 17912902819951822195 - 5646770101675234126 - 15573306932819096276 - 4758405545863482220 - 7216157059662308903 - 5502158873643706387 - 15108922535060768617 - 5491558681199354700 - 18350474900665525109 - 15190037752693953036 - 17411353686479605512 - 12258897571827613118 - 14119755389067763941 - 4753679948540725779 - 8337818043130916921 - 16551317428984357254 - 2707004360768664818 - 6465721769240061502 - 8132094879699321965 - 6795404745535059397 - 14462718767334455805 - 6113724564439377093 - 13146256235160797215 - 1241192493374572077 - 13018575274399579234 - 7343738444176544790 - 9599152601268642801 - 12797359411031704797 - 10175860064669295644 - 7663976226311323290 - 16045148899254928993 - 6587365537689360100 - 14926181263652885738 - 11057016571685063825 - 11574619060238502434 - 894827397848194018 - 2375192706578325948 - 9592667208480817039 - 11632186791661274942 - 11743339922943929062 - 1499561529162544247 - 948270756520396313 - 6841514893615804338 - 16895894165983749133 - 12760113486386773161 - 10316393095044339846 - 2808149728906376039 - 4577729166617804641 - 15878203270828380512 - 12159856899460557607 - 16401966270784925039 - 13703235828125908965 - 8217866906322789475 - 9211632184155679570 - 11111651373357368944 - 17640788901914951619 - 6792822383765862073 - 18061924544116450169 - 9436378252498679448 - 4458722208509575818 - 6034406588632393305 - 14816646944272162649 - 138194924977623443 - 15596320980990824012 - 12301195492059219541 - 13834750789127766439 - 1204524306023886050 - 15363133096392070863 - 12408063731949899568 - 3381237095788978962 - 15035708039266375057 - 15520167763123795952 - 502309266672748278 - 15789773558864482061 - 6582341717764178990 - 1974176855187298409 - 11915423680871385683 - 14010664758385690810 - 17773687691883612951 - 574035821170072339 - 13400956510153680374 - 12725170784205124486 - 2395650900435004906 - 6693438811263003514 - 6721658043245960168 - 1314475145545084692 - 15249277904583121423 - 10495353025269458671 - 15159745149031414970 - 13549997004086666295 - 6163157753455540046 - 16795271496376618129 - 3446235441568115468 - 8770576388012584819 - 10901260234870783004 - 11905811876774412952 - 4859007370590074533 - 18321360744669247296 - 149593614613851662 - 16684443475213896998 - 6689023493705820156 - 12860911923327197762 - 15812481719359863190 - 11128280823149323477 - 3255706166971391764 - 14786367190455492567 - 3785824395222472965 - 10823542274683292235 - 13249656743621513144 - 15793380991656641504 - 732122365848928568 - 14939694193888499370 - 13144162389086683969 - 9429109771226952298 - 17729816496430879681 - 11077661704251967142 - 2621304554568702565 - 9385059778434368421 - 14203382899255954515 - 6012093655693704712 - 16207819942499785577 - 4502090935915637323 - 9495016594161345114 - 12636017273154187901 - 2897494169871979697 - 14185923395245048393 - 5095150295067142356 - 10214290467161590775 - 5190086744227302393 - 17212599803849087231 - 465816217519226066 - 1750147651096625822 - 5994193402555014414 - 5264719330888591226 - 3401857805039875323 - 1445654113107514901 - 14138990935534866273 - 15668896640647016294 - 5737728875218183219 - 17249084056589068385 - 18254563909328505321 - 15750037654276632904 - 11121974791251290772 - 5978888750754466241 - 8889260741872114810 - 4747860697660210768 - 17048526165086336820 - 4054112028194783106 - 6938467658411184333 - 12982152377953076112 - 4196853978726079783 - 2175349381112603518 - 3784051276405904862 - 221248721105895659 - 5647694285588324749 - 4200225056987130659 - 2695671588831458243 - 8839772794713247554 - 12212976363431249396 - 3489732583322888353 - 5547221163310471790 - 1827588699821069081 - 17866135417617074562 - 3181922582949110124 - 10286725530397007766 - 1118172031856817028 - 7346319743713496484 - 4748038856156779402 - 5847596696702621933 - 2531357294975150270 - 522839523479360477 - 13896540934619858178 - 11076458389499186531 - 2752038081986354656 - 15727718023992713164 - 4728018142447912884 - 15518664270597186063 - 2134693874838389131 - 10228895972860682916 - 11459821992144236506 - 7869788186870232011 - 14970890481879241566 - 8756941675290048218 - 10869254214162790925 - 4377255505598592686 - 1216538299119732450 - 10132312819630456949 - 11863534461176728537 - 18286057519007701831 - 1677069787769523732 - 1104163571263092446 - 1121928605306773068 - 12430223770234903720 - 9840298539596873639 - 12693163215434654919 - 3700880022863857158 - 5276174972450466522 - 12814001426429226986 - 15706239720435893291 - 18042109094879760253 - 10948615188017653951 - 15418100339082629183 - 10069459694679290961 - 17174683563421007447 - 13915750074112662351 - 3093270745496639167 - 11663441915142931092 - 578446115736726622 - 13264473419900147559 - 6667495605547652544 - 3482199446278800566 - 5089686380256313942 - 13030172275929047244 - 8907410104086258772 - 8883694939182477848 - 15317614598641847523 - 2607706018662263347 - 3732632051731565844 - 13283514949726179699 - 18262833696957702227 - 10937980486533216958 - 14059491137820776060 - 5958511359126346725 - 14810779933281216412 - 4875580198187922563 - 1716469496271165568 - 233421718396168540 - 9531475641935766997 - 2333078153872791210 - 16578820316563458547 - 11250701909169607395 - 15651561691176830486 - 15185866523902386864 - 10624677799139518407 - 16535155009388086574 - 6964584063087669942 - 4275162838434199970 - 3346445358120280306 - 11859451888921262770 - 14200620539315128865 - 16942683791977556558 - 16545367021290979448 - 10185105092876908363 - 9032625984754504084 - 10840870981553070388 - 953505980170313013 - 4759681357099865694 - 2563804318228320264 - 18074208644259464665 - 304371792302462860 - 11516390401314470367 - 12318693479523493913 - 2479492432100862863 - 1205835192505990906 - 9385295035871814889 - 11542112419936942421 - 17422237772384476687 - 5139964178785829996 - 8556489727584771937 - 8550553698582458016 - 3340724501593222760 - 15323697355437391430 - 3099300620123954197 - 10529159653326641864 - 7504508221263058116 - 16757640508849246008 - - diff --git a/Sources/CompletionScoringTestSupport/RepeatableRandomNumberGenerator.swift b/Sources/CompletionScoringTestSupport/RepeatableRandomNumberGenerator.swift deleted file mode 100644 index c4c0d8c93..000000000 --- a/Sources/CompletionScoringTestSupport/RepeatableRandomNumberGenerator.swift +++ /dev/null @@ -1,160 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import CompletionScoring -import Foundation -import SwiftExtensions - -package struct RepeatableRandomNumberGenerator: RandomNumberGenerator { - private let seed: [UInt64] - private static let startIndex = (0, 1, 2, 3) - private var nextIndex = Self.startIndex - - package init() { - self.seed = try! PropertyListDecoder().decode( - [UInt64].self, - from: loadTestResource(name: "RandomSeed", withExtension: "plist") - ) - } - - @discardableResult - func increment(value: inout Int, range: Range) -> Bool { - value += 1 - let wrapped = (value == range.upperBound) - if wrapped { - value = range.lowerBound - } - return wrapped - } - - mutating func advance() { - // This iterates through "K choose N" or "seed.count choose 4" unique combinations of 4 seed indexes. Given 1024 values in seed, that produces 45,545,029,376 unique combination of the values. - if increment(value: &nextIndex.3, range: (nextIndex.2 + 1)..<(seed.count - 0)) { - if increment(value: &nextIndex.2, range: (nextIndex.1 + 1)..<(seed.count - 1)) { - if increment(value: &nextIndex.1, range: (nextIndex.0 + 1)..<(seed.count - 2)) { - if increment(value: &nextIndex.0, range: 0..<(seed.count - 3)) { - nextIndex = Self.startIndex - return - } - nextIndex.1 = (nextIndex.0 + 1) - } - nextIndex.2 = (nextIndex.1 + 1) - } - nextIndex.3 = (nextIndex.2 + 1) - } - } - - package mutating func next() -> UInt64 { - let result = seed[nextIndex.0] ^ seed[nextIndex.1] ^ seed[nextIndex.2] ^ seed[nextIndex.3] - advance() - return result - } - - static func generateSeed() { - let numbers: [UInt64] = (0..<1024).map { _ in - let lo = UInt64.random(in: 0...UInt64.max) - let hi = UInt64.random(in: 0...UInt64.max) - return (hi << 32) | lo - } - let header = - """ - - - - - """ - let body = numbers.map { number in " \(number)" } - let footer = - """ - - - """ - - print(([header] + body + [footer]).joined(separator: "\n")) - } - - package mutating func randomLowercaseASCIIString(lengthRange: ClosedRange) -> String { - let length = lengthRange.randomElement(using: &self) - let utf8Bytes = (0.., - lengthRange: ClosedRange - ) -> [String] { - let count = countRange.randomElement(using: &self) - var strings: [String] = [] - for _ in (0.. Data { - let file = - skTestSupportInputsDirectory - .appending(component: "\(name).\(ext)") - return try Data(contentsOf: file) -} - -extension ClosedRange { - func randomElement(using randomness: inout Generator) -> Element { - return randomElement(using: &randomness)! // Closed ranges always have a value - } -} diff --git a/Sources/CompletionScoringTestSupport/SymbolGenerator.swift b/Sources/CompletionScoringTestSupport/SymbolGenerator.swift deleted file mode 100644 index 8e64e567f..000000000 --- a/Sources/CompletionScoringTestSupport/SymbolGenerator.swift +++ /dev/null @@ -1,201 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import CompletionScoring -import Foundation - -package struct SymbolGenerator: Sendable { - package static let shared = Self() - - struct TermTable { - struct Entry: Codable { - var word: String - var count: Int - } - private var replicatedTerms: [String] - - init() throws { - let entries = try JSONDecoder().decode( - [Entry].self, - from: loadTestResource(name: "CommonFunctionTerms", withExtension: "json") - ) - let repeatedWords: [[String]] = entries.map { entry in - var word = entry.word - // Make the first letter lowercase if the word isn't something like 'URL'. - if word.count > 1 { - let first = entry.word.startIndex - let second = entry.word.index(after: first) - if word[first].isUppercase && !word[second].isUppercase { - let head = word[first].lowercased() - let tail = word.dropFirst(1) - word = head + tail - } - } - return Array(repeating: word, count: entry.count) - } - replicatedTerms = Array(repeatedWords.joined()) - } - - func randomTerm(using randomness: inout RepeatableRandomNumberGenerator) -> String { - replicatedTerms.randomElement(using: &randomness)! - } - } - - let termTable = try! TermTable() - let segmentCountWeights = WeightedChoices([ - (0.5, 1), - (0.375, 2), - (0.125, 3), - ]) - - package func randomSegment( - using randomness: inout RepeatableRandomNumberGenerator, - capitalizeFirstTerm: Bool - ) -> String { - let count = segmentCountWeights.select(using: &randomness) - return (0.. 0 || capitalizeFirstTerm - return capitalize ? term.capitalized : term - }.joined() - } - - package func randomType(using randomness: inout RepeatableRandomNumberGenerator) -> String { - randomSegment(using: &randomness, capitalizeFirstTerm: true) - } - - let argumentCountWeights = WeightedChoices([ - (0.333, 0), - (0.333, 1), - (0.250, 2), - (0.083, 3), - ]) - - package struct Function { - var baseName: String - var arguments: [Argument] - - package var filterText: String { - let argPattern: String - if arguments.hasContent { - argPattern = arguments.map { argument in - (argument.label ?? "") + ":" - }.joined() - } else { - argPattern = "" - } - return baseName + "(" + argPattern + ")" - } - - package var displayText: String { - let argPattern: String - if arguments.hasContent { - argPattern = arguments.map { argument in - (argument.label ?? "_") + ": " + argument.type - }.joined(separator: ", ") - } else { - argPattern = "" - } - return baseName + "(" + argPattern + ")" - } - } - - struct Argument { - var label: String? - var type: String - } - - let argumentLabeledWeights = WeightedChoices([ - (31 / 32.0, true), - (01 / 32.0, false), - ]) - - func randomArgument(using randomness: inout RepeatableRandomNumberGenerator) -> Argument { - let labeled = argumentLabeledWeights.select(using: &randomness) - let label = labeled ? randomSegment(using: &randomness, capitalizeFirstTerm: false) : nil - return Argument(label: label, type: randomType(using: &randomness)) - } - - package func randomFunction(using randomness: inout RepeatableRandomNumberGenerator) -> Function { - let argCount = argumentCountWeights.select(using: &randomness) - return Function( - baseName: randomSegment(using: &randomness, capitalizeFirstTerm: false), - arguments: Array(count: argCount) { - randomArgument(using: &randomness) - } - ) - } - - let initializerCounts = WeightedChoices([ - (32 / 64.0, 1), - (16 / 64.0, 2), - (8 / 64.0, 3), - (4 / 64.0, 4), - (2 / 64.0, 5), - (1 / 64.0, 6), - (1 / 64.0, 0), - ]) - - let initializerArgumentCounts = WeightedChoices([ - (512 / 1024.0, 1), - (256 / 1024.0, 2), - (128 / 1024.0, 3), - (64 / 1024.0, 4), - (58 / 1024.0, 0), - (4 / 1024.0, 16), - (2 / 1024.0, 32), - ]) - - package func randomInitializers( - typeName: String, - using randomness: inout RepeatableRandomNumberGenerator - ) -> [Function] { - let initializerCount = initializerCounts.select(using: &randomness) - return Array(count: initializerCount) { - let argumentCount = initializerArgumentCounts.select(using: &randomness) - let arguments: [Argument] = Array(count: argumentCount) { - randomArgument(using: &randomness) - } - return Function(baseName: typeName, arguments: arguments) - } - } - - let capitalizedPatternWeights = WeightedChoices([ - (7 / 8.0, false), - (1 / 8.0, true), - ]) - - package func randomPatternText( - lengthRange: Range, - using randomness: inout RepeatableRandomNumberGenerator - ) -> String { - var text = "" - while text.count < lengthRange.upperBound { - text = randomSegment( - using: &randomness, - capitalizeFirstTerm: capitalizedPatternWeights.select(using: &randomness) - ) - } - let length = lengthRange.randomElement(using: &randomness) ?? 0 - return String(text.prefix(length)) - } - - func randomAPIs( - functionCount: Int, - typeCount: Int, - using randomness: inout RepeatableRandomNumberGenerator - ) -> [String] { - let functions = (0..(_ value: T) {} - -func duration(of body: () -> Void) -> TimeInterval { - let start = ProcessInfo.processInfo.systemUptime - body() - return ProcessInfo.processInfo.systemUptime - start -} - -extension RandomNumberGenerator { - mutating func nextBool() -> Bool { - (next() & 0x01 == 0x01) - } -} - -package func withEachPermutation(_ a: T, _ b: T, body: (T, T) -> Void) { - body(a, b) - body(b, a) -} - -extension XCTestCase { - private func heatUp() { - var integers = 1024 - var elapsed = 0.0 - while elapsed < 1.0 { - elapsed += duration { - let integers = Array(count: integers) { - UInt64.random(in: 0...UInt64.max) - } - DispatchQueue.concurrentPerform(iterations: 128) { _ in - integers.withUnsafeBytes { bytes in - var hasher = Hasher() - hasher.combine(bytes: bytes) - drain(hasher.finalize()) - } - } - } - integers *= 2 - } - } - - private func coolDown() { - Thread.sleep(forTimeInterval: 2.0) - } - - #if canImport(Darwin) - func induceThermalRange(_ range: ClosedRange) { - var temperature: Int - repeat { - temperature = ProcessInfo.processInfo.thermalLevel() - if temperature < range.lowerBound { - print("Too Cold: \(temperature)") - heatUp() - } else if temperature > range.upperBound { - print("Too Hot: \(temperature)") - coolDown() - } - } while !range.contains(temperature) - } - - private static let targetThermalRange: ClosedRange? = { - if ProcessInfo.processInfo.environment["SOURCEKIT_LSP_PERFORMANCE_MEASUREMENTS_ENABLE_THERMAL_THROTTLING"] - != nil - { - // This range is arbitrary. All that matters is that the same values are used on the baseline and the branch. - let target = - ProcessInfo.processInfo.environment["SOURCEKIT_LSP_PERFORMANCE_MEASUREMENTS_THERMAL_TARGET"].flatMap( - Int.init - ) - ?? 75 - let variance = - ProcessInfo.processInfo.environment["SOURCEKIT_LSP_PERFORMANCE_MEASUREMENTS_THERMAL_VARIANCE"].flatMap( - Int.init - ) - ?? 5 - return (target - variance)...(target + variance) - } else { - return nil - } - }() - #endif - - private static let measurementsLogFile: String? = { - UserDefaults.standard.string(forKey: "TestMeasurementLogPath") - }() - - static let printBeginingOfLog = AtomicBool(initialValue: true) - - private static func openPerformanceLog() throws -> FileHandle? { - try measurementsLogFile.map { path in - if !FileManager.default.fileExists(atPath: path) { - try FileManager.default.createDirectory( - at: URL(fileURLWithPath: path).deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try FileManager.default.createFile(at: URL(fileURLWithPath: path), contents: Data()) - } - let logFD = try FileHandle(forWritingAtPath: path).unwrap(orThrow: "Opening \(path) failed") - try logFD.seekToEnd() - if printBeginingOfLog.value { - try logFD.print("========= \(Date().description(with: .current)) =========") - printBeginingOfLog.value = false - } - return logFD - } - } - - func tryOrFailTest(_ expression: @autoclosure () throws -> R?, message: String) -> R? { - do { - return try expression() - } catch { - XCTFail("\(message): \(error)") - return nil - } - } - - /// Run `body()` `iterations`, gathering timing stats, and print them. - /// In between runs, coax for the machine into an arbitrary but consistent thermal state by either sleeping or doing - /// pointless work so that results are more comparable run to run, no matter else is happening on the machine. - package func gaugeTiming(iterations: Int = 1, testName: String = #function, _ body: () -> Void) { - let logFD = tryOrFailTest(try Self.openPerformanceLog(), message: "Failed to open performance log") - var timings = Timings() - for iteration in 0.. Int { - var thermalLevel: UInt64 = 0 - var size: size_t = MemoryLayout.size - sysctlbyname("machdep.xcpm.cpu_thermal_level", &thermalLevel, &size, nil, 0) - return Int(thermalLevel) - } -} -#endif - -extension String { - fileprivate func dropSuffix(_ suffix: String) -> String { - if hasSuffix(suffix) { - return String(dropLast(suffix.count)) - } - return self - } - - fileprivate func dropPrefix(_ prefix: String) -> String { - if hasPrefix(prefix) { - return String(dropFirst(prefix.count)) - } - return self - } - - package func allocateCopyOfUTF8Buffer() -> CompletionScoring.Pattern.UTF8Bytes { - withUncachedUTF8Bytes { utf8Buffer in - UnsafeBufferPointer.allocate(copyOf: utf8Buffer) - } - } -} - -extension FileHandle { - func write(_ text: String) throws { - try text.withUncachedUTF8Bytes { bytes in - try write(contentsOf: bytes) - } - } - - func print(_ text: String) throws { - try write(text) - try write("\n") - } -} - -extension Double { - func format(_ specifier: StringLiteralType) -> String { - String(format: specifier, self) - } -} - -extension Int { - func format(_ specifier: StringLiteralType) -> String { - String(format: specifier, self) - } -} - -extension CandidateBatch { - package init(symbols: [String]) { - self.init(candidates: symbols, contentType: .codeCompletionSymbol) - } -} diff --git a/Sources/CompletionScoringTestSupport/Timings.swift b/Sources/CompletionScoringTestSupport/Timings.swift deleted file mode 100644 index 908af4613..000000000 --- a/Sources/CompletionScoringTestSupport/Timings.swift +++ /dev/null @@ -1,117 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import CompletionScoring -import Foundation - -package struct Timings { - package struct Stats { - package private(set) var min: Double - package private(set) var max: Double - private var total: Double - private var count: Int - - fileprivate init(initialValue: Double) { - total = initialValue - min = initialValue - max = initialValue - count = 1 - } - - fileprivate mutating func append(_ value: Double) { - count += 1 - total += value - min = Swift.min(min, value) - max = Swift.max(max, value) - } - - var average: Double { - total / Double(count) - } - } - - package private(set) var stats: Stats? = nil - private(set) var values: [Double] = [] - - package init(_ values: [Double] = []) { - for value in values { - append(value) - } - } - - private var hasVariation: Bool { - return values.count >= 2 - } - - package var meanAverageDeviation: Double { - if let stats = stats, hasVariation { - var sumOfDiviations = 0.0 - for value in values { - sumOfDiviations += abs(value - stats.average) - } - return sumOfDiviations / Double(values.count) - } else { - return 0 - } - } - - package var standardDeviation: Double { - if let stats = stats, hasVariation { - var sumOfSquares = 0.0 - for value in values { - let deviation = (value - stats.average) - sumOfSquares += deviation * deviation - } - let variance = sumOfSquares / Double(values.count - 1) - return sqrt(variance) - } else { - return 0 - } - } - - package var standardError: Double { - if hasVariation { - return standardDeviation / sqrt(Double(values.count)) - } else { - return 0 - } - } - - /// There's 95% confidence that the true mean is with this distance from the sampled mean. - var confidenceOfMean_95Percent: Double { - if stats != nil { - return 1.96 * standardError - } - return 0 - } - - @discardableResult - mutating func append(_ value: Double) -> Stats { - values.append(value) - stats.mutateWrappedValue { stats in - stats.append(value) - } - return stats.lazyInitialize { - Stats(initialValue: value) - } - } -} - -extension Optional { - mutating func mutateWrappedValue(mutator: (inout Wrapped) -> Void) { - if var wrapped = self { - self = nil // Avoid COW for clients. - mutator(&wrapped) - self = wrapped - } - } -} diff --git a/Sources/CompletionScoringTestSupport/WeightedChoices.swift b/Sources/CompletionScoringTestSupport/WeightedChoices.swift deleted file mode 100644 index b3a534453..000000000 --- a/Sources/CompletionScoringTestSupport/WeightedChoices.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import CompletionScoring -import Foundation - -package struct WeightedChoices: Sendable { - package typealias WeightedChoice = (likelihood: Double, value: T) - private var choices: [T] = [] - - package init(_ choices: [WeightedChoice]) { - precondition(choices.hasContent) - let smallest = choices.map(\.likelihood).min()! - let samples = 1.0 / (smallest / 2.0) + 1 - for choice in choices { - precondition(choice.likelihood > 0) - precondition(choice.likelihood <= 1.0) - self.choices.append(contentsOf: Array(repeating: choice.value, count: Int(choice.likelihood * samples))) - } - } - - package func select(using randomness: inout RepeatableRandomNumberGenerator) -> T { - choices.randomElement(using: &randomness)! - } -} diff --git a/Sources/Csourcekitd/CMakeLists.txt b/Sources/Csourcekitd/CMakeLists.txt deleted file mode 100644 index c5ece5ee9..000000000 --- a/Sources/Csourcekitd/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_library(Csourcekitd STATIC - sourcekitd.c) -target_include_directories(Csourcekitd PUBLIC - include) diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h deleted file mode 100644 index fefd676fa..000000000 --- a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h +++ /dev/null @@ -1,418 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifndef SWIFT_C_CODE_COMPLETION_H -#define SWIFT_C_CODE_COMPLETION_H - -#include -#include -#include - -/// Global state across completions including compiler instance caching. -typedef void *swiftide_api_connection_t; - -/// Opaque completion item handle, used to retrieve additional information that -/// may be more expensive to compute. -typedef void *swiftide_api_completion_item_t; - -typedef enum swiftide_api_completion_kind_t: uint32_t { - SWIFTIDE_COMPLETION_KIND_NONE = 0, - SWIFTIDE_COMPLETION_KIND_IMPORT = 1, - SWIFTIDE_COMPLETION_KIND_UNRESOLVEDMEMBER = 2, - SWIFTIDE_COMPLETION_KIND_DOTEXPR = 3, - SWIFTIDE_COMPLETION_KIND_STMTOREXPR = 4, - SWIFTIDE_COMPLETION_KIND_POSTFIXEXPRBEGINNING = 5, - SWIFTIDE_COMPLETION_KIND_POSTFIXEXPR = 6, - /* obsoleted */SWIFTIDE_COMPLETION_KIND_POSTFIXEXPRPAREN = 7, - SWIFTIDE_COMPLETION_KIND_KEYPATHEXPROBJC = 8, - SWIFTIDE_COMPLETION_KIND_KEYPATHEXPRSWIFT = 9, - SWIFTIDE_COMPLETION_KIND_TYPEDECLRESULTBEGINNING = 10, - SWIFTIDE_COMPLETION_KIND_TYPESIMPLEBEGINNING = 11, - SWIFTIDE_COMPLETION_KIND_TYPEIDENTIFIERWITHDOT = 12, - SWIFTIDE_COMPLETION_KIND_TYPEIDENTIFIERWITHOUTDOT = 13, - SWIFTIDE_COMPLETION_KIND_CASESTMTKEYWORD = 14, - SWIFTIDE_COMPLETION_KIND_CASESTMTBEGINNING = 15, - SWIFTIDE_COMPLETION_KIND_NOMINALMEMBERBEGINNING = 16, - SWIFTIDE_COMPLETION_KIND_ACCESSORBEGINNING = 17, - SWIFTIDE_COMPLETION_KIND_ATTRIBUTEBEGIN = 18, - SWIFTIDE_COMPLETION_KIND_ATTRIBUTEDECLPAREN = 19, - SWIFTIDE_COMPLETION_KIND_POUNDAVAILABLEPLATFORM = 20, - SWIFTIDE_COMPLETION_KIND_CALLARG = 21, - SWIFTIDE_COMPLETION_KIND_LABELEDTRAILINGCLOSURE = 22, - SWIFTIDE_COMPLETION_KIND_RETURNSTMTEXPR = 23, - SWIFTIDE_COMPLETION_KIND_YIELDSTMTEXPR = 24, - SWIFTIDE_COMPLETION_KIND_FOREACHSEQUENCE = 25, - SWIFTIDE_COMPLETION_KIND_AFTERPOUNDEXPR = 26, - SWIFTIDE_COMPLETION_KIND_AFTERPOUNDDIRECTIVE = 27, - SWIFTIDE_COMPLETION_KIND_PLATFORMCONDITON = 28, - SWIFTIDE_COMPLETION_KIND_AFTERIFSTMTELSE = 29, - SWIFTIDE_COMPLETION_KIND_GENERICREQUIREMENT = 30, - SWIFTIDE_COMPLETION_KIND_PRECEDENCEGROUP = 31, - SWIFTIDE_COMPLETION_KIND_STMTLABEL = 32, - SWIFTIDE_COMPLETION_KIND_EFFECTSSPECIFIER = 33, - SWIFTIDE_COMPLETION_KIND_FOREACHPATTERNBEGINNING = 34, - SWIFTIDE_COMPLETION_KIND_TYPEATTRBEGINNING = 35, - SWIFTIDE_COMPLETION_KIND_OPTIONALBINDING = 36, - SWIFTIDE_COMPLETION_KIND_FOREACHKWIN = 37, - SWIFTIDE_COMPLETION_KIND_WITHOUTCONSTRAINTTYPE = 38, - SWIFTIDE_COMPLETION_KIND_THENSTMTEXPR = 39, - SWIFTIDE_COMPLETION_KIND_TYPEBEGINNING = 40, - SWIFTIDE_COMPLETION_KIND_TYPESIMPLEORCOMPOSITION = 41, - SWIFTIDE_COMPLETION_KIND_TYPEPOSSIBLEFUNCTIONPARAMBEGINNING = 42, - SWIFTIDE_COMPLETION_KIND_TYPEATTRINHERITANCEBEGINNING = 43, -} swiftide_api_completion_kind_t; - -typedef enum swiftide_api_completion_item_kind_t: uint32_t { - SWIFTIDE_COMPLETION_ITEM_KIND_DECLARATION = 0, - SWIFTIDE_COMPLETION_ITEM_KIND_KEYWORD = 1, - SWIFTIDE_COMPLETION_ITEM_KIND_PATTERN = 2, - SWIFTIDE_COMPLETION_ITEM_KIND_LITERAL = 3, - SWIFTIDE_COMPLETION_ITEM_KIND_BUILTINOPERATOR = 4, -} swiftide_api_completion_item_kind_t; - -typedef enum swiftide_api_completion_item_decl_kind_t: uint32_t { - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_MODULE = 0, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_CLASS = 1, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_STRUCT = 2, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_ENUM = 3, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_ENUMELEMENT = 4, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_PROTOCOL = 5, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_ASSOCIATEDTYPE = 6, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_TYPEALIAS = 7, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_GENERICTYPEPARAM = 8, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_CONSTRUCTOR = 9, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_DESTRUCTOR = 10, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_SUBSCRIPT = 11, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_STATICMETHOD = 12, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_INSTANCEMETHOD = 13, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_PREFIXOPERATORFUNCTION = 14, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_POSTFIXOPERATORFUNCTION = 15, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_INFIXOPERATORFUNCTION = 16, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_FREEFUNCTION = 17, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_STATICVAR = 18, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_INSTANCEVAR = 19, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_LOCALVAR = 20, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_GLOBALVAR = 21, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_PRECEDENCEGROUP = 22, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_ACTOR = 23, - SWIFTIDE_COMPLETION_ITEM_DECL_KIND_MACRO = 24, -} swiftide_api_completion_item_decl_kind_t; - -typedef enum swiftide_api_completion_type_relation_t: uint32_t { - SWIFTIDE_COMPLETION_TYPE_RELATION_NOTAPPLICABLE = 0, - SWIFTIDE_COMPLETION_TYPE_RELATION_UNKNOWN = 1, - SWIFTIDE_COMPLETION_TYPE_RELATION_UNRELATED = 2, - SWIFTIDE_COMPLETION_TYPE_RELATION_INVALID = 3, - SWIFTIDE_COMPLETION_TYPE_RELATION_CONVERTIBLE = 4, - SWIFTIDE_COMPLETION_TYPE_RELATION_IDENTICAL = 5, -} swiftide_api_completion_type_relation_t; - -typedef enum swiftide_api_completion_semantic_context_t: uint32_t { - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_NONE = 0, - /* obsoleted */SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_EXPRESSIONSPECIFIC = 1, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_LOCAL = 2, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_CURRENTNOMINAL = 3, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_SUPER = 4, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_OUTSIDENOMINAL = 5, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_CURRENTMODULE = 6, - SWIFTIDE_COMPLETION_SEMANTIC_CONTEXT_OTHERMODULE = 7, -} swiftide_api_completion_semantic_context_t; - -typedef enum swiftide_api_completion_flair_t: uint32_t { - SWIFTIDE_COMPLETION_FLAIR_EXPRESSIONSPECIFIC = 1 << 0, - SWIFTIDE_COMPLETION_FLAIR_SUPERCHAIN = 1 << 1, - SWIFTIDE_COMPLETION_FLAIR_ARGUMENTLABELS = 1 << 2, - SWIFTIDE_COMPLETION_FLAIR_COMMONKEYWORDATCURRENTPOSITION = 1 << 3, - SWIFTIDE_COMPLETION_FLAIR_RAREKEYWORDATCURRENTPOSITION = 1 << 4, - SWIFTIDE_COMPLETION_FLAIR_RARETYPEATCURRENTPOSITION = 1 << 5, - SWIFTIDE_COMPLETION_FLAIR_EXPRESSIONATNONSCRIPTORMAINFILESCOPE = 1 << 6, -} swiftide_api_completion_flair_t; - -typedef enum swiftide_api_completion_not_recommended_reason_t: uint32_t { - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_NONE = 0, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_REDUNDANT_IMPORT = 1, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_DEPRECATED = 2, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_INVALID_ASYNC_CONTEXT = 3, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_CROSS_ACTOR_REFERENCE = 4, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_VARIABLE_USED_IN_OWN_DEFINITION = 5, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_REDUNDANT_IMPORT_INDIRECT = 6, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_SOFTDEPRECATED = 7, - SWIFTIDE_COMPLETION_NOT_RECOMMENDED_NON_ASYNC_ALTERNATIVE_USED_IN_ASYNC_CONTEXT = 8, -} swiftide_api_completion_not_recommended_reason_t; - -typedef enum swiftide_api_completion_diagnostic_severity_t: uint32_t { - SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_NONE = 0, - SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_ERROR = 1, - SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_WARNING = 2, - SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_REMARK = 3, - SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_NOTE = 4, -} swiftide_api_completion_diagnostic_severity_t; - -typedef void *swiftide_api_completion_request_t; - -typedef void *swiftide_api_completion_response_t; - -typedef void *swiftide_api_fuzzy_match_pattern_t; - -typedef void *swiftide_api_cache_invalidation_options_t; - -/// swiftide equivalent of sourcekitd_request_handle_t -typedef const void *swiftide_api_request_handle_t; - - -typedef struct { - _Nonnull swiftide_api_connection_t (*_Nonnull connection_create_with_inspection_instance)( - void *_Null_unspecified opqueSwiftIDEInspectionInstance - ); - - void (*_Nonnull connection_dispose)( - _Null_unspecified swiftide_api_connection_t - ); - - void (*_Nonnull connection_mark_cached_compiler_instance_should_be_invalidated)( - _Null_unspecified swiftide_api_connection_t, - _Null_unspecified swiftide_api_cache_invalidation_options_t - ); - - /// Override the contents of the file \p path with \p contents. If \p contents - /// is NULL, go back to using the real the file system. - void (*_Nonnull set_file_contents)( - _Null_unspecified swiftide_api_connection_t connection, - const char *_Null_unspecified path, - const char *_Null_unspecified contents - ); - - /// Cancel the request with \p handle. - void (*_Nonnull cancel_request)( - _Null_unspecified swiftide_api_connection_t _conn, - _Null_unspecified swiftide_api_request_handle_t handle - ); - - _Null_unspecified swiftide_api_completion_request_t (*_Nonnull completion_request_create)( - const char *_Null_unspecified path, - uint32_t offset, - char *_Null_unspecified const *_Null_unspecified const compiler_args, - uint32_t num_compiler_args - ); - - void (*_Nonnull completion_request_dispose)( - _Null_unspecified swiftide_api_completion_request_t - ); - - void (*_Nonnull completion_request_set_annotate_result)( - _Null_unspecified swiftide_api_completion_request_t, - bool - ); - - void (*_Nonnull completion_request_set_include_objectliterals)( - _Null_unspecified swiftide_api_completion_request_t, - bool - ); - - void (*_Nonnull completion_request_set_add_inits_to_top_level)( - _Null_unspecified swiftide_api_completion_request_t, - bool - ); - - - void (*_Nonnull completion_request_set_add_call_with_no_default_args)( - _Null_unspecified swiftide_api_completion_request_t, - bool - ); - - /// Same as swiftide_complete but supports cancellation. - /// This request is identified by \p handle. Calling swiftide_cancel_request - /// with that handle cancels the request. - /// Note that the caller is responsible for creating a unique request handle. - /// This differs from the sourcekitd functions in which SourceKit creates a - /// unique handle and passes it to the client via an out parameter. - _Null_unspecified swiftide_api_completion_response_t (*_Nonnull complete_cancellable)( - _Null_unspecified swiftide_api_connection_t _conn, - _Null_unspecified swiftide_api_completion_request_t _req, - _Null_unspecified swiftide_api_request_handle_t handle - ); - - void (*_Nonnull completion_result_dispose)( - _Null_unspecified swiftide_api_completion_response_t - ); - - bool (*_Nonnull completion_result_is_error)( - _Null_unspecified swiftide_api_completion_response_t - ); - - /// Result has the same lifetime as the result. - const char *_Null_unspecified (*_Nonnull completion_result_get_error_description)( - _Null_unspecified swiftide_api_completion_response_t - ); - - bool (*_Nonnull completion_result_is_cancelled)( - _Null_unspecified swiftide_api_completion_response_t - ); - - /// Copies a string representation of the completion result. This string should - /// be disposed of with \c free when done. - const char *_Null_unspecified (*_Nonnull completion_result_description_copy)( - _Null_unspecified swiftide_api_completion_response_t - ); - - void (*_Nonnull completion_result_get_completions)( - _Null_unspecified swiftide_api_completion_response_t, - void (^_Null_unspecified completions_handler)( - const _Null_unspecified swiftide_api_completion_item_t *_Null_unspecified completions, - const char *_Null_unspecified *_Null_unspecified filter_names, - uint64_t num_completions - ) - ); - - _Null_unspecified swiftide_api_completion_item_t (*_Nonnull completion_result_get_completion_at_index)( - _Null_unspecified swiftide_api_completion_response_t, - uint64_t index - ); - - swiftide_api_completion_kind_t (*_Nonnull completion_result_get_kind)( - _Null_unspecified swiftide_api_completion_response_t - ); - - void (*_Nonnull completion_result_foreach_baseexpr_typename)( - _Null_unspecified swiftide_api_completion_response_t, - bool (^_Null_unspecified handler)(const char *_Null_unspecified ) - ); - - bool (*_Nonnull completion_result_is_reusing_astcontext)( - _Null_unspecified swiftide_api_completion_response_t - ); - - /// Copies a string representation of the completion item. This string should - /// be disposed of with \c free when done. - const char *_Null_unspecified (*_Nonnull completion_item_description_copy)( - _Null_unspecified swiftide_api_completion_item_t - ); - - - void (*_Nonnull completion_item_get_label)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - bool annotate, - void (^_Null_unspecified handler)(const char *_Null_unspecified) - ); - - void (*_Nonnull completion_item_get_source_text)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - void (^_Null_unspecified handler)(const char *_Null_unspecified) - ); - - void (*_Nonnull completion_item_get_type_name)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - bool annotate, - void (^_Null_unspecified handler)(const char *_Null_unspecified) - ); - - void (*_Nonnull completion_item_get_doc_brief)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - void (^_Null_unspecified handler)(const char *_Null_unspecified) - ); - - void (*_Nullable completion_item_get_doc_full_as_xml)( - _Nonnull swiftide_api_completion_response_t, - _Nonnull swiftide_api_completion_item_t, - void (^_Nonnull handler)(const char *_Nullable)); - - void (*_Nullable completion_item_get_doc_raw)( - _Nonnull swiftide_api_completion_response_t, - _Nonnull swiftide_api_completion_item_t, - void (^_Nonnull handler)(const char *_Nullable)); - - void (*_Nonnull completion_item_get_associated_usrs)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - void (^_Null_unspecified handler)(const char *_Null_unspecified *_Null_unspecified, uint64_t) - ); - - uint32_t (*_Nonnull completion_item_get_kind)( - _Null_unspecified swiftide_api_completion_item_t - ); - - uint32_t (*_Nonnull completion_item_get_associated_kind)( - _Null_unspecified swiftide_api_completion_item_t - ); - - uint32_t (*_Nonnull completion_item_get_semantic_context)( - _Null_unspecified swiftide_api_completion_item_t - ); - - uint32_t (*_Nonnull completion_item_get_flair)( - _Null_unspecified swiftide_api_completion_item_t - ); - - bool (*_Nonnull completion_item_is_not_recommended)( - _Null_unspecified swiftide_api_completion_item_t - ); - - uint32_t (*_Nonnull completion_item_not_recommended_reason)( - _Null_unspecified swiftide_api_completion_item_t - ); - - bool (*_Nonnull completion_item_has_diagnostic)( - _Null_unspecified swiftide_api_completion_item_t _item - ); - - void (*_Nonnull completion_item_get_diagnostic)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - void (^_Null_unspecified handler)(swiftide_api_completion_diagnostic_severity_t, const char *_Null_unspecified) - ); - - bool (*_Nonnull completion_item_is_system)( - _Null_unspecified swiftide_api_completion_item_t - ); - - void (*_Nonnull completion_item_get_module_name)( - _Null_unspecified swiftide_api_completion_response_t _response, - _Null_unspecified swiftide_api_completion_item_t _item, - void (^_Null_unspecified handler)(const char *_Null_unspecified) - ); - - uint32_t (*_Nonnull completion_item_get_num_bytes_to_erase)( - _Null_unspecified swiftide_api_completion_item_t - ); - - uint32_t (*_Nonnull completion_item_get_type_relation)( - _Null_unspecified swiftide_api_completion_item_t - ); - - /// Returns 0 for items not in an external module, and ~0u if the other module - /// is not imported or the depth is otherwise unknown. - uint32_t (*_Nonnull completion_item_import_depth)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t - ); - - _Null_unspecified swiftide_api_fuzzy_match_pattern_t (*_Nonnull fuzzy_match_pattern_create)( - const char *_Null_unspecified pattern - ); - - bool (*_Nonnull fuzzy_match_pattern_matches_candidate)( - _Null_unspecified swiftide_api_fuzzy_match_pattern_t pattern, - const char *_Null_unspecified candidate, - double *_Null_unspecified outScore - ); - - void (*_Nonnull fuzzy_match_pattern_dispose)( - _Null_unspecified swiftide_api_fuzzy_match_pattern_t - ); -} sourcekitd_ide_api_functions_t; - -#endif - diff --git a/Sources/Csourcekitd/include/module.modulemap b/Sources/Csourcekitd/include/module.modulemap deleted file mode 100644 index d6c621c52..000000000 --- a/Sources/Csourcekitd/include/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -module Csourcekitd { - header "sourcekitd_functions.h" - header "CodeCompletionSwiftInterop.h" - header "plugin.h" - export * -} diff --git a/Sources/Csourcekitd/include/plugin.h b/Sources/Csourcekitd/include/plugin.h deleted file mode 100644 index c08ff3282..000000000 --- a/Sources/Csourcekitd/include/plugin.h +++ /dev/null @@ -1,483 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifndef SWIFT_SOURCEKITD_PLUGIN_H -#define SWIFT_SOURCEKITD_PLUGIN_H - -#include "sourcekitd_functions.h" - - -typedef void *sourcekitd_api_variant_functions_t; - -typedef sourcekitd_api_variant_type_t (*sourcekitd_api_variant_functions_get_type_t)( - sourcekitd_api_variant_t obj -); -typedef bool (*sourcekitd_api_variant_functions_array_apply_t)( - sourcekitd_api_variant_t array, - _Null_unspecified sourcekitd_api_variant_array_applier_f_t applier, - void *_Null_unspecified context -); -typedef bool (*sourcekitd_api_variant_functions_array_get_bool_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef double (*sourcekitd_api_variant_functions_array_get_double_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef size_t (*sourcekitd_api_variant_functions_array_get_count_t)( - sourcekitd_api_variant_t array -); -typedef int64_t (*sourcekitd_api_variant_functions_array_get_int64_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef const char *_Null_unspecified (*sourcekitd_api_variant_functions_array_get_string_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef _Null_unspecified sourcekitd_api_uid_t (*sourcekitd_api_variant_functions_array_get_uid_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef sourcekitd_api_variant_t (*sourcekitd_api_variant_functions_array_get_value_t)( - sourcekitd_api_variant_t array, - size_t index -); -typedef bool (*sourcekitd_api_variant_functions_bool_get_value_t)( - sourcekitd_api_variant_t obj -); -typedef double (*sourcekitd_api_variant_functions_double_get_value_t)( - sourcekitd_api_variant_t obj -); -typedef bool (*sourcekitd_api_variant_functions_dictionary_apply_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_variant_dictionary_applier_f_t applier, - void *_Null_unspecified context -); -typedef bool (*sourcekitd_api_variant_functions_dictionary_get_bool_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef double (*sourcekitd_api_variant_functions_dictionary_get_double_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef int64_t (*sourcekitd_api_variant_functions_dictionary_get_int64_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef const char *_Null_unspecified (*sourcekitd_api_variant_functions_dictionary_get_string_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef sourcekitd_api_variant_t (*sourcekitd_api_variant_functions_dictionary_get_value_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef _Null_unspecified sourcekitd_api_uid_t (*sourcekitd_api_variant_functions_dictionary_get_uid_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_uid_t key -); -typedef size_t (*sourcekitd_api_variant_functions_string_get_length_t)( - sourcekitd_api_variant_t obj -); -typedef const char *_Null_unspecified (*sourcekitd_api_variant_functions_string_get_ptr_t)( - sourcekitd_api_variant_t obj -); -typedef int64_t (*sourcekitd_api_variant_functions_int64_get_value_t)( - sourcekitd_api_variant_t obj -); -typedef _Null_unspecified sourcekitd_api_uid_t (*sourcekitd_api_variant_functions_uid_get_value_t)( - sourcekitd_api_variant_t obj -); -typedef size_t (*sourcekitd_api_variant_functions_data_get_size_t)( - sourcekitd_api_variant_t obj -); -typedef const void *_Null_unspecified (*sourcekitd_api_variant_functions_data_get_ptr_t)( - sourcekitd_api_variant_t obj -); - -/// Handle the request specified by the \c sourcekitd_api_object_t and keep track -/// of it using the \c sourcekitd_api_request_handle_t. If the cancellation handler -/// specified by \c sourcekitd_api_plugin_initialize_register_cancellation_handler -/// is called with the this request handle, the request should be cancelled. -typedef bool (^sourcekitd_api_cancellable_request_handler_t)( - _Null_unspecified sourcekitd_api_object_t, - _Null_unspecified sourcekitd_api_request_handle_t, - void (^_Null_unspecified SWIFT_SENDABLE)(_Null_unspecified sourcekitd_api_response_t) -); -typedef void (^sourcekitd_api_cancellation_handler_t)(_Null_unspecified sourcekitd_api_request_handle_t); -typedef _Null_unspecified sourcekitd_api_uid_t (*sourcekitd_api_uid_get_from_cstr_t)(const char *_Null_unspecified string); -typedef const char *_Null_unspecified (*sourcekitd_api_uid_get_string_ptr_t)(_Null_unspecified sourcekitd_api_uid_t); - -typedef void *sourcekitd_api_plugin_initialize_params_t; -typedef void (*sourcekitd_api_plugin_initialize_t)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t -); - -typedef struct { - _Null_unspecified sourcekitd_api_variant_functions_t (*_Nonnull variant_functions_create)(void); - - void (*_Nonnull variant_functions_set_get_type)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_get_type_t f - ); - void (*_Nonnull variant_functions_set_array_apply)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_apply_t f - ); - void (*_Nonnull variant_functions_set_array_get_bool)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_bool_t f - ); - void (*_Nonnull variant_functions_set_array_get_double)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_double_t f - ); - void (*_Nonnull variant_functions_set_array_get_count)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_count_t f - ); - void (*_Nonnull variant_functions_set_array_get_int64)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_int64_t f - ); - void (*_Nonnull variant_functions_set_array_get_string)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_string_t f - ); - void (*_Nonnull variant_functions_set_array_get_uid)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_uid_t f - ); - void (*_Nonnull variant_functions_set_array_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_array_get_value_t f - ); - void (*_Nonnull variant_functions_set_bool_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_bool_get_value_t f - ); - void (*_Nonnull variant_functions_set_double_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_double_get_value_t f - ); - void (*_Nonnull variant_functions_set_dictionary_apply)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_apply_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_bool)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_bool_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_double)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_double_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_int64)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_int64_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_string)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_string_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_value_t f - ); - void (*_Nonnull variant_functions_set_dictionary_get_uid)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_dictionary_get_uid_t f - ); - void (*_Nonnull variant_functions_set_string_get_length)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_string_get_length_t f - ); - void (*_Nonnull variant_functions_set_string_get_ptr)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_string_get_ptr_t f - ); - void (*_Nonnull variant_functions_set_int64_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_int64_get_value_t f - ); - void (*_Nonnull variant_functions_set_uid_get_value)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_uid_get_value_t f - ); - void (*_Nonnull variant_functions_set_data_get_size)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_data_get_size_t f - ); - void (*_Nonnull variant_functions_set_data_get_ptr)( - _Nonnull sourcekitd_api_variant_functions_t funcs, - _Nonnull sourcekitd_api_variant_functions_data_get_ptr_t f - ); - - bool (*_Nonnull plugin_initialize_is_client_only)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t - ); - - uint64_t (*_Nonnull plugin_initialize_custom_buffer_start)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t - ); - - // The following would be more idiomatically written as follows but marking `sourcekitd_api_uid_get_from_cstr_t` as - // `Sendable` hits https://github.com/swiftlang/swift/issues/83892. - // _Null_unspecified SWIFT_SENDABLE sourcekitd_api_uid_get_from_cstr_t (*_Nonnull plugin_initialize_uid_get_from_cstr)( - // _Null_unspecified sourcekitd_api_plugin_initialize_params_t - // ); - _Null_unspecified sourcekitd_api_uid_t (*_Null_unspecified SWIFT_SENDABLE (*_Nonnull plugin_initialize_uid_get_from_cstr)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t - ))(const char *_Null_unspecified string); - - // The following would be more idiomatically written as follows but marking `sourcekitd_api_uid_get_string_ptr_t` as - // `Sendable` hits https://github.com/swiftlang/swift/issues/83892. - // _Null_unspecified SWIFT_SENDABLE sourcekitd_api_uid_get_string_ptr_t (*_Nonnull plugin_initialize_uid_get_string_ptr)( - // _Null_unspecified sourcekitd_api_plugin_initialize_params_t - // ); - const char *_Null_unspecified (*_Null_unspecified SWIFT_SENDABLE (*_Nonnull plugin_initialize_uid_get_string_ptr)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t - ))(_Null_unspecified sourcekitd_api_uid_t); - - void (*_Nonnull plugin_initialize_register_custom_buffer)( - _Nonnull sourcekitd_api_plugin_initialize_params_t, - uint64_t kind, - _Nonnull sourcekitd_api_variant_functions_t funcs - ); -} sourcekitd_plugin_api_functions_t; - -typedef struct { - void (*_Nonnull plugin_initialize_register_cancellable_request_handler)( - _Nonnull sourcekitd_api_plugin_initialize_params_t, - _Nonnull SWIFT_SENDABLE sourcekitd_api_cancellable_request_handler_t - ); - - /// Adds a function that will be called when a request is cancelled. - /// The cancellation handler is called even for cancelled requests that are handled by - /// sourcekitd itself and not the plugin. If the plugin doesn't know the request - /// handle to be cancelled, it should ignore the cancellation request. - void (*_Nonnull plugin_initialize_register_cancellation_handler)( - _Nonnull sourcekitd_api_plugin_initialize_params_t, - _Nonnull SWIFT_SENDABLE sourcekitd_api_cancellation_handler_t - ); - - void *_Null_unspecified(*_Nonnull plugin_initialize_get_swift_ide_inspection_instance)( - _Null_unspecified sourcekitd_api_plugin_initialize_params_t - ); - - //============================================================================// - // Request - //============================================================================// - - sourcekitd_api_variant_type_t (*_Nonnull request_get_type)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_dictionary_get_value)( - _Null_unspecified sourcekitd_api_object_t dict, - _Nonnull sourcekitd_api_uid_t key - ); - - /// The underlying C string for the specified key. NULL if the value for the - /// specified key is not a C string value or if there is no value for the - /// specified key. - const char *_Null_unspecified (*_Nonnull request_dictionary_get_string)( - _Nonnull sourcekitd_api_object_t dict, - _Nonnull sourcekitd_api_uid_t key - ); - - /// The underlying \c int64 value for the specified key. 0 if the - /// value for the specified key is not an integer value or if there is no - /// value for the specified key. - int64_t (*_Nonnull request_dictionary_get_int64)( - _Nonnull sourcekitd_api_object_t dict, - _Nonnull sourcekitd_api_uid_t key - ); - - /// The underlying \c bool value for the specified key. false if the - /// value for the specified key is not a Boolean value or if there is no - /// value for the specified key. - bool (*_Nonnull request_dictionary_get_bool)( - _Nonnull sourcekitd_api_object_t dict, - _Nonnull sourcekitd_api_uid_t key - ); - - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull request_dictionary_get_uid)( - _Nonnull sourcekitd_api_object_t dict, - _Nonnull sourcekitd_api_uid_t key - ); - - size_t (*_Nonnull request_array_get_count)( - _Null_unspecified sourcekitd_api_object_t array - ); - - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_array_get_value)( - _Null_unspecified sourcekitd_api_object_t array, - size_t index - ); - - const char *_Null_unspecified (*_Nonnull request_array_get_string)( - _Null_unspecified sourcekitd_api_object_t array, - size_t index - ); - - int64_t (*_Nonnull request_array_get_int64)( - _Null_unspecified sourcekitd_api_object_t array, - size_t index - ); - - bool (*_Nonnull request_array_get_bool)( - _Null_unspecified sourcekitd_api_object_t array, - size_t index - ); - - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull request_array_get_uid)( - _Null_unspecified sourcekitd_api_object_t array, - size_t index - ); - - int64_t (*_Nonnull request_int64_get_value)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - bool (*_Nonnull request_bool_get_value)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - size_t (*_Nonnull request_string_get_length)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - const char *_Null_unspecified (*_Nonnull request_string_get_ptr)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull request_uid_get_value)( - _Null_unspecified sourcekitd_api_object_t obj - ); - - //============================================================================// - // Response - //============================================================================// - - _Nonnull sourcekitd_api_response_t (*_Nonnull response_retain)( - _Nonnull sourcekitd_api_response_t object - ); - - _Null_unspecified sourcekitd_api_response_t (*_Nonnull response_error_create)( - sourcekitd_api_error_t kind, - const char *_Null_unspecified description - ); - - _Nonnull sourcekitd_api_response_t (*_Nonnull response_dictionary_create)( - const _Null_unspecified sourcekitd_api_uid_t *_Null_unspecified keys, - const _Null_unspecified sourcekitd_api_response_t *_Null_unspecified values, - size_t count - ); - - void (*_Nonnull response_dictionary_set_value)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - _Nonnull sourcekitd_api_response_t value - ); - - void (*_Nonnull response_dictionary_set_string)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - const char *_Nonnull string - ); - - void (*_Nonnull response_dictionary_set_stringbuf)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - const char *_Nonnull buf, - size_t length - ); - - void (*_Nonnull response_dictionary_set_int64)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - int64_t val - ); - - void (*_Nonnull response_dictionary_set_bool)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - bool val - ); - - void (*_Nonnull response_dictionary_set_double)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - double val - ); - - void (*_Nonnull response_dictionary_set_uid)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - _Nonnull sourcekitd_api_uid_t uid - ); - - _Nonnull sourcekitd_api_response_t (*_Nonnull response_array_create)( - const _Null_unspecified sourcekitd_api_response_t *_Null_unspecified objects, - size_t count - ); - - void (*_Nonnull response_array_set_value)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - _Nonnull sourcekitd_api_response_t value - ); - - void (*_Nonnull response_array_set_string)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - const char *_Nonnull string - ); - - void (*_Nonnull response_array_set_stringbuf)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - const char *_Nonnull buf, - size_t length - ); - - void (*_Nonnull response_array_set_int64)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - int64_t val - ); - - void (*_Nonnull response_array_set_double)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - double val - ); - - void (*_Nonnull response_array_set_uid)( - _Nonnull sourcekitd_api_response_t array, - size_t index, - _Nonnull sourcekitd_api_uid_t uid - ); - - void (*_Nonnull response_dictionary_set_custom_buffer)( - _Nonnull sourcekitd_api_response_t dict, - _Nonnull sourcekitd_api_uid_t key, - const void *_Nonnull ptr, - size_t size - ); -} sourcekitd_service_plugin_api_functions_t; - -#endif diff --git a/Sources/Csourcekitd/include/sourcekitd_functions.h b/Sources/Csourcekitd/include/sourcekitd_functions.h deleted file mode 100644 index fc5944f92..000000000 --- a/Sources/Csourcekitd/include/sourcekitd_functions.h +++ /dev/null @@ -1,334 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#ifndef SOURCEKITDFUNCTIONS_H -#define SOURCEKITDFUNCTIONS_H - -#include -#include -#include - -#define SWIFT_SENDABLE __attribute__((__swift_attr__("@Sendable"))) - -// Avoid including to make sure we don't call the -// functions directly. But we need the types to form the function pointers. -// These are supposed to stay stable across toolchains. - -typedef void *sourcekitd_api_object_t; -typedef struct sourcekitd_api_uid_s *sourcekitd_api_uid_t; -typedef void *sourcekitd_api_response_t; -typedef const void *sourcekitd_api_request_handle_t; - -typedef struct { - uint64_t data[3]; -} sourcekitd_api_variant_t; - -typedef enum { - SOURCEKITD_API_VARIANT_TYPE_NULL = 0, - SOURCEKITD_API_VARIANT_TYPE_DICTIONARY = 1, - SOURCEKITD_API_VARIANT_TYPE_ARRAY = 2, - SOURCEKITD_API_VARIANT_TYPE_INT64 = 3, - SOURCEKITD_API_VARIANT_TYPE_STRING = 4, - SOURCEKITD_API_VARIANT_TYPE_UID = 5, - SOURCEKITD_API_VARIANT_TYPE_BOOL = 6, - SOURCEKITD_API_VARIANT_TYPE_DOUBLE = 7, - SOURCEKITD_API_VARIANT_TYPE_DATA = 8, -} sourcekitd_api_variant_type_t; - -typedef void *sourcekitd_api_variant_functions_t; - -typedef sourcekitd_api_variant_type_t (*sourcekitd_api_variant_functions_get_type_t)( - sourcekitd_api_variant_t obj -); - -typedef size_t (*sourcekitd_api_variant_functions_array_get_count_t)( - sourcekitd_api_variant_t array -); - -typedef sourcekitd_api_variant_t (*sourcekitd_api_variant_functions_array_get_value_t)( - sourcekitd_api_variant_t array, - size_t index -); - -typedef bool (*sourcekitd_api_variant_array_applier_f_t)( - size_t index, - sourcekitd_api_variant_t value, - void *_Null_unspecified context -); - -typedef bool (*sourcekitd_api_variant_dictionary_applier_f_t)( - _Null_unspecified sourcekitd_api_uid_t key, - sourcekitd_api_variant_t value, - void *_Null_unspecified context -); - -typedef bool (*sourcekitd_api_variant_functions_dictionary_apply_t)( - sourcekitd_api_variant_t dict, - _Null_unspecified sourcekitd_api_variant_dictionary_applier_f_t applier, - void *_Null_unspecified context -); - -typedef enum { - SOURCEKITD_API_ERROR_CONNECTION_INTERRUPTED = 1, - SOURCEKITD_API_ERROR_REQUEST_INVALID = 2, - SOURCEKITD_API_ERROR_REQUEST_FAILED = 3, - SOURCEKITD_API_ERROR_REQUEST_CANCELLED = 4 -} sourcekitd_api_error_t; - -typedef void (^sourcekitd_api_interrupted_connection_handler_t)(void); -typedef void (^sourcekitd_api_response_receiver_t)( - sourcekitd_api_response_t _Nullable resp -); - -typedef sourcekitd_api_uid_t _Nullable (^sourcekitd_api_uid_from_str_handler_t)( - const char *_Nullable uidStr -); -typedef const char *_Nullable (^sourcekitd_api_str_from_uid_handler_t)( - sourcekitd_api_uid_t _Nullable uid -); - -typedef void *sourcekitd_api_plugin_initialize_params_t; - -typedef struct { - void (*_Nonnull initialize)(void); - void (*_Nonnull shutdown)(void); - - void (*_Nullable register_plugin_path)( - const char *_Nullable clientPlugin, - const char *_Nullable servicePlugin - ); - - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull uid_get_from_cstr)( - const char *_Nonnull string - ); - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull uid_get_from_buf)( - const char *_Nonnull buf, - size_t length - ); - size_t (*_Nonnull uid_get_length)( - sourcekitd_api_uid_t _Nonnull obj - ); - const char *_Null_unspecified (*_Nonnull uid_get_string_ptr)( - sourcekitd_api_uid_t _Nonnull obj - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_retain)( - sourcekitd_api_object_t _Nonnull object - ); - void (*_Nonnull request_release)( - sourcekitd_api_object_t _Nonnull object - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_dictionary_create)( - const _Nullable sourcekitd_api_uid_t *_Nullable keys, - const _Nullable sourcekitd_api_object_t *_Nullable values, - size_t count - ); - void (*_Nonnull request_dictionary_set_value)( - sourcekitd_api_object_t _Nonnull dict, - sourcekitd_api_uid_t _Nonnull key, - sourcekitd_api_object_t _Nonnull value - ); - void (*_Nonnull request_dictionary_set_string)( - sourcekitd_api_object_t _Nonnull dict, - sourcekitd_api_uid_t _Nonnull key, - const char *_Nonnull string - ); - void (*_Nonnull request_dictionary_set_stringbuf)( - sourcekitd_api_object_t _Nonnull dict, - sourcekitd_api_uid_t _Nonnull key, - const char *_Nonnull buf, - size_t length - ); - void (*_Nonnull request_dictionary_set_int64)( - sourcekitd_api_object_t _Nonnull dict, - sourcekitd_api_uid_t _Nonnull key, - int64_t val - ); - void (*_Nonnull request_dictionary_set_uid)( - sourcekitd_api_object_t _Nonnull dict, - sourcekitd_api_uid_t _Nonnull key, - sourcekitd_api_uid_t _Nonnull uid - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_array_create)( - const _Nullable sourcekitd_api_object_t *_Nullable objects, - size_t count - ); - void (*_Nonnull request_array_set_value)( - sourcekitd_api_object_t _Nonnull array, - size_t index, - sourcekitd_api_object_t _Nonnull value - ); - void (*_Nonnull request_array_set_string)( - sourcekitd_api_object_t _Nonnull array, - size_t index, - const char *_Nonnull string - ); - void (*_Nonnull request_array_set_stringbuf)( - sourcekitd_api_object_t _Nonnull array, - size_t index, - const char *_Nonnull buf, - size_t length - ); - void (*_Nonnull request_array_set_int64)( - sourcekitd_api_object_t _Nonnull array, - size_t index, - int64_t val - ); - void (*_Nonnull request_array_set_uid)( - sourcekitd_api_object_t _Nonnull array, - size_t index, - sourcekitd_api_uid_t _Nonnull uid - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_int64_create)( - int64_t val - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_string_create)( - const char *_Nonnull string - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_uid_create)( - sourcekitd_api_uid_t _Nonnull uid - ); - _Null_unspecified sourcekitd_api_object_t (*_Nonnull request_create_from_yaml)( - const char *_Nonnull yaml, - char *_Nullable *_Nullable error - ); - void (*_Nonnull request_description_dump)( - sourcekitd_api_object_t _Nonnull obj - ); - char *_Null_unspecified (*_Nonnull request_description_copy)( - sourcekitd_api_object_t _Nonnull obj - ); - void (*_Nonnull response_dispose)( - sourcekitd_api_response_t _Nonnull obj - ); - bool (*_Nonnull response_is_error)( - sourcekitd_api_response_t _Nonnull obj - ); - sourcekitd_api_error_t (*_Nonnull response_error_get_kind)( - sourcekitd_api_response_t _Nonnull err - ); - const char *_Null_unspecified (*_Nonnull response_error_get_description)( - sourcekitd_api_response_t _Nonnull err - ); - sourcekitd_api_variant_t (*_Nonnull response_get_value)( - sourcekitd_api_response_t _Nonnull resp - ); - sourcekitd_api_variant_type_t (*_Nonnull variant_get_type)( - sourcekitd_api_variant_t obj - ); - sourcekitd_api_variant_t (*_Nonnull variant_dictionary_get_value)( - sourcekitd_api_variant_t dict, - sourcekitd_api_uid_t _Nonnull key - ); - const char *_Null_unspecified (*_Nonnull variant_dictionary_get_string)( - sourcekitd_api_variant_t dict, - sourcekitd_api_uid_t _Nonnull key - ); - int64_t (*_Nonnull variant_dictionary_get_int64)( - sourcekitd_api_variant_t dict, - sourcekitd_api_uid_t _Nonnull key - ); - bool (*_Nonnull variant_dictionary_get_bool)( - sourcekitd_api_variant_t dict, - sourcekitd_api_uid_t _Nonnull key - ); - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull variant_dictionary_get_uid)( - sourcekitd_api_variant_t dict, - sourcekitd_api_uid_t _Nonnull key - ); - size_t (*_Nonnull variant_array_get_count)( - sourcekitd_api_variant_t array - ); - sourcekitd_api_variant_t (*_Nonnull variant_array_get_value)( - sourcekitd_api_variant_t array, - size_t index - ); - const char *_Null_unspecified (*_Nonnull variant_array_get_string)( - sourcekitd_api_variant_t array, - size_t index - ); - int64_t (*_Nonnull variant_array_get_int64)( - sourcekitd_api_variant_t array, - size_t index - ); - bool (*_Nonnull variant_array_get_bool)( - sourcekitd_api_variant_t array, - size_t index - ); - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull variant_array_get_uid)( - sourcekitd_api_variant_t array, - size_t index - ); - int64_t (*_Nonnull variant_int64_get_value)( - sourcekitd_api_variant_t obj - ); - bool (*_Nonnull variant_bool_get_value)( - sourcekitd_api_variant_t obj - ); - double (*_Nullable variant_double_get_value)( - sourcekitd_api_variant_t obj - ); - size_t (*_Nonnull variant_string_get_length)( - sourcekitd_api_variant_t obj - ); - const char *_Null_unspecified (*_Nonnull variant_string_get_ptr)( - sourcekitd_api_variant_t obj - ); - size_t (*_Nullable variant_data_get_size)( - sourcekitd_api_variant_t obj - ); - const void *_Null_unspecified (*_Nullable variant_data_get_ptr)( - sourcekitd_api_variant_t obj - ); - _Null_unspecified sourcekitd_api_uid_t (*_Nonnull variant_uid_get_value)( - sourcekitd_api_variant_t obj - ); - void (*_Nonnull response_description_dump)( - sourcekitd_api_response_t _Nonnull resp - ); - void (*_Nonnull response_description_dump_filedesc)( - sourcekitd_api_response_t _Nonnull resp, - int fd - ); - char *_Null_unspecified (*_Nonnull response_description_copy)( - sourcekitd_api_response_t _Nonnull resp - ); - void (*_Nonnull variant_description_dump)( - sourcekitd_api_variant_t obj - ); - void (*_Nonnull variant_description_dump_filedesc)( - sourcekitd_api_variant_t obj, - int fd - ); - char *_Null_unspecified (*_Nonnull variant_description_copy)( - sourcekitd_api_variant_t obj - ); - _Null_unspecified sourcekitd_api_response_t (*_Nonnull send_request_sync)( - _Nonnull sourcekitd_api_object_t req - ); - void (*_Nonnull send_request)( - sourcekitd_api_object_t _Nonnull req, - _Nullable sourcekitd_api_request_handle_t *_Nullable out_handle, - _Nullable SWIFT_SENDABLE sourcekitd_api_response_receiver_t receiver - ); - void (*_Nonnull cancel_request)( - _Nullable sourcekitd_api_request_handle_t handle - ); - void (*_Nonnull set_notification_handler)( - _Nullable SWIFT_SENDABLE sourcekitd_api_response_receiver_t receiver - ); - void (*_Nonnull set_uid_handlers)( - _Nullable SWIFT_SENDABLE sourcekitd_api_uid_from_str_handler_t uid_from_str, - _Nullable SWIFT_SENDABLE sourcekitd_api_str_from_uid_handler_t str_from_uid - ); -} sourcekitd_api_functions_t; - -#endif diff --git a/Sources/Csourcekitd/sourcekitd.c b/Sources/Csourcekitd/sourcekitd.c deleted file mode 100644 index 7deedcd71..000000000 --- a/Sources/Csourcekitd/sourcekitd.c +++ /dev/null @@ -1,13 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -// This file is here to prevent the package manager from warning about a target with no sources. diff --git a/Sources/Diagnose/CMakeLists.txt b/Sources/Diagnose/CMakeLists.txt deleted file mode 100644 index 4ed6d14e9..000000000 --- a/Sources/Diagnose/CMakeLists.txt +++ /dev/null @@ -1,46 +0,0 @@ -add_library(Diagnose STATIC - CommandLineArgumentsReducer.swift - DebugCommand.swift - DiagnoseCommand.swift - GenericError.swift - IndexCommand.swift - MergeSwiftFiles.swift - OSLogScraper.swift - ReduceCommand.swift - ReduceFrontendCommand.swift - ReduceSourceKitDRequest.swift - ReduceSwiftFrontend.swift - ReproducerBundle.swift - RequestInfo.swift - RunSourcekitdRequestCommand.swift - SourceKitD+RunWithYaml.swift - SourceKitDRequestExecutor.swift - SourceReducer.swift - StderrStreamConcurrencySafe.swift - SwiftFrontendCrashScraper.swift - Toolchain+SwiftFrontend.swift - Toolchain+PluginPaths.swift - TraceFromSignpostsCommand.swift) - -set_target_properties(Diagnose PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) - -target_link_libraries(Diagnose PUBLIC - BuildServerIntegration - InProcessClient - SKLogging - SKOptions - SourceKitD - SwiftExtensions - ToolchainRegistry - ArgumentParser - SwiftSyntax::SwiftIDEUtils - SwiftSyntax::SwiftSyntax - SwiftSyntax::SwiftParser - TSCBasic -) - -target_link_libraries(Diagnose PRIVATE - SKUtilities - TSCExtensions -) diff --git a/Sources/Diagnose/CommandLineArgumentsReducer.swift b/Sources/Diagnose/CommandLineArgumentsReducer.swift deleted file mode 100644 index ecea363b9..000000000 --- a/Sources/Diagnose/CommandLineArgumentsReducer.swift +++ /dev/null @@ -1,148 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging - -// MARK: - Entry point - -extension RequestInfo { - @MainActor - func reduceCommandLineArguments( - using executor: SourceKitRequestExecutor, - progressUpdate: (_ progress: Double, _ message: String) -> Void - ) async throws -> RequestInfo { - try await withoutActuallyEscaping(progressUpdate) { progressUpdate in - let reducer = CommandLineArgumentReducer(sourcekitdExecutor: executor, progressUpdate: progressUpdate) - return try await reducer.run(initialRequestInfo: self) - } - } -} - -// MARK: - FileProducer - -/// Reduces the compiler arguments needed to reproduce a sourcekitd crash. -private class CommandLineArgumentReducer { - /// The executor that is used to run a sourcekitd request and check whether it - /// still crashes. - private let sourcekitdExecutor: SourceKitRequestExecutor - - /// A callback to be called when the reducer has made progress reducing the request - private let progressUpdate: (_ progress: Double, _ message: String) -> Void - - /// The number of command line arguments when the reducer was started. - private var initialCommandLineCount: Int = 0 - - init( - sourcekitdExecutor: SourceKitRequestExecutor, - progressUpdate: @escaping (_ progress: Double, _ message: String) -> Void - ) { - self.sourcekitdExecutor = sourcekitdExecutor - self.progressUpdate = progressUpdate - } - - @MainActor - func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo { - var requestInfo = initialRequestInfo - requestInfo = try await reduce(initialRequestInfo: requestInfo, simultaneousRemove: 10) - requestInfo = try await reduce(initialRequestInfo: requestInfo, simultaneousRemove: 1) - return requestInfo - } - - /// Reduce the command line arguments of the given `RequestInfo`. - /// - /// If `simultaneousRemove` is set, the reducer will try to remove that many arguments at once. This is useful to - /// quickly remove multiple arguments from the request. - @MainActor - private func reduce(initialRequestInfo: RequestInfo, simultaneousRemove: Int) async throws -> RequestInfo { - guard initialRequestInfo.compilerArgs.count > simultaneousRemove else { - // Trying to remove more command line arguments than we have. This isn't going to work. - return initialRequestInfo - } - - var requestInfo = initialRequestInfo - self.initialCommandLineCount = requestInfo.compilerArgs.count - - var argumentIndexToRemove = requestInfo.compilerArgs.count - 1 - while argumentIndexToRemove + 1 >= simultaneousRemove { - defer { - // argumentIndexToRemove can become negative by being decremented in the code below - let progress = 1 - (Double(max(argumentIndexToRemove, 0)) / Double(initialCommandLineCount)) - progressUpdate(progress, "Reduced compiler arguments to \(requestInfo.compilerArgs.count)") - } - var numberOfArgumentsToRemove = simultaneousRemove - // If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well. - if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-X") ?? false { - numberOfArgumentsToRemove += 1 - } - - let rangeToRemove = (argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove - if let reduced = try await tryRemoving(rangeToRemove, from: requestInfo) { - requestInfo = reduced - argumentIndexToRemove -= numberOfArgumentsToRemove - continue - } - - // If removing the argument failed and the argument is preceded by an argument starting with `-`, try removing - // that as well. E.g. removing `-F` followed by a search path. - if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-") ?? false { - numberOfArgumentsToRemove += 1 - - // If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well. - if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-X") ?? false { - numberOfArgumentsToRemove += 1 - } - - let rangeToRemove = (argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove - if let reduced = try await tryRemoving(rangeToRemove, from: requestInfo) { - requestInfo = reduced - argumentIndexToRemove -= numberOfArgumentsToRemove - continue - } - } - - argumentIndexToRemove -= simultaneousRemove - } - - return requestInfo - } - - @MainActor - private func tryRemoving( - _ argumentsToRemove: ClosedRange, - from requestInfo: RequestInfo - ) async throws -> RequestInfo? { - logger.debug("Try removing the following compiler arguments:\n\(requestInfo.compilerArgs[argumentsToRemove])") - var reducedRequestInfo = requestInfo - reducedRequestInfo.compilerArgs.removeSubrange(argumentsToRemove) - - let result = try await sourcekitdExecutor.run(request: reducedRequestInfo) - if case .reproducesIssue = result { - logger.debug("Reduction successful") - return reducedRequestInfo - } else { - // The reduced request did not crash. We did not find a reduced test case, so return `nil`. - logger.debug("Reduction did not reproduce the issue") - return nil - } - } -} - -fileprivate extension Array { - /// Access index in the array if it's in bounds or return `nil` if `index` is outside of the array's bounds. - subscript(safe index: Int) -> Element? { - if index < 0 || index >= count { - return nil - } - return self[index] - } -} diff --git a/Sources/Diagnose/DebugCommand.swift b/Sources/Diagnose/DebugCommand.swift deleted file mode 100644 index 42d5639dd..000000000 --- a/Sources/Diagnose/DebugCommand.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import ArgumentParser - -package struct DebugCommand: ParsableCommand { - package static let configuration = CommandConfiguration( - commandName: "debug", - abstract: "Commands to debug sourcekit-lsp. Intended for developers of sourcekit-lsp", - subcommands: [ - IndexCommand.self, - ReduceCommand.self, - ReduceFrontendCommand.self, - RunSourceKitdRequestCommand.self, - TraceFromSignpostsCommand.self, - ] - ) - - package init() {} -} diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift deleted file mode 100644 index 5a251f23d..000000000 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ /dev/null @@ -1,516 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import ArgumentParser -import Foundation -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions -import TSCExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import class TSCUtility.PercentProgressAnimation - -/// When diagnosis is started, a progress bar displayed on the terminal that shows how far the diagnose command has -/// progressed. -/// Can't be a member of `DiagnoseCommand` because then `DiagnoseCommand` is no longer codable, which it needs to be -/// to be a `AsyncParsableCommand`. -@MainActor -private var progressBar: PercentProgressAnimation? = nil - -/// The last progress that was reported on the progress bar. This ensures that when the progress indicator uses the -/// `MultiLinePercentProgressAnimation` (eg. because stderr is redirected to a file) we don't emit status updates -/// without making any real progress. -@MainActor -private var lastProgress: (Int, String)? = nil - -/// A component of the diagnostic bundle that's collected in independent stages. -private enum BundleComponent: String, CaseIterable, ExpressibleByArgument { - case crashReports = "crash-reports" - case logs = "logs" - case swiftVersions = "swift-versions" - case sourcekitdCrashes = "sourcekitd-crashes" - case swiftFrontendCrashes = "swift-frontend-crashes" -} - -package struct DiagnoseCommand: AsyncParsableCommand { - package static let configuration: CommandConfiguration = CommandConfiguration( - commandName: "diagnose", - abstract: "Creates a bundle containing information that help diagnose issues with sourcekit-lsp" - ) - - @Option( - name: .customLong("os-log-history"), - help: "If no request file is passed, how many minutes of OS Log history should be scraped for a crash." - ) - var osLogScrapeDuration: Int = 60 - - @Option( - name: .customLong("toolchain"), - help: """ - The toolchain used to reduce the sourcekitd issue. \ - If not specified, the toolchain is found in the same way that sourcekit-lsp finds it - """ - ) - var toolchainOverride: String? - - @Option( - parsing: .upToNextOption, - help: """ - A space separated list of components to include in the diagnostic bundle. Includes all components by default. - - Possible options are: \(BundleComponent.allCases.map(\.rawValue).joined(separator: ", ")) - """ - ) - private var components: [BundleComponent] = BundleComponent.allCases - - @Option( - help: """ - The directory to which the diagnostic bundle should be written. No file or directory should exist at this path. \ - After sourcekit-lsp diagnose runs, a directory will exist at this path that contains the diagnostic bundle. - """ - ) - var bundleOutputPath: String? = nil - - var toolchainRegistry: ToolchainRegistry { - get throws { - let installPath = Bundle.main.bundleURL - return ToolchainRegistry(installPath: installPath) - } - } - - @MainActor - var toolchain: Toolchain? { - get async throws { - if let toolchainOverride { - return Toolchain(URL(fileURLWithPath: toolchainOverride)) - } - return try await toolchainRegistry.default - } - } - - /// Request infos of crashes that should be diagnosed. - func requestInfos() throws -> [(name: String, info: RequestInfo)] { - #if canImport(OSLog) - return try OSLogScraper(searchDuration: TimeInterval(osLogScrapeDuration * 60)).getCrashedRequests() - #else - throw GenericError("Reduction of sourcekitd crashes is not supported on platforms other than macOS") - #endif - } - - private var directoriesToScanForCrashReports: [String] { - ["/Library/Logs/DiagnosticReports", "~/Library/Logs/DiagnosticReports"] - } - - package init() {} - - @MainActor - private func addSourcekitdCrashReproducer(toBundle bundlePath: URL) async throws { - reportProgress(.reproducingSourcekitdCrash(progress: 0), message: "Trying to reduce recent sourcekitd crashes") - for (name, requestInfo) in try requestInfos() { - reportProgress(.reproducingSourcekitdCrash(progress: 0), message: "Reducing sourcekitd crash \(name)") - do { - try await reduce( - requestInfo: requestInfo, - toolchain: toolchain, - bundlePath: bundlePath.appending(component: "sourcekitd-crash"), - progressUpdate: { (progress, message) in - reportProgress( - .reproducingSourcekitdCrash(progress: progress), - message: "Reducing sourcekitd crash \(name): \(message)" - ) - } - ) - // If reduce didn't throw, we have found a reproducer. Stop. - // Looking further probably won't help because other crashes are likely the same cause. - break - } catch { - // Reducing this request failed. Continue reducing the next one, maybe that one succeeds. - logger.info("Reducing sourcekitd crash failed: \(error.forLogging)") - } - } - } - - @MainActor - private func addSwiftFrontendCrashReproducer(toBundle bundlePath: URL) async throws { - reportProgress( - .reproducingSwiftFrontendCrash(progress: 0), - message: "Trying to reduce recent Swift compiler crashes" - ) - - let crashInfos = SwiftFrontendCrashScraper(directoriesToScanForCrashReports: directoriesToScanForCrashReports) - .findSwiftFrontendCrashes() - .filter { $0.date > Date().addingTimeInterval(-TimeInterval(osLogScrapeDuration * 60)) } - .sorted(by: { $0.date > $1.date }) - - for crashInfo in crashInfos { - let dateFormatter = DateFormatter() - dateFormatter.timeZone = NSTimeZone.local - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .medium - let progressMessagePrefix = "Reducing Swift compiler crash at \(dateFormatter.string(from: crashInfo.date))" - - reportProgress(.reproducingSwiftFrontendCrash(progress: 0), message: progressMessagePrefix) - - let toolchainPath = crashInfo.swiftFrontend - .deletingLastPathComponent() - .deletingLastPathComponent() - - guard let toolchain = Toolchain(toolchainPath), - let sourcekitd = toolchain.sourcekitd - else { - continue - } - - let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd, - pluginPaths: toolchain.pluginPaths, - swiftFrontend: crashInfo.swiftFrontend, - reproducerPredicate: nil - ) - - do { - let reducedRequesInfo = try await reduceFrontendIssue( - frontendArgs: crashInfo.frontendArgs, - using: executor, - progressUpdate: { (progress, message) in - reportProgress( - .reproducingSwiftFrontendCrash(progress: progress), - message: "\(progressMessagePrefix): \(message)" - ) - } - ) - - let bundleDirectory = bundlePath.appending(component: "swift-frontend-crash") - try makeReproducerBundle(for: reducedRequesInfo, toolchain: toolchain, bundlePath: bundleDirectory) - - // If reduce didn't throw, we have found a reproducer. Stop. - // Looking further probably won't help because other crashes are likely the same cause. - break - } catch { - // Reducing this request failed. Continue reducing the next one, maybe that one succeeds. - } - } - } - - /// Execute body and if it throws, log the error. - @MainActor - private func orPrintError(_ body: @MainActor () async throws -> Void) async { - do { - try await body() - } catch { - print(error) - } - } - - @MainActor - private func addOsLog(toBundle bundlePath: URL) async throws { - #if os(macOS) - reportProgress(.collectingLogMessages(progress: 0), message: "Collecting log messages") - let outputFileUrl = bundlePath.appending(component: "log.txt") - try FileManager.default.createFile(at: outputFileUrl, contents: nil) - let fileHandle = try FileHandle(forWritingTo: outputFileUrl) - let bytesCollected = AtomicInt32(initialValue: 0) - let processExited = AtomicBool(initialValue: false) - // 50 MB is an average log size collected by sourcekit-lsp diagnose. - // It's a good proxy to show some progress indication for the majority of the time. - let expectedLogSize = 50_000_000 - let process = Process( - arguments: [ - "/usr/bin/log", - "show", - "--predicate", #"subsystem = "org.swift.sourcekit-lsp" AND process = "sourcekit-lsp""#, - "--info", - "--debug", - "--signpost", - ], - outputRedirection: .stream( - stdout: { @Sendable bytes in - try? fileHandle.write(contentsOf: bytes) - bytesCollected.value += Int32(bytes.count) - var progress = Double(bytesCollected.value) / Double(expectedLogSize) - if progress > 1 { - // The log is larger than we expected. Halt at 100% - progress = 1 - } - Task(priority: .high) { - // We have launched an async task to call `reportProgress`, which means that the process might have exited - // before we execute this task. To avoid overriding a more recent progress, add a guard. - if !processExited.value { - await reportProgress(.collectingLogMessages(progress: progress), message: "Collecting log messages") - } - } - }, - stderr: { @Sendable _ in } - ) - ) - try process.launch() - try await process.waitUntilExit() - processExited.value = true - #endif - } - - @MainActor - private func addNonDarwinLogs(toBundle bundlePath: URL) async throws { - reportProgress(.collectingLogMessages(progress: 0), message: "Collecting log files") - - let destinationDir = bundlePath.appending(component: "logs") - try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true) - - let logFileDirectoryURL = FileManager.default.homeDirectoryForCurrentUser - .appending(components: ".sourcekit-lsp", "logs") - let enumerator = FileManager.default.enumerator(at: logFileDirectoryURL, includingPropertiesForKeys: nil) - while let fileUrl = enumerator?.nextObject() as? URL { - guard fileUrl.lastPathComponent.hasPrefix("sourcekit-lsp") else { - continue - } - try? FileManager.default.copyItem( - at: fileUrl, - to: destinationDir.appending(component: fileUrl.lastPathComponent) - ) - } - } - - @MainActor - private func addLogs(toBundle bundlePath: URL) async throws { - try await addNonDarwinLogs(toBundle: bundlePath) - try await addOsLog(toBundle: bundlePath) - } - - @MainActor - private func addCrashLogs(toBundle bundlePath: URL) throws { - #if os(macOS) - reportProgress(.collectingCrashReports, message: "Collecting crash reports") - - let destinationDir = bundlePath.appending(component: "crashes") - try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true) - - let processesToIncludeCrashReportsOf = ["SourceKitService", "sourcekit-lsp", "swift-frontend"] - - for directoryToScan in directoriesToScanForCrashReports { - let diagnosticReports = URL(filePath: (directoryToScan as NSString).expandingTildeInPath) - let enumerator = FileManager.default.enumerator(at: diagnosticReports, includingPropertiesForKeys: nil) - while let fileUrl = enumerator?.nextObject() as? URL { - guard processesToIncludeCrashReportsOf.contains(where: { fileUrl.lastPathComponent.hasPrefix($0) }) else { - continue - } - try? FileManager.default.copyItem( - at: fileUrl, - to: destinationDir.appending(component: fileUrl.lastPathComponent) - ) - } - } - #endif - } - - @MainActor - private func addSwiftVersion(toBundle bundlePath: URL) async throws { - let outputFileUrl = bundlePath.appending(component: "swift-versions.txt") - try FileManager.default.createFile(at: outputFileUrl, contents: nil) - let fileHandle = try FileHandle(forWritingTo: outputFileUrl) - - let toolchains = try await toolchainRegistry.toolchains - - for (index, toolchain) in toolchains.enumerated() { - reportProgress( - .collectingSwiftVersions(progress: Double(index) / Double(toolchains.count)), - message: "Determining Swift version of \(toolchain.identifier)" - ) - - guard let swiftUrl = toolchain.swift else { - continue - } - - try fileHandle.write(contentsOf: "\(swiftUrl.filePath) --version\n".data(using: .utf8)!) - let process = Process( - arguments: [try swiftUrl.filePath, "--version"], - outputRedirection: .stream( - stdout: { @Sendable bytes in try? fileHandle.write(contentsOf: bytes) }, - stderr: { @Sendable _ in } - ) - ) - try process.launch() - try await process.waitUntilExit() - fileHandle.write("\n".data(using: .utf8)!) - } - } - - @MainActor - private func reportProgress(_ state: DiagnoseProgressState, message: String) { - let progress: (step: Int, message: String) = (Int(state.progress * 100), message) - if lastProgress == nil || progress != lastProgress! { - progressBar?.update(step: Int(state.progress * 100), total: 100, text: message) - lastProgress = progress - } - } - - @MainActor - package func run() async throws { - // IMPORTANT: When adding information to this message, also add it to the message displayed in VS Code - // (captureDiagnostics.ts in the vscode-swift repository) - print( - """ - sourcekit-lsp diagnose collects information that helps the developers of sourcekit-lsp diagnose and fix issues. - This information contains: - - Crash logs from SourceKit - - Log messages emitted by SourceKit - - Versions of Swift installed on your system - - If possible, a minimized project that caused SourceKit to crash - - If possible, a minimized project that caused the Swift compiler to crash - - All information is collected locally. - - """ - ) - - progressBar = PercentProgressAnimation( - stream: stderrStreamConcurrencySafe, - header: "Diagnosing sourcekit-lsp issues" - ) - - let dateFormatter = ISO8601DateFormatter() - dateFormatter.timeZone = NSTimeZone.local - let date = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: "-") - let bundlePath = - if let bundleOutputPath = self.bundleOutputPath { - URL(fileURLWithPath: bundleOutputPath) - } else { - FileManager.default.temporaryDirectory - .appending(components: "sourcekit-lsp-diagnose", "sourcekit-lsp-diagnose-\(date)") - } - try FileManager.default.createDirectory(at: bundlePath, withIntermediateDirectories: true) - - if components.isEmpty || components.contains(.crashReports) { - await orPrintError { try addCrashLogs(toBundle: bundlePath) } - } - if components.isEmpty || components.contains(.logs) { - await orPrintError { try await addLogs(toBundle: bundlePath) } - } - if components.isEmpty || components.contains(.swiftVersions) { - await orPrintError { try await addSwiftVersion(toBundle: bundlePath) } - } - if components.isEmpty || components.contains(.sourcekitdCrashes) { - await orPrintError { try await addSourcekitdCrashReproducer(toBundle: bundlePath) } - } - if components.isEmpty || components.contains(.swiftFrontendCrashes) { - await orPrintError { try await addSwiftFrontendCrashReproducer(toBundle: bundlePath) } - } - - progressBar?.complete(success: true) - - print( - """ - - Bundle created. - When filing an issue at https://github.com/swiftlang/sourcekit-lsp/issues/new, - please attach the bundle located at - \(try bundlePath.filePath) - """ - ) - - #if os(macOS) - // Reveal the bundle in Finder on macOS. - // Don't open the bundle in Finder if the user manually specified a log output path. In that case they are running - // `sourcekit-lsp diagnose` as part of a larger logging script (like the Swift for VS Code extension) and the caller - // is responsible for showing the diagnose bundle location to the user - if self.bundleOutputPath == nil { - do { - _ = try await Process.run(arguments: ["open", "-R", bundlePath.filePath], workingDirectory: nil) - } catch { - // If revealing the bundle in Finder should fail, we don't care. We still printed the bundle path to stdout. - } - } - #endif - } - - @MainActor - private func reduce( - requestInfo: RequestInfo, - toolchain: Toolchain?, - bundlePath: URL, - progressUpdate: (_ progress: Double, _ message: String) -> Void - ) async throws { - guard let toolchain else { - throw GenericError("Unable to find a toolchain") - } - guard let sourcekitd = toolchain.sourcekitd else { - throw GenericError("Unable to find sourcekitd.framework") - } - guard let swiftFrontend = toolchain.swiftFrontend else { - throw GenericError("Unable to find swift-frontend") - } - - let requestInfo = requestInfo - let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd, - pluginPaths: toolchain.pluginPaths, - swiftFrontend: swiftFrontend, - reproducerPredicate: nil - ) - - let reducedRequesInfo = try await requestInfo.reduce(using: executor, progressUpdate: progressUpdate) - - try makeReproducerBundle(for: reducedRequesInfo, toolchain: toolchain, bundlePath: bundlePath) - } -} - -/// Describes the state that the diagnose command is in. This is used to compute a progress bar. -private enum DiagnoseProgressState: Comparable { - case collectingCrashReports - case collectingLogMessages(progress: Double) - case collectingSwiftVersions(progress: Double) - case reproducingSourcekitdCrash(progress: Double) - case reproducingSwiftFrontendCrash(progress: Double) - - var allFinalStates: [DiagnoseProgressState] { - return [ - .collectingCrashReports, - .collectingLogMessages(progress: 1), - .collectingSwiftVersions(progress: 1), - .reproducingSourcekitdCrash(progress: 1), - .reproducingSwiftFrontendCrash(progress: 1), - ] - } - - /// An estimate of how long this state takes in seconds. - /// - /// The actual values are never displayed. We use these values to allocate a portion of the overall progress to this - /// state. - var estimatedDuration: Double { - switch self { - case .collectingCrashReports: - return 1 - case .collectingLogMessages: - return 15 - case .collectingSwiftVersions: - return 10 - case .reproducingSourcekitdCrash: - return 60 - case .reproducingSwiftFrontendCrash: - return 60 - } - } - - var progress: Double { - let estimatedTotalDuration = allFinalStates.reduce(0, { $0 + $1.estimatedDuration }) - var elapsedEstimatedDuration = allFinalStates.filter { $0 < self }.reduce(0, { $0 + $1.estimatedDuration }) - switch self { - case .collectingCrashReports: break - case .collectingLogMessages(let progress), .collectingSwiftVersions(let progress), - .reproducingSourcekitdCrash(let progress), .reproducingSwiftFrontendCrash(let progress): - elapsedEstimatedDuration += progress * self.estimatedDuration - } - return elapsedEstimatedDuration / estimatedTotalDuration - } -} diff --git a/Sources/Diagnose/GenericError.swift b/Sources/Diagnose/GenericError.swift deleted file mode 100644 index 93d0ccb53..000000000 --- a/Sources/Diagnose/GenericError.swift +++ /dev/null @@ -1,19 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -struct GenericError: Error, CustomStringConvertible { - let description: String - - init(_ description: String) { - self.description = description - } -} diff --git a/Sources/Diagnose/IndexCommand.swift b/Sources/Diagnose/IndexCommand.swift deleted file mode 100644 index 5b782014d..000000000 --- a/Sources/Diagnose/IndexCommand.swift +++ /dev/null @@ -1,124 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public import ArgumentParser -import Foundation -import InProcessClient -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKOptions -import SourceKitLSP -import SwiftExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import class TSCUtility.PercentProgressAnimation - -private actor IndexLogMessageHandler: MessageHandler { - var hasSeenError: Bool = false - - /// Queue to ensure that we don't have two interleaving `print` calls. - let queue = AsyncQueue() - - nonisolated func handle(_ notification: some NotificationType) { - if let notification = notification as? LogMessageNotification { - queue.async { - await self.handle(notification) - } - } - } - - func handle(_ notification: LogMessageNotification) { - self.hasSeenError = notification.type == .warning - print(notification.message) - } - - nonisolated func handle( - _ request: Request, - id: RequestID, - reply: @escaping @Sendable (LSPResult) -> Void - ) { - reply(.failure(.methodNotFound(Request.method))) - } - -} - -package struct IndexCommand: AsyncParsableCommand { - package static let configuration: CommandConfiguration = CommandConfiguration( - commandName: "index", - abstract: "Index a project and print all the processes executed for it as well as their outputs" - ) - - @Option( - name: .customLong("toolchain"), - help: """ - The toolchain used to reduce the sourcekitd issue. \ - If not specified, the toolchain is found in the same way that sourcekit-lsp finds it - """ - ) - var toolchainOverride: String? - - @Option( - name: .customLong("experimental-feature"), - help: """ - Enable an experimental sourcekit-lsp feature. - Available features are: \(ExperimentalFeature.allNonInternalCases.map(\.rawValue).joined(separator: ", ")) - """ - ) - var experimentalFeatures: [ExperimentalFeature] = [] - - @Option(help: "The path to the project that should be indexed") - var project: String - - package init() {} - - package func run() async throws { - let options = SourceKitLSPOptions( - backgroundIndexing: true, - experimentalFeatures: Set(experimentalFeatures) - ) - - let installPath = - if let toolchainOverride, let toolchain = Toolchain(URL(fileURLWithPath: toolchainOverride)) { - toolchain.path - } else { - Bundle.main.bundleURL - } - - let messageHandler = IndexLogMessageHandler() - let inProcessClient = try await InProcessSourceKitLSPClient( - toolchainRegistry: ToolchainRegistry(installPath: installPath), - options: options, - workspaceFolders: [WorkspaceFolder(uri: DocumentURI(URL(fileURLWithPath: project)))], - messageHandler: messageHandler - ) - let start = ContinuousClock.now - _ = try await inProcessClient.send(SynchronizeRequest(index: true)) - print("Indexing finished in \(start.duration(to: .now))") - if await messageHandler.hasSeenError { - throw ExitCode(1) - } - } -} - -fileprivate extension SourceKitLSPServer { - func handle(_ request: R, requestID: RequestID) async throws -> R.Response { - return try await withCheckedThrowingContinuation { continuation in - self.handle(request, id: requestID) { result in - continuation.resume(with: result) - } - } - } -} - -extension ExperimentalFeature: ArgumentParser.ExpressibleByArgument {} diff --git a/Sources/Diagnose/MergeSwiftFiles.swift b/Sources/Diagnose/MergeSwiftFiles.swift deleted file mode 100644 index 1cfa401cb..000000000 --- a/Sources/Diagnose/MergeSwiftFiles.swift +++ /dev/null @@ -1,49 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging - -extension RequestInfo { - /// Check if the issue reproduces when merging all `.swift` input files into a single file. - /// - /// Returns `nil` if the issue didn't reproduce with all `.swift` files merged. - @MainActor - func mergeSwiftFiles( - using executor: SourceKitRequestExecutor, - progressUpdate: (_ progress: Double, _ message: String) -> Void - ) async throws -> RequestInfo? { - let swiftFilePaths = compilerArgs.filter { $0.hasSuffix(".swift") } - let mergedFile = try swiftFilePaths.map { try String(contentsOfFile: $0, encoding: .utf8) } - .joined(separator: "\n\n\n\n") - - progressUpdate(0, "Merging all .swift files into a single file") - - let compilerArgs = compilerArgs.filter { $0 != "-primary-file" && !$0.hasSuffix(".swift") } + ["$FILE"] - let mergedRequestInfo = RequestInfo( - requestTemplate: requestTemplate, - contextualRequestTemplates: contextualRequestTemplates, - offset: offset, - compilerArgs: compilerArgs, - fileContents: mergedFile - ) - - let result = try await executor.run(request: mergedRequestInfo) - if case .reproducesIssue = result { - logger.debug("Successfully merged all .swift input files") - return mergedRequestInfo - } else { - logger.debug("Merging .swift files did not reproduce the issue") - return nil - } - } -} diff --git a/Sources/Diagnose/OSLogScraper.swift b/Sources/Diagnose/OSLogScraper.swift deleted file mode 100644 index 287c79e5b..000000000 --- a/Sources/Diagnose/OSLogScraper.swift +++ /dev/null @@ -1,151 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -#if canImport(OSLog) -import OSLog -import SKLogging -import RegexBuilder - -/// Reads oslog messages to find recent sourcekitd crashes. -struct OSLogScraper { - /// How far into the past we should search for crashes in OSLog. - var searchDuration: TimeInterval - - private func getLogEntries(matching predicate: NSPredicate) throws -> [any OSLogEntryWithPayload & OSLogEntry] { - let logStore = try OSLogStore.local() - let startPoint = logStore.position(date: Date().addingTimeInterval(-searchDuration)) - return - try logStore - .getEntries( - at: startPoint, - matching: predicate - ).compactMap { $0 as? (OSLogEntryWithPayload & OSLogEntry) } - } - - private func crashedSourceKitLSPRequests() throws -> [(name: String, logCategory: String)] { - let predicate = NSPredicate( - format: #"subsystem CONTAINS "sourcekit-lsp" AND composedMessage CONTAINS "sourcekitd crashed (1/""# - ) - return try getLogEntries(matching: predicate).map { - (name: "Crash at \($0.date)", logCategory: $0.category) - } - } - - /// Get the `RequestInfo` for a crash that was logged in `logCategory`. - private func requestInfo(for logCategory: String) throws -> RequestInfo { - let predicate = NSPredicate( - format: - #"subsystem CONTAINS "sourcekit-lsp" AND composedMessage CONTAINS "sourcekitd crashed" AND category = %@"#, - logCategory - ) - enum LogSection { - case request - case fileContents - case contextualRequest - } - var section = LogSection.request - var request = "" - var fileContents = "" - var contextualRequests: [String] = [] - let sourcekitdCrashedRegex = Regex { - "sourcekitd crashed (" - OneOrMore(.digit) - "/" - OneOrMore(.digit) - ")" - } - let contextualRequestRegex = Regex { - "Contextual request " - OneOrMore(.digit) - " / " - OneOrMore(.digit) - ":" - } - - for entry in try getLogEntries(matching: predicate) { - for line in entry.composedMessage.components(separatedBy: "\n") { - if try sourcekitdCrashedRegex.wholeMatch(in: line) != nil { - continue - } - if line == "Request:" { - continue - } - if line == "File contents:" { - section = .fileContents - continue - } - if line == "File contents:" { - section = .fileContents - continue - } - if try contextualRequestRegex.wholeMatch(in: line) != nil { - section = .contextualRequest - contextualRequests.append("") - continue - } - if line == "--- End Chunk" { - continue - } - switch section { - case .request: - request += line + "\n" - case .fileContents: - fileContents += line + "\n" - case .contextualRequest: - if !contextualRequests.isEmpty { - contextualRequests[contextualRequests.count - 1] += line + "\n" - } else { - // Should never happen because we have appended at least one element to `contextualRequests` when switching - // to the `contextualRequest` section. - logger.fault("Dropping contextual request line: \(line)") - } - } - } - } - - var requestInfo = try RequestInfo(request: request) - - let contextualRequestInfos = contextualRequests.compactMap { contextualRequest in - orLog("Processsing contextual request") { - try RequestInfo(request: contextualRequest) - } - }.filter { contextualRequest in - if contextualRequest.fileContents != requestInfo.fileContents { - logger.error("Contextual request concerns a different file than the crashed request. Ignoring it") - return false - } - return true - } - requestInfo.contextualRequestTemplates = contextualRequestInfos.map(\.requestTemplate) - if requestInfo.compilerArgs.isEmpty { - requestInfo.compilerArgs = contextualRequestInfos.last(where: { !$0.compilerArgs.isEmpty })?.compilerArgs ?? [] - } - requestInfo.fileContents = fileContents - - return requestInfo - } - - /// Get information about sourcekitd crashes that haven logged to OSLog. - /// This information can be used to reduce the crash. - /// - /// Name is a human readable name that identifies the crash. - func getCrashedRequests() throws -> [(name: String, info: RequestInfo)] { - let crashedRequests = try crashedSourceKitLSPRequests().reversed() - return crashedRequests.compactMap { (name: String, logCategory: String) -> (name: String, info: RequestInfo)? in - guard let requestInfo = try? requestInfo(for: logCategory) else { - return nil - } - return (name, requestInfo) - } - } -} -#endif diff --git a/Sources/Diagnose/ReduceCommand.swift b/Sources/Diagnose/ReduceCommand.swift deleted file mode 100644 index 0f1c14ab2..000000000 --- a/Sources/Diagnose/ReduceCommand.swift +++ /dev/null @@ -1,107 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import ArgumentParser -import Foundation -import SourceKitD -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import class TSCUtility.PercentProgressAnimation - -package struct ReduceCommand: AsyncParsableCommand { - package static let configuration: CommandConfiguration = CommandConfiguration( - commandName: "reduce", - abstract: "Reduce a single sourcekitd crash" - ) - - @Option(name: .customLong("request-file"), help: "Path to a sourcekitd request to reduce.") - var sourcekitdRequestPath: String - - @Option( - name: .customLong("toolchain"), - help: """ - The toolchain used to reduce the sourcekitd issue. \ - If not specified, the toolchain is found in the same way that sourcekit-lsp finds it - """ - ) - var toolchainOverride: String? - - #if canImport(Darwin) - // Creating an NSPredicate from a string is not supported in corelibs-foundation. - @Option( - help: """ - If the sourcekitd response matches this predicate, consider it as reproducing the issue. - sourcekitd crashes are always considered as reproducers. - - The predicate is an NSPredicate. `stdout` and `stderr` are standard output and standard error of the \ - sourcekitd execution using `sourcekit-lsp run-sourcekitd-request`, respectively. - """ - ) - var predicate: String? - - private var nsPredicate: NSPredicate? { predicate.map { NSPredicate(format: $0) } } - #else - private var nsPredicate: NSPredicate? { nil } - #endif - - @MainActor - var toolchain: Toolchain? { - get async throws { - if let toolchainOverride { - return Toolchain(URL(fileURLWithPath: toolchainOverride)) - } - return await ToolchainRegistry(installPath: Bundle.main.bundleURL).default - } - } - - package init() {} - - @MainActor - package func run() async throws { - guard let toolchain = try await toolchain else { - throw GenericError("Unable to find toolchain") - } - guard let sourcekitd = toolchain.sourcekitd else { - throw GenericError("Unable to find sourcekitd.framework") - } - guard let swiftFrontend = toolchain.swiftFrontend else { - throw GenericError("Unable to find sourcekitd.framework") - } - let pluginPaths = toolchain.pluginPaths - - let progressBar = PercentProgressAnimation(stream: stderrStreamConcurrencySafe, header: "Reducing sourcekitd issue") - - let request = try String(contentsOfFile: sourcekitdRequestPath, encoding: .utf8) - let requestInfo = try RequestInfo(request: request) - - let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd, - pluginPaths: pluginPaths, - swiftFrontend: swiftFrontend, - reproducerPredicate: nsPredicate - ) - - let reduceRequestInfo = try await requestInfo.reduce(using: executor) { progress, message in - progressBar.update(step: Int(progress * 100), total: 100, text: message) - } - - progressBar.complete(success: true) - - let reducedSourceFile = FileManager.default.temporaryDirectory.appending(component: "reduced.swift") - try reduceRequestInfo.fileContents.write(to: reducedSourceFile, atomically: true, encoding: .utf8) - - print("Reduced Request:") - print(try reduceRequestInfo.requests(for: reducedSourceFile).joined(separator: "\n\n\n\n")) - } -} diff --git a/Sources/Diagnose/ReduceFrontendCommand.swift b/Sources/Diagnose/ReduceFrontendCommand.swift deleted file mode 100644 index a779237f7..000000000 --- a/Sources/Diagnose/ReduceFrontendCommand.swift +++ /dev/null @@ -1,115 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import ArgumentParser -import Foundation -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import class TSCUtility.PercentProgressAnimation - -package struct ReduceFrontendCommand: AsyncParsableCommand { - package static let configuration: CommandConfiguration = CommandConfiguration( - commandName: "reduce-frontend", - abstract: "Reduce a single swift-frontend crash" - ) - - #if canImport(Darwin) - // Creating an NSPredicate from a string is not supported in corelibs-foundation. - @Option( - help: """ - If the sourcekitd response matches this predicate, consider it as reproducing the issue. - sourcekitd crashes are always considered as reproducers. - - The predicate is an NSPredicate. `stdout` and `stderr` are standard output and standard error of the \ - swift-frontend execution, respectively. - - Example: - - stderr CONTAINS "failed to produce diagnostic for expression" - """ - ) - var predicate: String? - - private var nsPredicate: NSPredicate? { predicate.map { NSPredicate(format: $0) } } - #else - private var nsPredicate: NSPredicate? { nil } - #endif - - @Option( - name: .customLong("toolchain"), - help: """ - The toolchain used to reduce the swift-frontend issue. \ - If not specified, the toolchain is found in the same way that sourcekit-lsp finds it - """ - ) - var toolchainOverride: String? - - @Option( - parsing: .remaining, - help: """ - The swift-frontend arguments that exhibit the issue that should be reduced. - """ - ) - var frontendArgs: [String] - - @MainActor - var toolchain: Toolchain? { - get async throws { - if let toolchainOverride { - return Toolchain(URL(fileURLWithPath: toolchainOverride)) - } - return await ToolchainRegistry(installPath: Bundle.main.bundleURL).default - } - } - - package init() {} - - @MainActor - package func run() async throws { - guard let sourcekitd = try await toolchain?.sourcekitd else { - throw GenericError("Unable to find sourcekitd.framework") - } - guard let swiftFrontend = try await toolchain?.swiftFrontend else { - throw GenericError("Unable to find swift-frontend") - } - - let progressBar = PercentProgressAnimation( - stream: stderrStreamConcurrencySafe, - header: "Reducing swift-frontend crash" - ) - - let executor = OutOfProcessSourceKitRequestExecutor( - sourcekitd: sourcekitd, - pluginPaths: nil, - swiftFrontend: swiftFrontend, - reproducerPredicate: nsPredicate - ) - - defer { - progressBar.complete(success: true) - } - let reducedRequestInfo = try await reduceFrontendIssue( - frontendArgs: frontendArgs, - using: executor - ) { progress, message in - progressBar.update(step: Int(progress * 100), total: 100, text: message) - } - - print("Reduced compiler arguments:") - print(reducedRequestInfo.compilerArgs.joined(separator: " ")) - - print("") - print("Reduced file contents:") - print(reducedRequestInfo.fileContents) - } -} diff --git a/Sources/Diagnose/ReduceSourceKitDRequest.swift b/Sources/Diagnose/ReduceSourceKitDRequest.swift deleted file mode 100644 index b532254da..000000000 --- a/Sources/Diagnose/ReduceSourceKitDRequest.swift +++ /dev/null @@ -1,36 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -extension RequestInfo { - /// Reduce the input file of this request and the command line arguments. - @MainActor - func reduce( - using executor: SourceKitRequestExecutor, - progressUpdate: (_ progress: Double, _ message: String) -> Void - ) async throws -> RequestInfo { - var requestInfo = self - - // How much time of the reduction is expected to be spent reducing the source compared to command line argument - // reduction. - let sourceReductionPercentage = 0.7 - - requestInfo = try await requestInfo.reduceInputFile(using: executor) { progress, message in - let progress = progress * sourceReductionPercentage - progressUpdate(progress, message) - } - requestInfo = try await requestInfo.reduceCommandLineArguments(using: executor) { progress, message in - let progress = sourceReductionPercentage + progress * (1 - sourceReductionPercentage) - progressUpdate(progress, message) - } - return requestInfo - } -} diff --git a/Sources/Diagnose/ReduceSwiftFrontend.swift b/Sources/Diagnose/ReduceSwiftFrontend.swift deleted file mode 100644 index 03c6f3b8a..000000000 --- a/Sources/Diagnose/ReduceSwiftFrontend.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -@MainActor -package func reduceFrontendIssue( - frontendArgs: [String], - using executor: SourceKitRequestExecutor, - progressUpdate: (_ progress: Double, _ message: String) -> Void -) async throws -> RequestInfo { - let requestInfo = try RequestInfo(frontendArgs: frontendArgs) - let initialResult = try await executor.run(request: requestInfo) - guard case .reproducesIssue = initialResult else { - throw GenericError("Unable to reproduce the swift-frontend issue") - } - let mergedSwiftFilesRequestInfo = try await requestInfo.mergeSwiftFiles(using: executor) { progress, message in - progressUpdate(0, message) - } - guard let mergedSwiftFilesRequestInfo else { - throw GenericError("Merging all .swift files did not reproduce the issue. Unable to reduce it.") - } - return try await mergedSwiftFilesRequestInfo.reduce(using: executor, progressUpdate: progressUpdate) -} diff --git a/Sources/Diagnose/ReproducerBundle.swift b/Sources/Diagnose/ReproducerBundle.swift deleted file mode 100644 index 9b2e3d10f..000000000 --- a/Sources/Diagnose/ReproducerBundle.swift +++ /dev/null @@ -1,69 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SwiftExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath - -/// Create a folder that contains all files that should be necessary to reproduce a sourcekitd crash. -/// - Parameters: -/// - requestInfo: The reduced request info -/// - toolchain: The toolchain that was used to reduce the request -/// - bundlePath: The path to which to write the reproducer bundle -func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bundlePath: URL) throws { - try FileManager.default.createDirectory(at: bundlePath, withIntermediateDirectories: true) - try requestInfo.fileContents.write( - to: bundlePath.appending(component: "input.swift"), - atomically: true, - encoding: .utf8 - ) - try toolchain.path.realpath.filePath - .write( - to: bundlePath.appending(component: "toolchain.txt"), - atomically: true, - encoding: .utf8 - ) - if requestInfo.requestTemplate == RequestInfo.fakeRequestTemplateForFrontendIssues { - let command = - "swift-frontend \\\n" - + requestInfo.compilerArgs.replacing(["$FILE"], with: ["./input.swift"]).joined(separator: " \\\n") - try command.write(to: bundlePath.appending(component: "command.sh"), atomically: true, encoding: .utf8) - } else { - let requests = try requestInfo.requests(for: bundlePath.appending(component: "input.swift")) - for (index, request) in requests.enumerated() { - try request.write( - to: bundlePath.appending(component: "request-\(index).yml"), - atomically: true, - encoding: .utf8 - ) - } - } - for compilerArg in requestInfo.compilerArgs { - // Find the first slash so we are also able to copy files from eg. - // `-fmodule-map-file=/path/to/module.modulemap` - // `-I/foo/bar` - guard let firstSlash = compilerArg.firstIndex(of: "/") else { - continue - } - let path = compilerArg[firstSlash...] - guard !path.contains(".app"), !path.contains(".xctoolchain"), !path.contains("/usr/") else { - // Don't include files in Xcode (.app), Xcode toolchains or usr because they are most likely binary files that - // aren't user specific and would bloat the reproducer bundle. - continue - } - let dest = URL(fileURLWithPath: try bundlePath.filePath + path) - try? FileManager.default.createDirectory(at: dest.deletingLastPathComponent(), withIntermediateDirectories: true) - try? FileManager.default.copyItem(at: URL(fileURLWithPath: String(path)), to: dest) - } -} diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift deleted file mode 100644 index 60885fd60..000000000 --- a/Sources/Diagnose/RequestInfo.swift +++ /dev/null @@ -1,217 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import RegexBuilder -import SwiftExtensions - -/// All the information necessary to replay a sourcektid request. -package struct RequestInfo: Sendable { - /// The JSON request object. Contains the following dynamic placeholders: - /// - `$COMPILER_ARGS`: Will be replaced by the compiler arguments of the request - /// - `$FILE`: Will be replaced with a path to the file that contains the reduced source code. - /// - `$FILE_CONTENTS`: Will be replaced by the contents of the reduced source file inside quotes - /// - `$OFFSET`: To be replaced by `offset` before running the request - var requestTemplate: String - - /// Requests that should be executed before `requestTemplate` to set up state in sourcekitd so that `requestTemplate` - /// can reproduce an issue, eg. sending an `editor.open` before a `codecomplete.open` so that we have registered the - /// compiler arguments in the SourceKit plugin. - /// - /// These request templates receive the same substitutions as `requestTemplate`. - var contextualRequestTemplates: [String] - - /// The offset at which the sourcekitd request should be run. Replaces the - /// `$OFFSET` placeholder in the request template. - var offset: Int - - /// The compiler arguments of the request. Replaces the `$COMPILER_ARGS`placeholder in the request template. - package var compilerArgs: [String] - - /// The contents of the file that the sourcekitd request operates on. - package var fileContents: String - - package func requests(for file: URL) throws -> [String] { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] - guard var compilerArgs = String(data: try encoder.encode(compilerArgs), encoding: .utf8) else { - throw GenericError("Failed to encode compiler arguments") - } - // Drop the opening `[` and `]`. The request template already contains them - compilerArgs = String(compilerArgs.dropFirst().dropLast()) - let quotedFileContents = - try String(data: JSONEncoder().encode(try String(contentsOf: file, encoding: .utf8)), encoding: .utf8) ?? "" - return try (contextualRequestTemplates + [requestTemplate]).map { requestTemplate in - requestTemplate - .replacingOccurrences(of: "$OFFSET", with: String(offset)) - .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) - .replacingOccurrences(of: "$FILE_CONTENTS", with: quotedFileContents) - .replacingOccurrences(of: "$FILE", with: try file.filePath.replacing(#"\"#, with: #"\\"#)) - } - } - - /// A fake value that is used to indicate that we are reducing a `swift-frontend` issue instead of a sourcekitd issue. - static let fakeRequestTemplateForFrontendIssues = """ - { - key.request: sourcekit-lsp-fake-request-for-frontend-crash - key.compilerargs: [ - $COMPILER_ARGS - ] - } - """ - - package init( - requestTemplate: String, - contextualRequestTemplates: [String], - offset: Int, - compilerArgs: [String], - fileContents: String - ) { - self.requestTemplate = requestTemplate - self.contextualRequestTemplates = contextualRequestTemplates - self.offset = offset - self.compilerArgs = compilerArgs - self.fileContents = fileContents - } - - /// Creates `RequestInfo` from the contents of the JSON sourcekitd request at `requestPath`. - /// - /// The contents of the source file are read from disk. - package init(request: String) throws { - var requestTemplate = request - - requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: #"key.sourcetext: $FILE_CONTENTS\#n"#) - - let sourceFilePath: URL - (requestTemplate, offset) = try extractOffset(from: requestTemplate) - (requestTemplate, sourceFilePath) = try extractSourceFile(from: requestTemplate) - (requestTemplate, compilerArgs) = try extractCompilerArguments(from: requestTemplate) - - self.requestTemplate = requestTemplate - self.contextualRequestTemplates = [] - - fileContents = try String(contentsOf: sourceFilePath, encoding: .utf8) - } - - /// Create a `RequestInfo` that is used to reduce a `swift-frontend issue` - init(frontendArgs: [String]) throws { - var frontendArgsWithFilelistInlined: [String] = [] - - var iterator = frontendArgs.makeIterator() - - // Inline the file list so we can reduce the compiler arguments by removing individual source files. - // A couple `output-filelist`-related compiler arguments don't work with the file list inlined. Remove them as they - // are unlikely to be responsible for the swift-frontend cache. - // `-index-system-modules` is invalid when no output file lists are specified. - while let frontendArg = iterator.next() { - switch frontendArg { - case "-supplementary-output-file-map", "-output-filelist", "-index-unit-output-path-filelist", - "-index-system-modules": - _ = iterator.next() - case "-filelist": - guard let fileList = iterator.next() else { - throw GenericError("Expected file path after -filelist command line argument") - } - frontendArgsWithFilelistInlined += try String(contentsOfFile: fileList, encoding: .utf8) - .split(separator: "\n") - .map { String($0) } - default: - frontendArgsWithFilelistInlined.append(frontendArg) - } - } - - // File contents are not known because there are multiple input files. Will usually be set after running - // `mergeSwiftFiles`. - self.init( - requestTemplate: Self.fakeRequestTemplateForFrontendIssues, - contextualRequestTemplates: [], - offset: 0, - compilerArgs: frontendArgsWithFilelistInlined, - fileContents: "" - ) - } -} - -private func extractOffset(from requestTemplate: String) throws -> (template: String, offset: Int) { - let offsetRegex = Regex { - "key.offset: " - Capture(ZeroOrMore(.digit)) - } - guard let offsetMatch = requestTemplate.matches(of: offsetRegex).only else { - return (requestTemplate, 0) - } - let requestTemplate = requestTemplate.replacing(offsetRegex, with: "key.offset: $OFFSET") - return (requestTemplate, Int(offsetMatch.1)!) -} - -private func extractSourceFile(from requestTemplate: String) throws -> (template: String, sourceFile: URL) { - var requestTemplate = requestTemplate - let sourceFileRegex = Regex { - #"key.sourcefile: ""# - Capture(ZeroOrMore(#/[^"]/#)) - "\"" - } - let nameRegex = Regex { - #"key.name: ""# - Capture(ZeroOrMore(#/[^"]/#)) - "\"" - } - let sourceFileMatch = requestTemplate.matches(of: sourceFileRegex).only - let nameMatch = requestTemplate.matches(of: nameRegex).only - - let sourceFilePath: String? - if let sourceFileMatch { - sourceFilePath = String(sourceFileMatch.1) - requestTemplate.replace(sourceFileMatch.1, with: "$FILE") - } else { - sourceFilePath = nil - } - - let namePath: String? - if let nameMatch { - namePath = String(nameMatch.1) - requestTemplate.replace(nameMatch.1, with: "$FILE") - } else { - namePath = nil - } - switch (sourceFilePath, namePath) { - case (let sourceFilePath?, let namePath?): - if sourceFilePath != namePath { - throw GenericError("Mismatching key.sourcefile and key.name in the request: \(sourceFilePath) vs. \(namePath)") - } - return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) - case (let sourceFilePath?, nil): - return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) - case (nil, let namePath?): - return (requestTemplate, URL(fileURLWithPath: namePath)) - case (nil, nil): - throw GenericError("Failed to find key.sourcefile or key.name in the request") - } -} - -private func extractCompilerArguments( - from requestTemplate: String -) throws -> (template: String, compilerArgs: [String]) { - let lines = requestTemplate.components(separatedBy: "\n") - guard - let compilerArgsStartIndex = lines.firstIndex(where: { $0.contains("key.compilerargs: [") }), - let compilerArgsEndIndex = lines[compilerArgsStartIndex...].firstIndex(where: { - $0.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("]") - }) - else { - return (requestTemplate, []) - } - let template = lines[...compilerArgsStartIndex] + ["$COMPILER_ARGS"] + lines[compilerArgsEndIndex...] - let compilerArgsJson = "[" + lines[(compilerArgsStartIndex + 1)..? - let req = sourcekitd.api.request_create_from_yaml(buffer.baseAddress!, &error) - if let error { - throw GenericError("Failed to parse sourcekitd request from YAML: \(String(cString: error))") - } - guard let req else { - throw GenericError("Failed to parse request from YAML but did not produce error") - } - return req - } - let response = await withCheckedContinuation { continuation in - var handle: sourcekitd_api_request_handle_t? = nil - sourcekitd.api.send_request(request, &handle) { resp in - continuation.resume(returning: SKDResponse(resp!, sourcekitd: sourcekitd)) - } - } - lastResponse = response - - print(response.description) - } - - switch lastResponse?.error { - case .requestFailed, .requestInvalid, .requestCancelled, .timedOut, .missingRequiredSymbol: - throw ExitCode(1) - case .connectionInterrupted: - throw ExitCode(255) - case nil: - break - } - } -} diff --git a/Sources/Diagnose/SourceKitD+RunWithYaml.swift b/Sources/Diagnose/SourceKitD+RunWithYaml.swift deleted file mode 100644 index 6df846b92..000000000 --- a/Sources/Diagnose/SourceKitD+RunWithYaml.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import Foundation -package import SourceKitD - -extension SourceKitD { - /// Parse the request from YAML and execute it. - package func run(requestYaml: String) async throws -> SKDResponse { - let request = try requestYaml.cString(using: .utf8)?.withUnsafeBufferPointer { buffer in - var error: UnsafeMutablePointer? - let req = api.request_create_from_yaml(buffer.baseAddress!, &error) - if let error { - throw GenericError("Failed to parse sourcekitd request from YAML: \(String(cString: error))") - } - return req - } - return await withCheckedContinuation { continuation in - var handle: sourcekitd_api_request_handle_t? = nil - api.send_request(request!, &handle) { response in - continuation.resume(returning: SKDResponse(response!, sourcekitd: self)) - } - } - } -} diff --git a/Sources/Diagnose/SourceKitDRequestExecutor.swift b/Sources/Diagnose/SourceKitDRequestExecutor.swift deleted file mode 100644 index a63380a2c..000000000 --- a/Sources/Diagnose/SourceKitDRequestExecutor.swift +++ /dev/null @@ -1,189 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import SourceKitD -import SwiftExtensions -import TSCExtensions - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import struct TSCBasic.ProcessResult - -/// The different states in which a sourcekitd request can finish. -package enum SourceKitDRequestResult: Sendable { - /// The request succeeded. - case success(response: String) - - /// The request failed but did not crash. - case error - - /// Running the request reproduces the issue that should be reduced. - case reproducesIssue -} - -fileprivate extension String { - init?(bytes: [UInt8], encoding: Encoding) { - self = bytes.withUnsafeBytes { buffer in - guard let baseAddress = buffer.baseAddress else { - return "" - } - let data = Data(bytes: baseAddress, count: buffer.count) - return String(data: data, encoding: encoding)! - } - - } -} - -/// An executor that can run a sourcekitd request and indicate whether the request reprodes a specified issue. -package protocol SourceKitRequestExecutor { - @MainActor func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult - @MainActor func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult -} - -extension SourceKitRequestExecutor { - @MainActor - func run(request: RequestInfo) async throws -> SourceKitDRequestResult { - if request.requestTemplate == RequestInfo.fakeRequestTemplateForFrontendIssues { - return try await runSwiftFrontend(request: request) - } else { - return try await runSourceKitD(request: request) - } - } -} - -/// Runs `sourcekit-lsp run-sourcekitd-request` to check if a sourcekit-request crashes. -package class OutOfProcessSourceKitRequestExecutor: SourceKitRequestExecutor { - /// The path to `sourcekitd.framework/sourcekitd`. - private let sourcekitd: URL - private let pluginPaths: PluginPaths? - - /// The path to `swift-frontend`. - private let swiftFrontend: URL - - private let temporaryDirectory: URL - - /// The file to which we write the reduce source file. - private var temporarySourceFile: URL { - temporaryDirectory.appending(component: "reduce.swift") - } - - /// If this predicate evaluates to true on the sourcekitd response, the request is - /// considered to reproduce the issue. - private let reproducerPredicate: NSPredicate? - - package init(sourcekitd: URL, pluginPaths: PluginPaths?, swiftFrontend: URL, reproducerPredicate: NSPredicate?) { - self.sourcekitd = sourcekitd - self.pluginPaths = pluginPaths - self.swiftFrontend = swiftFrontend - self.reproducerPredicate = reproducerPredicate - temporaryDirectory = FileManager.default.temporaryDirectory.appending(component: "sourcekitd-execute-\(UUID())") - try? FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true) - } - - deinit { - try? FileManager.default.removeItem(at: temporaryDirectory) - } - - /// The `SourceKitDRequestResult` for the given process result, evaluating the reproducer predicate, if it was - /// specified. - private func requestResult(for result: ProcessResult) -> SourceKitDRequestResult { - if let reproducerPredicate { - if let outputStr = try? String(bytes: result.output.get(), encoding: .utf8), - let stderrStr = try? String(bytes: result.stderrOutput.get(), encoding: .utf8) - { - let exitCode: Int32? = - switch result.exitStatus { - case .terminated(code: let exitCode): exitCode - default: nil - } - - let dict: [String: Any] = [ - "stdout": outputStr, - "stderr": stderrStr, - "exitCode": exitCode as Any, - ] - - if reproducerPredicate.evaluate(with: dict) { - return .reproducesIssue - } else { - return .error - } - } else { - return .error - } - } - - switch result.exitStatus { - case .terminated(code: 0): - if let outputStr = try? String(bytes: result.output.get(), encoding: .utf8) { - return .success(response: outputStr) - } else { - return .error - } - case .terminated(code: 1): - // The request failed but did not crash. It doesn't reproduce the issue. - return .error - default: - // Exited with a non-zero and non-one exit code. Looks like it crashed, so reproduces a crasher. - return .reproducesIssue - } - } - - package func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult { - try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - - let arguments = request.compilerArgs.replacing(["$FILE"], with: [try temporarySourceFile.filePath]) - - let process = Process(arguments: [try swiftFrontend.filePath] + arguments) - try process.launch() - let result = try await process.waitUntilExit() - - return requestResult(for: result) - } - - package func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult { - var arguments = [ - ProcessInfo.processInfo.arguments[0], - "debug", - "run-sourcekitd-request", - "--sourcekitd", - try sourcekitd.filePath, - ] - if let pluginPaths { - arguments += [ - "--sourcekit-plugin-path", - try pluginPaths.servicePlugin.filePath, - "--sourcekit-client-plugin-path", - try pluginPaths.clientPlugin.filePath, - ] - } - - try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let requestStrings = try request.requests(for: temporarySourceFile) - for (index, requestString) in requestStrings.enumerated() { - let temporaryRequestFile = temporaryDirectory.appending(component: "request-\(index).yml") - try requestString.write( - to: temporaryRequestFile, - atomically: true, - encoding: .utf8 - ) - arguments += [ - "--request-file", - try temporaryRequestFile.filePath, - ] - } - - let result = try await Process.run(arguments: arguments, workingDirectory: nil) - return requestResult(for: result) - } -} diff --git a/Sources/Diagnose/SourceReducer.swift b/Sources/Diagnose/SourceReducer.swift deleted file mode 100644 index 6cdb250df..000000000 --- a/Sources/Diagnose/SourceReducer.swift +++ /dev/null @@ -1,716 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging -import SourceKitD -import SwiftExtensions -@_spi(FixItApplier) import SwiftIDEUtils -import SwiftParser -import SwiftSyntax - -// MARK: - Entry point - -extension RequestInfo { - @MainActor - package func reduceInputFile( - using executor: SourceKitRequestExecutor, - progressUpdate: (_ progress: Double, _ message: String) -> Void - ) async throws -> RequestInfo { - try await withoutActuallyEscaping(progressUpdate) { progressUpdate in - let reducer = SourceReducer(sourcekitdExecutor: executor, progressUpdate: progressUpdate) - return try await reducer.run(initialRequestInfo: self) - } - } -} - -// MARK: - SourceReducer - -/// The return value of a source reducer, indicating whether edits were made or if the reducer has finished reducing -/// the source file. -private enum ReducerResult { - /// The reduction step produced edits that should be applied to the source file. - case edits([SourceEdit]) - - /// The reduction step was not able to produce any further modifications to the source file. Reduction is done. - case done - - init(doneIfEmpty edits: [SourceEdit]) { - if edits.isEmpty { - self = .done - } else { - self = .edits(edits) - } - } -} - -/// The return value of `runReductionStep`, indicating whether applying the edits from a reducer reduced the issue, -/// failed to reproduce the issue or if no changes were applied by the reducer. -private enum ReductionStepResult { - case reduced(RequestInfo) - case didNotReproduce - case noChange -} - -/// Reduces an input source file while continuing to reproduce the crash -@MainActor -private class SourceReducer { - /// The executor that is used to run a sourcekitd request and check whether it - /// still crashes. - private let sourcekitdExecutor: SourceKitRequestExecutor - - /// A callback to call to report progress - private let progressUpdate: (_ progress: Double, _ message: String) -> Void - - /// The number of import declarations that the file had when the source reducer was started. - private var initialImportCount: Int = 0 - - /// The byte size of the file when source reduction was started. This gets reset every time an import gets inlined. - private var fileSizeAfterLastImportInline: Int = 0 - - init( - sourcekitdExecutor: SourceKitRequestExecutor, - progressUpdate: @escaping (_ progress: Double, _ message: String) -> Void - ) { - self.sourcekitdExecutor = sourcekitdExecutor - self.progressUpdate = progressUpdate - } - - /// Reduce the file contents in `initialRequest` to a smaller file that still reproduces a crash. - @MainActor - func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo { - var requestInfo = initialRequestInfo - self.initialImportCount = Parser.parse(source: requestInfo.fileContents).numberOfImports - self.fileSizeAfterLastImportInline = initialRequestInfo.fileContents.utf8.count - - try await validateRequestInfoReproducesIssue(requestInfo: requestInfo) - - requestInfo = try await mergeDuplicateTopLevelItems(requestInfo) - requestInfo = try await removeTopLevelItems(requestInfo) - requestInfo = try await removeFunctionBodies(requestInfo) - requestInfo = try await removeMembersAndCodeBlockItemsBodies(requestInfo) - while let importInlined = try await inlineFirstImport(requestInfo) { - requestInfo = importInlined - requestInfo = try await removeTopLevelItems(requestInfo) - requestInfo = try await removeFunctionBodies(requestInfo) - requestInfo = try await removeMembersAndCodeBlockItemsBodies(requestInfo) - } - - requestInfo = try await removeComments(requestInfo) - - return requestInfo - } - - // MARK: Reduction steps - - private func validateRequestInfoReproducesIssue(requestInfo: RequestInfo) async throws { - let reductionResult = try await runReductionStep(requestInfo: requestInfo) { tree in .edits([]) } - switch reductionResult { - case .reduced: - break - case .didNotReproduce: - throw GenericError("Initial request info did not reproduce the issue") - case .noChange: - preconditionFailure("The reduction step always returns empty edits and not `done` so we shouldn't hit this") - } - } - - /// Remove the bodies of functions by an empty body. - private func removeFunctionBodies(_ requestInfo: RequestInfo) async throws -> RequestInfo { - try await runStatefulReductionStep( - requestInfo: requestInfo, - reducer: RemoveFunctionBodies() - ) - } - - /// Merge any top level items. These can happen if eg. the frontend reducer merged multiple files into one, resulting - /// in duplicate import statements. Merging these is important because `RemoveTopLevelItems` keeps track of nodes - /// that have already been visited by node contents and thus fails to remove multiple occurrences of the same import - /// statement in one go. - private func mergeDuplicateTopLevelItems(_ requestInfo: RequestInfo) async throws -> RequestInfo { - let reductionResult = try await runReductionStep(requestInfo: requestInfo, reduce: mergeDuplicateTopLevelItems(in:)) - switch reductionResult { - case .reduced(let reducedRequestInfo): - return reducedRequestInfo - case .didNotReproduce, .noChange: - return requestInfo - } - } - - /// Removes top level items in the source file. - private func removeTopLevelItems(_ requestInfo: RequestInfo) async throws -> RequestInfo { - var requestInfo = requestInfo - - // Try removing multiple top-level items at once first. This is useful eg. after inlining a Swift interface or after - // merging .swift files. Once that's done, go into a more fine-grained mode. - for simultaneousRemove in [100, 10, 1] { - let reducedRequestInfo = try await runStatefulReductionStep( - requestInfo: requestInfo, - reducer: RemoveTopLevelItems(simultaneousRemove: simultaneousRemove) - ) - requestInfo = reducedRequestInfo - } - return requestInfo - } - - /// Remove members and code block items. - /// - /// When `simultaneousRemove` is set, this automatically removes `simultaneousRemove` number of adjacent items. - /// This can significantly speed up the reduction of large files with many top-level items. - private func removeMembersAndCodeBlockItemsBodies( - _ requestInfo: RequestInfo - ) async throws -> RequestInfo { - var requestInfo = requestInfo - // Run removal of members and code block items in a loop. Sometimes the removal of a code block item further down in the - // file can remove the last reference to a member which can then be removed as well. - while true { - let reducedRequestInfo = try await runStatefulReductionStep( - requestInfo: requestInfo, - reducer: RemoveMembersAndCodeBlockItems() - ) - if reducedRequestInfo.fileContents == requestInfo.fileContents { - // No changes were made during reduction. We are done. - break - } - requestInfo = reducedRequestInfo - } - return requestInfo - } - - /// Remove comments from the source file. - private func removeComments(_ requestInfo: RequestInfo) async throws -> RequestInfo { - let reductionResult = try await runReductionStep(requestInfo: requestInfo, reduce: removeComments(from:)) - switch reductionResult { - case .reduced(let reducedRequestInfo): - return reducedRequestInfo - case .didNotReproduce, .noChange: - return requestInfo - } - } - - /// Replace the first `import` declaration in the source file by the contents of the Swift interface. - private func inlineFirstImport(_ requestInfo: RequestInfo) async throws -> RequestInfo? { - // Don't report progress after inlining an import because it might increase the file size before we have a chance - // to increase `fileSizeAfterLastImportInline`. Progress will get updated again on the next successful reduction - // step. - let reductionResult = try await runReductionStep(requestInfo: requestInfo, reportProgress: false) { tree in - let edits = await Diagnose.inlineFirstImport( - in: tree, - executor: sourcekitdExecutor, - compilerArgs: requestInfo.compilerArgs - ) - return edits - } - switch reductionResult { - case .reduced(let requestInfo): - self.fileSizeAfterLastImportInline = requestInfo.fileContents.utf8.count - return requestInfo - case .didNotReproduce, .noChange: - return nil - } - } - - // MARK: Primitives to run reduction steps - - private func logSuccessfulReduction(_ requestInfo: RequestInfo, tree: SourceFileSyntax) { - // The number of imports can grow if inlining a single module adds more than 1 new import. - // To keep progress between 0 and 1, clamp the number of imports to the initial import count. - let numberOfImports = min(tree.numberOfImports, initialImportCount) - let fileSize = requestInfo.fileContents.utf8.count - - let progressPerRemovedImport = Double(1) / Double(initialImportCount + 1) - let removeImportProgress = Double(initialImportCount - numberOfImports) * progressPerRemovedImport - let fileReductionProgress = - (1 - Double(fileSize) / Double(fileSizeAfterLastImportInline)) * progressPerRemovedImport - var progress = removeImportProgress + fileReductionProgress - if progress < 0 || progress > 1 { - logger.fault( - "Trying to report progress \(progress) from remove import progress \(removeImportProgress) and file reduction progress \(fileReductionProgress)" - ) - progress = max(min(progress, 1), 0) - } - progressUpdate(progress, "Reduced to \(numberOfImports) imports and \(fileSize) bytes") - } - - /// Run a single reduction step. - /// - /// If the request still crashes after applying the edits computed by `reduce`, return the reduced request info. - /// Otherwise, return `nil` - @MainActor - private func runReductionStep( - requestInfo: RequestInfo, - reportProgress: Bool = true, - reduce: (_ tree: SourceFileSyntax) async throws -> ReducerResult - ) async throws -> ReductionStepResult { - let tree = Parser.parse(source: requestInfo.fileContents) - let edits: [SourceEdit] - switch try await reduce(tree) { - case .edits(let edit): edits = edit - case .done: return .noChange - } - let reducedSource = FixItApplier.apply(edits: edits, to: tree) - - var adjustedOffset = requestInfo.offset - for edit in edits { - if edit.range.upperBound < AbsolutePosition(utf8Offset: requestInfo.offset) { - adjustedOffset -= (edit.range.upperBound.utf8Offset - edit.range.lowerBound.utf8Offset) - adjustedOffset += edit.replacement.utf8.count - } - } - - let reducedRequestInfo = RequestInfo( - requestTemplate: requestInfo.requestTemplate, - contextualRequestTemplates: requestInfo.contextualRequestTemplates, - offset: adjustedOffset, - compilerArgs: requestInfo.compilerArgs, - fileContents: reducedSource - ) - - logger.debug("Try reduction to the following input file:\n\(reducedSource)") - let result = try await sourcekitdExecutor.run(request: reducedRequestInfo) - if case .reproducesIssue = result { - logger.debug("Reduction successful") - if reportProgress { - logSuccessfulReduction(reducedRequestInfo, tree: tree) - } - return .reduced(reducedRequestInfo) - } else { - logger.debug("Reduction did not reproduce the issue") - return .didNotReproduce - } - } - - /// Run a reducer that can carry state between reduction steps. - /// - /// This invokes `reduce(tree:)` on `reducer` as long as the `reduce` method returns non-empty edits. - /// When `reducer.reduce(tree:)` returns empty edits, it indicates that it can't reduce the file any further - /// and this method return the reduced request info. - func runStatefulReductionStep( - requestInfo: RequestInfo, - reducer: any StatefulReducer - ) async throws -> RequestInfo { - /// Error to indicate that `reducer.reduce(tree:)` did not produce any edits. - /// Will always be caught within the function. - struct StatefulReducerFinishedReducing: Error {} - - var reproducer = requestInfo - while true { - let reduced = try await runReductionStep(requestInfo: reproducer) { tree in - return reducer.reduce(tree: tree) - } - switch reduced { - case .reduced(let reduced): - reproducer = reduced - case .didNotReproduce: - // Continue the loop and run the reducer again. - break - case .noChange: - // The reducer finished reducing the source file. We are done - return reproducer - } - } - } -} - -// MARK: - Reduce functions - -/// See `SourceReducer.runReductionStep` -private protocol StatefulReducer { - func reduce(tree: SourceFileSyntax) -> ReducerResult -} - -// MARK: Remove function bodies - -/// Tries removing the contents of function bodies one at a time. -private class RemoveFunctionBodies: StatefulReducer { - /// The function bodies that should not be removed. - /// - /// When we tried removing a function, it gets added to this list. - /// That way, if replacing it did not result in a reduced reproducer, we won't try replacing it again - /// on the next invocation of `reduce(tree:)`. - var keepFunctionBodies: [String] = [] - - func reduce(tree: SourceFileSyntax) -> ReducerResult { - let visitor = Visitor(keepFunctionBodies: keepFunctionBodies) - visitor.walk(tree) - keepFunctionBodies = visitor.keepFunctionBodies - return ReducerResult(doneIfEmpty: visitor.edits) - } - - private class Visitor: SyntaxAnyVisitor { - var keepFunctionBodies: [String] - var edits: [SourceEdit] = [] - - init(keepFunctionBodies: [String]) { - self.keepFunctionBodies = keepFunctionBodies - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if !edits.isEmpty { - // We already produced an edit. We only want to replace one function at a time. - return .skipChildren - } - return .visitChildren - } - - override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { - if !edits.isEmpty { - return .skipChildren - } - guard node.statements.count > 0 else { - return .skipChildren - } - if keepFunctionBodies.contains(node.statements.description.trimmingCharacters(in: .whitespacesAndNewlines)) { - return .visitChildren - } - keepFunctionBodies.append(node.statements.description.trimmingCharacters(in: .whitespacesAndNewlines)) - edits.append( - SourceEdit( - range: node.statements.position.. ReducerResult { - if tree.statements.count <= simultaneousRemove { - // There are fewer top-level items in the source file than we want to remove. Since removing everything in the - // source file is very unlikely to be successful, we are done. - return .done - } - let visitor = Visitor(keepMembers: keepItems, maxEdits: simultaneousRemove) - visitor.walk(tree) - keepItems = visitor.keepItems - return ReducerResult(doneIfEmpty: visitor.edits) - } - - private class Visitor: SyntaxAnyVisitor { - var keepItems: [String] - var edits: [SourceEdit] = [] - let maxEdits: Int - - init(keepMembers: [String], maxEdits: Int) { - self.keepItems = keepMembers - self.maxEdits = maxEdits - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - guard edits.count < maxEdits else { - return .skipChildren - } - return .visitChildren - } - - override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind { - guard node.parent?.parent?.is(SourceFileSyntax.self) ?? false else { - return .skipChildren - } - guard edits.count < maxEdits else { - return .skipChildren - } - if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) { - return .visitChildren - } else { - keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) - edits.append(SourceEdit(range: node.position.. ReducerResult { - let visitor = Visitor(keepMembers: keepItems) - visitor.walk(tree) - keepItems = visitor.keepItems - return ReducerResult(doneIfEmpty: visitor.edits) - } - - private class Visitor: SyntaxAnyVisitor { - var keepItems: [String] - var edits: [SourceEdit] = [] - - init(keepMembers: [String]) { - self.keepItems = keepMembers - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - guard edits.isEmpty else { - return .skipChildren - } - return .visitChildren - } - - override func visit(_ node: MemberBlockItemSyntax) -> SyntaxVisitorContinueKind { - guard edits.isEmpty else { - return .skipChildren - } - if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) { - return .visitChildren - } else { - keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) - edits.append(SourceEdit(range: node.position.. SyntaxVisitorContinueKind { - guard edits.isEmpty else { - return .skipChildren - } - if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) { - return .visitChildren - } else { - keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) - edits.append(SourceEdit(range: node.position.. ReducerResult { - class DuplicateTopLevelItemMerger: SyntaxVisitor { - var seenItems: Set = [] - var edits: [SourceEdit] = [] - - override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind { - guard node.parent?.parent?.is(SourceFileSyntax.self) ?? false else { - return .skipChildren - } - if !seenItems.insert(node.trimmedDescription).inserted { - edits.append( - SourceEdit( - range: node.positionAfterSkippingLeadingTrivia.. ReducerResult { - class CommentRemover: SyntaxVisitor { - var edits: [SourceEdit] = [] - - private func removeComments(from trivia: Trivia, startPosition: AbsolutePosition) { - var position = startPosition - var previousTriviaPiece: TriviaPiece? - for triviaPiece in trivia { - defer { - previousTriviaPiece = triviaPiece - position += triviaPiece.sourceLength - } - if triviaPiece.isComment || (triviaPiece.isNewline && previousTriviaPiece?.isComment ?? false) { - edits.append(SourceEdit(range: position..<(position + triviaPiece.sourceLength), replacement: "")) - } - } - } - - override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { - removeComments(from: node.leadingTrivia, startPosition: node.position) - removeComments(from: node.trailingTrivia, startPosition: node.endPositionBeforeTrailingTrivia) - return .skipChildren - } - } - - let remover = CommentRemover(viewMode: .sourceAccurate) - remover.walk(tree) - return .edits(remover.edits) -} - -fileprivate extension TriviaPiece { - var isComment: Bool { - switch self { - case .blockComment, .docBlockComment, .docLineComment, .lineComment: - return true - default: - return false - } - } -} - -// MARK: Inline first include - -private class FirstImportFinder: SyntaxAnyVisitor { - var firstImport: ImportDeclSyntax? - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if firstImport == nil { - return .visitChildren - } else { - return .skipChildren - } - } - - override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { - if firstImport == nil { - firstImport = node - } - return .skipChildren - } - - static func findFirstImport(in tree: some SyntaxProtocol) -> ImportDeclSyntax? { - let visitor = FirstImportFinder(viewMode: .sourceAccurate) - visitor.walk(tree) - return visitor.firstImport - } -} - -/// Return the generated interface of the given module. -/// -/// `compilerArgs` are the compiler args used to generate the interface. Initially these are the compiler arguments of -/// the file that imports the module. If `areFallbackArgs` is set, we have synthesized fallback arguments that only -/// contain a target and SDK. This is useful when reducing a swift-frontend crash because sourcekitd requires driver -/// arguments but the swift-frontend crash has frontend args. -@MainActor -private func getSwiftInterface( - _ moduleName: String, - executor: SourceKitRequestExecutor, - compilerArgs: [String], - areFallbackArgs: Bool = false -) async throws -> String { - // We use `RequestInfo` and its template to add the compiler arguments to the request. - let requestTemplate = """ - { - key.request: source.request.editor.open.interface, - key.name: "fake", - key.compilerargs: [ - $COMPILER_ARGS - ], - key.modulename: "\(moduleName)" - } - """ - let requestInfo = RequestInfo( - requestTemplate: requestTemplate, - contextualRequestTemplates: [], - offset: 0, - compilerArgs: compilerArgs, - fileContents: "" - ) - - let result: String - switch try await executor.run(request: requestInfo) { - case .success(let response): - result = response - case .error where !areFallbackArgs: - var fallbackArgs: [String] = [] - var argsIterator = compilerArgs.makeIterator() - while let arg = argsIterator.next() { - if arg == "-target" || arg == "-sdk" { - fallbackArgs.append(arg) - if let value = argsIterator.next() { - fallbackArgs.append(value) - } - } - } - return try await getSwiftInterface( - moduleName, - executor: executor, - compilerArgs: fallbackArgs, - areFallbackArgs: true - ) - default: - throw GenericError("Failed to get Swift Interface for \(moduleName)") - } - - // Extract the line containing the source text and parse that using JSON decoder. - // We can't parse the entire response using `JSONEncoder` because the sourcekitd response isn't actually valid JSON - // (it doesn't quote keys, for example). So, extract the string, which is actually correctly JSON encoded. - let quotedSourceText = result.components(separatedBy: "\n").compactMap { (line) -> Substring? in - let prefix = " key.sourcetext: " - guard line.hasPrefix(prefix) else { - return nil - } - var line: Substring = line[...] - line = line.dropFirst(prefix.count) - if line.hasSuffix(",") { - line = line.dropLast() - } - return line - }.only - guard let quotedSourceText else { - throw GenericError("Failed to decode Swift interface response for \(moduleName)") - } - // Filter control characters. JSONDecoder really doensn't like them and they are likely not important if they occur eg. in a comment. - let sanitizedData = Data(quotedSourceText.utf8.filter { $0 >= 32 }) - return try JSONDecoder().decode(String.self, from: sanitizedData) -} - -@MainActor -private func inlineFirstImport( - in tree: SourceFileSyntax, - executor: SourceKitRequestExecutor, - compilerArgs: [String] -) async -> ReducerResult { - guard let firstImport = FirstImportFinder.findFirstImport(in: tree) else { - return .done - } - guard let moduleName = firstImport.path.only?.name else { - return .done - } - guard let interface = try? await getSwiftInterface(moduleName.text, executor: executor, compilerArgs: compilerArgs) - else { - return .done - } - let edit = SourceEdit(range: firstImport.position.. [URL] { - var crashReports: [URL] = [] - for directoryToScan in directoriesToScanForCrashReports { - let diagnosticReports = URL(fileURLWithPath: (directoryToScan as NSString).expandingTildeInPath) - let enumerator = FileManager.default.enumerator(at: diagnosticReports, includingPropertiesForKeys: nil) - while let fileUrl = enumerator?.nextObject() as? URL { - if fileUrl.lastPathComponent.hasPrefix("swift-frontend"), fileUrl.pathExtension == "ips" { - crashReports.append(fileUrl) - } - } - } - return crashReports - } - - /// Find `swift-frontend` crashes - func findSwiftFrontendCrashes() -> [SwiftFrontendCrash] { - return crashReports().compactMap { (crashReportUrl) -> SwiftFrontendCrash? in - guard let fileContents = try? String(contentsOf: crashReportUrl, encoding: .utf8) else { - return nil - } - // The first line contains some summary data that we're not interested in. Remove it - guard let firstNewline = fileContents.firstIndex(of: "\n") else { - return nil - } - let interestingString = fileContents[firstNewline...] - let dateFormatter = DateFormatter() - dateFormatter.timeZone = NSTimeZone.local - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS Z" - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(dateFormatter) - guard let decoded = try? decoder.decode(IpsCrashReport.self, from: interestingString.data(using: .utf8)!) else { - return nil - } - - let commandLineString = decoded.asi.swiftFrontend - .compactMap { (entry) -> Substring? in - guard let range = entry.firstRange(of: "Program arguments: ") else { - return nil - } - return entry[range.upperBound...] - } - .first - - guard let commandLineString else { - return nil - } - - let commandLine = splitShellEscapedCommand(String(commandLineString)) - guard let swiftFrontendPath = commandLine.first else { - return nil - } - let swiftFrontendUrl = URL(fileURLWithPath: swiftFrontendPath) - - return SwiftFrontendCrash( - date: decoded.procLaunch, - swiftFrontend: swiftFrontendUrl, - frontendArgs: Array(commandLine.dropFirst()) - ) - } - } -} diff --git a/Sources/Diagnose/Toolchain+PluginPaths.swift b/Sources/Diagnose/Toolchain+PluginPaths.swift deleted file mode 100644 index bcf673f17..000000000 --- a/Sources/Diagnose/Toolchain+PluginPaths.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SourceKitD -import ToolchainRegistry - -extension Toolchain { - var pluginPaths: PluginPaths? { - guard let sourceKitClientPlugin, let sourceKitServicePlugin else { - return nil - } - return PluginPaths(clientPlugin: sourceKitClientPlugin, servicePlugin: sourceKitServicePlugin) - } -} diff --git a/Sources/Diagnose/Toolchain+SwiftFrontend.swift b/Sources/Diagnose/Toolchain+SwiftFrontend.swift deleted file mode 100644 index bb7e1bb0b..000000000 --- a/Sources/Diagnose/Toolchain+SwiftFrontend.swift +++ /dev/null @@ -1,25 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath - -extension Toolchain { - /// The path to `swift-frontend` in the toolchain, found relative to `swift`. - /// - /// - Note: Not discovered as part of the toolchain because `swift-frontend` is only needed in the diagnose commands. - package var swiftFrontend: URL? { - return swift?.deletingLastPathComponent().appending(component: "swift-frontend") - } -} diff --git a/Sources/Diagnose/TraceFromSignpostsCommand.swift b/Sources/Diagnose/TraceFromSignpostsCommand.swift deleted file mode 100644 index 1fa6e89ec..000000000 --- a/Sources/Diagnose/TraceFromSignpostsCommand.swift +++ /dev/null @@ -1,248 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import ArgumentParser -import Foundation -import RegexBuilder -import SwiftExtensions - -/// Shared instance of the regex that is used to extract Signpost lines from `log stream --signpost`. -private struct LogParseRegex { - @MainActor static let shared = LogParseRegex() - - let dateComponent = Reference(Substring.self) - let processIdComponent = Reference(Substring.self) - let signpostIdComponent = Reference(Substring.self) - let eventTypeComponent = Reference(Substring.self) - let categoryComponent = Reference(Substring.self) - let messageComponent = Reference(Substring.self) - private(set) var regex: - Regex.RegexOutput>! - - private init() { - regex = Regex { - Capture(as: dateComponent) { - #/[-0-9]+ [0-9:.+-]+/# - } - " " - #/[0-9a-fx]+/# // Thread ID - ZeroOrMore(.whitespace) - "Signpost" - ZeroOrMore(.whitespace) - #/[0-9a-fx]+/# // Activity - ZeroOrMore(.whitespace) - Capture(as: processIdComponent) { - ZeroOrMore(.digit) - } - ZeroOrMore(.whitespace) - ZeroOrMore(.digit) // TTL - ZeroOrMore(.whitespace) - "[spid 0x" - Capture(as: signpostIdComponent) { - OneOrMore(.hexDigit) - } - ", process, " - ZeroOrMore(.whitespace) - Capture(as: eventTypeComponent) { - #/(begin|event|end)/# - } - "]" - ZeroOrMore(.whitespace) - ZeroOrMore(.whitespace.inverted) // Process name - ZeroOrMore(.whitespace) - "[" - ZeroOrMore(.any) // subsystem - ":" - Capture(as: categoryComponent) { - ZeroOrMore(.any) - } - "]" - Capture(as: messageComponent) { - ZeroOrMore(.any) - } - } - } -} - -/// A signpost event extracted from a log. -private struct Signpost { - /// ID that identifies the signpost across the log. - /// - /// There might be multiple signposts with the same `signpostId` across multiple processes. - struct ID: Hashable { - let processId: Int - let signpostId: Int - } - - enum EventType: String { - case begin - case event - case end - } - - let date: Date - let processId: Int - let signpostId: Int - let eventType: EventType - let category: String - let message: String - - var id: ID { - ID(processId: processId, signpostId: signpostId) - } - - @MainActor - init?(logLine line: Substring) { - let regex = LogParseRegex.shared - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ" - guard let match = try? regex.regex.wholeMatch(in: line) else { - return nil - } - guard let date = dateFormatter.date(from: String(match[regex.dateComponent])), - let processId = Int(match[regex.processIdComponent]), - let signpostId = Int(match[regex.signpostIdComponent], radix: 16), - let eventType = Signpost.EventType(rawValue: String(match[regex.eventTypeComponent])) - else { - return nil - } - self.date = date - self.processId = processId - self.signpostId = signpostId - self.eventType = eventType - self.category = String(match[regex.categoryComponent]) - self.message = String(match[regex.messageComponent]) - } -} - -/// A trace event in the *Trace Event Format* that can be opened using Perfetto. -/// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/mobilebasic -private struct TraceEvent: Codable { - enum EventType: String, Codable { - case begin = "B" - case end = "E" - } - - /// The name of the event, as displayed in Trace Viewer - let name: String? - /// The event categories. - /// - /// This is a comma separated list of categories for the event. - /// The categories can be used to hide events in the Trace Viewer UI. - let cat: String - - /// The event type. - /// - /// This is a single character which changes depending on the type of event being output. - let ph: EventType - - /// The process ID for the process that output this event. - let pid: Int - - /// The thread ID for the thread that output this event. - /// - /// We use the signpost IDs as thread IDs to show each signpost on a single lane in the trace. - let tid: Int - - /// The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity. - let ts: Double - - init(beginning signpost: Signpost) { - self.name = signpost.message - self.cat = signpost.category - self.ph = .begin - self.pid = signpost.processId - self.tid = signpost.signpostId - self.ts = signpost.date.timeIntervalSince1970 * 1_000_000 - } - - init(ending signpost: Signpost) { - self.name = nil - self.cat = signpost.category - self.ph = .end - self.pid = signpost.processId - self.tid = signpost.signpostId - self.ts = signpost.date.timeIntervalSince1970 * 1_000_000 - } -} - -package struct TraceFromSignpostsCommand: AsyncParsableCommand { - package static let configuration: CommandConfiguration = CommandConfiguration( - commandName: "trace-from-signposts", - abstract: "Generate a Trace Event Format file from signposts captured using OS Log", - discussion: """ - Extracts signposts captured using 'log stream --signpost ..' and generates a trace file that can be opened using \ - Perfetto to visualize which requests were running concurrently. - """ - ) - - @Option(name: .customLong("log-file"), help: "The log file that was captured using 'log stream --signpost ...'") - var logFile: String - - @Option(help: "The trace output file to generate") - var output: String - - @Option( - name: .customLong("category-filter"), - help: "If specified, only include signposts from this logging category in the output file" - ) - var categoryFilter: String? - - package init() {} - - private func traceEvents(from signpostsById: [Signpost.ID: [Signpost]]) -> [TraceEvent] { - var traceEvents: [TraceEvent] = [] - for signposts in signpostsById.values { - guard let begin = signposts.filter({ $0.eventType == .begin }).only else { - continue - } - // Each begin event should to be paired with an end event. - // If a begin event exists before the previous begin event is ended, a nested timeline is shown. - // We display signpost events to last until the next signpost event. - let events = signposts.filter { $0.eventType == .event } - traceEvents.append(TraceEvent(beginning: begin)) - var hadPreviousEvent = false - for event in events { - if hadPreviousEvent { - traceEvents.append(TraceEvent(ending: event)) - } - hadPreviousEvent = true - traceEvents.append(TraceEvent(beginning: event)) - } - if let end = signposts.filter({ $0.eventType == .end }).only { - if hadPreviousEvent { - traceEvents.append(TraceEvent(ending: end)) - } - traceEvents.append(TraceEvent(ending: end)) - } - } - return traceEvents - } - - @MainActor - package func run() async throws { - let log = try String(contentsOf: URL(fileURLWithPath: logFile), encoding: .utf8) - - var signpostsById: [Signpost.ID: [Signpost]] = [:] - for line in log.split(separator: "\n") { - guard let signpost = Signpost(logLine: line) else { - continue - } - if let categoryFilter, signpost.category != categoryFilter { - continue - } - signpostsById[signpost.id, default: []].append(signpost) - } - let traceEvents = traceEvents(from: signpostsById) - try JSONEncoder().encode(traceEvents).write(to: URL(fileURLWithPath: output)) - } -} diff --git a/Sources/DocumentationLanguageService/BuildSystemIntegrationExtensions.swift b/Sources/DocumentationLanguageService/BuildSystemIntegrationExtensions.swift deleted file mode 100644 index 8af477ed4..000000000 --- a/Sources/DocumentationLanguageService/BuildSystemIntegrationExtensions.swift +++ /dev/null @@ -1,72 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import SKLogging - -extension BuildServerManager { - /// Retrieves the name of the Swift module for a given target. - /// - /// **Note:** prefer using ``module(for:in:)`` over ths function. This function - /// only exists for cases where you want the Swift module name of a target where - /// you don't know one of its Swift document URIs in advance. E.g. when handling - /// requests for Markdown/Tutorial files in DocC since they don't have compile - /// commands that could be used to find the module name. - /// - /// - Parameter target: The build target identifier - /// - Returns: The name of the Swift module or nil if it could not be determined - func moduleName(for target: BuildTargetIdentifier) async -> String? { - let sourceFiles = - await orLog( - "Failed to retreive source files from target \(target.uri)", - { try await self.sourceFiles(in: [target]).flatMap(\.sources) } - ) ?? [] - for sourceFile in sourceFiles { - let language = await defaultLanguage(for: sourceFile.uri, in: target) - guard language == .swift else { - continue - } - if let moduleName = await moduleName(for: sourceFile.uri, in: target) { - return moduleName - } - } - return nil - } - - /// Finds the SwiftDocC documentation catalog associated with a target, if any. - /// - /// - Parameter target: The build target identifier - /// - Returns: The URL of the documentation catalog or nil if one could not be found - func doccCatalog(for target: BuildTargetIdentifier) async -> URL? { - let sourceFiles = - await orLog( - "Failed to retrieve source files from target \(target.uri)", - { try await self.sourceFiles(in: [target]).flatMap(\.sources) } - ) ?? [] - let catalogURLs = sourceFiles.compactMap { sourceItem -> URL? in - guard sourceItem.dataKind == .sourceKit, - let data = SourceKitSourceItemData(fromLSPAny: sourceItem.data), - data.kind == .doccCatalog - else { - return nil - } - return sourceItem.uri.fileURL - }.sorted(by: { $0.absoluteString < $1.absoluteString }) - if catalogURLs.count > 1 { - logger.error("Multiple SwiftDocC catalogs found in build target \(target.uri)") - } - return catalogURLs.first - } -} diff --git a/Sources/DocumentationLanguageService/DocCCatalogIndexManager.swift b/Sources/DocumentationLanguageService/DocCCatalogIndexManager.swift deleted file mode 100644 index ad15a090d..000000000 --- a/Sources/DocumentationLanguageService/DocCCatalogIndexManager.swift +++ /dev/null @@ -1,190 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging -import SKUtilities -@_spi(LinkCompletion) @preconcurrency import SwiftDocC - -final actor DocCCatalogIndexManager { - private let server: DocCServer - - /// The cache of DocCCatalogIndex for a given SwiftDocC catalog URL - /// - /// - Note: The capacity has been chosen without scientific measurements. The - /// feeling is that switching between SwiftDocC catalogs is rare and 5 catalog - /// indexes won't take up much memory. - private var indexCache = LRUCache>(capacity: 5) - - init(server: DocCServer) { - self.server = server - } - - func invalidate(_ url: URL) { - indexCache.removeValue(forKey: url) - } - - func index(for catalogURL: URL) async throws(DocCIndexError) -> DocCCatalogIndex { - if let existingCatalog = indexCache[catalogURL] { - return try existingCatalog.get() - } - do { - let convertResponse = try await server.convert( - externalIDsToConvert: [], - documentPathsToConvert: [], - includeRenderReferenceStore: true, - documentationBundleLocation: catalogURL, - documentationBundleDisplayName: "unknown", - documentationBundleIdentifier: "unknown", - symbolGraphs: [], - emitSymbolSourceFileURIs: true, - markupFiles: [], - tutorialFiles: [], - convertRequestIdentifier: UUID().uuidString - ) - guard let renderReferenceStoreData = convertResponse.renderReferenceStore else { - throw DocCIndexError.unexpectedlyNilRenderReferenceStore - } - let renderReferenceStore = try JSONDecoder().decode(RenderReferenceStore.self, from: renderReferenceStoreData) - let catalogIndex = DocCCatalogIndex(from: renderReferenceStore) - indexCache[catalogURL] = .success(catalogIndex) - return catalogIndex - } catch { - if error is CancellationError { - // Don't cache cancellation errors - throw .cancelled - } - let internalError = error as? DocCIndexError ?? DocCIndexError.internalError(error) - indexCache[catalogURL] = .failure(internalError) - throw internalError - } - } -} - -/// Represents a potential error that the ``DocCCatalogIndexManager`` could encounter while indexing -enum DocCIndexError: LocalizedError { - case internalError(any Error) - case unexpectedlyNilRenderReferenceStore - case cancelled - - var errorDescription: String? { - switch self { - case .internalError(let internalError): - return "An internal error occurred: \(internalError.localizedDescription)" - case .unexpectedlyNilRenderReferenceStore: - return "Did not receive a RenderReferenceStore from the DocC server" - case .cancelled: - return "The request was cancelled" - } - } -} - -struct DocCCatalogIndex: Sendable { - /// A map from an asset name to its DataAsset contents. - let assets: [String: DataAsset] - - /// An array of DocCSymbolLink and their associated document URLs. - let documentationExtensions: [(link: DocCSymbolLink, documentURL: URL?)] - - /// A map from article name to its TopicRenderReference. - let articles: [String: TopicRenderReference] - - /// A map from tutorial name to its TopicRenderReference. - let tutorials: [String: TopicRenderReference] - - // A map from tutorial overview name to its TopicRenderReference. - let tutorialOverviews: [String: TopicRenderReference] - - /// Retrieves the documentation extension URL for the given symbol if one exists. - /// - /// - Parameter symbolInformation: The `DocCSymbolInformation` representing the symbol to search for. - func documentationExtension(for symbolInformation: DocCSymbolInformation) -> URL? { - documentationExtensions.filter { symbolInformation.matches($0.link) }.first?.documentURL - } - - init(from renderReferenceStore: RenderReferenceStore) { - // Assets - var assets: [String: DataAsset] = [:] - for (reference, asset) in renderReferenceStore.assets { - var asset = asset - asset.variants = asset.variants.compactMapValues { url in - orLog("Failed to convert asset from RenderReferenceStore") { try url.withScheme("doc-asset") } - } - assets[reference.assetName] = asset - } - self.assets = assets - // Markdown and Tutorial content - var documentationExtensionToSourceURL: [(link: DocCSymbolLink, documentURL: URL?)] = [] - var articles: [String: TopicRenderReference] = [:] - var tutorials: [String: TopicRenderReference] = [:] - var tutorialOverviews: [String: TopicRenderReference] = [:] - for (renderReferenceKey, topicContentValue) in renderReferenceStore.topics { - guard let topicRenderReference = topicContentValue.renderReference as? TopicRenderReference else { - continue - } - // Article and Tutorial URLs in SwiftDocC are always of the form `doc://///`. - // Therefore, we only really need to store the filename in these cases which will always be the last path component. - let lastPathComponent = renderReferenceKey.url.lastPathComponent - - switch topicRenderReference.kind { - case .article: - articles[lastPathComponent] = topicRenderReference - case .tutorial: - tutorials[lastPathComponent] = topicRenderReference - case .overview: - tutorialOverviews[lastPathComponent] = topicRenderReference - default: - guard topicContentValue.isDocumentationExtensionContent, renderReferenceKey.url.pathComponents.count > 2 else { - continue - } - // Documentation extensions are always of the form `doc:///documentation/`. - // We want to parse the `SymbolPath` in this case and store it in the index for lookups later. - let linkString = renderReferenceKey.url.pathComponents[2...].joined(separator: "/") - guard let doccSymbolLink = DocCSymbolLink(linkString: linkString) else { - continue - } - documentationExtensionToSourceURL.append((link: doccSymbolLink, documentURL: topicContentValue.source)) - } - } - self.documentationExtensions = documentationExtensionToSourceURL - self.articles = articles - self.tutorials = tutorials - self.tutorialOverviews = tutorialOverviews - } -} - -private enum WithSchemeError: LocalizedError { - case failedToRetrieveComponents(URL) - case failedToEncode(URLComponents) - - var errorDescription: String? { - switch self { - case .failedToRetrieveComponents(let url): - "Failed to retrieve components for URL \(url.absoluteString)" - case .failedToEncode(let components): - "Failed to encode URL components \(String(reflecting: components))" - } - } -} - -fileprivate extension URL { - func withScheme(_ scheme: String) throws(WithSchemeError) -> URL { - guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else { - throw WithSchemeError.failedToRetrieveComponents(self) - } - components.scheme = scheme - guard let result = components.url else { - throw WithSchemeError.failedToEncode(components) - } - return result - } -} diff --git a/Sources/DocumentationLanguageService/DocCDocumentationManager.swift b/Sources/DocumentationLanguageService/DocCDocumentationManager.swift deleted file mode 100644 index 970a3a426..000000000 --- a/Sources/DocumentationLanguageService/DocCDocumentationManager.swift +++ /dev/null @@ -1,132 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import SKLogging -import SwiftDocC - -struct DocCDocumentationManager: Sendable { - private let doccServer: DocCServer - private let referenceResolutionService: DocCReferenceResolutionService - private let catalogIndexManager: DocCCatalogIndexManager - - private let buildServerManager: BuildServerManager - - init(buildServerManager: BuildServerManager) { - let symbolResolutionServer = DocumentationServer(qualityOfService: .unspecified) - doccServer = DocCServer( - peer: symbolResolutionServer, - qualityOfService: .default - ) - catalogIndexManager = DocCCatalogIndexManager(server: doccServer) - referenceResolutionService = DocCReferenceResolutionService() - symbolResolutionServer.register(service: referenceResolutionService) - self.buildServerManager = buildServerManager - } - - func filesDidChange(_ events: [FileEvent]) async { - for event in events { - for target in await buildServerManager.targets(for: event.uri) { - guard let catalogURL = await buildServerManager.doccCatalog(for: target) else { - continue - } - await catalogIndexManager.invalidate(catalogURL) - } - } - } - - func catalogIndex(for catalogURL: URL) async throws(DocCIndexError) -> DocCCatalogIndex { - try await catalogIndexManager.index(for: catalogURL) - } - - /// Generates the SwiftDocC RenderNode for a given symbol, tutorial, or markdown file. - /// - /// - Parameters: - /// - symbolUSR: The USR of the symbol to render - /// - symbolGraph: The symbol graph that includes the given symbol USR - /// - overrideDocComments: An array of documentation comment lines that will override the comments in the symbol graph - /// - markupFile: The markdown article or symbol extension to render - /// - tutorialFile: The tutorial file to render - /// - moduleName: The name of the Swift module that will be rendered - /// - catalogURL: The URL pointing to the docc catalog that this symbol, tutorial, or markdown file is a part of - /// - Throws: A ResponseError if something went wrong - /// - Returns: The DoccDocumentationResponse containing the RenderNode if successful - func renderDocCDocumentation( - symbolUSR: String? = nil, - symbolGraph: String? = nil, - overrideDocComments: [String]? = nil, - markupFile: String? = nil, - tutorialFile: String? = nil, - moduleName: String?, - catalogURL: URL? - ) async throws -> DoccDocumentationResponse { - // Make inputs consumable by DocC - var externalIDsToConvert: [String]? = nil - var overridingDocumentationComments: [String: [String]] = [:] - if let symbolUSR { - externalIDsToConvert = [symbolUSR] - if let overrideDocComments { - overridingDocumentationComments[symbolUSR] = overrideDocComments - } - } - var symbolGraphs: [Data] = [] - if let symbolGraphData = symbolGraph?.data(using: .utf8) { - symbolGraphs.append(symbolGraphData) - } - var markupFiles: [Data] = [] - if let markupFile = markupFile?.data(using: .utf8) { - markupFiles.append(markupFile) - } - var tutorialFiles: [Data] = [] - if let tutorialFile = tutorialFile?.data(using: .utf8) { - tutorialFiles.append(tutorialFile) - } - // Store the convert request identifier in order to fulfill index requests from SwiftDocC - let convertRequestIdentifier = UUID().uuidString - var catalogIndex: DocCCatalogIndex? = nil - if let catalogURL { - catalogIndex = try await catalogIndexManager.index(for: catalogURL) - } - referenceResolutionService.addContext( - DocCReferenceResolutionContext( - catalogURL: catalogURL, - catalogIndex: catalogIndex - ), - withKey: convertRequestIdentifier - ) - // Send the convert request to SwiftDocC and wait for the response - let convertResponse = try await doccServer.convert( - externalIDsToConvert: externalIDsToConvert, - documentPathsToConvert: nil, - includeRenderReferenceStore: false, - documentationBundleLocation: nil, - documentationBundleDisplayName: moduleName ?? "Unknown", - documentationBundleIdentifier: "unknown", - symbolGraphs: symbolGraphs, - overridingDocumentationComments: overridingDocumentationComments, - emitSymbolSourceFileURIs: false, - markupFiles: markupFiles, - tutorialFiles: tutorialFiles, - convertRequestIdentifier: convertRequestIdentifier - ) - guard let renderNodeData = convertResponse.renderNodes.first else { - throw ResponseError.internalError("SwiftDocC did not return any render nodes") - } - guard let renderNode = String(data: renderNodeData, encoding: .utf8) else { - throw ResponseError.internalError("Failed to encode render node from SwiftDocC") - } - return DoccDocumentationResponse(renderNode: renderNode) - } -} diff --git a/Sources/DocumentationLanguageService/DocCReferenceResolutionService.swift b/Sources/DocumentationLanguageService/DocCReferenceResolutionService.swift deleted file mode 100644 index 220056084..000000000 --- a/Sources/DocumentationLanguageService/DocCReferenceResolutionService.swift +++ /dev/null @@ -1,233 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import IndexStoreDB -import LanguageServerProtocol -import SKLogging -import SemanticIndex -@_spi(Linkcompletion) @preconcurrency import SwiftDocC -import SwiftExtensions - -final class DocCReferenceResolutionService: DocumentationService, Sendable { - /// The message type that this service accepts. - static let symbolResolutionMessageType: DocumentationServer.MessageType = "resolve-reference" - - /// The message type that this service responds with when the requested symbol resolution was successful. - static let symbolResolutionResponseMessageType: DocumentationServer.MessageType = "resolve-reference-response" - - static let handlingTypes = [symbolResolutionMessageType] - - private let contextMap = ThreadSafeBox<[String: DocCReferenceResolutionContext]>(initialValue: [:]) - - init() {} - - func addContext(_ context: DocCReferenceResolutionContext, withKey key: String) { - contextMap.value[key] = context - } - - @discardableResult func removeContext(forKey key: String) -> DocCReferenceResolutionContext? { - contextMap.value.removeValue(forKey: key) - } - - func context(forKey key: String) -> DocCReferenceResolutionContext? { - contextMap.value[key] - } - - func process( - _ message: DocumentationServer.Message, - completion: @escaping (DocumentationServer.Message) -> Void - ) { - do { - let response = try process(message) - completion(response) - } catch { - completion(createResponseWithErrorMessage(error.localizedDescription)) - } - } - - private func process( - _ message: DocumentationServer.Message - ) throws(ReferenceResolutionError) -> DocumentationServer.Message { - // Decode the message payload - guard let payload = message.payload else { - throw ReferenceResolutionError.nilMessagePayload - } - let request = try decode(ConvertRequestContextWrapper.self, from: payload) - // Attempt to resolve the reference in the request - let resolvedReference = try resolveReference(request: request); - // Encode the response payload - let encodedResolvedReference = try encode(resolvedReference) - return createResponse(payload: encodedResolvedReference) - } - - private func resolveReference( - request: ConvertRequestContextWrapper - ) throws(ReferenceResolutionError) -> OutOfProcessReferenceResolver.Response { - guard let convertRequestIdentifier = request.convertRequestIdentifier else { - throw .missingConvertRequestIdentifier - } - guard let context = context(forKey: convertRequestIdentifier) else { - throw .missingContext - } - switch request.payload { - case .symbol(let symbolUSR): - throw .symbolNotFound(symbolUSR) - case .asset(let assetReference): - guard let catalog = context.catalogIndex else { - throw .indexNotAvailable - } - guard let dataAsset = catalog.assets[assetReference.assetName] else { - throw .assetNotFound - } - return .asset(dataAsset) - case .topic(let topicURL): - // Check if this is a link to another documentation article - let relevantPathComponents = topicURL.pathComponents.filter { $0 != "/" } - let resolvedReference: TopicRenderReference? = - switch relevantPathComponents.first { - case NodeURLGenerator.Path.documentationFolderName: - context.catalogIndex?.articles[topicURL.lastPathComponent] - case NodeURLGenerator.Path.tutorialsFolderName: - context.catalogIndex?.tutorials[topicURL.lastPathComponent] - default: - nil - } - if let resolvedReference { - return .resolvedInformation(OutOfProcessReferenceResolver.ResolvedInformation(resolvedReference, url: topicURL)) - } - // Otherwise this must be a link to a symbol - let urlString = topicURL.absoluteString - guard let doccSymbolLink = DocCSymbolLink(linkString: urlString) else { - throw .invalidURLInRequest - } - // Don't bother checking to see if the symbol actually exists in the index. This can be time consuming and - // it would be better to report errors/warnings for unresolved symbols directly within the document, anyway. - return .resolvedInformation( - OutOfProcessReferenceResolver.ResolvedInformation( - symbolURL: topicURL, - symbolName: doccSymbolLink.symbolName - ) - ) - } - } - - private func decode(_ type: T.Type, from data: Data) throws(ReferenceResolutionError) -> T { - do { - return try JSONDecoder().decode(T.self, from: data) - } catch { - throw .decodingFailure(error.localizedDescription) - } - } - - private func encode(_ value: T) throws(ReferenceResolutionError) -> Data { - do { - return try JSONEncoder().encode(value) - } catch { - throw .decodingFailure(error.localizedDescription) - } - } - - private func createResponseWithErrorMessage(_ message: String) -> DocumentationServer.Message { - let errorMessage = OutOfProcessReferenceResolver.Response.errorMessage(message) - let encodedErrorMessage = orLog("Encoding error message for OutOfProcessReferenceResolver.Response") { - try JSONEncoder().encode(errorMessage) - } - return createResponse(payload: encodedErrorMessage) - } - - private func createResponse(payload: Data?) -> DocumentationServer.Message { - DocumentationServer.Message( - type: DocCReferenceResolutionService.symbolResolutionResponseMessageType, - payload: payload - ) - } -} - -struct DocCReferenceResolutionContext { - let catalogURL: URL? - let catalogIndex: DocCCatalogIndex? -} - -fileprivate extension OutOfProcessReferenceResolver.ResolvedInformation { - init(symbolURL: URL, symbolName: String) { - self = OutOfProcessReferenceResolver.ResolvedInformation( - kind: .unknownSymbol, - url: symbolURL, - title: symbolName, - abstract: "", - language: .swift, - availableLanguages: [.swift], - platforms: [], - declarationFragments: nil - ) - } - - init(_ renderReference: TopicRenderReference, url: URL) { - let kind: DocumentationNode.Kind - switch renderReference.kind { - case .article: - kind = .article - case .tutorial, .overview: - kind = .tutorial - case .symbol: - kind = .unknownSymbol - case .section: - kind = .unknown - } - - self.init( - kind: kind, - url: url, - title: renderReference.title, - abstract: renderReference.abstract.map(\.plainText).joined(), - language: .swift, - availableLanguages: [.swift, .objectiveC], - topicImages: renderReference.images - ) - } -} - -enum ReferenceResolutionError: LocalizedError { - case nilMessagePayload - case invalidURLInRequest - case decodingFailure(String) - case encodingFailure(String) - case missingConvertRequestIdentifier - case missingContext - case indexNotAvailable - case symbolNotFound(String) - case assetNotFound - - var errorDescription: String? { - switch self { - case .nilMessagePayload: - return "Nil message payload provided." - case .decodingFailure(let error): - return "The service was unable to decode the given symbol resolution request: '\(error)'." - case .encodingFailure(let error): - return "The service failed to encode the result after resolving the symbol: \(error)" - case .invalidURLInRequest: - return "Failed to initialize an 'AbsoluteSymbolLink' from the given URL." - case .missingConvertRequestIdentifier: - return "The given request was missing a convert request identifier." - case .missingContext: - return "The given convert request identifier is not associated with any symbol resolution context." - case .indexNotAvailable: - return "An index was not available to complete this request." - case .symbolNotFound(let symbol): - return "Unable to find symbol '\(symbol)' in the index." - case .assetNotFound: - return "The requested asset could not be found." - } - } -} diff --git a/Sources/DocumentationLanguageService/DocCServer.swift b/Sources/DocumentationLanguageService/DocCServer.swift deleted file mode 100644 index 9471c8e26..000000000 --- a/Sources/DocumentationLanguageService/DocCServer.swift +++ /dev/null @@ -1,201 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -@preconcurrency import SwiftDocC - -struct DocCServer { - private let server: DocumentationServer - - init(peer peerServer: DocumentationServer? = nil, qualityOfService: DispatchQoS) { - server = DocumentationServer.createDefaultServer(qualityOfService: qualityOfService, peer: peerServer) - } - - /// Sends a request to SwiftDocC that will convert in-memory documentation. - /// - /// - Parameters: - /// - externalIDsToConvert: The external IDs of the symbols to convert. - /// - documentPathsToConvert: The paths of the documentation nodes to convert. - /// - includeRenderReferenceStore: Whether the conversion's render reference store should be included in - /// the response. - /// - documentationBundleLocation: The file location of the documentation bundle to convert, if any. - /// - documentationBundleDisplayName: The name of the documentation bundle to convert. - /// - documentationBundleIdentifier: The identifier of the documentation bundle to convert. - /// - symbolGraphs: The symbol graph data included in the documentation bundle to convert. - /// - overridingDocumentationComments: The mapping of external symbol identifiers to lines of a documentation - /// comment that overrides the value in the symbol graph. - /// - emitSymbolSourceFileURIs: Whether the conversion's rendered documentation should include source file - /// location metadata. - /// - markupFiles: The article and documentation extension file data included in the documentation bundle to convert. - /// - tutorialFiles: The tutorial file data included in the documentation bundle to convert. - /// - convertRequestIdentifier: A unique identifier for the request. Can be used to map additional data alongside - /// a request for use later on. - /// - Throws: A ``DocCServerError`` representing the type of error that occurred. - func convert( - externalIDsToConvert: [String]?, - documentPathsToConvert: [String]?, - includeRenderReferenceStore: Bool, - documentationBundleLocation: URL?, - documentationBundleDisplayName: String, - documentationBundleIdentifier: String, - symbolGraphs: [Data], - overridingDocumentationComments: [String: [String]] = [:], - emitSymbolSourceFileURIs: Bool, - markupFiles: [Data], - tutorialFiles: [Data], - convertRequestIdentifier: String - ) async throws(DocCServerError) -> ConvertResponse { - let request = ConvertRequest( - bundleInfo: DocumentationBundle.Info( - displayName: documentationBundleDisplayName, - id: DocumentationBundle.Identifier(rawValue: documentationBundleIdentifier), - defaultCodeListingLanguage: nil, - defaultAvailability: nil, - defaultModuleKind: nil - ), - externalIDsToConvert: externalIDsToConvert, - documentPathsToConvert: documentPathsToConvert, - includeRenderReferenceStore: includeRenderReferenceStore, - bundleLocation: documentationBundleLocation, - symbolGraphs: symbolGraphs, - overridingDocumentationComments: overridingDocumentationComments.mapValues { - $0.map { ConvertRequest.Line(text: $0) } - }, - knownDisambiguatedSymbolPathComponents: nil, - emitSymbolSourceFileURIs: emitSymbolSourceFileURIs, - markupFiles: markupFiles, - tutorialFiles: tutorialFiles, - miscResourceURLs: [], - symbolIdentifiersWithExpandedDocumentation: nil - ) - let response = try await makeRequest( - messageType: ConvertService.convertMessageType, - messageIdentifier: convertRequestIdentifier, - request: request - ) - guard let responsePayload = response.payload else { - throw .unexpectedlyNilPayload(response.type.rawValue) - } - // Check for an error response from SwiftDocC - guard response.type != ConvertService.convertResponseErrorMessageType else { - let convertServiceError: ConvertServiceError - do { - convertServiceError = try JSONDecoder().decode(ConvertServiceError.self, from: responsePayload) - } catch { - throw .messagePayloadDecodingFailure(messageType: response.type.rawValue, decodingError: error) - } - throw .internalError(convertServiceError) - } - guard response.type == ConvertService.convertResponseMessageType else { - throw .unknownMessageType(response.type.rawValue) - } - // Decode the SwiftDocC.ConvertResponse and wrap it in our own Sendable type - let doccConvertResponse: SwiftDocC.ConvertResponse - do { - doccConvertResponse = try JSONDecoder().decode(SwiftDocC.ConvertResponse.self, from: responsePayload) - } catch { - throw .decodingFailure(error) - } - return ConvertResponse(doccConvertResponse: doccConvertResponse) - } - - private func makeRequest( - messageType: DocumentationServer.MessageType, - messageIdentifier: String, - request: Request - ) async throws(DocCServerError) -> DocumentationServer.Message { - let result: Result = await withCheckedContinuation { continuation in - // Encode the request in JSON format - let encodedPayload: Data - do { - encodedPayload = try JSONEncoder().encode(request) - } catch { - return continuation.resume(returning: .failure(.encodingFailure(error))) - } - // Encode the full message in JSON format - let message = DocumentationServer.Message( - type: messageType, - identifier: messageIdentifier, - payload: encodedPayload - ) - let encodedMessage: Data - do { - encodedMessage = try JSONEncoder().encode(message) - } catch { - return continuation.resume(returning: .failure(.encodingFailure(error))) - } - // Send the request to the server and decode the response - server.process(encodedMessage) { response in - do { - let decodedMessage = try JSONDecoder().decode(DocumentationServer.Message.self, from: response) - continuation.resume(returning: .success(decodedMessage)) - } catch { - continuation.resume(returning: .failure(.decodingFailure(error))) - } - } - } - return try result.get() - } -} - -/// A Sendable wrapper around ``SwiftDocC.ConvertResponse`` -struct ConvertResponse: Sendable, Codable { - /// The render nodes that were created as part of the conversion, encoded as JSON. - let renderNodes: [Data] - - /// The render reference store that was created as part of the bundle's conversion, encoded as JSON. - /// - /// The ``RenderReferenceStore`` contains compiled information for documentation nodes that were registered as part of - /// the conversion. This information can be used as a lightweight index of the available documentation content in the bundle that's - /// been converted. - let renderReferenceStore: Data? - - /// Creates a conversion response given the render nodes that were created as part of the conversion. - init(renderNodes: [Data], renderReferenceStore: Data? = nil) { - self.renderNodes = renderNodes - self.renderReferenceStore = renderReferenceStore - } - - /// Creates a conversion response given a SwiftDocC conversion response - init(doccConvertResponse: SwiftDocC.ConvertResponse) { - self.renderNodes = doccConvertResponse.renderNodes - self.renderReferenceStore = doccConvertResponse.renderReferenceStore - } -} - -/// Represents a potential error that the ``DocCServer`` could encounter while processing requests -enum DocCServerError: LocalizedError { - case encodingFailure(_ encodingError: Error) - case decodingFailure(_ decodingError: Error) - case messagePayloadDecodingFailure(messageType: String, decodingError: Error) - case unknownMessageType(_ messageType: String) - case unexpectedlyNilPayload(_ messageType: String) - case internalError(_ underlyingError: LocalizedError) - - var errorDescription: String? { - switch self { - case .encodingFailure(let encodingError): - return "Failed to encode message: \(encodingError.localizedDescription)" - case .decodingFailure(let decodingError): - return "Failed to decode a received message: \(decodingError.localizedDescription)" - case .messagePayloadDecodingFailure(let messageType, let decodingError): - return - "Received a message of type '\(messageType)' and failed to decode its payload: \(decodingError.localizedDescription)." - case .unknownMessageType(let messageType): - return "Received an unknown message type: '\(messageType)'." - case .unexpectedlyNilPayload(let messageType): - return "Received a message of type '\(messageType)' with a 'nil' payload." - case .internalError(let underlyingError): - return underlyingError.errorDescription - } - } -} diff --git a/Sources/DocumentationLanguageService/DocCSymbolInformation.swift b/Sources/DocumentationLanguageService/DocCSymbolInformation.swift deleted file mode 100644 index a99a76a75..000000000 --- a/Sources/DocumentationLanguageService/DocCSymbolInformation.swift +++ /dev/null @@ -1,59 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -@_spi(LinkCompletion) @preconcurrency import SwiftDocC -import SwiftExtensions -import SymbolKit - -struct DocCSymbolInformation { - struct Component { - let name: String - let information: LinkCompletionTools.SymbolInformation - - init(fromModuleName moduleName: String) { - self.name = moduleName - self.information = LinkCompletionTools.SymbolInformation(fromModuleName: moduleName) - } - - init(fromSymbol symbol: SymbolGraph.Symbol) { - self.name = symbol.pathComponents.last ?? symbol.names.title - self.information = LinkCompletionTools.SymbolInformation(symbol: symbol) - } - } - - let components: [Component] - - init(components: [Component]) { - self.components = components - } - - func matches(_ link: DocCSymbolLink) -> Bool { - guard link.components.count == components.count else { - return false - } - return zip(link.components, components).allSatisfy { linkComponent, symbolComponent in - linkComponent.name == symbolComponent.name && symbolComponent.information.matches(linkComponent.disambiguation) - } - } -} - -private typealias KindIdentifier = SymbolGraph.Symbol.KindIdentifier - -extension LinkCompletionTools.SymbolInformation { - init(fromModuleName moduleName: String) { - self.init( - kind: KindIdentifier.module.identifier, - symbolIDHash: Self.hash(uniqueSymbolID: moduleName) - ) - } -} diff --git a/Sources/DocumentationLanguageService/DocCSymbolLink.swift b/Sources/DocumentationLanguageService/DocCSymbolLink.swift deleted file mode 100644 index 8ab5333b1..000000000 --- a/Sources/DocumentationLanguageService/DocCSymbolLink.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import IndexStoreDB -import SemanticIndex -@_spi(LinkCompletion) @preconcurrency import SwiftDocC -import SymbolKit - -struct DocCSymbolLink: Sendable { - let linkString: String - let components: [(name: String, disambiguation: LinkCompletionTools.ParsedDisambiguation)] - - var symbolName: String { - components.last!.name - } - - init?(linkString: String) { - let components = LinkCompletionTools.parse(linkString: linkString) - guard !components.isEmpty else { - return nil - } - self.linkString = linkString - self.components = components - } -} diff --git a/Sources/DocumentationLanguageService/DoccDocumentationError.swift b/Sources/DocumentationLanguageService/DoccDocumentationError.swift deleted file mode 100644 index 07247a348..000000000 --- a/Sources/DocumentationLanguageService/DoccDocumentationError.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocol - -enum DocCDocumentationError: LocalizedError { - case unsupportedLanguage(Language) - case indexNotAvailable - case symbolNotFound(String) - - var errorDescription: String? { - switch self { - case .unsupportedLanguage(let language): - return "Documentation preview is not available for \(language.description) files" - case .indexNotAvailable: - return "The index is not available to complete the request" - case .symbolNotFound(let symbolName): - return "Could not find symbol \(symbolName) in the project" - } - } -} - -extension ResponseError { - static func requestFailed(doccDocumentationError: DocCDocumentationError) -> ResponseError { - return ResponseError.requestFailed(doccDocumentationError.localizedDescription) - } -} diff --git a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift b/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift deleted file mode 100644 index 6db2ee025..000000000 --- a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift +++ /dev/null @@ -1,266 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import Foundation -@preconcurrency import IndexStoreDB -package import LanguageServerProtocol -import Markdown -import SKLogging -import SKUtilities -import SemanticIndex -import SourceKitLSP - -extension DocumentationLanguageService { - package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { - guard let sourceKitLSPServer else { - throw ResponseError.internalError("SourceKit-LSP is shutting down") - } - guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else { - throw ResponseError.workspaceNotOpen(req.textDocument.uri) - } - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - var moduleName: String? = nil - var catalogURL: URL? = nil - if let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri) { - moduleName = await workspace.buildServerManager.moduleName(for: target) - catalogURL = await workspace.buildServerManager.doccCatalog(for: target) - } - - switch snapshot.language { - case .tutorial: - return try await tutorialDocumentation( - for: snapshot, - in: workspace, - moduleName: moduleName, - catalogURL: catalogURL - ) - case .markdown: - return try await markdownDocumentation( - for: snapshot, - in: workspace, - moduleName: moduleName, - catalogURL: catalogURL - ) - case .swift: - guard let position = req.position else { - throw ResponseError.invalidParams("A position must be provided for Swift files") - } - - return try await swiftDocumentation( - for: snapshot, - at: position, - in: workspace, - moduleName: moduleName, - catalogURL: catalogURL - ) - default: - throw ResponseError.requestFailed(doccDocumentationError: .unsupportedLanguage(snapshot.language)) - } - } - - private func tutorialDocumentation( - for snapshot: DocumentSnapshot, - in workspace: Workspace, - moduleName: String?, - catalogURL: URL? - ) async throws -> DoccDocumentationResponse { - return try await documentationManager.renderDocCDocumentation( - tutorialFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL - ) - } - - private func markdownDocumentation( - for snapshot: DocumentSnapshot, - in workspace: Workspace, - moduleName: String?, - catalogURL: URL? - ) async throws -> DoccDocumentationResponse { - guard let sourceKitLSPServer else { - throw ResponseError.internalError("SourceKit-LSP is shutting down") - } - let documentationManager = documentationManager - guard case .symbol(let symbolName) = MarkdownTitleFinder.find(parsing: snapshot.text) else { - // This is an article that can be rendered on its own - return try await documentationManager.renderDocCDocumentation( - markupFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL - ) - } - guard let moduleName, symbolName == moduleName else { - // This is a symbol extension page. Find the symbol so that we can include it in the request. - guard let index = await workspace.index(checkedFor: .deletedFiles) else { - throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable) - } - return try await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in - guard let symbolLink = DocCSymbolLink(linkString: symbolName), - let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence( - ofDocCSymbolLink: symbolLink, - fetchSymbolGraph: { location in - return try await sourceKitLSPServer.primaryLanguageService( - for: location.documentUri, - workspace.buildServerManager.defaultLanguageInCanonicalTarget(for: location.documentUri), - in: workspace - ) - .symbolGraph(forOnDiskContentsAt: location, in: workspace, manager: onDiskDocumentManager) - } - ) - else { - throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName)) - } - let symbolGraph = try await sourceKitLSPServer.primaryLanguageService( - for: symbolOccurrence.location.documentUri, - workspace.buildServerManager.defaultLanguageInCanonicalTarget(for: symbolOccurrence.location.documentUri), - in: workspace - ).symbolGraph(forOnDiskContentsAt: symbolOccurrence.location, in: workspace, manager: onDiskDocumentManager) - return try await documentationManager.renderDocCDocumentation( - symbolUSR: symbolOccurrence.symbol.usr, - symbolGraph: symbolGraph, - markupFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL - ) - } - } - // This is a page representing the module itself. - // Create a dummy symbol graph and tell SwiftDocC to convert the module name. - // The version information isn't really all that important since we're creating - // what is essentially an empty symbol graph. - return try await documentationManager.renderDocCDocumentation( - symbolUSR: moduleName, - symbolGraph: emptySymbolGraph(forModule: moduleName), - markupFile: snapshot.text, - moduleName: moduleName, - catalogURL: catalogURL - ) - } - - private func swiftDocumentation( - for snapshot: DocumentSnapshot, - at position: Position, - in workspace: Workspace, - moduleName: String?, - catalogURL: URL? - ) async throws -> DoccDocumentationResponse { - guard let sourceKitLSPServer else { - throw ResponseError.internalError("SourceKit-LSP is shutting down") - } - let (symbolGraph, symbolUSR, overrideDocComments) = try await sourceKitLSPServer.primaryLanguageService( - for: snapshot.uri, - snapshot.language, - in: workspace - ).symbolGraph(for: snapshot, at: position) - // Locate the documentation extension and include it in the request if one exists - let markupExtensionFile = await sourceKitLSPServer.withOnDiskDocumentManager { - [documentationManager, documentManager = try documentManager] onDiskDocumentManager in - await orLog("Finding markup extension file for symbol \(symbolUSR)") { - try await Self.findMarkupExtensionFile( - workspace: workspace, - documentationManager: documentationManager, - documentManager: documentManager, - catalogURL: catalogURL, - for: symbolUSR, - fetchSymbolGraph: { location in - try await sourceKitLSPServer.primaryLanguageService( - for: location.documentUri, - snapshot.language, - in: workspace - ) - .symbolGraph(forOnDiskContentsAt: location, in: workspace, manager: onDiskDocumentManager) - - } - ) - } - } - return try await documentationManager.renderDocCDocumentation( - symbolUSR: symbolUSR, - symbolGraph: symbolGraph, - overrideDocComments: overrideDocComments, - markupFile: markupExtensionFile, - moduleName: moduleName, - catalogURL: catalogURL - ) - } - - private static func findMarkupExtensionFile( - workspace: Workspace, - documentationManager: DocCDocumentationManager, - documentManager: DocumentManager, - catalogURL: URL?, - for symbolUSR: String, - fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String? - ) async throws -> String? { - guard let catalogURL else { - return nil - } - let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL) - guard let index = await workspace.index(checkedFor: .deletedFiles) else { - return nil - } - let symbolInformation = try await index.doccSymbolInformation( - ofUSR: symbolUSR, - fetchSymbolGraph: fetchSymbolGraph - ) - guard let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation) else { - return nil - } - return documentManager.latestSnapshotOrDisk( - DocumentURI(markupExtensionFileURL), - language: .markdown - )?.text - } -} - -struct MarkdownTitleFinder: MarkupVisitor { - enum Title { - case plainText(String) - case symbol(String) - } - - static func find(parsing text: String) -> Title? { - let document = Markdown.Document(parsing: text, options: [.parseSymbolLinks]) - var visitor = MarkdownTitleFinder() - return visitor.visit(document) - } - - mutating func defaultVisit(_ markup: any Markup) -> Title? { - for child in markup.children { - if let value = visit(child) { - return value - } - } - return nil - } - - mutating func visitHeading(_ heading: Heading) -> Title? { - guard heading.level == 1 else { - return nil - } - if let symbolLink = heading.child(at: 0) as? SymbolLink { - // Remove the surrounding backticks to find the symbol name - let plainText = symbolLink.plainText - var startIndex = plainText.startIndex - if plainText.hasPrefix("``") { - startIndex = plainText.index(plainText.startIndex, offsetBy: 2) - } - var endIndex = plainText.endIndex - if plainText.hasSuffix("``") { - endIndex = plainText.index(plainText.endIndex, offsetBy: -2) - } - return .symbol(String(plainText[startIndex.. Bool { - return true - } - - package func initialize( - _ initialize: InitializeRequest - ) async throws -> InitializeResult { - return InitializeResult( - capabilities: ServerCapabilities() - ) - } - - package func shutdown() async { - // Nothing to tear down - } - - package func addStateChangeHandler( - handler: @escaping @Sendable (LanguageServerState, LanguageServerState) -> Void - ) async { - // There is no underlying language server with which to report state - } - - package func openDocument( - _ notification: DidOpenTextDocumentNotification, - snapshot: DocumentSnapshot - ) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func closeDocument(_ notification: DidCloseTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package func reopenDocument(_ notification: ReopenTextDocumentNotification) async { - // The DocumentationLanguageService does not do anything with document events - } - - package static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] { - return [] - } - - package func changeDocument( - _ notification: DidChangeTextDocumentNotification, - preEditSnapshot: DocumentSnapshot, - postEditSnapshot: DocumentSnapshot, - edits: [SwiftSyntax.SourceEdit] - ) async { - // The DocumentationLanguageService does not do anything with document events - } -} diff --git a/Sources/DocumentationLanguageService/EmptySymbolGraph.swift b/Sources/DocumentationLanguageService/EmptySymbolGraph.swift deleted file mode 100644 index b472b82d9..000000000 --- a/Sources/DocumentationLanguageService/EmptySymbolGraph.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import SymbolKit - -/// Generates a JSON string that represents an empty symbol graph for the given module name. -func emptySymbolGraph(forModule moduleName: String) throws -> String? { - let symbolGraph = SymbolGraph( - metadata: SymbolGraph.Metadata( - formatVersion: SymbolGraph.SemanticVersion(major: 0, minor: 0, patch: 0), - generator: "SourceKit-LSP" - ), - module: SymbolGraph.Module(name: moduleName, platform: SymbolGraph.Platform()), - symbols: [], - relationships: [] - ) - let data = try JSONEncoder().encode(symbolGraph) - return String(data: data, encoding: .utf8) -} diff --git a/Sources/DocumentationLanguageService/IndexStoreDB+Extensions.swift b/Sources/DocumentationLanguageService/IndexStoreDB+Extensions.swift deleted file mode 100644 index fe5922ddd..000000000 --- a/Sources/DocumentationLanguageService/IndexStoreDB+Extensions.swift +++ /dev/null @@ -1,130 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import IndexStoreDB -import SKLogging -import SemanticIndex -@preconcurrency @_spi(LinkCompletion) import SwiftDocC -import SwiftExtensions -import SymbolKit - -extension CheckedIndex { - /// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given `DocCSymbolLink`. - /// - /// If the `DocCSymbolLink` has an ambiguous definition, the most important role of this function is to deterministically return - /// the same result every time. - func primaryDefinitionOrDeclarationOccurrence( - ofDocCSymbolLink symbolLink: DocCSymbolLink, - fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String? - ) async throws -> SymbolOccurrence? { - guard let topLevelSymbolName = symbolLink.components.last?.name else { - throw DocCCheckedIndexError.emptyDocCSymbolLink - } - // Find all occurrences of the symbol by name alone - var topLevelSymbolOccurrences: [SymbolOccurrence] = [] - forEachCanonicalSymbolOccurrence(byName: topLevelSymbolName) { symbolOccurrence in - topLevelSymbolOccurrences.append(symbolOccurrence) - return true // continue - } - // Determine which of the symbol occurrences actually matches the symbol link - var result: [SymbolOccurrence] = [] - for occurrence in topLevelSymbolOccurrences { - let info = try await doccSymbolInformation(ofUSR: occurrence.symbol.usr, fetchSymbolGraph: fetchSymbolGraph) - if info.matches(symbolLink) { - result.append(occurrence) - } - } - // Ensure that this is deterministic by sorting the results - result.sort() - if result.count > 1 { - logger.debug("Multiple symbols found for DocC symbol link '\(symbolLink.linkString)'") - } - return result.first - } - - /// Find the DocCSymbolLink for a given symbol USR. - /// - /// - Parameters: - /// - usr: The symbol USR to find in the index. - /// - fetchSymbolGraph: Callback that returns a SymbolGraph for a given SymbolLocation - func doccSymbolInformation( - ofUSR usr: String, - fetchSymbolGraph: (SymbolLocation) async throws -> String? - ) async throws -> DocCSymbolInformation { - guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { - throw DocCCheckedIndexError.emptyDocCSymbolLink - } - let moduleName = topLevelSymbolOccurrence.location.moduleName - var symbols = [topLevelSymbolOccurrence] - // Find any parent symbols - var symbolOccurrence: SymbolOccurrence = topLevelSymbolOccurrence - while let parentSymbolOccurrence = symbolOccurrence.parent(self) { - symbols.insert(parentSymbolOccurrence, at: 0) - symbolOccurrence = parentSymbolOccurrence - } - // Fetch symbol information from the symbol graph - var components = [DocCSymbolInformation.Component(fromModuleName: moduleName)] - for symbolOccurence in symbols { - guard let rawSymbolGraph = try await fetchSymbolGraph(symbolOccurence.location) else { - throw DocCCheckedIndexError.noSymbolGraph(symbolOccurence.symbol.usr) - } - let symbolGraph = try JSONDecoder().decode(SymbolGraph.self, from: Data(rawSymbolGraph.utf8)) - guard let symbol = symbolGraph.symbols[symbolOccurence.symbol.usr] else { - throw DocCCheckedIndexError.symbolNotFound(symbolOccurence.symbol.usr) - } - components.append(DocCSymbolInformation.Component(fromSymbol: symbol)) - } - return DocCSymbolInformation(components: components) - } -} - -enum DocCCheckedIndexError: LocalizedError { - case emptyDocCSymbolLink - case noSymbolGraph(String) - case symbolNotFound(String) - - var errorDescription: String? { - switch self { - case .emptyDocCSymbolLink: - "The provided DocCSymbolLink was empty and could not be resolved" - case .noSymbolGraph(let usr): - "Unable to locate symbol graph for \(usr)" - case .symbolNotFound(let usr): - "Symbol \(usr) was not found in its symbol graph" - } - } -} - -extension SymbolOccurrence { - func parent(_ index: CheckedIndex) -> SymbolOccurrence? { - let allParentRelations = - relations - .filter { $0.roles.contains(.childOf) } - .sorted() - if allParentRelations.count > 1 { - logger.debug("Symbol \(symbol.usr) has multiple parent symbols") - } - guard let parentRelation = allParentRelations.first else { - return nil - } - if parentRelation.symbol.kind == .extension { - let allSymbolOccurrences = index.occurrences(relatedToUSR: parentRelation.symbol.usr, roles: .extendedBy) - .sorted() - if allSymbolOccurrences.count > 1 { - logger.debug("Extension \(parentRelation.symbol.usr) extends multiple symbols") - } - return allSymbolOccurrences.first - } - return index.primaryDefinitionOrDeclarationOccurrence(ofUSR: parentRelation.symbol.usr) - } -} diff --git a/Sources/InProcessClient/CMakeLists.txt b/Sources/InProcessClient/CMakeLists.txt deleted file mode 100644 index 1c8646ff1..000000000 --- a/Sources/InProcessClient/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -add_library(InProcessClient STATIC - InProcessSourceKitLSPClient.swift - LanguageServiceRegistry+staticallyKnownServices.swift) - -set_target_properties(InProcessClient PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) - -target_link_libraries(InProcessClient PUBLIC - BuildServerIntegration - ClangLanguageService - LanguageServerProtocol - SKLogging - SKOptions - SourceKitLSP - SwiftLanguageService - ToolchainRegistry -) - -target_link_libraries(InProcessClient PRIVATE - TSCExtensions -) diff --git a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift b/Sources/InProcessClient/InProcessSourceKitLSPClient.swift deleted file mode 100644 index 928461ef1..000000000 --- a/Sources/InProcessClient/InProcessSourceKitLSPClient.swift +++ /dev/null @@ -1,149 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -public import Foundation -public import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -public import SKOptions -package import SourceKitLSP -import SwiftExtensions -import TSCExtensions -package import ToolchainRegistry - -import struct TSCBasic.AbsolutePath - -/// Launches a `SourceKitLSPServer` in-process and allows sending messages to it. -public final class InProcessSourceKitLSPClient: Sendable { - private let server: SourceKitLSPServer - - private let nextRequestID = AtomicUInt32(initialValue: 0) - - public convenience init( - toolchainPath: URL?, - options: SourceKitLSPOptions = SourceKitLSPOptions(), - capabilities: ClientCapabilities = ClientCapabilities(), - workspaceFolders: [WorkspaceFolder], - messageHandler: any MessageHandler - ) async throws { - try await self.init( - toolchainRegistry: ToolchainRegistry(installPath: toolchainPath), - options: options, - capabilities: capabilities, - workspaceFolders: workspaceFolders, - messageHandler: messageHandler - ) - } - - /// Create a new `SourceKitLSPServer`. An `InitializeRequest` is automatically sent to the server. - /// - /// `messageHandler` handles notifications and requests sent from the SourceKit-LSP server to the client. - package init( - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions = SourceKitLSPOptions(), - hooks: Hooks = Hooks(), - capabilities: ClientCapabilities = ClientCapabilities(), - workspaceFolders: [WorkspaceFolder], - messageHandler: any MessageHandler - ) async throws { - let serverToClientConnection = LocalConnection(receiverName: "client") - self.server = SourceKitLSPServer( - client: serverToClientConnection, - toolchainRegistry: toolchainRegistry, - languageServerRegistry: .staticallyKnownServices, - options: options, - hooks: hooks, - onExit: { - serverToClientConnection.close() - } - ) - serverToClientConnection.start(handler: messageHandler) - _ = try await self.send( - InitializeRequest( - processId: nil, - rootPath: nil, - rootURI: nil, - initializationOptions: nil, - capabilities: capabilities, - trace: .off, - workspaceFolders: workspaceFolders - ) - ) - } - - /// Send the request to `server` and return the request result. - /// - /// - Important: Because this is an async function, Swift concurrency makes no guarantees about the execution ordering - /// of this request with regard to other requests to the server. If execution of requests in a particular order is - /// necessary and the response of the request is not awaited, use the version of the function that takes a - /// completion handler - public func send(_ request: R) async throws -> R.Response { - let requestId = ThreadSafeBox(initialValue: nil) - return try await withTaskCancellationHandler { - return try await withCheckedThrowingContinuation { continuation in - if Task.isCancelled { - // Check if the task has been cancelled before we send the request to LSP to avoid any kind of work if - // possible. - return continuation.resume(throwing: CancellationError()) - } - requestId.value = self.send(request) { - continuation.resume(with: $0) - } - if Task.isCancelled, let requestId = requestId.takeValue() { - // The task might have been cancelled after the above cancellation check but before `requestId` was assigned - // a value. To cover that case, check for cancellation here again. Note that we won't cancel twice from here - // and the `onCancel` handler because we take the request ID out of the `ThreadSafeBox` before sending the - // `CancelRequestNotification`. - self.send(CancelRequestNotification(id: requestId)) - } - } - } onCancel: { - if let requestId = requestId.takeValue() { - self.send(CancelRequestNotification(id: requestId)) - } - } - } - - /// Send the request to `server` and return the request result via a completion handler. - @discardableResult - public func send( - _ request: R, - reply: @Sendable @escaping (LSPResult) -> Void - ) -> RequestID { - let requestID = RequestID.string("sk-\(Int(nextRequestID.fetchAndIncrement()))") - server.handle(request, id: requestID, reply: reply) - return requestID - } - - /// Send the request to `server` and return the request result via a completion handler. - /// - /// The request ID must not start with `sk-` to avoid conflicting with the request IDs that are created by - /// `send(:reply:)`. - public func send( - _ request: R, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) { - if case .string(let string) = id { - if string.starts(with: "sk-") { - logger.fault("Manually specified request ID must not have reserved prefix 'sk-'") - } - } - server.handle(request, id: id, reply: reply) - } - - /// Send the notification to `server`. - public func send(_ notification: some NotificationType) { - server.handle(notification) - } -} diff --git a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift b/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift deleted file mode 100644 index ca95726d0..000000000 --- a/Sources/InProcessClient/LanguageServiceRegistry+staticallyKnownServices.swift +++ /dev/null @@ -1,33 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import ClangLanguageService -import LanguageServerProtocol -package import SourceKitLSP -import SwiftLanguageService - -#if canImport(DocumentationLanguageService) -import DocumentationLanguageService -#endif - -extension LanguageServiceRegistry { - /// All types conforming to `LanguageService` that are known at compile time. - package static let staticallyKnownServices = { - var registry = LanguageServiceRegistry() - registry.register(ClangLanguageService.self, for: [.c, .cpp, .objective_c, .objective_cpp]) - registry.register(SwiftLanguageService.self, for: [.swift]) - #if canImport(DocumentationLanguageService) - registry.register(DocumentationLanguageService.self, for: [.markdown, .tutorial, .swift]) - #endif - return registry - }() -} diff --git a/Sources/LanguageServerProtocol/CMakeLists.txt b/Sources/LanguageServerProtocol/CMakeLists.txt index 9c05561bd..027e954d3 100644 --- a/Sources/LanguageServerProtocol/CMakeLists.txt +++ b/Sources/LanguageServerProtocol/CMakeLists.txt @@ -153,3 +153,5 @@ set_target_properties(LanguageServerProtocol PROPERTIES target_link_libraries(LanguageServerProtocol PUBLIC $<$>:swiftDispatch> $<$>:Foundation>) + +set_property(GLOBAL APPEND PROPERTY SWIFTTOOLSPROTOCOLS_EXPORTS LanguageServerProtocol) diff --git a/Sources/LanguageServerProtocolExtensions/CMakeLists.txt b/Sources/LanguageServerProtocolExtensions/CMakeLists.txt deleted file mode 100644 index 34d50f9db..000000000 --- a/Sources/LanguageServerProtocolExtensions/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ - -add_library(LanguageServerProtocolExtensions STATIC - Connection+Send.swift - DocumentURI+CustomLogStringConvertible.swift - DocumentURI+symlinkTarget.swift - Language+Inference.swift - LocalConnection.swift - QueueBasedMessageHandler.swift - RequestAndReply.swift - ResponseError+Init.swift - WorkDoneProgressManager.swift -) -set_target_properties(LanguageServerProtocolExtensions PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(LanguageServerProtocolExtensions PUBLIC - SourceKitD) -target_link_libraries(LanguageServerProtocolExtensions PRIVATE - LanguageServerProtocol - LanguageServerProtocolJSONRPC - SKLogging - SwiftExtensions - TSCBasic - $<$>:Foundation>) diff --git a/Sources/LanguageServerProtocolExtensions/Connection+Send.swift b/Sources/LanguageServerProtocolExtensions/Connection+Send.swift deleted file mode 100644 index 235e506e2..000000000 --- a/Sources/LanguageServerProtocolExtensions/Connection+Send.swift +++ /dev/null @@ -1,34 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -import SwiftExtensions - -extension Connection { - /// Send the given request to the connection and await its result. - /// - /// This method automatically sends a `CancelRequestNotification` to the - /// connection if the task it is executing in is being cancelled. - /// - /// - Warning: Because this message is `async`, it does not provide any ordering - /// guarantees. If you need to guarantee that messages are sent in-order - /// use the version with a completion handler. - package func send(_ request: R) async throws -> R.Response { - return try await withCancellableCheckedThrowingContinuation { continuation in - return self.send(request) { result in - continuation.resume(with: result) - } - } cancel: { requestID in - self.send(CancelRequestNotification(id: requestID)) - } - } -} diff --git a/Sources/LanguageServerProtocolExtensions/DocumentURI+symlinkTarget.swift b/Sources/LanguageServerProtocolExtensions/DocumentURI+symlinkTarget.swift deleted file mode 100644 index a8dfa10cf..000000000 --- a/Sources/LanguageServerProtocolExtensions/DocumentURI+symlinkTarget.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SwiftExtensions - -extension DocumentURI { - /// If this is a file URI pointing to a symlink, return the realpath of the URI, otherwise return `nil`. - package var symlinkTarget: DocumentURI? { - guard let fileUrl = fileURL else { - return nil - } - guard let realpath = try? DocumentURI(fileUrl.realpath) else { - return nil - } - if realpath == self { - return nil - } - return realpath - } -} diff --git a/Sources/LanguageServerProtocolExtensions/Language+Inference.swift b/Sources/LanguageServerProtocolExtensions/Language+Inference.swift deleted file mode 100644 index 489ed5ee7..000000000 --- a/Sources/LanguageServerProtocolExtensions/Language+Inference.swift +++ /dev/null @@ -1,46 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol - -extension Language { - package enum SemanticKind { - case clang - case swift - } - - package var semanticKind: SemanticKind? { - switch self { - case .swift: - return .swift - case .c, .cpp, .objective_c, .objective_cpp: - return .clang - default: - return nil - } - } - - package init?(inferredFromFileExtension uri: DocumentURI) { - // URL.pathExtension is only set for file URLs but we want to also infer a file extension for non-file URLs like - // untitled:file.cpp - let pathExtension = uri.fileURL?.pathExtension ?? (uri.pseudoPath as NSString).pathExtension - switch pathExtension { - case "c": self = .c - case "cpp", "cc", "cxx", "hpp": self = .cpp - case "m": self = .objective_c - case "mm", "h": self = .objective_cpp - case "swift": self = .swift - default: return nil - } - } -} diff --git a/Sources/LanguageServerProtocolExtensions/ResponseError+Init.swift b/Sources/LanguageServerProtocolExtensions/ResponseError+Init.swift deleted file mode 100644 index 3860f33df..000000000 --- a/Sources/LanguageServerProtocolExtensions/ResponseError+Init.swift +++ /dev/null @@ -1,46 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SourceKitD - -extension ResponseError { - package init(_ error: some Error) { - switch error { - case let error as ResponseError: - self = error - case let error as SKDError: - self.init(error) - case is CancellationError: - self = .cancelled - default: - self = .unknown("Unknown error: \(error)") - } - } - - private init(_ value: SKDError) { - switch value { - case .requestCancelled: - self = .cancelled - case .timedOut: - self = .unknown("sourcekitd request timed out") - case .requestFailed(let desc): - self = .unknown("sourcekitd request failed: \(desc)") - case .requestInvalid(let desc): - self = .unknown("sourcekitd invalid request \(desc)") - case .missingRequiredSymbol(let desc): - self = .unknown("sourcekitd missing required symbol '\(desc)'") - case .connectionInterrupted: - self = .unknown("sourcekitd connection interrupted") - } - } -} diff --git a/Sources/LanguageServerProtocolExtensions/WorkDoneProgressManager.swift b/Sources/LanguageServerProtocolExtensions/WorkDoneProgressManager.swift deleted file mode 100644 index 07d35e864..000000000 --- a/Sources/LanguageServerProtocolExtensions/WorkDoneProgressManager.swift +++ /dev/null @@ -1,163 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -import SwiftExtensions - -/// Represents a single `WorkDoneProgress` task that gets communicated with the client. -/// -/// The work done progress is started when the object is created and ended when the object is destroyed. -/// In between, updates can be sent to the client. -package actor WorkDoneProgressManager { - private enum Status: Equatable { - case inProgress(message: String?, percentage: Int?) - case done(message: String?) - } - - /// The token with which the work done progress has been created. `nil` if no work done progress has been created yet, - /// either because we didn't send the `WorkDoneProgress` request yet, because the work done progress creation failed, - /// or because the work done progress has been ended. - private var token: ProgressToken? - - /// The queue on which progress updates are sent to the client. - private let progressUpdateQueue = AsyncQueue() - - private let connectionToClient: any Connection - - /// Closure that wait until the connection between SourceKit-LSP and the editor has been initialized. Other than that, - /// the closure should not perform any work. - private let waitUntilClientInitialized: () async -> Void - - /// A string with which the `token` of the generated `WorkDoneProgress` sent to the client starts. - /// - /// A UUID will be appended to this prefix to make the token unique. The token prefix can be used to classify the work - /// done progress into a category, which makes debugging easier because the tokens have semantic meaning and also - /// allows clients to interpret what the `WorkDoneProgress` represents (for example Swift for VS Code explicitly - /// recognizes work done progress that indicates that sourcekitd has crashed to offer a diagnostic bundle to be - /// generated). - private let tokenPrefix: String - - private let title: String - - /// The next status that should be sent to the client by `sendProgressUpdateImpl`. - /// - /// While progress updates are being queued in `progressUpdateQueue` this status can evolve. The next - /// `sendProgressUpdateImpl` call will pick up the latest status. - /// - /// For example, if we receive two update calls to 25% and 50% in quick succession the `sendProgressUpdateImpl` - /// scheduled from the 25% update will already pick up the new 50% status. The `sendProgressUpdateImpl` call scheduled - /// from the 50% update will then realize that the `lastStatus` is already up-to-date and be a no-op. - private var pendingStatus: Status - - /// The last status that was sent to the client. Used so we don't send no-op updates to the client. - private var lastStatus: Status? = nil - - package init?( - connectionToClient: any Connection, - waitUntilClientInitialized: @escaping () async -> Void, - tokenPrefix: String, - initialDebounce: Duration? = nil, - title: String, - message: String? = nil, - percentage: Int? = nil - ) { - self.tokenPrefix = tokenPrefix - self.connectionToClient = connectionToClient - self.waitUntilClientInitialized = waitUntilClientInitialized - self.title = title - self.pendingStatus = .inProgress(message: message, percentage: percentage) - progressUpdateQueue.async { - if let initialDebounce { - try? await Task.sleep(for: initialDebounce) - } - await self.sendProgressUpdateAssumingOnProgressUpdateQueue() - } - } - - /// Send the necessary messages to the client to update the work done progress to `status`. - /// - /// Must be called on `progressUpdateQueue` - private func sendProgressUpdateAssumingOnProgressUpdateQueue() async { - let statusToSend = pendingStatus - guard statusToSend != lastStatus else { - return - } - await waitUntilClientInitialized() - switch statusToSend { - case .inProgress(let message, let percentage): - if let token { - connectionToClient.send( - WorkDoneProgress( - token: token, - value: .report(WorkDoneProgressReport(cancellable: false, message: message, percentage: percentage)) - ) - ) - } else { - let token = ProgressToken.string("\(tokenPrefix).\(UUID().uuidString)") - do { - _ = try await connectionToClient.send(CreateWorkDoneProgressRequest(token: token)) - } catch { - return - } - connectionToClient.send( - WorkDoneProgress( - token: token, - value: .begin(WorkDoneProgressBegin(title: title, message: message, percentage: percentage)) - ) - ) - self.token = token - } - case .done(let message): - if let token { - connectionToClient.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd(message: message)))) - self.token = nil - } - } - lastStatus = statusToSend - } - - package func update(message: String? = nil, percentage: Int? = nil) { - pendingStatus = .inProgress(message: message, percentage: percentage) - progressUpdateQueue.async { - await self.sendProgressUpdateAssumingOnProgressUpdateQueue() - } - } - - /// Ends the work done progress. Any further update calls are no-ops. - /// - /// `end` must be should be called before the `WorkDoneProgressManager` is deallocated. - package func end(message: String? = nil) { - pendingStatus = .done(message: message) - progressUpdateQueue.async { - await self.sendProgressUpdateAssumingOnProgressUpdateQueue() - } - } - - deinit { - guard case .done = pendingStatus else { - // If there is still a pending work done progress, end it. We know that we don't have any pending updates on - // `progressUpdateQueue` because they would capture `self` strongly and thus we wouldn't be deallocating this - // object. - // This is a fallback logic to ensure we don't leave pending work done progresses in the editor if the - // `WorkDoneProgressManager` is destroyed without a call to `end` (eg. because its owning object is destroyed). - // Calling `end()` is preferred because it ends the work done progress even if there are pending status updates - // in `progressUpdateQueue`, which keep the `WorkDoneProgressManager` alive and thus prevent the work done - // progress to be implicitly ended by the deinitializer. - if let token { - connectionToClient.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd()))) - } - return - } - } -} diff --git a/Sources/LanguageServerProtocolJSONRPC/CMakeLists.txt b/Sources/LanguageServerProtocolJSONRPC/CMakeLists.txt deleted file mode 100644 index 1983f44aa..000000000 --- a/Sources/LanguageServerProtocolJSONRPC/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -add_library(LanguageServerProtocolJSONRPC STATIC - DisableSigpipe.swift - JSONRPCConnection.swift - LoggableMessageTypes.swift - MessageCoding.swift - MessageSplitting.swift) -set_target_properties(LanguageServerProtocolJSONRPC PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(LanguageServerProtocolJSONRPC PRIVATE - LanguageServerProtocol - SKLogging) -target_link_libraries(LanguageServerProtocolJSONRPC PRIVATE - $<$>:swiftDispatch> - $<$>:Foundation>) diff --git a/Sources/BuildServerIntegration/BuildServerMessageDependencyTracker.swift b/Sources/LanguageServerProtocolTransport/BuildServerMessageDependencyTracker.swift similarity index 92% rename from Sources/BuildServerIntegration/BuildServerMessageDependencyTracker.swift rename to Sources/LanguageServerProtocolTransport/BuildServerMessageDependencyTracker.swift index 1850badce..769b93e84 100644 --- a/Sources/BuildServerIntegration/BuildServerMessageDependencyTracker.swift +++ b/Sources/LanguageServerProtocolTransport/BuildServerMessageDependencyTracker.swift @@ -11,14 +11,13 @@ //===----------------------------------------------------------------------===// import BuildServerProtocol -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging +public import LanguageServerProtocol +@_spi(SourceKitLSP) import SKLogging import SwiftExtensions /// A lightweight way of describing tasks that are created from handling BSP /// requests or notifications for the purpose of dependency tracking. -package enum BuildServerMessageDependencyTracker: QueueBasedMessageHandlerDependencyTracker { +public enum BuildServerMessageDependencyTracker: QueueBasedMessageHandlerDependencyTracker { /// A task that modifies some state. It is a barrier for all requests that read state. case stateChange @@ -43,7 +42,7 @@ package enum BuildServerMessageDependencyTracker: QueueBasedMessageHandlerDepend } } - package init(_ notification: some NotificationType) { + public init(_ notification: some NotificationType) { switch notification { case is FileOptionsChangedNotification: self = .stateChange @@ -74,7 +73,7 @@ package enum BuildServerMessageDependencyTracker: QueueBasedMessageHandlerDepend } } - package init(_ request: some RequestType) { + public init(_ request: some RequestType) { switch request { case is BuildShutdownRequest: self = .stateChange diff --git a/Sources/LanguageServerProtocolTransport/CMakeLists.txt b/Sources/LanguageServerProtocolTransport/CMakeLists.txt new file mode 100644 index 000000000..834f034fe --- /dev/null +++ b/Sources/LanguageServerProtocolTransport/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(LanguageServerProtocolTransport STATIC + BuildServerMessageDependencyTracker.swift + DisableSigpipe.swift + JSONRPCConnection.swift + LocalConnection.swift + LoggableMessageTypes.swift + MessageCoding.swift + MessageSplitting.swift + QueueBasedMessageHandler.swift + RequestAndReply.swift + ResponseError+Init.swift) +set_target_properties(LanguageServerProtocolTransport PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) +target_link_libraries(LanguageServerProtocolTransport PRIVATE + LanguageServerProtocol + BuildServerProtocol + SKLogging) +target_link_libraries(LanguageServerProtocolTransport PRIVATE + $<$>:swiftDispatch> + $<$>:Foundation>) + +set_property(GLOBAL APPEND PROPERTY SWIFTTOOLSPROTOCOLS_EXPORTS LanguageServerProtocolTransport) diff --git a/Sources/LanguageServerProtocolJSONRPC/DisableSigpipe.swift b/Sources/LanguageServerProtocolTransport/DisableSigpipe.swift similarity index 100% rename from Sources/LanguageServerProtocolJSONRPC/DisableSigpipe.swift rename to Sources/LanguageServerProtocolTransport/DisableSigpipe.swift diff --git a/Sources/LanguageServerProtocolExtensions/DocumentURI+CustomLogStringConvertible.swift b/Sources/LanguageServerProtocolTransport/DocumentURI+CustomLogStringConvertible.swift similarity index 88% rename from Sources/LanguageServerProtocolExtensions/DocumentURI+CustomLogStringConvertible.swift rename to Sources/LanguageServerProtocolTransport/DocumentURI+CustomLogStringConvertible.swift index 4243812d8..7fb87cba2 100644 --- a/Sources/LanguageServerProtocolExtensions/DocumentURI+CustomLogStringConvertible.swift +++ b/Sources/LanguageServerProtocolTransport/DocumentURI+CustomLogStringConvertible.swift @@ -1,4 +1,3 @@ -//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // @@ -12,7 +11,7 @@ import Foundation import LanguageServerProtocol -import SKLogging +@_spi(SourceKitLSP) public import SKLogging // MARK: - DocumentURI diff --git a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift b/Sources/LanguageServerProtocolTransport/JSONRPCConnection.swift similarity index 99% rename from Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift rename to Sources/LanguageServerProtocolTransport/JSONRPCConnection.swift index c2c1c6965..0142ef105 100644 --- a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift +++ b/Sources/LanguageServerProtocolTransport/JSONRPCConnection.swift @@ -13,8 +13,8 @@ public import Dispatch public import Foundation public import LanguageServerProtocol -import SKLogging -import SwiftExtensions +@_spi(SourceKitLSP) import SKLogging +@_spi(SourceKitLSP) import SwiftExtensions #if canImport(Android) import Android diff --git a/Sources/LanguageServerProtocolExtensions/LocalConnection.swift b/Sources/LanguageServerProtocolTransport/LocalConnection.swift similarity index 89% rename from Sources/LanguageServerProtocolExtensions/LocalConnection.swift rename to Sources/LanguageServerProtocolTransport/LocalConnection.swift index 0c766bd2f..99a3726ea 100644 --- a/Sources/LanguageServerProtocolExtensions/LocalConnection.swift +++ b/Sources/LanguageServerProtocolTransport/LocalConnection.swift @@ -12,10 +12,9 @@ import Dispatch import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolJSONRPC -import SKLogging -import SwiftExtensions +public import LanguageServerProtocol +@_spi(SourceKitLSP) import SKLogging +@_spi(SourceKitLSP) import SwiftExtensions /// A connection between two message handlers in the same process. /// @@ -29,7 +28,7 @@ import SwiftExtensions /// conn.send(...) // handled by server /// conn.close() /// ``` -package final class LocalConnection: Connection, Sendable { +public final class LocalConnection: Connection, Sendable { private enum State { case ready, started, closed } @@ -48,11 +47,11 @@ package final class LocalConnection: Connection, Sendable { /// - Important: Must only be accessed from `queue` nonisolated(unsafe) private var handler: MessageHandler? = nil - package init(receiverName: String) { + public init(receiverName: String) { self.name = receiverName } - package convenience init(receiverName: String, handler: MessageHandler) { + public convenience init(receiverName: String, handler: MessageHandler) { self.init(receiverName: receiverName) self.start(handler: handler) } @@ -65,7 +64,7 @@ package final class LocalConnection: Connection, Sendable { } } - package func start(handler: MessageHandler) { + public func start(handler: MessageHandler) { queue.sync { precondition(state == .ready) state = .started @@ -81,7 +80,7 @@ package final class LocalConnection: Connection, Sendable { state = .closed } - package func close() { + public func close() { queue.sync { closeAssumingOnQueue() } @@ -91,7 +90,7 @@ package final class LocalConnection: Connection, Sendable { return .string("sk-\(_nextRequestID.fetchAndIncrement())") } - package func send(_ notification: Notification) { + public func send(_ notification: Notification) { logger.info( """ Sending notification to \(self.name, privacy: .public) @@ -104,7 +103,7 @@ package final class LocalConnection: Connection, Sendable { handler.handle(notification) } - package func send( + public func send( _ request: Request, id: RequestID, reply: @Sendable @escaping (LSPResult) -> Void diff --git a/Sources/LanguageServerProtocolJSONRPC/LoggableMessageTypes.swift b/Sources/LanguageServerProtocolTransport/LoggableMessageTypes.swift similarity index 60% rename from Sources/LanguageServerProtocolJSONRPC/LoggableMessageTypes.swift rename to Sources/LanguageServerProtocolTransport/LoggableMessageTypes.swift index 5da77bf20..e9986f5a1 100644 --- a/Sources/LanguageServerProtocolJSONRPC/LoggableMessageTypes.swift +++ b/Sources/LanguageServerProtocolTransport/LoggableMessageTypes.swift @@ -11,26 +11,26 @@ //===----------------------------------------------------------------------===// import Foundation -package import LanguageServerProtocol -package import SKLogging +public import LanguageServerProtocol +@_spi(SourceKitLSP) public import SKLogging // MARK: - RequestType -package struct AnyRequestType: CustomLogStringConvertible { +@_spi(SourceKitLSP) public struct AnyRequestType: CustomLogStringConvertible { let request: any RequestType - package init(request: any RequestType) { + @_spi(SourceKitLSP) public init(request: any RequestType) { self.request = request } - package var description: String { + @_spi(SourceKitLSP) public var description: String { return """ \(type(of: request).method) \(request.prettyPrintedJSON) """ } - package var redactedDescription: String { + @_spi(SourceKitLSP) public var redactedDescription: String { return """ \(type(of: request).method) \(request.prettyPrintedRedactedJSON) @@ -39,28 +39,28 @@ package struct AnyRequestType: CustomLogStringConvertible { } extension RequestType { - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { return AnyRequestType(request: self).forLogging } } // MARK: - NotificationType -package struct AnyNotificationType: CustomLogStringConvertible { +@_spi(SourceKitLSP) public struct AnyNotificationType: CustomLogStringConvertible { let notification: any NotificationType - package init(notification: any NotificationType) { + @_spi(SourceKitLSP) public init(notification: any NotificationType) { self.notification = notification } - package var description: String { + @_spi(SourceKitLSP) public var description: String { return """ \(type(of: notification).method) \(notification.prettyPrintedJSON) """ } - package var redactedDescription: String { + @_spi(SourceKitLSP) public var redactedDescription: String { return """ \(type(of: notification).method) \(notification.prettyPrintedRedactedJSON) @@ -69,28 +69,28 @@ package struct AnyNotificationType: CustomLogStringConvertible { } extension NotificationType { - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { return AnyNotificationType(notification: self).forLogging } } // MARK: - ResponseType -package struct AnyResponseType: CustomLogStringConvertible { +@_spi(SourceKitLSP) public struct AnyResponseType: CustomLogStringConvertible { let response: any ResponseType - package init(response: any ResponseType) { + @_spi(SourceKitLSP) public init(response: any ResponseType) { self.response = response } - package var description: String { + @_spi(SourceKitLSP) public var description: String { return """ \(type(of: response)) \(response.prettyPrintedJSON) """ } - package var redactedDescription: String { + @_spi(SourceKitLSP) public var redactedDescription: String { return """ \(type(of: response)) \(response.prettyPrintedRedactedJSON) @@ -99,7 +99,7 @@ package struct AnyResponseType: CustomLogStringConvertible { } extension ResponseType { - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { return AnyResponseType(response: self).forLogging } } diff --git a/Sources/LanguageServerProtocolJSONRPC/MessageCoding.swift b/Sources/LanguageServerProtocolTransport/MessageCoding.swift similarity index 100% rename from Sources/LanguageServerProtocolJSONRPC/MessageCoding.swift rename to Sources/LanguageServerProtocolTransport/MessageCoding.swift diff --git a/Sources/LanguageServerProtocolJSONRPC/MessageSplitting.swift b/Sources/LanguageServerProtocolTransport/MessageSplitting.swift similarity index 100% rename from Sources/LanguageServerProtocolJSONRPC/MessageSplitting.swift rename to Sources/LanguageServerProtocolTransport/MessageSplitting.swift diff --git a/Sources/LanguageServerProtocolExtensions/QueueBasedMessageHandler.swift b/Sources/LanguageServerProtocolTransport/QueueBasedMessageHandler.swift similarity index 93% rename from Sources/LanguageServerProtocolExtensions/QueueBasedMessageHandler.swift rename to Sources/LanguageServerProtocolTransport/QueueBasedMessageHandler.swift index 7fc5bc14c..1f4c4bb48 100644 --- a/Sources/LanguageServerProtocolExtensions/QueueBasedMessageHandler.swift +++ b/Sources/LanguageServerProtocolTransport/QueueBasedMessageHandler.swift @@ -11,17 +11,16 @@ //===----------------------------------------------------------------------===// import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolJSONRPC -import SKLogging -package import SwiftExtensions +public import LanguageServerProtocol +@_spi(SourceKitLSP) import SKLogging +@_spi(SourceKitLSP) public import SwiftExtensions /// Side structure in which `QueueBasedMessageHandler` can keep track of active requests etc. /// /// All of these could be requirements on `QueueBasedMessageHandler` but having them in a separate type means that /// types conforming to `QueueBasedMessageHandler` only have to have a single member and it also ensures that these /// fields are not accessible outside of the implementation of `QueueBasedMessageHandler`. -package actor QueueBasedMessageHandlerHelper { +public actor QueueBasedMessageHandlerHelper { /// The category in which signposts for message handling should be logged. fileprivate let signpostLoggingCategory: String @@ -48,7 +47,7 @@ package actor QueueBasedMessageHandlerHelper { /// just returned a response. private var recentlyFinishedRequests: [RequestID] = [] - package init(signpostLoggingCategory: String, createLoggingScope: Bool) { + public init(signpostLoggingCategory: String, createLoggingScope: Bool) { self.signpostLoggingCategory = signpostLoggingCategory self.createLoggingScope = createLoggingScope } @@ -57,7 +56,7 @@ package actor QueueBasedMessageHandlerHelper { /// /// Cancellation is performed automatically when a `$/cancelRequest` notification is received. This can be called to /// implicitly cancel requests based on some criteria. - package nonisolated func cancelRequest(id: RequestID) { + @_spi(SourceKitLSP) public nonisolated func cancelRequest(id: RequestID) { // Since the request is very cheap to execute and stops other requests // from performing more work, we execute it with a high priority. cancellationMessageHandlingQueue.async(priority: .high) { @@ -120,14 +119,14 @@ package actor QueueBasedMessageHandlerHelper { } } -package protocol QueueBasedMessageHandlerDependencyTracker: DependencyTracker { +public protocol QueueBasedMessageHandlerDependencyTracker: DependencyTracker { init(_ notification: some NotificationType) init(_ request: some RequestType) } /// A `MessageHandler` that handles all messages on an `AsyncQueue` and tracks dependencies between requests using /// `DependencyTracker`, ensuring that requests which depend on each other are not executed out-of-order. -package protocol QueueBasedMessageHandler: MessageHandler { +public protocol QueueBasedMessageHandler: MessageHandler { associatedtype DependencyTracker: QueueBasedMessageHandlerDependencyTracker /// The queue on which all messages (notifications, requests, responses) are @@ -165,10 +164,10 @@ package protocol QueueBasedMessageHandler: MessageHandler { } extension QueueBasedMessageHandler { - package func didReceive(notification: some NotificationType) {} - package func didReceive(request: some RequestType, id: RequestID) {} + public func didReceive(notification: some NotificationType) {} + public func didReceive(request: some RequestType, id: RequestID) {} - package func handle(_ notification: some NotificationType) { + public func handle(_ notification: some NotificationType) { messageHandlingHelper.withNotificationLoggingScopeIfNecessary { // Request cancellation needs to be able to overtake any other message we // are currently handling. Ordering is not important here. We thus don't @@ -199,7 +198,7 @@ extension QueueBasedMessageHandler { } } - package func handle( + public func handle( _ request: Request, id: RequestID, reply: @Sendable @escaping (LSPResult) -> Void diff --git a/Sources/LanguageServerProtocolExtensions/RequestAndReply.swift b/Sources/LanguageServerProtocolTransport/RequestAndReply.swift similarity index 77% rename from Sources/LanguageServerProtocolExtensions/RequestAndReply.swift rename to Sources/LanguageServerProtocolTransport/RequestAndReply.swift index 8140f6531..d4dc717ff 100644 --- a/Sources/LanguageServerProtocolExtensions/RequestAndReply.swift +++ b/Sources/LanguageServerProtocolTransport/RequestAndReply.swift @@ -10,18 +10,18 @@ // //===----------------------------------------------------------------------===// -package import LanguageServerProtocol -import SwiftExtensions +public import LanguageServerProtocol +@_spi(SourceKitLSP) import SwiftExtensions /// A request and a callback that returns the request's reply -package final class RequestAndReply: Sendable { - package let params: Params +public final class RequestAndReply: Sendable { + public let params: Params private let replyBlock: @Sendable (LSPResult) -> Void /// Whether a reply has been made. Every request must reply exactly once. private let replied: AtomicBool = AtomicBool(initialValue: false) - package init(_ request: Params, reply: @escaping @Sendable (LSPResult) -> Void) { + public init(_ request: Params, reply: @escaping @Sendable (LSPResult) -> Void) { self.params = request self.replyBlock = reply } @@ -31,7 +31,7 @@ package final class RequestAndReply: Sendable { } /// Call the `replyBlock` with the result produced by the given closure. - package func reply(_ body: @Sendable () async throws -> Params.Response) async { + public func reply(_ body: @Sendable () async throws -> Params.Response) async { precondition(!replied.value, "replied to request more than once") replied.value = true do { diff --git a/Sources/SourceKitLSP/TextEdit+IsNoop.swift b/Sources/LanguageServerProtocolTransport/ResponseError+Init.swift similarity index 60% rename from Sources/SourceKitLSP/TextEdit+IsNoop.swift rename to Sources/LanguageServerProtocolTransport/ResponseError+Init.swift index b5ac0ce4e..535a4d610 100644 --- a/Sources/SourceKitLSP/TextEdit+IsNoop.swift +++ b/Sources/LanguageServerProtocolTransport/ResponseError+Init.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -12,12 +12,15 @@ import LanguageServerProtocol -extension TextEdit { - /// Returns `true` the replaced text is the same as the new text - package func isNoOp(in snapshot: DocumentSnapshot) -> Bool { - if snapshot.text[snapshot.indexRange(of: range)] == newText { - return true +extension ResponseError { + package init(_ error: some Error) { + switch error { + case let error as ResponseError: + self = error + case is CancellationError: + self = .cancelled + default: + self = .unknown("Unknown error: \(error)") } - return false } } diff --git a/Sources/SKLogging/CMakeLists.txt b/Sources/SKLogging/CMakeLists.txt index 1c9b6a9f6..477ea120d 100644 --- a/Sources/SKLogging/CMakeLists.txt +++ b/Sources/SKLogging/CMakeLists.txt @@ -14,18 +14,18 @@ set_target_properties(SKLogging PROPERTIES target_link_libraries(SKLogging PRIVATE $<$>:Foundation>) target_link_libraries(SKLogging PUBLIC - SwiftExtensions - Crypto) + SwiftExtensions) add_library(SKLoggingForPlugin STATIC ${sources}) set_target_properties(SKLoggingForPlugin PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_compile_options(SKLoggingForPlugin PRIVATE $<$: - -DNO_CRYPTO_DEPENDENCY; "SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin" >) target_link_libraries(SKLoggingForPlugin PRIVATE $<$>:Foundation>) target_link_libraries(SKLoggingForPlugin PUBLIC SwiftExtensionsForPlugin) + +set_property(GLOBAL APPEND PROPERTY SWIFTTOOLSPROTOCOLS_EXPORTS SKLogging) diff --git a/Sources/SKLogging/CustomLogStringConvertible.swift b/Sources/SKLogging/CustomLogStringConvertible.swift index 573561606..c1f0861e9 100644 --- a/Sources/SKLogging/CustomLogStringConvertible.swift +++ b/Sources/SKLogging/CustomLogStringConvertible.swift @@ -12,13 +12,9 @@ public import Foundation -#if !NO_CRYPTO_DEPENDENCY -import Crypto -#endif - /// An object that can printed for logging and also offers a redacted description /// when logging in contexts in which private information shouldn't be captured. -package protocol CustomLogStringConvertible: CustomStringConvertible, Sendable { +public protocol CustomLogStringConvertible: CustomStringConvertible, Sendable { /// A full description of the object. var description: String { get } @@ -56,7 +52,7 @@ public final class CustomLogStringConvertibleWrapper: NSObject, Sendable { // We can't unconditionally mark it as @objc because eg. Linux doesn't have the Objective-C runtime. @objc #endif - package var redactedDescription: String { + @_spi(SourceKitLSP) public var redactedDescription: String { underlyingObject.redactedDescription } } @@ -65,7 +61,7 @@ extension CustomLogStringConvertible { /// Returns an object that can be passed to OSLog, which will print the /// `redactedDescription` if logging of private information is disabled and /// will log `description` otherwise. - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { return CustomLogStringConvertibleWrapper(self) } } @@ -73,13 +69,8 @@ extension CustomLogStringConvertible { extension String { /// A hash value that can be logged in a redacted description without /// disclosing any private information about the string. - package var hashForLogging: String { - #if NO_CRYPTO_DEPENDENCY + @_spi(SourceKitLSP) public var hashForLogging: String { return "" - #else - let hash = SHA256.hash(data: Data(self.utf8)).prefix(8).map { String(format: "%02x", $0) }.joined() - return "" - #endif } } @@ -96,13 +87,13 @@ private struct OptionalWrapper: CustomLogStringConvertible where Wrappe } extension Optional where Wrapped: CustomLogStringConvertible { - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { return CustomLogStringConvertibleWrapper(OptionalWrapper(optional: self)) } } extension Encodable { - package var prettyPrintedJSON: String { + @_spi(SourceKitLSP) public var prettyPrintedJSON: String { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] guard let data = try? encoder.encode(self) else { @@ -116,7 +107,7 @@ extension Encodable { return string.replacingOccurrences(of: "\\/", with: "/") } - package var prettyPrintedRedactedJSON: String { + @_spi(SourceKitLSP) public var prettyPrintedRedactedJSON: String { func redact(subject: Any) -> Any { if let subject = subject as? [String: Any] { return subject.mapValues { redact(subject: $0) } diff --git a/Sources/SKLogging/Error+ForLogging.swift b/Sources/SKLogging/Error+ForLogging.swift index 023536e4f..22ef2365c 100644 --- a/Sources/SKLogging/Error+ForLogging.swift +++ b/Sources/SKLogging/Error+ForLogging.swift @@ -33,7 +33,7 @@ private struct MaskedError: CustomLogStringConvertible { extension Error { /// A version of the error that can be used for logging and will only log the /// error code and a hash of the description in privacy-sensitive contexts. - package var forLogging: CustomLogStringConvertibleWrapper { + @_spi(SourceKitLSP) public var forLogging: CustomLogStringConvertibleWrapper { if let error = self as? CustomLogStringConvertible { return error.forLogging } else { diff --git a/Sources/SKLogging/Logging.swift b/Sources/SKLogging/Logging.swift index 14905aa21..c361080a1 100644 --- a/Sources/SKLogging/Logging.swift +++ b/Sources/SKLogging/Logging.swift @@ -15,9 +15,9 @@ import Foundation #if canImport(os) && !SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER @_exported public import os // os_log -package typealias LogLevel = os.OSLogType -package typealias Logger = os.Logger -package typealias Signposter = OSSignposter +@_spi(SourceKitLSP) public typealias LogLevel = os.OSLogType +public typealias Logger = os.Logger +@_spi(SourceKitLSP) public typealias Signposter = OSSignposter // -user-module-version of the 'os' module is 1062.100.1 in Xcode 16.3, which added the conformance of // `OSSignpostIntervalState` to `Sendable` @@ -32,17 +32,17 @@ extension OSSignpostIntervalState: @retroactive @unchecked Sendable {} #endif extension os.Logger { - package func makeSignposter() -> Signposter { + @_spi(SourceKitLSP) public func makeSignposter() -> Signposter { return OSSignposter(logger: self) } } #else -package typealias LogLevel = NonDarwinLogLevel -package typealias Logger = NonDarwinLogger -package typealias Signposter = NonDarwinSignposter +@_spi(SourceKitLSP) public typealias LogLevel = NonDarwinLogLevel +public typealias Logger = NonDarwinLogger +@_spi(SourceKitLSP) public typealias Signposter = NonDarwinSignposter #endif /// The logger that is used to log any messages. -package var logger: Logger { +@_spi(SourceKitLSP) public var logger: Logger { Logger(subsystem: LoggingScope.subsystem, category: LoggingScope.scope) } diff --git a/Sources/SKLogging/LoggingScope.swift b/Sources/SKLogging/LoggingScope.swift index c5484c850..dbcfb1d3c 100644 --- a/Sources/SKLogging/LoggingScope.swift +++ b/Sources/SKLogging/LoggingScope.swift @@ -11,8 +11,13 @@ //===----------------------------------------------------------------------===// import Foundation +@_spi(SourceKitLSP) import SwiftExtensions + +public final class LoggingScope { + + /// The name of the default logging subsystem if no task-local value is set. + fileprivate static let defaultSubsystem: ThreadSafeBox = .init(initialValue: nil) -package final class LoggingScope { /// The name of the current logging subsystem or `nil` if no logging scope is set. @TaskLocal fileprivate static var _subsystem: String? @@ -20,18 +25,24 @@ package final class LoggingScope { @TaskLocal fileprivate static var _scope: String? /// The name of the current logging subsystem. - package static var subsystem: String { - #if SKLOGGING_FOR_PLUGIN - return _subsystem ?? "org.swift.sourcekit-lsp.plugin" - #else - return _subsystem ?? "org.swift.sourcekit-lsp" - #endif + @_spi(SourceKitLSP) public static var subsystem: String { + if let _subsystem { + return _subsystem + } else if let defaultSubsystem = defaultSubsystem.value { + return defaultSubsystem + } else { + fatalError("SKLogging: default subsystem was not configured before first use") + } } /// The name of the current logging scope. - package static var scope: String { + @_spi(SourceKitLSP) public static var scope: String { return _scope ?? "default" } + + public static func configureDefaultLoggingSubsystem(_ subsystem: String) { + LoggingScope.defaultSubsystem.withLock { $0 = subsystem } + } } /// Logs all messages created from the operation to the given subsystem. @@ -40,7 +51,7 @@ package final class LoggingScope { /// /// - Note: Since this stores the logging subsystem in a task-local value, it only works when run inside a task. /// Outside a task, this is a no-op. -package func withLoggingSubsystemAndScope( +@_spi(SourceKitLSP) public func withLoggingSubsystemAndScope( subsystem: String, scope: String?, _ operation: @Sendable () throws -> Result @@ -51,7 +62,7 @@ package func withLoggingSubsystemAndScope( } /// Same as `withLoggingSubsystemAndScope` but allows the operation to be `async`. -package func withLoggingSubsystemAndScope( +@_spi(SourceKitLSP) public func withLoggingSubsystemAndScope( subsystem: String, scope: String?, @_inheritActorContext _ operation: @Sendable @concurrent () async throws -> Result @@ -69,7 +80,7 @@ package func withLoggingSubsystemAndScope( /// works when run inside a task. Outside a task, this is a no-op. /// - Warning: Be very careful with the dynamic creation of logging scopes. The logging scope is used as the os_log /// category, os_log only supports 4000 different loggers and thus at most 4000 different scopes must be used. -package func withLoggingScope( +@_spi(SourceKitLSP) public func withLoggingScope( _ scope: String, _ operation: () throws -> Result ) rethrows -> Result { @@ -82,7 +93,7 @@ package func withLoggingScope( /// Same as `withLoggingScope` but allows the operation to be `async`. /// /// - SeeAlso: ``withLoggingScope(_:_:)-6qtga`` -package func withLoggingScope( +@_spi(SourceKitLSP) public func withLoggingScope( _ scope: String, @_inheritActorContext _ operation: @Sendable @concurrent () async throws -> Result ) async rethrows -> Result { diff --git a/Sources/SKLogging/NonDarwinLogging.swift b/Sources/SKLogging/NonDarwinLogging.swift index 5b57f350f..bed3ff50c 100644 --- a/Sources/SKLogging/NonDarwinLogging.swift +++ b/Sources/SKLogging/NonDarwinLogging.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -package import SwiftExtensions +@_spi(SourceKitLSP) public import SwiftExtensions #if canImport(Darwin) import Foundation @@ -21,9 +21,9 @@ import Foundation // MARK: - Log settings -package enum LogConfig { +@_spi(SourceKitLSP) public enum LogConfig { /// The globally set log level - package static let logLevel = ThreadSafeBox( + @_spi(SourceKitLSP) public static let logLevel = ThreadSafeBox( initialValue: { if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_LEVEL"], let logLevel = NonDarwinLogLevel(envVar) @@ -39,7 +39,7 @@ package enum LogConfig { ) /// The globally set privacy level - package static let privacyLevel = ThreadSafeBox( + @_spi(SourceKitLSP) public static let privacyLevel = ThreadSafeBox( initialValue: { if let envVar = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_LOG_PRIVACY_LEVEL"], let privacyLevel = NonDarwinLogPrivacy(envVar) @@ -62,14 +62,14 @@ package enum LogConfig { /// /// For documentation of the different log levels see /// https://developer.apple.com/documentation/os/oslogtype. -package enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable { +@_spi(SourceKitLSP) public enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable { case debug case info case `default` case error case fault - package init?(_ value: String) { + @_spi(SourceKitLSP) public init?(_ value: String) { switch value.lowercased() { case "debug": self = .debug case "info": self = .info @@ -85,7 +85,7 @@ package enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable { } } - package init?(_ value: Int) { + @_spi(SourceKitLSP) public init?(_ value: Int) { switch value { case 0: self = .fault case 1: self = .error @@ -96,7 +96,7 @@ package enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable { } } - package var description: String { + @_spi(SourceKitLSP) public var description: String { switch self { case .debug: return "debug" @@ -119,12 +119,12 @@ package enum NonDarwinLogLevel: Comparable, CustomStringConvertible, Sendable { /// /// For documentation of the different privacy levels see /// https://developer.apple.com/documentation/os/oslogprivacy. -package enum NonDarwinLogPrivacy: Comparable, Sendable { +@_spi(SourceKitLSP) public enum NonDarwinLogPrivacy: Comparable, Sendable { case `public` case `private` case sensitive - package init?(_ value: String) { + @_spi(SourceKitLSP) public init?(_ value: String) { switch value.lowercased() { case "sensitive": self = .sensitive case "private": self = .private @@ -140,7 +140,7 @@ package enum NonDarwinLogPrivacy: Comparable, Sendable { /// sourcekit-lsp. /// /// This is used on platforms that don't have OSLog. -package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable { +@_spi(SourceKitLSP) public struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable { private enum LogPiece: Sendable { /// A segment of the log message that will always be displayed. case string(String) @@ -156,12 +156,12 @@ package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable private var pieces: [LogPiece] - package init(literalCapacity: Int, interpolationCount: Int) { + @_spi(SourceKitLSP) public init(literalCapacity: Int, interpolationCount: Int) { self.pieces = [] pieces.reserveCapacity(literalCapacity + interpolationCount) } - package mutating func appendLiteral(_ literal: String) { + @_spi(SourceKitLSP) public mutating func appendLiteral(_ literal: String) { pieces.append(.string(literal)) } @@ -184,26 +184,26 @@ package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable } } - package mutating func appendInterpolation(_ message: StaticString, privacy: NonDarwinLogPrivacy = .public) { + @_spi(SourceKitLSP) public mutating func appendInterpolation(_ message: StaticString, privacy: NonDarwinLogPrivacy = .public) { append(description: message.description, redactedDescription: "", privacy: privacy) } @_disfavoredOverload // Prefer to use the StaticString overload when possible. - package mutating func appendInterpolation( + @_spi(SourceKitLSP) public mutating func appendInterpolation( _ message: some CustomStringConvertible & Sendable, privacy: NonDarwinLogPrivacy = .private ) { append(description: message.description, redactedDescription: "", privacy: privacy) } - package mutating func appendInterpolation( + @_spi(SourceKitLSP) public mutating func appendInterpolation( _ message: some CustomLogStringConvertibleWrapper & Sendable, privacy: NonDarwinLogPrivacy = .private ) { append(description: message.description, redactedDescription: message.redactedDescription, privacy: privacy) } - package mutating func appendInterpolation( + @_spi(SourceKitLSP) public mutating func appendInterpolation( _ message: (some CustomLogStringConvertibleWrapper & Sendable)?, privacy: NonDarwinLogPrivacy = .private ) { @@ -214,18 +214,18 @@ package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable } } - package mutating func appendInterpolation(_ type: Any.Type, privacy: NonDarwinLogPrivacy = .public) { + @_spi(SourceKitLSP) public mutating func appendInterpolation(_ type: Any.Type, privacy: NonDarwinLogPrivacy = .public) { append(description: String(reflecting: type), redactedDescription: "", privacy: privacy) } - package mutating func appendInterpolation( + @_spi(SourceKitLSP) public mutating func appendInterpolation( _ message: some Numeric & Sendable, privacy: NonDarwinLogPrivacy = .public ) { append(description: String(describing: message), redactedDescription: "", privacy: privacy) } - package mutating func appendInterpolation(_ message: Bool, privacy: NonDarwinLogPrivacy = .public) { + @_spi(SourceKitLSP) public mutating func appendInterpolation(_ message: Bool, privacy: NonDarwinLogPrivacy = .public) { append(description: message.description, redactedDescription: "", privacy: privacy) } @@ -253,14 +253,14 @@ package struct NonDarwinLogInterpolation: StringInterpolationProtocol, Sendable /// sourcekit-lsp. /// /// This is used on platforms that don't have OSLog. -package struct NonDarwinLogMessage: ExpressibleByStringInterpolation, ExpressibleByStringLiteral, Sendable { +@_spi(SourceKitLSP) public struct NonDarwinLogMessage: ExpressibleByStringInterpolation, ExpressibleByStringLiteral, Sendable { fileprivate let value: NonDarwinLogInterpolation - package init(stringInterpolation: NonDarwinLogInterpolation) { + @_spi(SourceKitLSP) public init(stringInterpolation: NonDarwinLogInterpolation) { self.value = stringInterpolation } - package init(stringLiteral value: String) { + @_spi(SourceKitLSP) public init(stringLiteral value: String) { var interpolation = NonDarwinLogInterpolation(literalCapacity: 1, interpolationCount: 0) interpolation.appendLiteral(value) self.value = interpolation @@ -302,7 +302,7 @@ private let loggingQueue = AsyncQueue() /// not available. /// /// `overrideLogHandler` allows capturing of the logged messages for testing purposes. -package struct NonDarwinLogger: Sendable { +@_spi(SourceKitLSP) public struct NonDarwinLogger: Sendable { private let subsystem: String private let category: String private let logLevel: NonDarwinLogLevel @@ -317,7 +317,7 @@ package struct NonDarwinLogger: Sendable { /// - privacyLevel: The privacy level to log at. Any interpolation segments /// with a higher privacy level will be masked. /// - logHandler: The function that actually logs the message. - package init( + @_spi(SourceKitLSP) public init( subsystem: String, category: String, logLevel: NonDarwinLogLevel? = nil, @@ -335,7 +335,7 @@ package struct NonDarwinLogger: Sendable { /// /// Logging is performed asynchronously to allow the execution of the main /// program to finish as quickly as possible. - package func log( + @_spi(SourceKitLSP) public func log( level: NonDarwinLogLevel, _ message: @autoclosure @escaping @Sendable () -> NonDarwinLogMessage ) { @@ -363,27 +363,27 @@ package struct NonDarwinLogger: Sendable { } /// Log a message at the `debug` level. - package func debug(_ message: NonDarwinLogMessage) { + @_spi(SourceKitLSP) public func debug(_ message: NonDarwinLogMessage) { log(level: .debug, message) } /// Log a message at the `info` level. - package func info(_ message: NonDarwinLogMessage) { + @_spi(SourceKitLSP) public func info(_ message: NonDarwinLogMessage) { log(level: .info, message) } /// Log a message at the `default` level. - package func log(_ message: NonDarwinLogMessage) { + @_spi(SourceKitLSP) public func log(_ message: NonDarwinLogMessage) { log(level: .default, message) } /// Log a message at the `error` level. - package func error(_ message: NonDarwinLogMessage) { + @_spi(SourceKitLSP) public func error(_ message: NonDarwinLogMessage) { log(level: .error, message) } /// Log a message at the `fault` level. - package func fault(_ message: NonDarwinLogMessage) { + @_spi(SourceKitLSP) public func fault(_ message: NonDarwinLogMessage) { log(level: .fault, message) } @@ -391,22 +391,22 @@ package struct NonDarwinLogger: Sendable { /// /// Useful for testing to make sure all asynchronous log calls have actually /// written their data. - package static func flush() async { + @_spi(SourceKitLSP) public static func flush() async { await loggingQueue.async {}.value } - package func makeSignposter() -> NonDarwinSignposter { + @_spi(SourceKitLSP) public func makeSignposter() -> NonDarwinSignposter { return NonDarwinSignposter(logger: self) } } // MARK: - Signposter -package struct NonDarwinSignpostID: Sendable { +@_spi(SourceKitLSP) public struct NonDarwinSignpostID: Sendable { fileprivate let id: UInt32 } -package struct NonDarwinSignpostIntervalState: Sendable { +@_spi(SourceKitLSP) public struct NonDarwinSignpostIntervalState: Sendable { fileprivate let id: NonDarwinSignpostID } @@ -415,18 +415,18 @@ private let nextSignpostID = AtomicUInt32(initialValue: 0) /// A type that is API-compatible to `OSLogMessage` for all uses within sourcekit-lsp. /// /// Since non-Darwin platforms don't have signposts, the type just has no-op operations. -package struct NonDarwinSignposter: Sendable { +@_spi(SourceKitLSP) public struct NonDarwinSignposter: Sendable { private let logger: NonDarwinLogger fileprivate init(logger: NonDarwinLogger) { self.logger = logger } - package func makeSignpostID() -> NonDarwinSignpostID { + @_spi(SourceKitLSP) public func makeSignpostID() -> NonDarwinSignpostID { return NonDarwinSignpostID(id: nextSignpostID.fetchAndIncrement()) } - package func beginInterval( + @_spi(SourceKitLSP) public func beginInterval( _ name: StaticString, id: NonDarwinSignpostID, _ message: NonDarwinLogMessage @@ -435,11 +435,11 @@ package struct NonDarwinSignposter: Sendable { return NonDarwinSignpostIntervalState(id: id) } - package func emitEvent(_ name: StaticString, id: NonDarwinSignpostID, _ message: NonDarwinLogMessage = "") { + @_spi(SourceKitLSP) public func emitEvent(_ name: StaticString, id: NonDarwinSignpostID, _ message: NonDarwinLogMessage = "") { logger.log(level: .debug, "Signpost \(id.id) event: \(name) - \(message.value.string(for: logger.privacyLevel))") } - package func endInterval( + @_spi(SourceKitLSP) public func endInterval( _ name: StaticString, _ state: NonDarwinSignpostIntervalState, _ message: StaticString = "" diff --git a/Sources/SKLogging/OrLog.swift b/Sources/SKLogging/OrLog.swift index d8dbdf17e..9c012a096 100644 --- a/Sources/SKLogging/OrLog.swift +++ b/Sources/SKLogging/OrLog.swift @@ -22,7 +22,7 @@ private func logError(prefix: String, error: Error, level: LogLevel = .error) { } /// Like `try?`, but logs the error on failure. -package func orLog( +@_spi(SourceKitLSP) public func orLog( _ prefix: @autoclosure () -> String, level: LogLevel = .error, _ block: () throws -> R? @@ -38,7 +38,7 @@ package func orLog( /// Like ``orLog(_:level:_:)-66i2z`` but allows execution of an `async` body. /// /// - SeeAlso: ``orLog(_:level:_:)-66i2z`` -package func orLog( +@_spi(SourceKitLSP) public func orLog( _ prefix: @autoclosure () -> String, level: LogLevel = .error, _ block: () async throws -> R? diff --git a/Sources/SKLogging/SetGlobalLogFileHandler.swift b/Sources/SKLogging/SetGlobalLogFileHandler.swift index 2b2ea8a87..a161fce27 100644 --- a/Sources/SKLogging/SetGlobalLogFileHandler.swift +++ b/Sources/SKLogging/SetGlobalLogFileHandler.swift @@ -14,7 +14,7 @@ import RegexBuilder import SwiftExtensions #if canImport(Darwin) -package import Foundation +public import Foundation #else // TODO: @preconcurrency needed because stderr is not sendable on Linux https://github.com/swiftlang/swift/issues/75601 @preconcurrency package import Foundation @@ -179,7 +179,7 @@ private func cleanOldLogFilesImpl(logFileDirectory: URL, maxAge: TimeInterval) { /// when it does. /// /// No-op when using OSLog. -package func setUpGlobalLogFileHandler(logFileDirectory: URL, logFileMaxBytes: Int, logRotateCount: Int) async { +@_spi(SourceKitLSP) public func setUpGlobalLogFileHandler(logFileDirectory: URL, logFileMaxBytes: Int, logRotateCount: Int) async { #if !canImport(os) || SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER await setUpGlobalLogFileHandlerImpl( logFileDirectory: logFileDirectory, @@ -193,7 +193,7 @@ package func setUpGlobalLogFileHandler(logFileDirectory: URL, logFileMaxBytes: I /// haven't been modified within the last hour. /// /// No-op when using OSLog. -package func cleanOldLogFiles(logFileDirectory: URL, maxAge: TimeInterval) { +@_spi(SourceKitLSP) public func cleanOldLogFiles(logFileDirectory: URL, maxAge: TimeInterval) { #if !canImport(os) || SOURCEKIT_LSP_FORCE_NON_DARWIN_LOGGER cleanOldLogFilesImpl(logFileDirectory: logFileDirectory, maxAge: maxAge) #endif diff --git a/Sources/SKLogging/SplitLogMessage.swift b/Sources/SKLogging/SplitLogMessage.swift index e6bd5e5c9..64316685a 100644 --- a/Sources/SKLogging/SplitLogMessage.swift +++ b/Sources/SKLogging/SplitLogMessage.swift @@ -17,7 +17,7 @@ /// /// - Note: This will only split along newline boundary. If a single line is longer than `maxChunkSize`, it won't be /// split. This is fine for compiler argument splitting since a single argument is rarely longer than 800 characters. -package func splitLongMultilineMessage(message: String) -> [String] { +@_spi(SourceKitLSP) public func splitLongMultilineMessage(message: String) -> [String] { let maxChunkSize = 800 var chunks: [String] = [] for line in message.split(separator: "\n", omittingEmptySubsequences: false) { @@ -43,7 +43,7 @@ extension Logger { var redactedDescription: String } - package func logFullObjectInMultipleLogMessages( + @_spi(SourceKitLSP) public func logFullObjectInMultipleLogMessages( level: LogLevel = .default, header: StaticString, _ subject: some CustomLogStringConvertible diff --git a/Sources/SKOptions/BuildConfiguration.swift b/Sources/SKOptions/BuildConfiguration.swift deleted file mode 100644 index b58a94781..000000000 --- a/Sources/SKOptions/BuildConfiguration.swift +++ /dev/null @@ -1,16 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public enum BuildConfiguration: String, Codable, Sendable { - case debug - case release -} diff --git a/Sources/SKOptions/CMakeLists.txt b/Sources/SKOptions/CMakeLists.txt deleted file mode 100644 index 1679c3e66..000000000 --- a/Sources/SKOptions/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ - -add_library(SKOptions STATIC - BuildConfiguration.swift - ExperimentalFeatures.swift - SourceKitLSPOptions.swift - WorkspaceType.swift) -set_target_properties(SKOptions PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(SKOptions PUBLIC - LanguageServerProtocol - LanguageServerProtocolExtensions - SKLogging - TSCBasic) diff --git a/Sources/SKOptions/ExperimentalFeatures.swift b/Sources/SKOptions/ExperimentalFeatures.swift deleted file mode 100644 index 455f5b516..000000000 --- a/Sources/SKOptions/ExperimentalFeatures.swift +++ /dev/null @@ -1,79 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// An experimental feature that can be enabled by passing `--experimental-feature` -/// to `sourcekit-lsp` on the command line or through the configuration file. -/// The raw value of this feature is how it is named on the command line and in the configuration file. -public enum ExperimentalFeature: String, Codable, Sendable, CaseIterable { - /// Enable support for the `textDocument/onTypeFormatting` request. - case onTypeFormatting = "on-type-formatting" - - /// Enable support for the `workspace/_setOptions` request. - /// - /// - Note: Internal option - case setOptionsRequest = "set-options-request" - - /// Enable the `workspace/_sourceKitOptions` request. - /// - /// - Note: Internal option - case sourceKitOptionsRequest = "sourcekit-options-request" - - /// Enable the `sourceKit/_isIndexing` request. - /// - /// - Note: Internal option - case isIndexingRequest = "is-indexing-request" - - /// Indicate that the client can handle the experimental `structure` field in the `window/logMessage` notification. - case structuredLogs = "structured-logs" - - /// Enable the `workspace/_outputPaths` request. - /// - /// - Note: Internal option - case outputPathsRequest = "output-paths-request" - - /// Enable the `buildServerUpdates` option in the `workspace/synchronize` request. - /// - /// - Note: Internal option, for testing only - case synchronizeForBuildSystemUpdates = "synchronize-for-build-system-updates" - - /// Enable the `copyFileMap` option in the `workspace/synchronize` request. - /// - /// - Note: Internal option, for testing only - case synchronizeCopyFileMap = "synchronize-copy-file-map" - - /// All non-internal experimental features. - public static var allNonInternalCases: [ExperimentalFeature] { - allCases.filter { !$0.isInternal } - } - - /// Whether the feature is internal. - var isInternal: Bool { - switch self { - case .onTypeFormatting: - return false - case .setOptionsRequest: - return true - case .sourceKitOptionsRequest: - return true - case .isIndexingRequest: - return true - case .structuredLogs: - return false - case .outputPathsRequest: - return true - case .synchronizeForBuildSystemUpdates: - return true - case .synchronizeCopyFileMap: - return true - } - } -} diff --git a/Sources/SKOptions/SourceKitLSPOptions.swift b/Sources/SKOptions/SourceKitLSPOptions.swift deleted file mode 100644 index 4a7897118..000000000 --- a/Sources/SKOptions/SourceKitLSPOptions.swift +++ /dev/null @@ -1,608 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public import Foundation -public import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging - -import struct TSCBasic.AbsolutePath - -/// Options that can be used to modify SourceKit-LSP's behavior. -/// -/// See `ConfigurationFile.md` for a description of the configuration file's behavior. -public struct SourceKitLSPOptions: Sendable, Codable, Equatable { - public struct SwiftPMOptions: Sendable, Codable, Equatable { - /// The configuration to build the project for during background indexing - /// and the configuration whose build folder should be used for Swift - /// modules if background indexing is disabled. - /// - /// Equivalent to SwiftPM's `--configuration` option. - public var configuration: BuildConfiguration? - - /// Build artifacts directory path. If nil, the build system may choose a default value. - /// - /// This path can be specified as a relative path, which will be interpreted relative to the project root. - /// Equivalent to SwiftPM's `--scratch-path` option. - public var scratchPath: String? - - /// Equivalent to SwiftPM's `--swift-sdks-path` option. - public var swiftSDKsDirectory: String? - - /// Equivalent to SwiftPM's `--swift-sdk` option. - public var swiftSDK: String? - - /// Equivalent to SwiftPM's `--triple` option. - public var triple: String? - - /// Equivalent to SwiftPM's `--toolset` option. - public var toolsets: [String]? - - /// Traits to enable for the package. Equivalent to SwiftPM's `--traits` option. - public var traits: [String]? - - /// Extra arguments passed to the compiler for C files. Equivalent to SwiftPM's `-Xcc` option. - public var cCompilerFlags: [String]? - - /// Extra arguments passed to the compiler for C++ files. Equivalent to SwiftPM's `-Xcxx` option. - public var cxxCompilerFlags: [String]? - - /// Extra arguments passed to the compiler for Swift files. Equivalent to SwiftPM's `-Xswiftc` option. - public var swiftCompilerFlags: [String]? - - /// Extra arguments passed to the linker. Equivalent to SwiftPM's `-Xlinker` option. - public var linkerFlags: [String]? - - /// Extra arguments passed to the compiler for Swift files or plugins. Equivalent to SwiftPM's - /// `-Xbuild-tools-swiftc` option. - public var buildToolsSwiftCompilerFlags: [String]? - - /// Disables running subprocesses from SwiftPM in a sandbox. Equivalent to SwiftPM's `--disable-sandbox` option. - /// Useful when running `sourcekit-lsp` in a sandbox because nested sandboxes are not supported. - public var disableSandbox: Bool? - - /// Whether to skip building and running plugins when creating the in-memory build graph. - /// - /// - Note: Internal option, only exists as an escape hatch in case this causes unintentional interactions with - /// background indexing. - public var skipPlugins: Bool? - - public init( - configuration: BuildConfiguration? = nil, - scratchPath: String? = nil, - swiftSDKsDirectory: String? = nil, - swiftSDK: String? = nil, - triple: String? = nil, - toolsets: [String]? = nil, - traits: [String]? = nil, - cCompilerFlags: [String]? = nil, - cxxCompilerFlags: [String]? = nil, - swiftCompilerFlags: [String]? = nil, - linkerFlags: [String]? = nil, - buildToolsSwiftCompilerFlags: [String]? = nil, - disableSandbox: Bool? = nil, - skipPlugins: Bool? = nil - ) { - self.configuration = configuration - self.scratchPath = scratchPath - self.swiftSDKsDirectory = swiftSDKsDirectory - self.swiftSDK = swiftSDK - self.triple = triple - self.toolsets = toolsets - self.traits = traits - self.cCompilerFlags = cCompilerFlags - self.cxxCompilerFlags = cxxCompilerFlags - self.swiftCompilerFlags = swiftCompilerFlags - self.linkerFlags = linkerFlags - self.buildToolsSwiftCompilerFlags = buildToolsSwiftCompilerFlags - self.disableSandbox = disableSandbox - } - - static func merging(base: SwiftPMOptions, override: SwiftPMOptions?) -> SwiftPMOptions { - return SwiftPMOptions( - configuration: override?.configuration ?? base.configuration, - scratchPath: override?.scratchPath ?? base.scratchPath, - swiftSDKsDirectory: override?.swiftSDKsDirectory ?? base.swiftSDKsDirectory, - swiftSDK: override?.swiftSDK ?? base.swiftSDK, - triple: override?.triple ?? base.triple, - toolsets: override?.toolsets ?? base.toolsets, - traits: override?.traits ?? base.traits, - cCompilerFlags: override?.cCompilerFlags ?? base.cCompilerFlags, - cxxCompilerFlags: override?.cxxCompilerFlags ?? base.cxxCompilerFlags, - swiftCompilerFlags: override?.swiftCompilerFlags ?? base.swiftCompilerFlags, - linkerFlags: override?.linkerFlags ?? base.linkerFlags, - buildToolsSwiftCompilerFlags: override?.buildToolsSwiftCompilerFlags ?? base.buildToolsSwiftCompilerFlags, - disableSandbox: override?.disableSandbox ?? base.disableSandbox, - skipPlugins: override?.skipPlugins ?? base.skipPlugins - ) - } - } - - public struct CompilationDatabaseOptions: Sendable, Codable, Equatable { - /// Additional paths to search for a compilation database, relative to a workspace root. - public var searchPaths: [String]? - - public init(searchPaths: [String]? = nil) { - self.searchPaths = searchPaths - } - - static func merging( - base: CompilationDatabaseOptions, - override: CompilationDatabaseOptions? - ) -> CompilationDatabaseOptions { - return CompilationDatabaseOptions(searchPaths: override?.searchPaths ?? base.searchPaths) - } - } - - public struct FallbackBuildSystemOptions: Sendable, Codable, Equatable { - /// Extra arguments passed to the compiler for C files. - public var cCompilerFlags: [String]? - /// Extra arguments passed to the compiler for C++ files. - public var cxxCompilerFlags: [String]? - /// Extra arguments passed to the compiler for Swift files. - public var swiftCompilerFlags: [String]? - /// The SDK to use for fallback arguments. Default is to infer the SDK using `xcrun`. - public var sdk: String? - - public init( - cCompilerFlags: [String]? = nil, - cxxCompilerFlags: [String]? = nil, - swiftCompilerFlags: [String]? = nil, - sdk: String? = nil - ) { - self.cCompilerFlags = cCompilerFlags - self.cxxCompilerFlags = cxxCompilerFlags - self.swiftCompilerFlags = swiftCompilerFlags - self.sdk = sdk - } - - static func merging( - base: FallbackBuildSystemOptions, - override: FallbackBuildSystemOptions? - ) -> FallbackBuildSystemOptions { - return FallbackBuildSystemOptions( - cCompilerFlags: override?.cCompilerFlags ?? base.cCompilerFlags, - cxxCompilerFlags: override?.cxxCompilerFlags ?? base.cxxCompilerFlags, - swiftCompilerFlags: override?.swiftCompilerFlags ?? base.swiftCompilerFlags, - sdk: override?.sdk ?? base.sdk - ) - } - } - - public struct IndexOptions: Sendable, Codable, Equatable { - /// Path remappings for remapping index data for local use. - public var indexPrefixMap: [String: String]? - /// A hint indicating how many cores background indexing should use at most (value between 0 and 1). Background indexing is not required to honor this setting. - /// - /// - Note: Internal option, may not work as intended - public var maxCoresPercentageToUseForBackgroundIndexing: Double? - /// Number of seconds to wait for an update index store task to finish before terminating it. - public var updateIndexStoreTimeout: Int? - - public var maxCoresPercentageToUseForBackgroundIndexingOrDefault: Double { - return maxCoresPercentageToUseForBackgroundIndexing ?? 1 - } - - public var updateIndexStoreTimeoutOrDefault: Duration { - if let updateIndexStoreTimeout { - .seconds(updateIndexStoreTimeout) - } else { - .seconds(120) - } - } - - public init( - indexPrefixMap: [String: String]? = nil, - maxCoresPercentageToUseForBackgroundIndexing: Double? = nil, - updateIndexStoreTimeout: Int? = nil - ) { - self.indexPrefixMap = indexPrefixMap - self.maxCoresPercentageToUseForBackgroundIndexing = maxCoresPercentageToUseForBackgroundIndexing - self.updateIndexStoreTimeout = updateIndexStoreTimeout - } - - static func merging(base: IndexOptions, override: IndexOptions?) -> IndexOptions { - return IndexOptions( - indexPrefixMap: override?.indexPrefixMap ?? base.indexPrefixMap, - maxCoresPercentageToUseForBackgroundIndexing: override?.maxCoresPercentageToUseForBackgroundIndexing - ?? base.maxCoresPercentageToUseForBackgroundIndexing, - updateIndexStoreTimeout: override?.updateIndexStoreTimeout ?? base.updateIndexStoreTimeout - ) - } - } - - public struct LoggingOptions: Sendable, Codable, Equatable { - /// The level from which one onwards log messages should be written. - public var level: String? - - /// Whether potentially sensitive information should be redacted. - /// Default is `public`, which redacts potentially sensitive information. - public var privacyLevel: String? - - /// Write all input received by SourceKit-LSP on stdin to a file in this directory. - /// - /// Useful to record and replay an entire SourceKit-LSP session. - public var inputMirrorDirectory: String? - - /// Write all data sent from SourceKit-LSP to the client to a file in this directory. - /// - /// Useful to record the raw communication between SourceKit-LSP and the client on a low level. - public var outputMirrorDirectory: String? - - public init( - level: String? = nil, - privacyLevel: String? = nil, - inputMirrorDirectory: String? = nil, - outputMirrorDirectory: String? = nil - ) { - self.level = level - self.privacyLevel = privacyLevel - self.inputMirrorDirectory = inputMirrorDirectory - self.outputMirrorDirectory = outputMirrorDirectory - } - - static func merging(base: LoggingOptions, override: LoggingOptions?) -> LoggingOptions { - return LoggingOptions( - level: override?.level ?? base.level, - privacyLevel: override?.privacyLevel ?? base.privacyLevel, - inputMirrorDirectory: override?.inputMirrorDirectory ?? base.inputMirrorDirectory, - outputMirrorDirectory: override?.outputMirrorDirectory ?? base.outputMirrorDirectory - ) - } - } - - public struct SourceKitDOptions: Sendable, Codable, Equatable { - /// When set, load the SourceKit client plugin from this path instead of locating it inside the toolchain. - /// - /// - Note: Internal option, only to be used while running SourceKit-LSP tests - public var clientPlugin: String? - - /// When set, load the SourceKit service plugin from this path instead of locating it inside the toolchain. - /// - /// - Note: Internal option, only to be used while running SourceKit-LSP tests - public var servicePlugin: String? - - public init(clientPlugin: String? = nil, servicePlugin: String? = nil) { - self.clientPlugin = clientPlugin - self.servicePlugin = servicePlugin - } - - static func merging(base: SourceKitDOptions, override: SourceKitDOptions?) -> SourceKitDOptions { - return SourceKitDOptions( - clientPlugin: override?.clientPlugin ?? base.clientPlugin, - servicePlugin: override?.servicePlugin ?? base.servicePlugin - ) - } - } - - public enum BackgroundPreparationMode: String, Sendable, Codable, Equatable { - /// Build a target to prepare it. - case build - - /// Prepare a target without generating object files but do not do lazy type checking and function body skipping. - /// - /// This uses SwiftPM's `--experimental-prepare-for-indexing-no-lazy` flag. - case noLazy - - /// Prepare a target without generating object files. - case enabled - } - - /// Options for SwiftPM workspaces. - private var swiftPM: SwiftPMOptions? - public var swiftPMOrDefault: SwiftPMOptions { - get { swiftPM ?? .init() } - set { swiftPM = newValue } - } - - /// Dictionary with the following keys, defining options for workspaces with a compilation database. - private var compilationDatabase: CompilationDatabaseOptions? - public var compilationDatabaseOrDefault: CompilationDatabaseOptions { - get { compilationDatabase ?? .init() } - set { compilationDatabase = newValue } - } - - /// Dictionary with the following keys, defining options for files that aren't managed by any build server. - private var fallbackBuildSystem: FallbackBuildSystemOptions? - public var fallbackBuildSystemOrDefault: FallbackBuildSystemOptions { - get { fallbackBuildSystem ?? .init() } - set { fallbackBuildSystem = newValue } - } - - /// Number of milliseconds to wait for build settings from the build server before using fallback build settings. - public var buildSettingsTimeout: Int? - public var buildSettingsTimeoutOrDefault: Duration { - // The default timeout of 500ms was chosen arbitrarily without any measurements. - .milliseconds(buildSettingsTimeout ?? 500) - } - - /// Extra command line arguments passed to `clangd` when launching it. - public var clangdOptions: [String]? - - /// Options related to indexing. - private var index: IndexOptions? - public var indexOrDefault: IndexOptions { - get { index ?? .init() } - set { index = newValue } - } - - /// Options related to logging, changing SourceKit-LSP’s logging behavior on non-Apple platforms. - /// - /// On Apple platforms, logging is done through the [system log](Diagnose%20Bundle.md#Enable%20Extended%20Logging). - /// These options can only be set globally and not per workspace. - private var logging: LoggingOptions? - public var loggingOrDefault: LoggingOptions { - get { logging ?? .init() } - set { logging = newValue } - } - - /// Options modifying the behavior of sourcekitd. - private var sourcekitd: SourceKitDOptions? - public var sourcekitdOrDefault: SourceKitDOptions { - get { sourcekitd ?? .init() } - set { sourcekitd = newValue } - } - - /// Default workspace type. Overrides workspace type selection logic. - public var defaultWorkspaceType: WorkspaceType? - /// Directory in which generated interfaces and macro expansions should be stored. - public var generatedFilesPath: String? - - /// Whether background indexing is enabled. - public var backgroundIndexing: Bool? - - public var backgroundIndexingOrDefault: Bool { - return backgroundIndexing ?? true - } - - /// Determines how background indexing should prepare a target. - public var backgroundPreparationMode: BackgroundPreparationMode? - - public var backgroundPreparationModeOrDefault: BackgroundPreparationMode { - return backgroundPreparationMode ?? .enabled - } - - /// Whether sending a `textDocument/didChange` or `textDocument/didClose` notification for a document should cancel - /// all pending requests for that document. - public var cancelTextDocumentRequestsOnEditAndClose: Bool? = nil - - public var cancelTextDocumentRequestsOnEditAndCloseOrDefault: Bool { - return cancelTextDocumentRequestsOnEditAndClose ?? true - } - - /// Experimental features that are enabled. - public var experimentalFeatures: Set? = nil - - /// The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and - /// sending a `PublishDiagnosticsNotification`. - public var swiftPublishDiagnosticsDebounceDuration: Double? = nil - - public var swiftPublishDiagnosticsDebounceDurationOrDefault: Duration { - if let swiftPublishDiagnosticsDebounceDuration { - return .seconds(swiftPublishDiagnosticsDebounceDuration) - } - return .seconds(1) - } - - /// When a task is started that should be displayed to the client as a work done progress, how many milliseconds to - /// wait before actually starting the work done progress. This prevents flickering of the work done progress in the - /// client for short-lived index tasks which end within this duration. - public var workDoneProgressDebounceDuration: Double? = nil - - public var workDoneProgressDebounceDurationOrDefault: Duration { - if let workDoneProgressDebounceDuration { - return .seconds(workDoneProgressDebounceDuration) - } - return .seconds(1) - } - - /// The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. - /// - /// In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel - /// requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic - /// functionality. - /// - /// In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that - /// blocks sourcekitd. - public var sourcekitdRequestTimeout: Double? = nil - - public var sourcekitdRequestTimeoutOrDefault: Duration { - if let sourcekitdRequestTimeout { - return .seconds(sourcekitdRequestTimeout) - } - return .seconds(120) - } - - /// If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging - /// for some reason and won't recover. To restore semantic functionality, we terminate and restart it. - public var semanticServiceRestartTimeout: Double? = nil - - public var semanticServiceRestartTimeoutOrDefault: Duration { - if let semanticServiceRestartTimeout { - return .seconds(semanticServiceRestartTimeout) - } - return .seconds(300) - } - - /// Duration how long to wait for responses to `workspace/buildTargets` or `buildTarget/sources` request by the build - /// server before defaulting to an empty response. - public var buildServerWorkspaceRequestsTimeout: Double? = nil - - public var buildServerWorkspaceRequestsTimeoutOrDefault: Duration { - if let buildServerWorkspaceRequestsTimeout { - return .seconds(buildServerWorkspaceRequestsTimeout) - } - // The default value needs to strike a balance: If the build server is slow to respond, we don't want to constantly - // run into this timeout, which causes somewhat expensive computations because we trigger the `buildTargetsChanged` - // chain. - // At the same time, we do want to provide functionality based on fallback settings after some time. - // 15s seems like it should strike a balance here but there is no data backing this value up. - return .seconds(15) - } - - public init( - swiftPM: SwiftPMOptions? = .init(), - fallbackBuildSystem: FallbackBuildSystemOptions? = .init(), - buildSettingsTimeout: Int? = nil, - compilationDatabase: CompilationDatabaseOptions? = .init(), - clangdOptions: [String]? = nil, - index: IndexOptions? = .init(), - logging: LoggingOptions? = .init(), - sourcekitd: SourceKitDOptions? = .init(), - defaultWorkspaceType: WorkspaceType? = nil, - generatedFilesPath: String? = nil, - backgroundIndexing: Bool? = nil, - backgroundPreparationMode: BackgroundPreparationMode? = nil, - cancelTextDocumentRequestsOnEditAndClose: Bool? = nil, - experimentalFeatures: Set? = nil, - swiftPublishDiagnosticsDebounceDuration: Double? = nil, - workDoneProgressDebounceDuration: Double? = nil, - sourcekitdRequestTimeout: Double? = nil, - semanticServiceRestartTimeout: Double? = nil, - buildServerWorkspaceRequestsTimeout: Double? = nil - ) { - self.swiftPM = swiftPM - self.fallbackBuildSystem = fallbackBuildSystem - self.buildSettingsTimeout = buildSettingsTimeout - self.compilationDatabase = compilationDatabase - self.clangdOptions = clangdOptions - self.index = index - self.logging = logging - self.sourcekitd = sourcekitd - self.generatedFilesPath = generatedFilesPath - self.defaultWorkspaceType = defaultWorkspaceType - self.backgroundIndexing = backgroundIndexing - self.backgroundPreparationMode = backgroundPreparationMode - self.cancelTextDocumentRequestsOnEditAndClose = cancelTextDocumentRequestsOnEditAndClose - self.experimentalFeatures = experimentalFeatures - self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration - self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration - self.sourcekitdRequestTimeout = sourcekitdRequestTimeout - self.semanticServiceRestartTimeout = semanticServiceRestartTimeout - self.buildServerWorkspaceRequestsTimeout = buildServerWorkspaceRequestsTimeout - } - - public init?(fromLSPAny lspAny: LSPAny?) throws { - guard let lspAny else { - return nil - } - let jsonEncoded = try JSONEncoder().encode(lspAny) - self = try JSONDecoder().decode(Self.self, from: jsonEncoded) - } - - public var asLSPAny: LSPAny { - get throws { - let jsonEncoded = try JSONEncoder().encode(self) - return try JSONDecoder().decode(LSPAny.self, from: jsonEncoded) - } - } - - public init?(path: URL?) { - guard let path, let contents = try? Data(contentsOf: path) else { - return nil - } - guard - let decoded = orLog( - "Parsing config.json", - { try JSONDecoder().decode(Self.self, from: contents) } - ) - else { - return nil - } - - logger.log("Read options from \(path)") - logger.logFullObjectInMultipleLogMessages(header: "Config file options", loggingProxy) - - self = decoded - } - - public static func merging(base: SourceKitLSPOptions, override: SourceKitLSPOptions?) -> SourceKitLSPOptions { - return SourceKitLSPOptions( - swiftPM: SwiftPMOptions.merging(base: base.swiftPMOrDefault, override: override?.swiftPM), - fallbackBuildSystem: FallbackBuildSystemOptions.merging( - base: base.fallbackBuildSystemOrDefault, - override: override?.fallbackBuildSystem - ), - buildSettingsTimeout: override?.buildSettingsTimeout, - compilationDatabase: CompilationDatabaseOptions.merging( - base: base.compilationDatabaseOrDefault, - override: override?.compilationDatabase - ), - clangdOptions: override?.clangdOptions ?? base.clangdOptions, - index: IndexOptions.merging(base: base.indexOrDefault, override: override?.index), - logging: LoggingOptions.merging(base: base.loggingOrDefault, override: override?.logging), - sourcekitd: SourceKitDOptions.merging(base: base.sourcekitdOrDefault, override: override?.sourcekitd), - defaultWorkspaceType: override?.defaultWorkspaceType ?? base.defaultWorkspaceType, - generatedFilesPath: override?.generatedFilesPath ?? base.generatedFilesPath, - backgroundIndexing: override?.backgroundIndexing ?? base.backgroundIndexing, - backgroundPreparationMode: override?.backgroundPreparationMode ?? base.backgroundPreparationMode, - cancelTextDocumentRequestsOnEditAndClose: override?.cancelTextDocumentRequestsOnEditAndClose - ?? base.cancelTextDocumentRequestsOnEditAndClose, - experimentalFeatures: override?.experimentalFeatures ?? base.experimentalFeatures, - swiftPublishDiagnosticsDebounceDuration: override?.swiftPublishDiagnosticsDebounceDuration - ?? base.swiftPublishDiagnosticsDebounceDuration, - workDoneProgressDebounceDuration: override?.workDoneProgressDebounceDuration - ?? base.workDoneProgressDebounceDuration, - sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout, - semanticServiceRestartTimeout: override?.semanticServiceRestartTimeout ?? base.semanticServiceRestartTimeout, - buildServerWorkspaceRequestsTimeout: override?.buildServerWorkspaceRequestsTimeout - ?? base.buildServerWorkspaceRequestsTimeout - ) - } - - package static func merging(base: SourceKitLSPOptions, workspaceFolder: DocumentURI) -> SourceKitLSPOptions { - return SourceKitLSPOptions.merging( - base: base, - override: SourceKitLSPOptions( - path: workspaceFolder.fileURL? - .appending(components: ".sourcekit-lsp", "config.json") - ) - ) - } - - public var generatedFilesAbsolutePath: URL { - if let generatedFilesPath { - return URL(fileURLWithPath: generatedFilesPath) - } - - return URL(fileURLWithPath: NSTemporaryDirectory()).appending(component: "sourcekit-lsp") - } - - public func hasExperimentalFeature(_ feature: ExperimentalFeature) -> Bool { - guard let experimentalFeatures else { - return false - } - return experimentalFeatures.contains(feature) - } -} - -extension SourceKitLSPOptions { - /// Options proxy to avoid public import of `SKLogging`. - /// - /// We can't conform `SourceKitLSPOptions` to `CustomLogStringConvertible` because that would require a public import - /// of `SKLogging`. Instead, define a package type that performs the logging of `SourceKitLSPOptions`. - package struct LoggingProxy: CustomLogStringConvertible { - let options: SourceKitLSPOptions - - package var description: String { - options.prettyPrintedJSON - } - - package var redactedDescription: String { - options.prettyPrintedRedactedJSON - } - } - - package var loggingProxy: LoggingProxy { - LoggingProxy(options: self) - } -} diff --git a/Sources/SKOptions/WorkspaceType.swift b/Sources/SKOptions/WorkspaceType.swift deleted file mode 100644 index 4d502d26a..000000000 --- a/Sources/SKOptions/WorkspaceType.swift +++ /dev/null @@ -1,17 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public enum WorkspaceType: String, Codable, Sendable { - case buildServer - case compilationDatabase - case swiftPM -} diff --git a/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift b/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift deleted file mode 100644 index f28a19e71..000000000 --- a/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol - -extension [CompletionItem] { - /// Remove `sortText` and `data` from all completion items as these are not stable across runs. Instead, sort items - /// by `sortText` to ensure we test them in the order that an editor would display them in. - package var clearingUnstableValues: [CompletionItem] { - return - self - .sorted(by: { ($0.sortText ?? "") < ($1.sortText ?? "") }) - .map { - var item = $0 - item.sortText = nil - item.data = nil - return item - } - } -} diff --git a/Sources/SKTestSupport/CreateBinary.swift b/Sources/SKTestSupport/CreateBinary.swift deleted file mode 100644 index fba3aec79..000000000 --- a/Sources/SKTestSupport/CreateBinary.swift +++ /dev/null @@ -1,38 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import SwiftExtensions -import ToolchainRegistry -import XCTest - -import class TSCBasic.Process - -/// Compiles the given Swift source code into a binary at `executablePath`. -package func createBinary(_ sourceCode: String, at executablePath: URL) async throws { - try await withTestScratchDir { scratchDir in - let sourceFile = scratchDir.appending(component: "source.swift") - try await sourceCode.writeWithRetry(to: sourceFile) - - var compilerArguments = try [ - sourceFile.filePath, - "-o", - executablePath.filePath, - ] - if let defaultSDKPath { - compilerArguments += ["-sdk", defaultSDKPath] - } - try await Process.checkNonZeroExit( - arguments: [unwrap(ToolchainRegistry.forTesting.default?.swiftc?.filePath)] + compilerArguments - ) - } -} diff --git a/Sources/SKTestSupport/CustomBuildServerTestProject.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift deleted file mode 100644 index 0ecddcd04..000000000 --- a/Sources/SKTestSupport/CustomBuildServerTestProject.swift +++ /dev/null @@ -1,315 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -package import BuildServerProtocol -package import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -package import SKOptions -package import SourceKitLSP -import SwiftExtensions -import ToolchainRegistry -import XCTest - -// MARK: - CustomBuildServer - -package actor CustomBuildServerInProgressRequestTracker { - private var inProgressRequests: [RequestID: Task] = [:] - private let queue = AsyncQueue() - - package init() {} - - private func setInProgressRequestImpl(_ id: RequestID, task: Task) { - guard inProgressRequests[id] == nil else { - logger.fault("Received duplicate request for id: \(id, privacy: .public)") - return - } - inProgressRequests[id] = task - } - - fileprivate nonisolated func setInProgressRequest(_ id: RequestID, task: Task) { - queue.async { - await self.setInProgressRequestImpl(id, task: task) - } - } - - private func markTaskAsFinishedImpl(_ id: RequestID) { - guard inProgressRequests[id] != nil else { - logger.fault("Cannot mark request \(id, privacy: .public) as finished because it is not being tracked.") - return - } - inProgressRequests[id] = nil - } - - fileprivate nonisolated func markTaskAsFinished(_ id: RequestID) { - queue.async { - await self.markTaskAsFinishedImpl(id) - } - } - - private func cancelTaskImpl(_ id: RequestID) { - guard let task = inProgressRequests[id] else { - logger.fault("Cannot cancel task \(id, privacy: .public) because it isn't tracked") - return - } - task.cancel() - } - - fileprivate nonisolated func cancelTask(_ id: RequestID) { - queue.async { - await self.cancelTaskImpl(id) - } - } -} - -/// A build server that can be injected into `CustomBuildServerTestProject`. -package protocol CustomBuildServer: MessageHandler { - var inProgressRequestsTracker: CustomBuildServerInProgressRequestTracker { get } - - init(projectRoot: URL, connectionToSourceKitLSP: any Connection) - - func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse - func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws - func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse - func onBuildExit(_ notification: OnBuildExitNotification) throws - func workspaceBuildTargetsRequest( - _ request: WorkspaceBuildTargetsRequest - ) async throws -> WorkspaceBuildTargetsResponse - func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse - func textDocumentSourceKitOptionsRequest( - _ request: TextDocumentSourceKitOptionsRequest - ) async throws -> TextDocumentSourceKitOptionsResponse? - func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse - func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse - nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws - func workspaceWaitForBuildSystemUpdatesRequest( - _ request: WorkspaceWaitForBuildSystemUpdatesRequest - ) async throws -> VoidResponse - nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws -} - -extension CustomBuildServer { - package nonisolated func handle(_ notification: some NotificationType) { - do { - switch notification { - case let notification as CancelRequestNotification: - try self.cancelRequest(notification) - case let notification as OnBuildExitNotification: - try self.onBuildExit(notification) - case let notification as OnBuildInitializedNotification: - try self.onBuildInitialized(notification) - case let notification as OnWatchedFilesDidChangeNotification: - try self.onWatchedFilesDidChange(notification) - default: - throw ResponseError.methodNotFound(type(of: notification).method) - } - } catch { - logger.error("Error while handling BSP notification: \(error.forLogging)") - } - } - - package nonisolated func handle( - _ request: Request, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) { - func handle(_ request: R, using handler: @Sendable @escaping (R) async throws -> R.Response) { - let task = Task { - defer { inProgressRequestsTracker.markTaskAsFinished(id) } - do { - reply(.success(try await handler(request) as! Request.Response)) - } catch { - reply(.failure(ResponseError(error))) - } - } - inProgressRequestsTracker.setInProgressRequest(id, task: task) - } - - switch request { - case let request as BuildShutdownRequest: - handle(request, using: self.buildShutdown(_:)) - case let request as BuildTargetSourcesRequest: - handle(request, using: self.buildTargetSourcesRequest) - case let request as InitializeBuildRequest: - handle(request, using: self.initializeBuildRequest) - case let request as TextDocumentSourceKitOptionsRequest: - handle(request, using: self.textDocumentSourceKitOptionsRequest) - case let request as WorkspaceBuildTargetsRequest: - handle(request, using: self.workspaceBuildTargetsRequest) - case let request as WorkspaceWaitForBuildSystemUpdatesRequest: - handle(request, using: self.workspaceWaitForBuildSystemUpdatesRequest) - case let request as BuildTargetPrepareRequest: - handle(request, using: self.prepareTarget) - default: - reply(.failure(ResponseError.methodNotFound(type(of: request).method))) - } - } -} - -package extension CustomBuildServer { - // MARK: Helper functions for the implementation of BSP methods - - func initializationResponse( - initializeData: SourceKitInitializeBuildResponseData = .init(sourceKitOptionsProvider: true) - ) -> InitializeBuildResponse { - InitializeBuildResponse( - displayName: "\(type(of: self))", - version: "", - bspVersion: "2.2.0", - capabilities: BuildServerCapabilities(), - dataKind: .sourceKit, - data: initializeData.encodeToLSPAny() - ) - } - - func initializationResponseSupportingBackgroundIndexing( - projectRoot: URL, - outputPathsProvider: Bool - ) throws -> InitializeBuildResponse { - return initializationResponse( - initializeData: SourceKitInitializeBuildResponseData( - indexDatabasePath: try projectRoot.appending(component: "index-db").filePath, - indexStorePath: try projectRoot.appending(component: "index-store").filePath, - outputPathsProvider: outputPathsProvider, - prepareProvider: true, - sourceKitOptionsProvider: true - ) - ) - } - - /// Returns a fake path that is unique to this target and file combination and can be used to identify this - /// combination in a unit's output path. - func fakeOutputPath(for file: String, in target: String) -> String { - #if os(Windows) - return #"C:\"# + target + #"\"# + file + ".o" - #else - return "/" + target + "/" + file + ".o" - #endif - } - - func sourceItem(for url: URL, outputPath: String) -> SourceItem { - SourceItem( - uri: URI(url), - kind: .file, - generated: false, - dataKind: .sourceKit, - data: SourceKitSourceItemData(outputPath: outputPath).encodeToLSPAny() - ) - } - - func dummyTargetSourcesResponse(files: some Sequence) -> BuildTargetSourcesResponse { - return BuildTargetSourcesResponse(items: [ - SourcesItem(target: .dummy, sources: files.map { SourceItem(uri: $0, kind: .file, generated: false) }) - ]) - } - - // MARK: Default implementation for all build server methods that usually don't need customization. - - func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { - return initializationResponse() - } - - nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws {} - - func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse { - return VoidResponse() - } - - nonisolated func onBuildExit(_ notification: OnBuildExitNotification) throws {} - - func workspaceBuildTargetsRequest( - _ request: WorkspaceBuildTargetsRequest - ) async throws -> WorkspaceBuildTargetsResponse { - return WorkspaceBuildTargetsResponse(targets: [ - BuildTarget( - id: .dummy, - capabilities: BuildTargetCapabilities(), - languageIds: [], - dependencies: [] - ) - ]) - } - - func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse { - return VoidResponse() - } - - func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse { - return VoidResponse() - } - - nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {} - - func workspaceWaitForBuildSystemUpdatesRequest( - _ request: WorkspaceWaitForBuildSystemUpdatesRequest - ) async throws -> VoidResponse { - return VoidResponse() - } - - nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws { - inProgressRequestsTracker.cancelTask(notification.id) - } -} - -// MARK: - CustomBuildServerTestProject - -/// A test project that launches a custom build server in-process. -/// -/// In contrast to `ExternalBuildServerTestProject`, the custom build server runs in-process and is implemented in -/// Swift. -package final class CustomBuildServerTestProject: MultiFileTestProject { - private let buildServerBox = ThreadSafeBox(initialValue: nil) - - package init( - files: [RelativeFileLocation: String], - buildServer buildServerType: BuildServer.Type, - capabilities: ClientCapabilities = ClientCapabilities(), - options: SourceKitLSPOptions? = nil, - hooks: Hooks = Hooks(), - enableBackgroundIndexing: Bool = false, - usePullDiagnostics: Bool = true, - pollIndex: Bool = true, - preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, - testScratchDir: URL? = nil, - testName: String = #function - ) async throws { - var hooks = hooks - XCTAssertNil(hooks.buildServerHooks.injectBuildServer) - hooks.buildServerHooks.injectBuildServer = { [buildServerBox] projectRoot, connectionToSourceKitLSP in - let buildServer = BuildServer(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP) - buildServerBox.value = buildServer - return LocalConnection(receiverName: "TestBuildServer", handler: buildServer) - } - try await super.init( - files: files, - capabilities: capabilities, - options: options, - hooks: hooks, - enableBackgroundIndexing: enableBackgroundIndexing, - usePullDiagnostics: usePullDiagnostics, - preInitialization: preInitialization, - testScratchDir: testScratchDir, - testName: testName - ) - - if pollIndex { - // Wait for the indexstore-db to finish indexing - try await testClient.send(SynchronizeRequest(index: true)) - } - } - - package func buildServer(file: StaticString = #filePath, line: UInt = #line) throws -> BuildServer { - try XCTUnwrap(buildServerBox.value, "Accessing build server before it has been created", file: file, line: line) - } -} diff --git a/Sources/SKTestSupport/DefaultSDKPath.swift b/Sources/SKTestSupport/DefaultSDKPath.swift deleted file mode 100644 index b36bebc6b..000000000 --- a/Sources/SKTestSupport/DefaultSDKPath.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import class TSCBasic.Process - -#if !os(macOS) -import Foundation -#endif - -private func xcrunMacOSSDKPath() -> String? { - guard var path = try? Process.checkNonZeroExit(arguments: ["/usr/bin/xcrun", "--show-sdk-path", "--sdk", "macosx"]) - else { - return nil - } - if path.last == "\n" { - path = String(path.dropLast()) - } - return path -} - -/// The default sdk path to use. -package let defaultSDKPath: String? = { - #if os(macOS) - return xcrunMacOSSDKPath() - #else - return ProcessInfo.processInfo.environment["SDKROOT"] - #endif -}() diff --git a/Sources/SKTestSupport/DocumentDiagnosticReport+full.swift b/Sources/SKTestSupport/DocumentDiagnosticReport+full.swift deleted file mode 100644 index 14ea465d8..000000000 --- a/Sources/SKTestSupport/DocumentDiagnosticReport+full.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol - -extension DocumentDiagnosticReport { - /// If this is a full diagnostic report, return it. Otherwise return `nil`. - package var fullReport: RelatedFullDocumentDiagnosticReport? { - guard case .full(let report) = self else { - return nil - } - return report - } -} diff --git a/Sources/SKTestSupport/DummyBuildServerManagerConnectionToClient.swift b/Sources/SKTestSupport/DummyBuildServerManagerConnectionToClient.swift deleted file mode 100644 index 3197a3e4b..000000000 --- a/Sources/SKTestSupport/DummyBuildServerManagerConnectionToClient.swift +++ /dev/null @@ -1,41 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import Foundation -package import LanguageServerProtocol - -package struct DummyBuildServerManagerConnectionToClient: BuildServerManagerConnectionToClient { - package var clientSupportsWorkDoneProgress: Bool = false - - package init() {} - - package func waitUntilInitialized() async {} - - package func send(_ notification: some NotificationType) {} - - package func nextRequestID() -> RequestID { - return .string(UUID().uuidString) - } - - package func send( - _ request: Request, - id: RequestID, - reply: @escaping @Sendable (LSPResult) -> Void - ) { - reply(.failure(ResponseError.unknown("Not implemented"))) - } - - package func watchFiles(_ fileWatchers: [FileSystemWatcher]) async {} - - func logMessageToIndexLog(message: String, type: WindowMessageType, structure: StructuredLogKind?) {} -} diff --git a/Sources/SKTestSupport/Expectations.swift b/Sources/SKTestSupport/Expectations.swift deleted file mode 100644 index 9cdf0fa6a..000000000 --- a/Sources/SKTestSupport/Expectations.swift +++ /dev/null @@ -1,27 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public import Testing - -public func expectThrowsError( - _ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - sourceLocation: SourceLocation = #_sourceLocation, - errorHandler: (_ error: Error) -> Void = { _ in } -) async { - do { - _ = try await expression() - Issue.record("Expression was expected to throw but did not throw", sourceLocation: sourceLocation) - } catch { - errorHandler(error) - } -} diff --git a/Sources/SKTestSupport/ExternalBuildServerTestProject.swift b/Sources/SKTestSupport/ExternalBuildServerTestProject.swift deleted file mode 100644 index 0fa35c02e..000000000 --- a/Sources/SKTestSupport/ExternalBuildServerTestProject.swift +++ /dev/null @@ -1,93 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import SKOptions -import SwiftExtensions -import XCTest - -private let sdkArgs = - if let defaultSDKPath { - """ - "-sdk", r"\(defaultSDKPath)", - """ - } else { - "" - } - -/// The path to the INPUTS directory of shared test projects. -private let skTestSupportInputsDirectory: URL = { - #if os(macOS) - var resources = - productsDirectory - .appending(components: "SourceKitLSP_SKTestSupport.bundle", "Contents", "Resources") - if !FileManager.default.fileExists(at: resources) { - // Xcode and command-line swiftpm differ about the path. - resources.deleteLastPathComponent() - resources.deleteLastPathComponent() - } - #else - let resources = - productsDirectory - .appending(component: "SourceKitLSP_SKTestSupport.resources") - #endif - guard FileManager.default.fileExists(at: resources) else { - fatalError("missing resources \(resources)") - } - return resources.appending(component: "INPUTS", directoryHint: .isDirectory).standardizedFileURL -}() - -/// Creates a project that uses a BSP server to provide build settings. -/// -/// The build server is implemented in Python on top of the code in `AbstractBuildServer.py`. -/// -/// The build server can contain `$SDK_ARGS`, which will replaced by `"-sdk", "/path/to/sdk"` on macOS and by an empty -/// string on all other platforms. -package class ExternalBuildServerTestProject: MultiFileTestProject { - package init( - files: [RelativeFileLocation: String], - buildServerConfigLocation: RelativeFileLocation = ".bsp/sourcekit-lsp.json", - buildServer: String, - options: SourceKitLSPOptions? = nil, - enableBackgroundIndexing: Bool = false, - testName: String = #function - ) async throws { - var files = files - files[buildServerConfigLocation] = """ - { - "name": "Test BSP-server", - "version": "1", - "bspVersion": "2.0", - "languages": ["swift"], - "argv": ["server.py"] - } - """ - files["server.py"] = """ - import sys - from typing import Dict, List, Optional - - sys.path.append(r"\(try skTestSupportInputsDirectory.filePath)") - - from AbstractBuildServer import AbstractBuildServer, LegacyBuildServer - - \(buildServer) - - BuildServer().run() - """.replacing("$SDK_ARGS", with: sdkArgs) - try await super.init( - files: files, - options: options, - enableBackgroundIndexing: enableBackgroundIndexing, - testName: testName - ) - } -} diff --git a/Sources/SKTestSupport/FileManager+createFiles.swift b/Sources/SKTestSupport/FileManager+createFiles.swift deleted file mode 100644 index 0b363e075..000000000 --- a/Sources/SKTestSupport/FileManager+createFiles.swift +++ /dev/null @@ -1,28 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation - -extension FileManager { - /// Creates files from a dictionary of path to contents. - /// - /// - parameters: - /// - root: The root directory that the paths are relative to. - /// - files: Dictionary from path (relative to root) to contents. - package func createFiles(root: URL, files: [String: String]) throws { - for (path, contents) in files { - let path = URL(fileURLWithPath: path, relativeTo: root) - try createDirectory(at: path.deletingLastPathComponent(), withIntermediateDirectories: true) - try contents.write(to: path, atomically: true, encoding: .utf8) - } - } -} diff --git a/Sources/SKTestSupport/FileManager+findFiles.swift b/Sources/SKTestSupport/FileManager+findFiles.swift deleted file mode 100644 index 002be95c0..000000000 --- a/Sources/SKTestSupport/FileManager+findFiles.swift +++ /dev/null @@ -1,39 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation - -extension FileManager { - /// Returns the URLs of all files with the given file extension in the given directory (recursively). - package func findFiles(withExtension extensionName: String, in directory: URL) -> [URL] { - var result: [URL] = [] - let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil) - while let url = enumerator?.nextObject() as? URL { - if url.pathExtension == extensionName { - result.append(url) - } - } - return result - } - - /// Returns the URLs of all files with the given file name in the given directory (recursively). - package func findFiles(named name: String, in directory: URL) -> [URL] { - var result: [URL] = [] - let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil) - while let url = enumerator?.nextObject() as? URL { - if url.lastPathComponent == name { - result.append(url) - } - } - return result - } -} diff --git a/Sources/SKTestSupport/FindTool.swift b/Sources/SKTestSupport/FindTool.swift deleted file mode 100644 index fa27c6270..000000000 --- a/Sources/SKTestSupport/FindTool.swift +++ /dev/null @@ -1,57 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import LanguageServerProtocolExtensions -import TSCExtensions - -import class TSCBasic.Process - -#if os(Windows) -import WinSDK -#endif - -// Returns the path to the given tool, as found by `xcrun --find` on macOS, or `which` on Linux. -package func findTool(name: String) async -> URL? { - #if os(macOS) - let cmd = ["/usr/bin/xcrun", "--find", name] - #elseif os(Windows) - var buf = [WCHAR](repeating: 0, count: Int(MAX_PATH)) - GetWindowsDirectoryW(&buf, DWORD(MAX_PATH)) - let wherePath = String(decodingCString: &buf, as: UTF16.self) - .appendingPathComponent("system32") - .appendingPathComponent("where.exe") - let cmd = [wherePath, name] - #elseif os(Android) - let cmd = ["/system/bin/which", name] - #else - let cmd = ["/usr/bin/which", name] - #endif - - guard let result = try? await Process.run(arguments: cmd, workingDirectory: nil) else { - return nil - } - guard var path = try? String(bytes: result.output.get(), encoding: .utf8) else { - return nil - } - #if os(Windows) - // where.exe returns all files that match the name. We only care about the first one. - if let newlineIndex = path.firstIndex(where: \.isNewline) { - path = String(path[.. Optional[Dict[str, object]]: - """ - Dispatch handling of the given method, received from SourceKit-LSP to the message handling function. - """ - method: str = str(message["method"]) - params: Dict[str, object] = message["params"] # type: ignore - if method == "build/exit": - return self.exit(params) - elif method == "build/initialize": - return self.initialize(params) - elif method == "build/initialized": - return self.initialized(params) - elif method == "build/shutdown": - return self.shutdown(params) - elif method == "buildTarget/prepare": - return self.buildtarget_prepare(params) - elif method == "buildTarget/sources": - return self.buildtarget_sources(params) - elif method == "textDocument/registerForChanges": - return self.register_for_changes(params) - elif method == "textDocument/sourceKitOptions": - return self.textdocument_sourcekitoptions(params) - elif method == "workspace/didChangeWatchedFiles": - return self.workspace_did_change_watched_files(params) - elif method == "workspace/buildTargets": - return self.workspace_build_targets(params) - elif method == "workspace/waitForBuildSystemUpdates": - return self.workspace_waitForBuildSystemUpdates(params) - - # ignore other notifications - if "id" in message: - raise RequestError(code=-32601, message=f"Method not found: {method}") - - def send_raw_message(self, message: Dict[str, object]): - """ - Send a raw message to SourceKit-LSP. The message needs to have all JSON-RPC wrapper fields. - - Subclasses should not call this directly - """ - message_str = json.dumps(message) - sys.stdout.buffer.write( - f"Content-Length: {len(message_str)}\r\n\r\n{message_str}".encode("utf-8") - ) - sys.stdout.flush() - - def send_notification(self, method: str, params: Dict[str, object]): - """ - Send a notification with the given method and parameters to SourceKit-LSP. - """ - message: Dict[str, object] = { - "jsonrpc": "2.0", - "method": method, - "params": params, - } - self.send_raw_message(message) - - # Message handling functions. - # Subclasses should override these to provide functionality. - - def exit(self, notification: Dict[str, object]) -> None: - pass - - def initialize(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["swift", "c", "cpp", "objective-c", "objective-c"]}, - "data": { - "sourceKitOptionsProvider": True, - }, - } - - def initialized(self, notification: Dict[str, object]) -> None: - pass - - def register_for_changes(self, notification: Dict[str, object]): - pass - - def textdocument_sourcekitoptions( - self, request: Dict[str, object] - ) -> Dict[str, object]: - raise RequestError( - code=-32601, message=f"'textDocument/sourceKitOptions' not implemented" - ) - - def shutdown(self, request: Dict[str, object]) -> Dict[str, object]: - return {} - - def buildtarget_prepare(self, request: Dict[str, object]) -> Dict[str, object]: - raise RequestError( - code=-32601, message=f"'buildTarget/prepare' not implemented" - ) - - def buildtarget_sources(self, request: Dict[str, object]) -> Dict[str, object]: - raise RequestError( - code=-32601, message=f"'buildTarget/sources' not implemented" - ) - - def workspace_did_change_watched_files(self, notification: Dict[str, object]) -> None: - pass - - def workspace_build_targets(self, request: Dict[str, object]) -> Dict[str, object]: - raise RequestError( - code=-32601, message=f"'workspace/buildTargets' not implemented" - ) - - def workspace_waitForBuildSystemUpdates(self, request: Dict[str, object]) -> Dict[str, object]: - return {} - - -class LegacyBuildServer(AbstractBuildServer): - def send_sourcekit_options_changed(self, uri: str, options: List[str]): - """ - Send a `build/sourceKitOptionsChanged` notification to SourceKit-LSP, informing it about new build settings - using the old push-based settings model. - """ - self.send_notification( - "build/sourceKitOptionsChanged", - { - "uri": uri, - "updatedOptions": {"options": options}, - }, - ) - - """ - A build server that doesn't declare the `sourceKitOptionsProvider` and uses the push-based settings model. - """ - - def initialize(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexDatabasePath": "some/index/db/path", - "indexStorePath": "some/index/store/path", - }, - } diff --git a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift deleted file mode 100644 index df3286c98..000000000 --- a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift +++ /dev/null @@ -1,198 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -@_spi(Testing) import BuildServerIntegration -package import Foundation -package import LanguageServerProtocol -import SKLogging -import SKOptions -import SourceKitLSP -import SwiftExtensions -import TSCBasic -import ToolchainRegistry - -package struct WindowsPlatformInfo { - package struct DefaultProperties { - /// XCTEST_VERSION - /// specifies the version string of the bundled XCTest. - public let xctestVersion: String - - /// SWIFT_TESTING_VERSION - /// specifies the version string of the bundled swift-testing. - public let swiftTestingVersion: String? - - /// SWIFTC_FLAGS - /// Specifies extra flags to pass to swiftc from Swift Package Manager. - public let extraSwiftCFlags: [String]? - } - - public let defaults: DefaultProperties -} -extension WindowsPlatformInfo.DefaultProperties: Decodable { - enum CodingKeys: String, CodingKey { - case xctestVersion = "XCTEST_VERSION" - case swiftTestingVersion = "SWIFT_TESTING_VERSION" - case extraSwiftCFlags = "SWIFTC_FLAGS" - } -} -extension WindowsPlatformInfo: Decodable { - enum CodingKeys: String, CodingKey { - case defaults = "DefaultProperties" - } -} -extension WindowsPlatformInfo { - package init(reading path: URL) throws { - let data: Data = try Data(contentsOf: path) - self = try PropertyListDecoder().decode(WindowsPlatformInfo.self, from: data) - } -} -package struct IndexedSingleSwiftFileTestProject { - enum Error: Swift.Error { - case swiftcNotFound - } - - package let testClient: TestSourceKitLSPClient - package let fileURI: DocumentURI - package let positions: DocumentPositions - - /// Writes a single file to a temporary directory on disk and compiles it to index it. - /// - /// - Parameters: - /// - markedText: The contents of the source file including location markers. - /// - allowBuildFailure: Whether to fail if the input source file fails to build or whether to continue the test - /// even if the input source is invalid. - /// - workspaceDirectory: If specified, the temporary files will be put in this directory. If `nil` a temporary - /// scratch directory will be created based on `testName`. - /// - cleanUp: Whether to remove the temporary directory when the SourceKit-LSP server shuts down. - package init( - _ markedText: String, - capabilities: ClientCapabilities = ClientCapabilities(), - indexSystemModules: Bool = false, - allowBuildFailure: Bool = false, - workspaceDirectory: URL? = nil, - cleanUp: Bool = cleanScratchDirectories, - testName: String = #function - ) async throws { - let testWorkspaceDirectory = try workspaceDirectory ?? testScratchDir(testName: testName) - - let testFileURL = testWorkspaceDirectory.appending(component: "test.swift") - let indexURL = testWorkspaceDirectory.appending(component: "index") - guard let swiftc = await ToolchainRegistry.forTesting.default?.swiftc else { - throw Error.swiftcNotFound - } - - // Create workspace with source file and compile_commands.json - - try extractMarkers(markedText).textWithoutMarkers.write(to: testFileURL, atomically: false, encoding: .utf8) - - var compilerArguments: [String] = [ - try testFileURL.filePath, - "-index-store-path", try indexURL.filePath, - "-typecheck", - ] - if let globalModuleCache = try globalModuleCache { - compilerArguments += [ - "-module-cache-path", try globalModuleCache.filePath, - ] - } - if !indexSystemModules { - compilerArguments.append("-index-ignore-system-modules") - } - - if let sdk = defaultSDKPath { - compilerArguments += ["-sdk", sdk] - - // The following are needed so we can import XCTest - let sdkUrl = URL(fileURLWithPath: sdk) - #if os(Windows) - let platform = sdkUrl.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() - let info = try WindowsPlatformInfo(reading: platform.appending(component: "Info.plist")) - let xctestModuleDir = - platform - .appending( - components: "Developer", - "Library", - "XCTest-\(info.defaults.xctestVersion)", - "usr", - "lib", - "swift", - "windows" - ) - compilerArguments += ["-I", try xctestModuleDir.filePath] - #else - let usrLibDir = - sdkUrl - .deletingLastPathComponent() - .deletingLastPathComponent() - .appending(components: "usr", "lib") - let frameworksDir = - sdkUrl - .deletingLastPathComponent() - .deletingLastPathComponent() - .appending(components: "Library", "Frameworks") - compilerArguments += [ - "-I", try usrLibDir.filePath, - "-F", try frameworksDir.filePath, - ] - #endif - } - - let compilationDatabase = JSONCompilationDatabase( - [ - CompilationDatabaseCompileCommand( - directory: try testWorkspaceDirectory.filePath, - filename: try testFileURL.filePath, - commandLine: [try swiftc.filePath] + compilerArguments - ) - ] - ) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] - try encoder.encode(compilationDatabase).write( - to: testWorkspaceDirectory.appending(component: JSONCompilationDatabaseBuildServer.dbName) - ) - - // Run swiftc to build the index store - do { - let compilerArgumentsCopy = compilerArguments - let output = try await withTimeout(defaultTimeoutDuration) { - try await Process.checkNonZeroExit(arguments: [swiftc.filePath] + compilerArgumentsCopy) - } - logger.debug("swiftc output:\n\(output)") - } catch { - if !allowBuildFailure { - throw error - } - } - - // Create the test client - self.testClient = try await TestSourceKitLSPClient( - options: try await SourceKitLSPOptions.testDefault(), - capabilities: capabilities, - workspaceFolders: [ - WorkspaceFolder(uri: DocumentURI(testWorkspaceDirectory)) - ], - cleanUp: { - if cleanUp { - try? FileManager.default.removeItem(at: testWorkspaceDirectory) - } - } - ) - - // Wait for the indexstore-db to finish indexing - try await testClient.send(SynchronizeRequest(index: true)) - - // Open the document - self.fileURI = DocumentURI(testFileURL) - self.positions = testClient.openDocument(markedText, uri: fileURI) - } -} diff --git a/Sources/SKTestSupport/LocationMarkers.swift b/Sources/SKTestSupport/LocationMarkers.swift deleted file mode 100644 index 16b0db0ed..000000000 --- a/Sources/SKTestSupport/LocationMarkers.swift +++ /dev/null @@ -1,69 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// Finds all marked ranges in the given text, see `Marker`. -private func findMarkedRanges(text: String) -> [Marker] { - var markers = [Marker]() - while let marker = nextMarkedRange(text: text, from: markers.last?.range.upperBound ?? text.startIndex) { - markers.append(marker) - } - return markers -} - -extension Character { - var isMarkerEmoji: Bool { - switch self { - case "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "ℹ️": - return true - default: return false - } - } -} - -private func nextMarkedRange(text: String, from: String.Index) -> Marker? { - guard let start = text[from...].firstIndex(where: { $0.isMarkerEmoji }) else { - return nil - } - let end = text.index(after: start) - - let markerRange = start.. -} - -package func extractMarkers(_ markedText: String) -> (markers: [String: Int], textWithoutMarkers: String) { - var text = "" - var markers = [String: Int]() - var lastIndex = markedText.startIndex - for marker in findMarkedRanges(text: markedText) { - text += markedText[lastIndex.. URL { - var url = relativeTo - for directory in directories { - url = url.appending(component: directory) - } - url = url.appending(component: fileName) - return url - } -} - -/// A test project that writes multiple files to disk and opens a `TestSourceKitLSPClient` client with a workspace -/// pointing to a temporary directory containing those files. -/// -/// The temporary files will be deleted when the `TestSourceKitLSPClient` is destructed. -package class MultiFileTestProject { - /// Information necessary to open a file in the LSP server by its filename. - package struct FileData { - /// The URI at which the file is stored on disk. - package let uri: DocumentURI - - /// The contents of the file including location markers. - package let markedText: String - } - - package let testClient: TestSourceKitLSPClient - - /// Information necessary to open a file in the LSP server by its filename. - private let fileData: [String: FileData] - - enum Error: Swift.Error { - /// No file with the given filename is known to the `MultiFileTestProject`. - case fileNotFound(name: String) - - /// Trying to delete a file in `changeDocument` that doesn't exist. - case deleteNonExistentFile - } - - /// The directory in which the temporary files are being placed. - package let scratchDirectory: URL - - package static func writeFilesToDisk( - files: [RelativeFileLocation: String], - scratchDirectory: URL - ) throws -> [String: FileData] { - try FileManager.default.createDirectory(at: scratchDirectory, withIntermediateDirectories: true) - - var fileData: [String: FileData] = [:] - for (fileLocation, markedText) in files { - // Drop trailing slashes from the test dir URL, so tests can write `$TEST_DIR_URL/someFile.swift` without ending - // up with double slashes. - var testDirUrl = scratchDirectory.absoluteString - while testDirUrl.hasSuffix("/") { - testDirUrl = String(testDirUrl.dropLast()) - } - let markedText = - markedText - .replacingOccurrences(of: "$TEST_DIR_URL", with: testDirUrl) - .replacingOccurrences( - of: "$TEST_DIR_BACKSLASH_ESCAPED", - with: try scratchDirectory.filePath.replacing(#"\"#, with: #"\\"#) - ) - .replacingOccurrences(of: "$TEST_DIR", with: try scratchDirectory.filePath) - let fileURL = fileLocation.url(relativeTo: scratchDirectory) - try FileManager.default.createDirectory( - at: fileURL.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try extractMarkers(markedText).textWithoutMarkers.write(to: fileURL, atomically: false, encoding: .utf8) - - if fileData[fileLocation.fileName] != nil { - // If we already have a file with this name, remove its data. That way we can't reference any of the two - // conflicting documents and will throw when trying to open them, instead of non-deterministically picking one. - fileData[fileLocation.fileName] = nil - } else { - fileData[fileLocation.fileName] = FileData( - uri: DocumentURI(fileURL), - markedText: markedText - ) - } - } - return fileData - } - - /// Writes the specified files to a temporary directory on disk and creates a `TestSourceKitLSPClient` for that - /// temporary directory. - /// - /// The file contents can contain location markers, which are returned when opening a document using - /// ``openDocument(_:)``. - /// - /// File contents can also contain `$TEST_DIR`, which gets replaced by the temporary directory. - package init( - files: [RelativeFileLocation: String], - workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = { - [WorkspaceFolder(uri: DocumentURI($0))] - }, - initializationOptions: LSPAny? = nil, - capabilities: ClientCapabilities = ClientCapabilities(), - options: SourceKitLSPOptions? = nil, - toolchainRegistry: ToolchainRegistry = .forTesting, - hooks: Hooks = Hooks(), - enableBackgroundIndexing: Bool = false, - usePullDiagnostics: Bool = true, - preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, - testScratchDir overrideTestScratchDir: URL? = nil, - cleanUp: (@Sendable () -> Void)? = nil, - testName: String = #function - ) async throws { - scratchDirectory = try overrideTestScratchDir ?? testScratchDir(testName: testName) - self.fileData = try Self.writeFilesToDisk(files: files, scratchDirectory: scratchDirectory) - - self.testClient = try await TestSourceKitLSPClient( - options: options, - hooks: hooks, - initializationOptions: initializationOptions, - capabilities: capabilities, - toolchainRegistry: toolchainRegistry, - usePullDiagnostics: usePullDiagnostics, - enableBackgroundIndexing: enableBackgroundIndexing, - workspaceFolders: workspaces(scratchDirectory), - preInitialization: preInitialization, - cleanUp: { [scratchDirectory] in - if cleanScratchDirectories { - try? FileManager.default.removeItem(at: scratchDirectory) - } - cleanUp?() - } - ) - } - - /// Opens the document with the given file name in the SourceKit-LSP server. - /// - /// - Returns: The URI for the opened document and the positions of the location markers. - package func openDocument( - _ fileName: String, - language: Language? = nil - ) throws -> (uri: DocumentURI, positions: DocumentPositions) { - guard let fileData = self.fileData[fileName] else { - throw Error.fileNotFound(name: fileName) - } - let positions = testClient.openDocument(fileData.markedText, uri: fileData.uri, language: language) - return (fileData.uri, positions) - } - - /// Returns the URI of the file with the given name. - package func uri(for fileName: String) throws -> DocumentURI { - guard let fileData = self.fileData[fileName] else { - throw Error.fileNotFound(name: fileName) - } - return fileData.uri - } - - /// Returns the position of the given marker in the given file. - package func position(of marker: String, in fileName: String) throws -> Position { - guard let fileData = self.fileData[fileName] else { - throw Error.fileNotFound(name: fileName) - } - return DocumentPositions(markedText: fileData.markedText)[marker] - } - - package func range(from fromMarker: String, to toMarker: String, in fileName: String) throws -> Range { - return try position(of: fromMarker, in: fileName).. Location { - let range = try self.range(from: fromMarker, to: toMarker, in: fileName) - return Location(uri: try self.uri(for: fileName), range: range) - } - - /// Modify the given file on disk, send a watched files did change notification to SourceKit-LSP and wait for that - /// notification to get handled. - /// - /// When `newMarkedContents` is `nil`, the file is deleted. - /// - /// Returns the URI and the document positions of the new document. - @discardableResult - package func changeFileOnDisk( - _ fileName: String, - newMarkedContents: String? - ) async throws -> (uri: DocumentURI, positions: DocumentPositions) { - let uri = try self.uri(for: fileName) - guard let url = uri.fileURL else { - throw Error.fileNotFound(name: fileName) - } - let positions: DocumentPositions - let newContents: String? - if let newMarkedContents { - (positions, newContents) = DocumentPositions.extract(from: newMarkedContents) - } else { - positions = DocumentPositions(markedText: "") - newContents = nil - } - - let fileExists = FileManager.default.fileExists(at: url) - let changeType: FileChangeType - switch (fileExists, newContents) { - case (false, let newContents?): - try await newContents.writeWithRetry(to: url) - changeType = .created - case (false, nil): - throw Error.deleteNonExistentFile - case (true, let newContents?): - try await newContents.writeWithRetry(to: url) - changeType = .changed - case (true, nil): - try FileManager.default.removeItem(at: url) - changeType = .deleted - } - testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: changeType)])) - // Ensure that we handle the `DidChangeWatchedFilesNotification`. - try await testClient.send(SynchronizeRequest()) - - return (uri, positions) - } -} diff --git a/Sources/SKTestSupport/PluginPaths.swift b/Sources/SKTestSupport/PluginPaths.swift deleted file mode 100644 index e7cd8bcca..000000000 --- a/Sources/SKTestSupport/PluginPaths.swift +++ /dev/null @@ -1,160 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging -package import SourceKitD -import SwiftExtensions -import ToolchainRegistry - -// Anchor class to lookup the testing bundle when swiftpm-testing-helper is used. -private final class TestingAnchor {} - -/// The path to the `SwiftSourceKitPluginTests` test bundle. This gives us a hook into the the build directory. -private let xctestBundle: URL = { - if Platform.current == .darwin { - for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { - return bundle.bundleURL - } - let bundle = Bundle(for: TestingAnchor.self) - if bundle.bundlePath.hasSuffix(".xctest") { - return bundle.bundleURL - } - preconditionFailure("Failed to find xctest bundle") - } else { - return URL( - fileURLWithPath: CommandLine.arguments.first!, - relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - ) - } -}() - -/// When running tests from Xcode, determine the build configuration of the package. -var inferredXcodeBuildConfiguration: String? { - if let xcodeBuildDirectory = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return URL(fileURLWithPath: xcodeBuildDirectory).lastPathComponent - } else { - return nil - } -} - -/// Shorthand for `FileManager.fileExists` -private func fileExists(at url: URL) -> Bool { - return FileManager.default.fileExists(atPath: url.path) -} - -/// Try to find the client and server plugin relative to `base`. -/// -/// Implementation detail of `sourceKitPluginPaths` which walks up the directory structure, repeatedly calling this method. -private func pluginPaths(relativeTo base: URL) -> PluginPaths? { - // When building in Xcode - if let buildConfiguration = inferredXcodeBuildConfiguration { - let frameworksDir = base.appending(components: "Products", buildConfiguration, "PackageFrameworks") - let clientPlugin = - frameworksDir - .appending(components: "SwiftSourceKitClientPlugin.framework", "SwiftSourceKitClientPlugin") - let servicePlugin = - frameworksDir - .appending(components: "SwiftSourceKitPlugin.framework", "SwiftSourceKitPlugin") - if fileExists(at: clientPlugin) && fileExists(at: servicePlugin) { - return PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin) - } - } - - // When creating an `xctestproducts` bundle - do { - let frameworksDir = base.appending(component: "PackageFrameworks") - let clientPlugin = - frameworksDir - .appending(components: "SwiftSourceKitClientPlugin.framework", "SwiftSourceKitClientPlugin") - let servicePlugin = - frameworksDir - .appending(components: "SwiftSourceKitPlugin.framework", "SwiftSourceKitPlugin") - if fileExists(at: clientPlugin) && fileExists(at: servicePlugin) { - return PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin) - } - } - - // When building using 'swift test' - do { - let clientPluginName: String - let servicePluginName: String - switch Platform.current { - case .darwin: - clientPluginName = "libSwiftSourceKitClientPlugin.dylib" - servicePluginName = "libSwiftSourceKitPlugin.dylib" - case .windows: - clientPluginName = "SwiftSourceKitClientPlugin.dll" - servicePluginName = "SwiftSourceKitPlugin.dll" - case .linux: - clientPluginName = "libSwiftSourceKitClientPlugin.so" - servicePluginName = "libSwiftSourceKitPlugin.so" - case nil: - logger.fault("Could not determine host OS. Falling back to using '.so' as dynamic library extension") - clientPluginName = "libSwiftSourceKitClientPlugin.so" - servicePluginName = "libSwiftSourceKitPlugin.so" - } - let clientPlugin = base.appending(component: clientPluginName) - let servicePlugin = base.appending(component: servicePluginName) - if fileExists(at: clientPlugin) && fileExists(at: servicePlugin) { - return PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin) - } - } - - return nil -} - -/// Returns the paths from which the SourceKit plugins should be loaded or throws an error if the plugins cannot be -/// found. -package var sourceKitPluginPaths: PluginPaths { - get async throws { - struct PluginLoadingError: Error, CustomStringConvertible { - let searchBase: URL - var description: String { - // We can't declare a dependency from the test *target* on the SourceKit plugin *product* - // (https://github.com/swiftlang/swift-package-manager/issues/8245). - // We thus require a build before running the tests to ensure the plugin dylibs are in the build products - // folder. - """ - Could not find SourceKit plugin. Ensure that you build the entire SourceKit-LSP package before running tests. - - Searching for plugin relative to \(searchBase) - """ - } - } - - var base: URL - if let pluginPaths = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_TEST_PLUGIN_PATHS"] { - if pluginPaths == "RELATIVE_TO_SOURCEKITD" { - let toolchain = await ToolchainRegistry.forTesting.default - guard let clientPlugin = toolchain?.sourceKitClientPlugin, let servicePlugin = toolchain?.sourceKitServicePlugin - else { - throw PluginLoadingError(searchBase: URL(string: "relative-to-sourcekitd://")!) - } - return PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin) - } else { - base = URL(fileURLWithPath: pluginPaths) - } - } else { - base = xctestBundle - } - var searchPath = base - while searchPath.pathComponents.count > 1 { - if let paths = pluginPaths(relativeTo: searchPath) { - return paths - } - searchPath = searchPath.deletingLastPathComponent() - } - - throw PluginLoadingError(searchBase: base) - } -} diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift deleted file mode 100644 index b47dce1c7..000000000 --- a/Sources/SKTestSupport/SkipUnless.swift +++ /dev/null @@ -1,547 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import Foundation -import InProcessClient -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import RegexBuilder -import SKLogging -import SourceKitD -import SourceKitLSP -import SwiftExtensions -import SwiftLanguageService -import TSCExtensions -import ToolchainRegistry -import XCTest - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import enum TSCBasic.ProcessEnv - -// MARK: - Skip checks - -/// Namespace for functions that are used to skip unsupported tests. -package actor SkipUnless { - private enum FeatureCheckResult { - case featureSupported - case featureUnsupported(skipMessage: String) - } - - private static let shared = SkipUnless() - - /// For any feature that has already been evaluated, the result of whether or not it should be skipped. - private var checkCache: [String: FeatureCheckResult] = [:] - - /// Throw an `XCTSkip` if any of the following conditions hold - /// - The Swift version of the toolchain used for testing (`ToolchainRegistry.forTesting.default`) is older than - /// `swiftVersion` - /// - The Swift version of the toolchain used for testing is equal to `swiftVersion` and `featureCheck` returns - /// `false`. This is used for features that are introduced in `swiftVersion` but are not present in all toolchain - /// snapshots. - /// - /// Having the version check indicates when the check tests can be removed (namely when the minimum required version - /// to test sourcekit-lsp is above `swiftVersion`) and it ensures that tests can’t stay in the skipped state over - /// multiple releases. - /// - /// Independently of these checks, the tests are never skipped in Swift CI (identified by the presence of the `SWIFTCI_USE_LOCAL_DEPS` environment). Swift CI is assumed to always build its own toolchain, which is thus - /// guaranteed to be up-to-date. - private func skipUnlessSupportedByToolchain( - swiftVersion: SwiftVersion, - featureName: String = #function, - file: StaticString, - line: UInt, - featureCheck: () async throws -> Bool - ) async throws { - return try await skipUnlessSupported(featureName: featureName, file: file, line: line) { - let toolchainSwiftVersion = try await unwrap(ToolchainRegistry.forTesting.default).swiftVersion - let requiredSwiftVersion = SwiftVersion(swiftVersion.major, swiftVersion.minor) - if toolchainSwiftVersion < requiredSwiftVersion { - return .featureUnsupported( - skipMessage: """ - Skipping because toolchain has Swift version \(toolchainSwiftVersion) \ - but test requires at least \(requiredSwiftVersion) - """ - ) - } else if toolchainSwiftVersion == requiredSwiftVersion { - logger.info("Checking if feature '\(featureName)' is supported") - defer { - logger.info("Done checking if feature '\(featureName)' is supported") - } - if try await !featureCheck() { - return .featureUnsupported(skipMessage: "Skipping because toolchain doesn't contain \(featureName)") - } else { - return .featureSupported - } - } else { - return .featureSupported - } - } - } - - private func skipUnlessSupported( - allowSkippingInCI: Bool = false, - featureName: String = #function, - file: StaticString, - line: UInt, - featureCheck: () async throws -> FeatureCheckResult - ) async throws { - let checkResult: FeatureCheckResult - if let cachedResult = checkCache[featureName] { - checkResult = cachedResult - } else if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] != nil && !allowSkippingInCI { - // In general, don't skip tests in CI. Toolchain should be up-to-date - checkResult = .featureSupported - } else { - checkResult = try await featureCheck() - } - checkCache[featureName] = checkResult - - if case .featureUnsupported(let skipMessage) = checkResult { - throw XCTSkip(skipMessage, file: file, line: line) - } - } - - /// A long test is a test that takes longer than 1-2s to execute. - package static func longTestsEnabled() throws { - if let value = ProcessInfo.processInfo.environment["SKIP_LONG_TESTS"], value == "1" || value == "YES" { - throw XCTSkip("Long tests disabled using the `SKIP_LONG_TESTS` environment variable") - } - } - - package static func platformIsDarwin(_ message: String) throws { - try XCTSkipUnless(Platform.current == .darwin, message) - } - - package static func platformIsWindows(_ message: String) throws { - try XCTSkipUnless(Platform.current == .windows, message) - } - - package static func platformSupportsTaskPriorityElevation() throws { - #if os(macOS) - guard #available(macOS 14.0, *) else { - // Priority elevation was implemented by https://github.com/apple/swift/pull/63019, which is available in the - // Swift 5.9 runtime included in macOS 14.0+ - throw XCTSkip("Priority elevation of tasks is only supported on macOS 14 and above") - } - #endif - } - - /// Check if we can use the build artifacts in the sourcekit-lsp build directory to build a macro package without - /// re-building swift-syntax. - package static func canBuildMacroUsingSwiftSyntaxFromSourceKitLSPBuild( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - try XCTSkipUnless( - Platform.current != .windows, - "Temporarily skipping as we need to fix these tests to use the cmake-built swift-syntax libraries on Windows." - ) - - return try await shared.skipUnlessSupported(file: file, line: line) { - do { - let project = try await SwiftPMTestProject( - files: [ - "MyMacros/MyMacros.swift": #""" - import SwiftParser - - func test() { - _ = Parser.parse(source: "let a") - } - """#, - "MyMacroClient/MyMacroClient.swift": """ - """, - ], - manifest: SwiftPMTestProject.macroPackageManifest - ) - try await SwiftPMTestProject.build( - at: project.scratchDirectory, - extraArguments: ["--experimental-prepare-for-indexing"] - ) - return .featureSupported - } catch { - return .featureUnsupported( - skipMessage: """ - Skipping because macro could not be built using build artifacts in the sourcekit-lsp build directory. \ - This usually happens if sourcekit-lsp was built using a different toolchain than the one used at test-time. - - Reason: - \(error) - """ - ) - } - } - } - - package static func canSwiftPMCompileForIOS( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupported(allowSkippingInCI: true, file: file, line: line) { - #if os(macOS) - let project = try await SwiftPMTestProject(files: [ - "MyFile.swift": """ - public func foo() {} - """ - ]) - do { - try await SwiftPMTestProject.build( - at: project.scratchDirectory, - extraArguments: [ - "--swift-sdk", "arm64-apple-ios", - ] - ) - return .featureSupported - } catch { - return .featureUnsupported(skipMessage: "Cannot build for iOS: \(error)") - } - #else - return .featureUnsupported(skipMessage: "Cannot build for iOS outside macOS by default") - #endif - } - } - - package static func canCompileForWasm( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupported(allowSkippingInCI: true, file: file, line: line) { - let swiftFrontend = try await unwrap(ToolchainRegistry.forTesting.default?.swift).deletingLastPathComponent() - .appending(component: "swift-frontend") - return try await withTestScratchDir { scratchDirectory in - let input = scratchDirectory.appending(component: "Input.swift") - try FileManager.default.createFile(at: input, contents: nil) - // If we can't compile for wasm, this fails complaining that it can't find the stdlib for wasm. - let result = try await withTimeout(defaultTimeoutDuration) { - try await Process.run( - arguments: [ - try swiftFrontend.filePath, - "-typecheck", - try input.filePath, - "-triple", - "wasm32-unknown-none-wasm", - "-enable-experimental-feature", - "Embedded", - "-Xcc", - "-fdeclspec", - ], - workingDirectory: nil - ) - } - if result.exitStatus == .terminated(code: 0) { - return .featureSupported - } - return .featureUnsupported(skipMessage: "Skipping because toolchain can not compile for wasm") - } - } - } - - package static func sourcekitdSupportsPlugin( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 2), file: file, line: line) { - let sourcekitd = try await getSourceKitD() - - do { - let response = try await sourcekitd.send( - \.codeCompleteSetPopularAPI, - sourcekitd.dictionary([ - sourcekitd.keys.codeCompleteOptions: [ - sourcekitd.keys.useNewAPI: 1 - ] - ]), - timeout: defaultTimeoutDuration - ) - return response[sourcekitd.keys.useNewAPI] == 1 - } catch { - return false - } - } - } - - package static func sourcekitdSupportsFullDocumentationInCompletion( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 2), file: file, line: line) { - let testClient = try await TestSourceKitLSPClient() - let uri = DocumentURI(for: .swift) - let positions = testClient.openDocument( - """ - /// A function - /// - /// with full docs - func sourcekitdSupportsFullDocumentationInCompletion() {} - 1️⃣ - """, - uri: uri - ) - let result = try await testClient.send( - CompletionRequest( - textDocument: TextDocumentIdentifier(uri), - position: positions["1️⃣"] - ) - ) - guard - let item = result.items.first(where: { - $0.label == "sourcekitdSupportsFullDocumentationInCompletion()" - }) - else { - XCTFail("Expected to find completion for 'sourcekitdSupportsFullDocumentationInCompletion'") - return false - } - let resolvedItem = try await testClient.send( - CompletionItemResolveRequest(item: item) - ) - guard case let .markupContent(markup) = resolvedItem.documentation else { - return false - } - return markup.value.contains("with full docs") - } - } - - package static func canLoadPluginsBuiltByToolchain( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupported(file: file, line: line) { - let project = try await SwiftPMTestProject( - files: [ - "Plugins/plugin.swift": #""" - import Foundation - import PackagePlugin - @main struct CodeGeneratorPlugin: BuildToolPlugin { - func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { - let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources") - guard let target = target as? SourceModuleTarget else { return [] } - let codeGenerator = try context.tool(named: "CodeGenerator").url - let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift") - return [.buildCommand( - displayName: "Generating code for \(target.name)", - executable: codeGenerator, - arguments: [ - generatedFile.path - ], - inputFiles: [], - outputFiles: [generatedFile] - )] - } - } - """#, - - "Sources/CodeGenerator/CodeGenerator.swift": #""" - import Foundation - try "let foo = 1".write( - to: URL(fileURLWithPath: CommandLine.arguments[1]), - atomically: true, - encoding: String.Encoding.utf8 - ) - """#, - - "Sources/TestLib/TestLib.swift": #""" - func useGenerated() { - _ = 1️⃣foo - } - """#, - ], - manifest: """ - // swift-tools-version: 6.0 - import PackageDescription - let package = Package( - name: "PluginTest", - targets: [ - .executableTarget(name: "CodeGenerator"), - .target( - name: "TestLib", - plugins: [.plugin(name: "CodeGeneratorPlugin")] - ), - .plugin( - name: "CodeGeneratorPlugin", - capability: .buildTool(), - dependencies: ["CodeGenerator"] - ), - ] - ) - """, - enableBackgroundIndexing: true - ) - - let (uri, positions) = try project.openDocument("TestLib.swift") - - let result = try await project.testClient.send( - DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]) - ) - - if result?.locations?.only == nil { - return .featureUnsupported(skipMessage: "Skipping because plugin protocols do not match.") - } - return .featureSupported - } - } - - package static func haveRawIdentifiers( - file: StaticString = #filePath, - line: UInt = #line - ) async throws { - return try await shared.skipUnlessSupportedByToolchain( - swiftVersion: SwiftVersion(6, 2), - file: file, - line: line - ) { - let testClient = try await TestSourceKitLSPClient() - let uri = DocumentURI(for: .swift) - testClient.openDocument( - """ - let `1 * 1` = 1 - _ = `1 * 1` - """, - uri: uri - ) - - let response = try unwrap( - await testClient.send(DocumentSemanticTokensRequest(textDocument: TextDocumentIdentifier(uri))) - ) - - let tokens = SyntaxHighlightingTokens(lspEncodedTokens: response.data) - return tokens.tokens.last - == SyntaxHighlightingToken( - range: Position(line: 1, utf16index: 4).. SourceKitD { - guard let sourcekitdPath = await ToolchainRegistry.forTesting.default?.sourcekitd else { - throw GenericError("Could not find SourceKitD") - } - let sourcekitd = try await SourceKitD.getOrCreate( - dylibPath: sourcekitdPath, - pluginPaths: try sourceKitPluginPaths - ) - - return sourcekitd - } -} - -// MARK: - Parsing Swift compiler version - -fileprivate extension String { - init?(bytes: [UInt8], encoding: Encoding) { - self = bytes.withUnsafeBytes { buffer in - guard let baseAddress = buffer.baseAddress else { - return "" - } - let data = Data(bytes: baseAddress, count: buffer.count) - return String(data: data, encoding: encoding)! - } - } -} - -private struct GenericError: Error, CustomStringConvertible { - var description: String - - init(_ message: String) { - self.description = message - } -} diff --git a/Sources/SKTestSupport/SourceKitD+send.swift b/Sources/SKTestSupport/SourceKitD+send.swift deleted file mode 100644 index 7879b36bb..000000000 --- a/Sources/SKTestSupport/SourceKitD+send.swift +++ /dev/null @@ -1,33 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd -package import SourceKitD - -extension SourceKitD { - /// Convenience overload of the `send` function for testing that doesn't restart sourcekitd if it does not respond - /// and doesn't pass any file contents. - package func send( - _ requestUid: KeyPath, - _ request: SKDRequestDictionary, - timeout: Duration = defaultTimeoutDuration - ) async throws -> SKDResponseDictionary { - return try await self.send( - requestUid, - request, - timeout: timeout, - restartTimeout: .seconds(60 * 60 * 24), - documentUrl: nil, - fileContents: nil - ) - } -} diff --git a/Sources/SKTestSupport/String+writeWithRetry.swift b/Sources/SKTestSupport/String+writeWithRetry.swift deleted file mode 100644 index df39b9044..000000000 --- a/Sources/SKTestSupport/String+writeWithRetry.swift +++ /dev/null @@ -1,56 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import SKLogging - -extension String { - /// Write this string to the given URL using UTF-8 encoding. - /// - /// Sometimes file writes fail on Windows because another process (like sourcekitd or clangd) still has exclusive - /// access to the file but releases it soon after. Retry to save the file if this happens. This matches what a user - /// would do. - package func writeWithRetry(to url: URL) async throws { - // Depending on the system, mtime resolution might not be perfectly accurate. Particularly containers appear to have - // imprecise mtimes. - // Wait a short time period before writing the new file to avoid situations like the following: - // - We index a source file and the unit receives a time stamp and wait for indexing to finish - // - We modify the source file but so quickly after the unit has been modified that the updated source file - // receives the same mtime as the unit file - // - We now assume that the we have an up-to-date index for this source file even though we do not. - // - // Waiting 10ms appears to be enough to avoid this situation on the systems we care about. - // - // Do determine the mtime accuracy on a system, run the following bash commands and look at the time gaps between - // the time stamps - // ``` - // mkdir /tmp/dir - // for x in $(seq 1 1000); do touch /tmp/dir/$x; done - // for x in /tmp/dir/*; do stat $x; done | grep Modify | sort | uniq - // ``` - try await Task.sleep(for: .milliseconds(10)) - - #if os(Windows) - try await repeatUntilExpectedResult(timeout: .seconds(10), sleepInterval: .milliseconds(200)) { - do { - try self.write(to: url, atomically: true, encoding: .utf8) - return true - } catch { - logger.error("Writing file contents to \(url) failed, will retry: \(error.forLogging)") - return false - } - } - #else - try self.write(to: url, atomically: true, encoding: .utf8) - #endif - } -} diff --git a/Sources/SKTestSupport/SwiftPMDependencyProject.swift b/Sources/SKTestSupport/SwiftPMDependencyProject.swift deleted file mode 100644 index cc5d0061b..000000000 --- a/Sources/SKTestSupport/SwiftPMDependencyProject.swift +++ /dev/null @@ -1,119 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import LanguageServerProtocolExtensions -import SwiftExtensions -import TSCExtensions -import XCTest - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import enum TSCBasic.ProcessEnv -import struct TSCBasic.ProcessResult - -/// A SwiftPM package that gets written to disk and for which a Git repository is initialized with a commit tagged -/// `1.0.0`. This repository can then be used as a dependency for another package, usually a `SwiftPMTestProject`. -package class SwiftPMDependencyProject { - /// The scratch directory created for the dependency project. - package let scratchDirectory: URL - - /// The directory in which the repository lives. - package var packageDirectory: URL { - return scratchDirectory.appending(component: "MyDependency") - } - - private func runGitCommand(_ arguments: [String], workingDirectory: URL) async throws { - enum Error: Swift.Error { - case cannotFindGit - case processedTerminatedWithNonZeroExitCode(ProcessResult) - } - guard let git = await findTool(name: "git") else { - if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] == nil { - // Never skip the test in CI, similar to what SkipUnless does. - throw XCTSkip("git cannot be found") - } - throw Error.cannotFindGit - } - // We can't use `workingDirectory` because Amazon Linux doesn't support working directories (or at least - // TSCBasic.Process doesn't support working directories on Amazon Linux) - let processResult = try await withTimeout(defaultTimeoutDuration) { - try await TSCBasic.Process.run( - arguments: [try git.filePath, "-C", try workingDirectory.filePath] + arguments, - workingDirectory: nil - ) - } - guard processResult.exitStatus == .terminated(code: 0) else { - throw Error.processedTerminatedWithNonZeroExitCode(processResult) - } - } - - package static let defaultPackageManifest: String = """ - // swift-tools-version: 5.7 - - import PackageDescription - - let package = Package( - name: "MyDependency", - products: [.library(name: "MyDependency", targets: ["MyDependency"])], - targets: [.target(name: "MyDependency")] - ) - """ - - package init( - files: [RelativeFileLocation: String], - manifest: String = defaultPackageManifest, - testName: String = #function - ) async throws { - scratchDirectory = try testScratchDir(testName: testName) - - var files = files - files["Package.swift"] = manifest - - for (fileLocation, markedContents) in files { - let fileURL = fileLocation.url(relativeTo: packageDirectory) - try FileManager.default.createDirectory( - at: fileURL.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try extractMarkers(markedContents).textWithoutMarkers.write(to: fileURL, atomically: true, encoding: .utf8) - } - - try await runGitCommand(["init"], workingDirectory: packageDirectory) - try await tag(changedFiles: files.keys.map { $0.url(relativeTo: packageDirectory) }, version: "1.0.0") - } - - package func tag(changedFiles: [URL], version: String) async throws { - try await runGitCommand( - ["add"] + changedFiles.map { try $0.filePath }, - workingDirectory: packageDirectory - ) - try await runGitCommand( - ["-c", "user.name=Dummy", "-c", "user.email=noreply@swift.org", "commit", "-m", "Version \(version)"], - workingDirectory: packageDirectory - ) - - try await runGitCommand(["tag", version], workingDirectory: self.packageDirectory) - } - - deinit { - if cleanScratchDirectories { - try? FileManager.default.removeItem(at: scratchDirectory) - } - } - - /// Function that makes sure the project stays alive until this is called. Otherwise, the `SwiftPMDependencyProject` - /// might get deinitialized, which deletes the package on disk. - package func keepAlive() { - withExtendedLifetime(self) { _ in } - } -} diff --git a/Sources/SKTestSupport/SwiftPMTestProject.swift b/Sources/SKTestSupport/SwiftPMTestProject.swift deleted file mode 100644 index 12a806906..000000000 --- a/Sources/SKTestSupport/SwiftPMTestProject.swift +++ /dev/null @@ -1,293 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol -import SKLogging -package import SKOptions -package import SourceKitLSP -import SwiftExtensions -import TSCBasic -import ToolchainRegistry -import XCTest - -package class SwiftPMTestProject: MultiFileTestProject { - enum Error: Swift.Error { - /// The `swift` executable could not be found. - case swiftNotFound - } - - package static let defaultPackageManifest: String = """ - // swift-tools-version: 5.7 - - import PackageDescription - - let package = Package( - name: "MyLibrary", - targets: [.target(name: "MyLibrary")] - ) - """ - - /// A manifest that defines two targets: - /// - A macro target named `MyMacro` - /// - And executable target named `MyMacroClient` - /// - /// It builds the macro using the swift-syntax that was already built as part of the SourceKit-LSP build. - /// Re-using the SwiftSyntax modules that are already built is significantly faster than building swift-syntax in - /// each test case run and does not require internet access. - package static var macroPackageManifest: String { - get async throws { - // Directories that we should search for the swift-syntax package. - // We prefer a checkout in the build folder. If that doesn't exist, we are probably using local dependencies - // (SWIFTCI_USE_LOCAL_DEPS), so search next to the sourcekit-lsp source repo - let swiftSyntaxSearchPaths = [ - productsDirectory - .deletingLastPathComponent() // arm64-apple-macosx - .deletingLastPathComponent() // debug - .appending(component: "checkouts"), - URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // SwiftPMTestProject.swift - .deletingLastPathComponent() // SKTestSupport - .deletingLastPathComponent() // Sources - .deletingLastPathComponent(), // sourcekit-lsp - ] - - let swiftSyntaxCShimsModulemap = - swiftSyntaxSearchPaths.map { swiftSyntaxSearchPath in - swiftSyntaxSearchPath - .appending(components: "swift-syntax", "Sources", "_SwiftSyntaxCShims", "include", "module.modulemap") - } - .first { FileManager.default.fileExists(at: $0) } - - guard let swiftSyntaxCShimsModulemap else { - struct SwiftSyntaxCShimsModulemapNotFoundError: Swift.Error {} - throw SwiftSyntaxCShimsModulemapNotFoundError() - } - - // Only link against object files that are listed in the `Objects.LinkFileList`. Otherwise we can get a situation - // where a `.swift` file is removed from swift-syntax, its `.o` file is still in the build directory because the - // build folder wasn't cleaned and thus we would link against the stale `.o` file. - let linkFileListURL = - productsDirectory - .appending(components: "SourceKitLSPPackageTests.product", "Objects.LinkFileList") - let linkFileListContents = try? String(contentsOf: linkFileListURL, encoding: .utf8) - guard let linkFileListContents else { - struct LinkFileListNotFoundError: Swift.Error { - let url: URL - } - throw LinkFileListNotFoundError(url: linkFileListURL) - } - let linkFileList = - Set( - linkFileListContents - .split(separator: "\n") - .map { - // Files are wrapped in single quotes if the path contains spaces. Drop the quotes. - if $0.hasPrefix("'") && $0.hasSuffix("'") { - return String($0.dropFirst().dropLast()) - } else { - return String($0) - } - } - ) - - let swiftSyntaxModulesToLink = [ - "SwiftBasicFormat", - "SwiftCompilerPlugin", - "SwiftCompilerPluginMessageHandling", - "SwiftDiagnostics", - "SwiftIfConfig", - "SwiftOperators", - "SwiftParser", - "SwiftParserDiagnostics", - "SwiftSyntax", - "SwiftSyntaxBuilder", - "SwiftSyntaxMacroExpansion", - "SwiftSyntaxMacros", - "_SwiftSyntaxCShims", - ] - - var objectFiles: [String] = [] - for moduleName in swiftSyntaxModulesToLink { - let dir = productsDirectory.appending(component: "\(moduleName).build") - let enumerator = FileManager.default.enumerator(at: dir, includingPropertiesForKeys: nil) - while let file = enumerator?.nextObject() as? URL { - if linkFileList.contains(try file.filePath) { - objectFiles.append(try file.filePath) - } - } - } - - let linkerFlags = objectFiles.map { - """ - "\($0)", - """ - }.joined(separator: "\n") - - let moduleSearchPath: String - if let toolchainVersion = try await ToolchainRegistry.forTesting.default?.swiftVersion, - toolchainVersion < SwiftVersion(6, 0) - { - moduleSearchPath = try productsDirectory.filePath - } else { - moduleSearchPath = "\(try productsDirectory.filePath)/Modules" - } - - return """ - // swift-tools-version: 5.10 - - import PackageDescription - import CompilerPluginSupport - - let package = Package( - name: "MyMacro", - platforms: [.macOS(.v10_15)], - targets: [ - .macro( - name: "MyMacros", - swiftSettings: [.unsafeFlags([ - "-I", #"\(moduleSearchPath)"#, - "-Xcc", #"-fmodule-map-file=\(try swiftSyntaxCShimsModulemap.filePath)"# - ])], - linkerSettings: [ - .unsafeFlags([ - \(linkerFlags) - ]) - ] - ), - .executableTarget(name: "MyMacroClient", dependencies: ["MyMacros"]), - ] - ) - """ - } - } - - /// Create a new SwiftPM package with the given files. - /// - /// If `index` is `true`, then the package will be built, indexing all modules within the package. - package init( - files: [RelativeFileLocation: String], - manifest: String = SwiftPMTestProject.defaultPackageManifest, - workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = { - [WorkspaceFolder(uri: DocumentURI($0))] - }, - initializationOptions: LSPAny? = nil, - capabilities: ClientCapabilities = ClientCapabilities(), - options: SourceKitLSPOptions? = nil, - hooks: Hooks = Hooks(), - enableBackgroundIndexing: Bool = false, - usePullDiagnostics: Bool = true, - pollIndex: Bool = true, - preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, - cleanUp: (@Sendable () -> Void)? = nil, - testName: String = #function - ) async throws { - var filesByPath: [RelativeFileLocation: String] = [:] - for (fileLocation, contents) in files { - let directories = - switch fileLocation.directories.first { - case "Sources", "Tests", "Plugins", "": - fileLocation.directories - case nil: - ["Sources", "MyLibrary"] - default: - ["Sources"] + fileLocation.directories - } - - filesByPath[RelativeFileLocation(directories: directories, fileLocation.fileName)] = contents - } - var manifest = manifest - if !manifest.contains("swift-tools-version") { - // Tests specify a shorthand package manifest that doesn't contain the tools version boilerplate. - manifest = """ - // swift-tools-version: 5.10 - - import PackageDescription - - \(manifest) - """ - } - filesByPath["Package.swift"] = manifest - - try await super.init( - files: filesByPath, - workspaces: workspaces, - initializationOptions: initializationOptions, - capabilities: capabilities, - options: options, - hooks: hooks, - enableBackgroundIndexing: enableBackgroundIndexing, - usePullDiagnostics: usePullDiagnostics, - preInitialization: preInitialization, - cleanUp: cleanUp, - testName: testName - ) - - if pollIndex { - // Wait for the indexstore-db to finish indexing - try await testClient.send(SynchronizeRequest(index: true)) - } - } - - /// Build a SwiftPM package package manifest is located in the directory at `path`. - package static func build(at path: URL, extraArguments: [String] = []) async throws { - guard let swift = await ToolchainRegistry.forTesting.default?.swift else { - throw Error.swiftNotFound - } - var arguments = - [ - try swift.filePath, - "build", - "--package-path", try path.filePath, - "--build-tests", - "-Xswiftc", "-index-ignore-system-modules", - "-Xcc", "-index-ignore-system-symbols", - ] + extraArguments - if let globalModuleCache = try globalModuleCache { - arguments += [ - "-Xswiftc", "-module-cache-path", "-Xswiftc", try globalModuleCache.filePath, - ] - } - let argumentsCopy = arguments - let output = try await withTimeout(defaultTimeoutDuration) { - try await Process.checkNonZeroExit(arguments: argumentsCopy) - } - logger.debug( - """ - 'swift build' output: - \(output) - """ - ) - } - - /// Resolve package dependencies for the package at `path`. - package static func resolvePackageDependencies(at path: URL) async throws { - guard let swift = await ToolchainRegistry.forTesting.default?.swift else { - throw Error.swiftNotFound - } - let arguments = [ - try swift.filePath, - "package", - "resolve", - "--package-path", try path.filePath, - ] - let output = try await withTimeout(defaultTimeoutDuration) { - try await Process.checkNonZeroExit(arguments: arguments) - } - logger.debug( - """ - 'swift package resolve' output: - \(output) - """ - ) - } -} diff --git a/Sources/SKTestSupport/TestBundle.swift b/Sources/SKTestSupport/TestBundle.swift deleted file mode 100644 index 2346216cb..000000000 --- a/Sources/SKTestSupport/TestBundle.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation - -/// The bundle of the currently executing test. -package let testBundle: Bundle = { - #if os(macOS) - if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) { - return bundle - } - fatalError("couldn't find the test bundle") - #else - return Bundle.main - #endif -}() - -/// The path to the built products directory, ie. `.build/debug/arm64-apple-macosx` or the platform-specific equivalent. -package let productsDirectory: URL = { - #if os(macOS) - return testBundle.bundleURL.deletingLastPathComponent() - #else - return testBundle.bundleURL - #endif -}() diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift deleted file mode 100644 index 8365d02b5..000000000 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ /dev/null @@ -1,486 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import InProcessClient -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import LanguageServerProtocolJSONRPC -import SKLogging -package import SKOptions -import SKUtilities -import SourceKitD -package import SourceKitLSP -import SwiftExtensions -import SwiftSyntax -package import ToolchainRegistry -import XCTest - -extension SourceKitLSPOptions { - package static func testDefault( - backgroundIndexing: Bool = true, - experimentalFeatures: Set = [.synchronizeCopyFileMap] - ) async throws -> SourceKitLSPOptions { - let pluginPaths = try await sourceKitPluginPaths - return SourceKitLSPOptions( - sourcekitd: SourceKitDOptions( - clientPlugin: try pluginPaths.clientPlugin.filePath, - servicePlugin: try pluginPaths.servicePlugin.filePath - ), - backgroundIndexing: backgroundIndexing, - experimentalFeatures: experimentalFeatures, - swiftPublishDiagnosticsDebounceDuration: 0, - workDoneProgressDebounceDuration: 0 - ) - } -} - -private struct NotificationTimeoutError: Error, CustomStringConvertible { - var description: String = "Failed to receive next notification within timeout" -} - -/// A list of notifications that has been received by the SourceKit-LSP server but not handled from the test case yet. -/// -/// We can't use an `AsyncStream` for this because an `AsyncStream` is cancelled if a task that calls -/// `AsyncStream.Iterator.next` is cancelled and we want to be able to wait for new notifications even if waiting for a -/// a previous notification timed out. -final class PendingNotifications: Sendable { - private let values = ThreadSafeBox<[any NotificationType]>(initialValue: []) - - nonisolated func add(_ value: any NotificationType) { - values.value.insert(value, at: 0) - } - - func next(timeout: Duration, pollingInterval: Duration = .milliseconds(10)) async throws -> any NotificationType { - for _ in 0.. = @Sendable (Request) -> Request.Response - - /// The ID that should be assigned to the next request sent to the `server`. - private let nextRequestID = AtomicUInt32(initialValue: 0) - - /// The server that handles the requests. - package let server: SourceKitLSPServer - - /// Whether pull or push-model diagnostics should be used. - /// - /// This is used to fail the `nextDiagnosticsNotification` function early in case the pull-diagnostics model is used - /// to avoid a fruitful debug for why no diagnostic request is being sent push diagnostics have been explicitly - /// disabled. - private let usePullDiagnostics: Bool - - /// The connection via which the server sends requests and notifications to us. - private let serverToClientConnection: LocalConnection - - /// Stream of the notifications that the server has sent to the client. - private let notifications: PendingNotifications - - /// The request handlers that have been set by `handleNextRequest`. - /// - /// Conceptually, this is an array of `RequestHandler` but - /// since we can't express this in the Swift type system, we use `[Any]`. - /// - /// `isOneShort` if the request handler should only serve a single request and should be removed from - /// `requestHandlers` after it has been called. - private let requestHandlers: ThreadSafeBox<[(requestHandler: Sendable, isOneShot: Bool)]> = - ThreadSafeBox(initialValue: []) - - /// A closure that is called when the `TestSourceKitLSPClient` is destructed. - /// - /// This allows e.g. a `IndexedSingleSwiftFileTestProject` to delete its temporary files when they are no longer needed. - private let cleanUp: @Sendable () -> Void - - /// - Parameters: - /// - serverOptions: The equivalent of the command line options with which sourcekit-lsp should be started - /// - useGlobalModuleCache: If `false`, the server will use its own module - /// cache in an empty temporary directory instead of the global module cache. - /// - initialize: Whether an `InitializeRequest` should be automatically sent to the SourceKit-LSP server. - /// `true` by default - /// - initializationOptions: Initialization options to pass to the SourceKit-LSP server. - /// - capabilities: The test client's capabilities. - /// - usePullDiagnostics: Whether to use push diagnostics or use push-based diagnostics. - /// - enableBackgroundIndexing: Whether background indexing should be enabled in the project. - /// - workspaceFolders: Workspace folders to open. - /// - preInitialization: A closure that is called after the test client is created but before SourceKit-LSP is - /// initialized. This can be used to eg. register request handlers. - /// - cleanUp: A closure that is called when the `TestSourceKitLSPClient` is destructed. - /// This allows e.g. a `IndexedSingleSwiftFileTestProject` to delete its temporary files when they are no longer - /// needed. - package init( - options: SourceKitLSPOptions? = nil, - hooks: Hooks = Hooks(), - initialize: Bool = true, - initializationOptions: LSPAny? = nil, - capabilities: ClientCapabilities = ClientCapabilities(), - toolchainRegistry: ToolchainRegistry = .forTesting, - usePullDiagnostics: Bool = true, - enableBackgroundIndexing: Bool = false, - workspaceFolders: [WorkspaceFolder]? = nil, - preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, - cleanUp: @Sendable @escaping () -> Void = {} - ) async throws { - var options = - if let options { - options - } else { - try await SourceKitLSPOptions.testDefault() - } - if let globalModuleCache = try globalModuleCache { - options.swiftPMOrDefault.swiftCompilerFlags = - (options.swiftPMOrDefault.swiftCompilerFlags ?? []) + ["-module-cache-path", try globalModuleCache.filePath] - } - options.backgroundIndexing = enableBackgroundIndexing - if options.sourcekitdRequestTimeout == nil { - options.sourcekitdRequestTimeout = defaultTimeout - } - - self.notifications = PendingNotifications() - - let serverToClientConnection = LocalConnection(receiverName: "client") - self.serverToClientConnection = serverToClientConnection - server = SourceKitLSPServer( - client: serverToClientConnection, - toolchainRegistry: toolchainRegistry, - languageServerRegistry: .staticallyKnownServices, - options: options, - hooks: hooks, - onExit: { - serverToClientConnection.close() - } - ) - - self.cleanUp = cleanUp - self.usePullDiagnostics = usePullDiagnostics - self.serverToClientConnection.start(handler: WeakMessageHandler(self)) - - var capabilities = capabilities - if usePullDiagnostics { - if capabilities.textDocument == nil { - capabilities.textDocument = TextDocumentClientCapabilities() - } - guard capabilities.textDocument!.diagnostic == nil else { - struct ConflictingDiagnosticsError: Error, CustomStringConvertible { - var description: String { - "usePullDiagnostics = false is not supported if capabilities already contain diagnostic options" - } - } - throw ConflictingDiagnosticsError() - } - capabilities.textDocument!.diagnostic = .init(dynamicRegistration: true) - self.handleSingleRequest { (request: RegisterCapabilityRequest) in - XCTAssertEqual(request.registrations.only?.method, DocumentDiagnosticsRequest.method) - return VoidResponse() - } - } - preInitialization?(self) - if initialize { - let capabilities = capabilities - try await withTimeout(defaultTimeoutDuration) { - _ = try await self.send( - InitializeRequest( - processId: nil, - rootPath: nil, - rootURI: nil, - initializationOptions: initializationOptions, - capabilities: capabilities, - trace: .off, - workspaceFolders: workspaceFolders - ) - ) - } - } - } - - deinit { - // It's really unfortunate that there are no async deinits. If we had async - // deinits, we could await the sending of a ShutdownRequest. - let shutdownSemaphore = WrappedSemaphore(name: "Shutdown") - server.handle(ShutdownRequest(), id: .number(Int(nextRequestID.fetchAndIncrement()))) { result in - shutdownSemaphore.signal() - } - shutdownSemaphore.waitOrXCTFail() - self.send(ExitNotification()) - - cleanUp() - - let flushSemaphore = WrappedSemaphore(name: "Flush log") - Task { - await NonDarwinLogger.flush() - flushSemaphore.signal() - } - flushSemaphore.waitOrXCTFail() - } - - // MARK: - Sending messages - - /// Send the request to `server` and return the request result. - package func send(_ request: R) async throws(ResponseError) -> R.Response { - let response = await withCheckedContinuation { continuation in - self.send(request) { result in - continuation.resume(returning: result) - } - } - return try response.get() - } - - /// Variant of `send` above that allows the response to be discarded if it is a `VoidResponse`. - package func send(_ request: R) async throws(ResponseError) where R.Response == VoidResponse { - let _: VoidResponse = try await self.send(request) - } - - /// Send the request to `server` and return the result via a completion handler. - /// - /// This version of the `send` function should only be used if some action needs to be performed after the request is - /// sent but before it returns a result. - @discardableResult - package func send( - _ request: R, - completionHandler: @Sendable @escaping (LSPResult) -> Void - ) -> RequestID { - let requestID = RequestID.number(Int(nextRequestID.fetchAndIncrement())) - let replyOutstanding = ThreadSafeBox(initialValue: true) - let timeoutTask = Task { - try await Task.sleep(for: defaultTimeoutDuration) - if replyOutstanding.takeValue() ?? false { - completionHandler( - .failure(ResponseError.unknown("\(R.method) request timed out after \(defaultTimeoutDuration)")) - ) - } - server.handle(CancelRequestNotification(id: requestID)) - } - server.handle(request, id: requestID) { result in - if replyOutstanding.takeValue() ?? false { - completionHandler(result) - } - timeoutTask.cancel() - } - return requestID - } - - /// Send the notification to `server`. - package func send(_ notification: some NotificationType) { - server.handle(notification) - } - - // MARK: - Handling messages sent to the editor - - /// Await the next notification that is sent to the client. - /// - /// - Note: This also returns any notifications sent before the call to - /// `nextNotification`. - package func nextNotification(timeout: Duration = defaultTimeoutDuration) async throws -> any NotificationType { - return try await notifications.next(timeout: timeout) - } - - /// Await the next diagnostic notification sent to the client. - /// - /// If the next notification is not a `PublishDiagnosticsNotification`, this - /// methods throws. - package func nextDiagnosticsNotification( - timeout: Duration = defaultTimeoutDuration - ) async throws -> PublishDiagnosticsNotification { - guard !usePullDiagnostics else { - struct PushDiagnosticsError: Error, CustomStringConvertible { - var description = "Client is using the diagnostics and will thus never receive a diagnostics notification" - } - throw PushDiagnosticsError() - } - return try await nextNotification(ofType: PublishDiagnosticsNotification.self, timeout: timeout) - } - - /// Waits for the next notification of the given type to be sent to the client that satisfies the given predicate. - /// Ignores any notifications that are of a different type or that don't satisfy the predicate. - package func nextNotification( - ofType: ExpectedNotificationType.Type, - satisfying predicate: (ExpectedNotificationType) throws -> Bool = { _ in true }, - timeout: Duration = defaultTimeoutDuration - ) async throws -> ExpectedNotificationType { - while true { - let nextNotification = try await nextNotification(timeout: timeout) - if let notification = nextNotification as? ExpectedNotificationType, try predicate(notification) { - return notification - } - } - } - - /// Asserts that the test client does not receive a notification of the given type and satisfying the given predicate - /// within the given duration. - /// - /// For stable tests, the code that triggered the notification should be run before this assertion instead of relying - /// on the duration. - /// - /// The duration should not be 0 because we need to allow `nextNotification` some time to get the notification out of - /// the `notifications` `AsyncStream`. - package func assertDoesNotReceiveNotification( - ofType: ExpectedNotificationType.Type, - satisfying predicate: (ExpectedNotificationType) -> Bool = { _ in true }, - within duration: Duration = .seconds(0.2) - ) async throws { - do { - let notification = try await nextNotification( - ofType: ExpectedNotificationType.self, - satisfying: predicate, - timeout: duration - ) - XCTFail("Did not expect to receive notification but received \(notification)") - } catch is NotificationTimeoutError {} - } - - /// Handle the next request of the given type that is sent to the client. - /// - /// The request handler will only handle a single request. If the request is called again, the request handler won't - /// call again - package func handleSingleRequest(_ requestHandler: @escaping RequestHandler) { - requestHandlers.value.append((requestHandler: requestHandler, isOneShot: true)) - } - - /// Handle all requests of the given type that are sent to the client. - package func handleMultipleRequests(_ requestHandler: @escaping RequestHandler) { - requestHandlers.value.append((requestHandler: requestHandler, isOneShot: false)) - } - - // MARK: - Conformance to MessageHandler - - /// - Important: Implementation detail of `TestSourceKitLSPServer`. Do not call from tests. - package func handle(_ notification: some NotificationType) { - notifications.add(notification) - } - - /// - Important: Implementation detail of `TestSourceKitLSPClient`. Do not call from tests. - package func handle( - _ params: Request, - id: LanguageServerProtocol.RequestID, - reply: @escaping (LSPResult) -> Void - ) { - requestHandlers.withLock { requestHandlers in - let requestHandlerIndexAndIsOneShot = requestHandlers.enumerated().compactMap { - (index, handlerAndIsOneShot) -> (RequestHandler, Int, Bool)? in - guard let handler = handlerAndIsOneShot.requestHandler as? RequestHandler else { - return nil - } - return (handler, index, handlerAndIsOneShot.isOneShot) - }.first - guard let (requestHandler, index, isOneShot) = requestHandlerIndexAndIsOneShot else { - reply(.failure(.methodNotFound(Request.method))) - return - } - reply(.success(requestHandler(params))) - if isOneShot { - requestHandlers.remove(at: index) - } - } - } - - // MARK: - Convenience functions - - /// Opens the document with the given text as the given URI. - /// - /// The version defaults to 0 and the language is inferred from the file's extension by default. - /// - /// If the text contained location markers like `1️⃣`, then these are stripped from the opened document and - /// `DocumentPositions` are returned that map these markers to their position in the source file. - @discardableResult - package func openDocument( - _ markedText: String, - uri: DocumentURI, - version: Int = 0, - language: Language? = nil - ) -> DocumentPositions { - let (markers, textWithoutMarkers) = extractMarkers(markedText) - var language = language - if language == nil { - guard let fileExtension = uri.fileURL?.pathExtension, - let inferredLanguage = Language(fileExtension: fileExtension) - else { - preconditionFailure("Unable to infer language for file \(uri)") - } - language = inferredLanguage - } - - self.send( - DidOpenTextDocumentNotification( - textDocument: TextDocumentItem( - uri: uri, - language: language!, - version: version, - text: textWithoutMarkers - ) - ) - ) - - return DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers) - } -} - -// MARK: - DocumentPositions - -/// Maps location marker like `1️⃣` to their position within a source file. -package struct DocumentPositions { - private let positions: [String: Position] - - package init(markers: [String: Int], textWithoutMarkers: String) { - if markers.isEmpty { - // No need to build a line table if we don't have any markers. - positions = [:] - return - } - - let lineTable = LineTable(textWithoutMarkers) - positions = markers.mapValues { offset in - let (line, column) = lineTable.lineAndUTF16ColumnOf(utf8Offset: offset) - return Position(line: line, utf16index: column) - } - } - - package init(markedText: String) { - let (markers, textWithoutMarker) = extractMarkers(markedText) - self.init(markers: markers, textWithoutMarkers: textWithoutMarker) - } - - fileprivate init(positions: [String: Position]) { - self.positions = positions - } - - package static func extract(from markedText: String) -> (positions: DocumentPositions, textWithoutMarkers: String) { - let (markers, textWithoutMarkers) = extractMarkers(markedText) - return (DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers), textWithoutMarkers) - } - - /// Returns the position of the given marker and traps if the document from which these `DocumentPositions` were - /// derived didn't contain the marker. - package subscript(_ marker: String) -> Position { - guard let position = positions[marker] else { - preconditionFailure("Could not find marker '\(marker)' in source code") - } - return position - } - - /// Returns all position makers within these `DocumentPositions`. - package var allMarkers: [String] { - return positions.keys.sorted() - } -} diff --git a/Sources/SKTestSupport/TextDocumentIdentifier+URI.swift b/Sources/SKTestSupport/TextDocumentIdentifier+URI.swift deleted file mode 100644 index 5f563d982..000000000 --- a/Sources/SKTestSupport/TextDocumentIdentifier+URI.swift +++ /dev/null @@ -1,28 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol - -import struct TSCBasic.AbsolutePath - -package extension TextDocumentIdentifier { - init(_ url: URL) { - self.init(DocumentURI(url)) - } -} - -package extension AbsolutePath { - var asURI: DocumentURI { - return DocumentURI(asURL) - } -} diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift deleted file mode 100644 index ff349df7c..000000000 --- a/Sources/SKTestSupport/Utils.swift +++ /dev/null @@ -1,125 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol -import SwiftExtensions - -import struct TSCBasic.AbsolutePath - -extension Language { - var fileExtension: String { - switch self { - case .objective_c: return "m" - case .markdown: return "md" - case .tutorial: return "tutorial" - default: return self.rawValue - } - } - - init?(fileExtension: String) { - switch fileExtension { - case "c": self = .c - case "cpp": self = .cpp - case "m": self = .objective_c - case "mm": self = .objective_cpp - case "swift": self = .swift - case "md": self = .markdown - case "tutorial": self = .tutorial - default: return nil - } - } -} - -extension DocumentURI { - /// Construct a `DocumentURI` by creating a unique URI for a document of the given language. - package init(for language: Language, testName: String = #function) { - let testBaseName = testName.prefix(while: \.isLetter) - - #if os(Windows) - let url = URL(fileURLWithPath: "C:/\(testBaseName)/\(UUID())/test.\(language.fileExtension)") - #else - let url = URL(fileURLWithPath: "/\(testBaseName)/\(UUID())/test.\(language.fileExtension)") - #endif - - self.init(url) - } -} - -package let cleanScratchDirectories = - (ProcessInfo.processInfo.environment["SOURCEKIT_LSP_KEEP_TEST_SCRATCH_DIR"] == nil) - -package func testScratchName(testName: String = #function) -> String { - var uuid = UUID().uuidString[...] - if let firstDash = uuid.firstIndex(of: "-") { - uuid = uuid[.. URL { - #if os(Windows) - // Use a shorter test scratch dir name on Windows to not exceed MAX_PATH length - let testScratchDirsName = "lsp-test" - #else - let testScratchDirsName = "sourcekit-lsp-test-scratch" - #endif - - let url = try FileManager.default.temporaryDirectory.realpath - .appending(component: testScratchDirsName) - .appending(component: testScratchName(testName: testName), directoryHint: .isDirectory) - - try? FileManager.default.removeItem(at: url) - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) - return url -} - -/// Execute `body` with a path to a temporary scratch directory for the given -/// test name. -/// -/// The temporary directory will be deleted at the end of `directory` unless the -/// `SOURCEKIT_LSP_KEEP_TEST_SCRATCH_DIR` environment variable is set. -package func withTestScratchDir( - _ body: (URL) async throws -> T, - testName: String = #function -) async throws -> T { - let scratchDirectory = try testScratchDir(testName: testName) - try FileManager.default.createDirectory(at: scratchDirectory, withIntermediateDirectories: true) - defer { - if cleanScratchDirectories { - try? FileManager.default.removeItem(at: scratchDirectory) - } - } - return try await body(scratchDirectory) -} - -var globalModuleCache: URL? { - get throws { - if let customModuleCache = ProcessInfo.processInfo.environment["SOURCEKIT_LSP_TEST_MODULE_CACHE"] { - if customModuleCache.isEmpty { - return nil - } - return URL(fileURLWithPath: customModuleCache) - } - return try FileManager.default.temporaryDirectory.realpath - .appending(components: "sourcekit-lsp-test-scratch", "shared-module-cache") - } -} diff --git a/Sources/SKTestSupport/WrappedSemaphore.swift b/Sources/SKTestSupport/WrappedSemaphore.swift deleted file mode 100644 index 370ace05e..000000000 --- a/Sources/SKTestSupport/WrappedSemaphore.swift +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Dispatch -import XCTest - -/// Wrapper around `DispatchSemaphore` so that Swift Concurrency doesn't complain about the usage of semaphores in the -/// tests. -/// -/// This should only be used for tests that test priority escalation and thus cannot await a `Task` (which would cause -/// priority elevations). -package struct WrappedSemaphore: Sendable { - private let name: String - private let semaphore = DispatchSemaphore(value: 0) - - package init(name: String) { - self.name = name - } - - package func signal(value: Int = 1) { - for _ in 0.. DispatchTimeoutResult { - semaphore.wait(timeout: timeout) - } - - /// Wait for a signal and throw an error if the semaphore is not signaled within `timeout`. - package func waitOrThrow(timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout))) throws { - struct TimeoutError: Error, CustomStringConvertible { - let name: String - var description: String { "\(name) timed out" } - } - switch self.wait(timeout: timeout) { - case .success: - break - case .timedOut: - throw TimeoutError(name: name) - } - } - - /// Wait for a signal and emit an XCTFail if the semaphore is not signaled within `timeout`. - package func waitOrXCTFail( - timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout)), - file: StaticString = #filePath, - line: UInt = #line - ) { - switch self.wait(timeout: timeout) { - case .success: - break - case .timedOut: - XCTFail("\(name) timed out", file: file, line: line) - } - } -} diff --git a/Sources/SKUtilities/CMakeLists.txt b/Sources/SKUtilities/CMakeLists.txt deleted file mode 100644 index 8c32531cb..000000000 --- a/Sources/SKUtilities/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -set(sources - Debouncer.swift - Dictionary+InitWithElementsKeyedBy.swift - LineTable.swift - LRUCache.swift -) - -add_library(SKUtilities STATIC ${sources}) -set_target_properties(SKUtilities PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(SKUtilities PRIVATE - SKLogging - SwiftExtensions - $<$>:Foundation>) - -add_library(SKUtilitiesForPlugin STATIC ${sources}) -set_target_properties(SKUtilitiesForPlugin PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_compile_options(SKUtilitiesForPlugin PRIVATE - $<$: - "SHELL:-module-alias SKLogging=SKLoggingForPlugin" - "SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin" - >) -target_link_libraries(SKUtilitiesForPlugin PRIVATE - SKLoggingForPlugin - SwiftExtensionsForPlugin - $<$>:Foundation>) diff --git a/Sources/SKUtilities/Debouncer.swift b/Sources/SKUtilities/Debouncer.swift deleted file mode 100644 index 881c650d4..000000000 --- a/Sources/SKUtilities/Debouncer.swift +++ /dev/null @@ -1,93 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// Debounces calls to a function/closure. If multiple calls to the closure are made, it allows aggregating the -/// parameters. -package actor Debouncer { - /// How long to wait for further `scheduleCall` calls before committing to actually calling `makeCall`. - private let debounceDuration: Duration - - /// When `scheduleCall` is called while another `scheduleCall` was waiting to commit its call, combines the parameters - /// of those two calls. - /// - /// ### Example - /// - /// Two `scheduleCall` calls that are made within a time period shorter than `debounceDuration` like the following - /// ```swift - /// debouncer.scheduleCall(5) - /// debouncer.scheduleCall(10) - /// ``` - /// will call `combineParameters(5, 10)` - private let combineParameters: (Parameter, Parameter) -> Parameter - - /// After the debounce duration has elapsed, commit the call. - private let makeCall: (Parameter) async -> Void - - /// In the time between the call to `scheduleCall` and the call actually being committed (ie. in the time that the - /// call can be debounced), the task that would commit the call (unless cancelled), the parameter with which this - /// call should be made and the time at which the call should be made. Keeping track of the time ensures that we don't - /// indefinitely debounce if a new `scheduleCall` is made every 0.4s but we debounce for 0.5s. - private var inProgressData: (parameter: Parameter, targetDate: ContinuousClock.Instant, task: Task)? - - package init( - debounceDuration: Duration, - combineResults: @escaping (Parameter, Parameter) -> Parameter, - makeCall: @Sendable @escaping (Parameter) async -> Void - ) { - self.debounceDuration = debounceDuration - self.combineParameters = combineResults - self.makeCall = makeCall - } - - /// Schedule a debounced call. If `scheduleCall` is called within `debounceDuration`, the parameters of the two - /// `scheduleCall` calls will be combined using `combineParameters` and the new debounced call will be scheduled - /// `debounceDuration` after the second `scheduleCall` call. - package func scheduleCall(_ parameter: Parameter) { - var parameter = parameter - var targetDate = ContinuousClock.now + debounceDuration - if let (inProgressParameter, inProgressTargetDate, inProgressTask) = inProgressData { - inProgressTask.cancel() - parameter = combineParameters(inProgressParameter, parameter) - targetDate = inProgressTargetDate - } - let task = Task { - do { - try await Task.sleep(until: targetDate) - try Task.checkCancellation() - } catch { - return - } - await self.flush() - } - inProgressData = (parameter, ContinuousClock.now + debounceDuration, task) - } - - /// If any debounced calls are in progress, make them now, even if the debounce duration hasn't expired yet. - package func flush() async { - guard let inProgressDataValue = inProgressData else { - return - } - inProgressData = nil - let parameter = inProgressDataValue.parameter - await makeCall(parameter) - } -} - -extension Debouncer { - package init(debounceDuration: Duration, _ makeCall: @Sendable @escaping () async -> Void) { - self.init(debounceDuration: debounceDuration, combineResults: { _, _ in }, makeCall: makeCall) - } - - package func scheduleCall() { - self.scheduleCall(()) - } -} diff --git a/Sources/SKUtilities/Dictionary+InitWithElementsKeyedBy.swift b/Sources/SKUtilities/Dictionary+InitWithElementsKeyedBy.swift deleted file mode 100644 index 223b106fc..000000000 --- a/Sources/SKUtilities/Dictionary+InitWithElementsKeyedBy.swift +++ /dev/null @@ -1,32 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SKLogging - -extension Dictionary { - /// Create a new dictionary from the given elements, assuming that they all have a unique value identified by `keyedBy`. - /// If two elements have the same key, log an error and choose the first value with that key. - public init(elements: some Sequence, keyedBy: KeyPath) { - self = [:] - self.reserveCapacity(elements.underestimatedCount) - for element in elements { - let key = element[keyPath: keyedBy] - if let existingElement = self[key] { - logger.error( - "Found duplicate key \(String(describing: key)): \(String(describing: existingElement)) vs. \(String(describing: element))" - ) - continue - } - self[key] = element - } - } -} diff --git a/Sources/SKUtilities/LRUCache.swift b/Sources/SKUtilities/LRUCache.swift deleted file mode 100644 index f20e809ac..000000000 --- a/Sources/SKUtilities/LRUCache.swift +++ /dev/null @@ -1,140 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A cache that stores key-value pairs up to a given capacity. -/// -/// The least recently used key-value pair is removed when the cache exceeds its capacity. -package struct LRUCache { - private struct Priority { - var next: Key? - var previous: Key? - - init(next: Key? = nil, previous: Key? = nil) { - self.next = next - self.previous = previous - } - } - - // The hash map for accessing cached key-value pairs. - private var cache: [Key: Value] - - // Doubly linked list of priorities keeping track of the first and last entries. - private var priorities: [Key: Priority] - private var firstPriority: Key? = nil - private var lastPriority: Key? = nil - - /// The maximum number of key-value pairs that can be stored in the cache. - package let capacity: Int - - /// The number of key-value pairs within the cache. - package var count: Int { cache.count } - - /// A collection containing just the keys of the cache. - /// - /// - Note: Keys will **not** be in the same order that they were added to the cache. - package var keys: some Collection { cache.keys } - - /// A collection containing just the values of the cache. - /// - /// - Note: Values will **not** be in the same order that they were added to the cache. - package var values: some Collection { cache.values } - - package init(capacity: Int) { - precondition(capacity > 0, "LRUCache capacity must be greater than 0") - self.capacity = capacity - self.cache = Dictionary(minimumCapacity: capacity) - self.priorities = Dictionary(minimumCapacity: capacity) - } - - /// Adds the given key as the first priority in the doubly linked list of priorities. - private mutating func addPriority(forKey key: Key) { - // Make sure the key doesn't already exist in the list - removePriority(forKey: key) - - guard let currentFirstPriority = firstPriority else { - firstPriority = key - lastPriority = key - priorities[key] = Priority() - return - } - priorities[key] = Priority(next: currentFirstPriority) - priorities[currentFirstPriority]?.previous = key - firstPriority = key - } - - /// Removes the given key from the doubly linked list of priorities. - private mutating func removePriority(forKey key: Key) { - guard let priority = priorities.removeValue(forKey: key) else { - return - } - // Update the first and last priorities - if firstPriority == key { - firstPriority = priority.next - } - if lastPriority == key { - lastPriority = priority.previous - } - // Update the previous and next keys in the priority list - if let previousPriority = priority.previous { - priorities[previousPriority]?.next = priority.next - } - if let nextPriority = priority.next { - priorities[nextPriority]?.previous = priority.previous - } - } - - /// Removes all key-value pairs from the cache. - package mutating func removeAll() { - cache.removeAll() - priorities.removeAll() - firstPriority = nil - lastPriority = nil - } - - /// Removes all the elements that satisfy the given predicate. - package mutating func removeAll(where shouldBeRemoved: (_ key: Key) throws -> Bool) rethrows { - cache = try cache.filter { entry in - guard try shouldBeRemoved(entry.key) else { - return true - } - removePriority(forKey: entry.key) - return false - } - } - - /// Removes the given key and its associated value from the cache. - /// - /// Returns the value that was associated with the key. - @discardableResult - package mutating func removeValue(forKey key: Key) -> Value? { - removePriority(forKey: key) - return cache.removeValue(forKey: key) - } - - package subscript(key: Key) -> Value? { - mutating _read { - addPriority(forKey: key) - yield cache[key] - } - set { - guard let newValue else { - removeValue(forKey: key) - return - } - cache[key] = newValue - addPriority(forKey: key) - if cache.count > capacity, let lastPriority { - removeValue(forKey: lastPriority) - } - } - } -} diff --git a/Sources/SKUtilities/LineTable.swift b/Sources/SKUtilities/LineTable.swift deleted file mode 100644 index 4e5ac8413..000000000 --- a/Sources/SKUtilities/LineTable.swift +++ /dev/null @@ -1,503 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SKLogging - -#if canImport(os) -import os -#endif - -package struct LineTable: Hashable, Sendable { - @usableFromInline - var impl: [String.Index] - - package var content: String - - package init(_ string: String) { - content = string - - var i = string.startIndex - impl = [i] - while i != string.endIndex { - let c = string[i] - string.formIndex(after: &i) - if c == "\n" || c == "\r\n" || c == "\r" { - impl.append(i) - } - } - } - - /// The number of lines in the line table. - package var lineCount: Int { impl.count } - - /// Translate String.Index to logical line/utf16 pair. - package func lineAndUTF16ColumnOf(_ index: String.Index, fromLine: Int = 0) -> (line: Int, utf16Column: Int) { - precondition(0 <= fromLine && fromLine < impl.count) - - // Binary search. - var lower = fromLine - var upper = impl.count - while true { - let mid = lower + (upper - lower) / 2 - let lineStartIndex = impl[mid] - if mid == lower || lineStartIndex == index { - return ( - line: mid, - utf16Column: content.utf16.distance(from: lineStartIndex, to: index) - ) - } else if lineStartIndex < index { - lower = mid - } else { - upper = mid - } - } - } -} - -extension LineTable { - - // MARK: - Editing - - /// Replace the line table's `content` in the given range and update the line data. - /// - /// - parameter fromLine: Starting line number (zero-based). - /// - parameter fromOff: Starting UTF-16 column offset (zero-based). - /// - parameter toLine: Ending line number (zero-based). - /// - parameter toOff: Ending UTF-16 column offset (zero-based). - /// - parameter replacement: The new text for the given range. - @inlinable - mutating package func replace( - fromLine: Int, - utf16Offset fromOff: Int, - toLine: Int, - utf16Offset toOff: Int, - with replacement: String - ) { - let start = self.stringIndexOf(line: fromLine, utf16Column: fromOff) - let end = self.stringIndexOf(line: toLine, utf16Column: toOff) - - var newText = self.content - newText.replaceSubrange(start..= lowerBound. - var utf8Range: (lower: Int, upper: Int) - var utf8Bounds: (lower: Int, upper: Int) - - var description: String { - """ - \(utf8Range.lower)..<\(utf8Range.upper) is out of bounds \ - \(utf8Bounds.lower)..<\(utf8Bounds.upper) - """ - } - - var redactedDescription: String { - description - } - } - - /// Replace the line table's `content` in the given range and update the line data. - /// If the given range is out-of-bounds, throws an error. - /// - /// - parameter utf8Offset: Starting UTF-8 offset (zero-based). - /// - parameter length: UTF-8 length. - /// - parameter replacement: The new text for the given range. - @inlinable - mutating package func tryReplace( - utf8Offset fromOff: Int, - length: Int, - with replacement: String - ) throws { - let utf8 = self.content.utf8 - guard - fromOff >= 0, length >= 0, - let start = utf8.index(utf8.startIndex, offsetBy: fromOff, limitedBy: utf8.endIndex), - let end = utf8.index(start, offsetBy: length, limitedBy: utf8.endIndex) - else { - throw OutOfBoundsError( - utf8Range: (lower: fromOff, upper: fromOff + length), - utf8Bounds: (lower: 0, upper: utf8.count) - ) - } - - var newText = self.content - newText.replaceSubrange(start.. String.Index - - /// Result of `lineSlice(at:)` - @usableFromInline - enum LineSliceResult { - /// The line index passed to `lineSlice(at:)` was negative. - case beforeFirstLine - /// The contents of the line at the index passed to `lineSlice(at:)`. - case line(Substring) - /// The line index passed to `lineSlice(at:)` was after the last line of the file - case afterLastLine - } - - /// Extracts the contents of the line at the given index. - /// - /// If `line` is out-of-bounds, logs a fault and returns either `beforeFirstLine` or `afterLastLine`. - @usableFromInline - func lineSlice(at line: Int, callerFile: StaticString, callerLine: UInt) -> LineSliceResult { - guard line >= 0 else { - logger.fault( - """ - Line \(line) is negative (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return .beforeFirstLine - } - guard line < impl.count else { - logger.fault( - """ - Line \(line) is out-of range (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return .afterLastLine - } - let start = impl[line] - let end: String.Index - if line + 1 < impl.count { - end = impl[line + 1] - } else { - end = content.endIndex - } - - return .line(content[start.. Substring? { - switch lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { - case .beforeFirstLine, .afterLastLine: - return nil - case .line(let line): - return line - } - } - - /// Converts the given UTF-16-based `line:column`` position to a `String.Index`. - /// - /// If the position does not refer to a valid position with in the source file, returns the closest valid position and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf16Column: UTF-16 column offset (zero-based). - @inlinable - package func stringIndexOf( - line: Int, - utf16Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> String.Index { - let lineSlice: Substring - switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { - case .beforeFirstLine: - return self.content.startIndex - case .afterLastLine: - return self.content.endIndex - case .line(let line): - lineSlice = line - } - guard utf16Column >= 0 else { - logger.fault( - """ - Column is negative while converting \(line):\(utf16Column) (UTF-16) to String.Index \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.startIndex - } - guard let index = content.utf16.index(lineSlice.startIndex, offsetBy: utf16Column, limitedBy: lineSlice.endIndex) - else { - logger.fault( - """ - Column is past line end while converting \(line):\(utf16Column) (UTF-16) to String.Index \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.endIndex - } - return index - } - - /// Converts the given UTF-8-based `line:column`` position to a `String.Index`. - /// - /// If the position does not refer to a valid position with in the source file, returns the closest valid position and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf8Column: UTF-8 column offset (zero-based). - @inlinable - package func stringIndexOf( - line: Int, - utf8Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> String.Index { - let lineSlice: Substring - switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { - case .beforeFirstLine: - return self.content.startIndex - case .afterLastLine: - return self.content.endIndex - case .line(let line): - lineSlice = line - } - - guard utf8Column >= 0 else { - logger.fault( - """ - Column is negative while converting \(line):\(utf8Column) (UTF-8) to String.Index. \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.startIndex - } - guard let index = content.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex) - else { - logger.fault( - """ - Column is after end of line while converting \(line):\(utf8Column) (UTF-8) to String.Index. \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.endIndex - } - - return index - } - - // MARK: line:column <-> UTF-8 offset - - /// Converts the given UTF-16-based `line:column`` position to a UTF-8 offset within the source file. - /// - /// If the position does not refer to a valid position with in the source file, returns the closest valid offset and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf16Column: UTF-16 column offset (zero-based). - @inlinable - package func utf8OffsetOf( - line: Int, - utf16Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Int { - let stringIndex = stringIndexOf( - line: line, - utf16Column: utf16Column, - callerFile: callerFile, - callerLine: callerLine - ) - return content.utf8.distance(from: content.startIndex, to: stringIndex) - } - - /// Converts the given UTF-8-based `line:column` position to a UTF-8 offset within the source file. - /// - /// If the position does not refer to a valid position with in the source file, returns the closest valid offset and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf8Column: UTF-8 column offset (zero-based). - @inlinable - package func utf8OffsetOf( - line: Int, - utf8Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Int { - let stringIndex = stringIndexOf( - line: line, - utf8Column: utf8Column, - callerFile: callerFile, - callerLine: callerLine - ) - return content.utf8.distance(from: content.startIndex, to: stringIndex) - } - - /// Converts the given UTF-8 offset to a zero-based UTF-16 line:column pair. - /// - /// If the position does not refer to a valid position with in the snapshot, returns the closest valid line:column - /// pair and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter utf8Offset: UTF-8 buffer offset (zero-based). - @inlinable - package func lineAndUTF16ColumnOf( - utf8Offset: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> (line: Int, utf16Column: Int) { - guard utf8Offset >= 0 else { - logger.fault( - """ - UTF-8 offset \(utf8Offset) is negative while converting it to UTF-16 line:column \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return (line: 0, utf16Column: 0) - } - guard utf8Offset <= content.utf8.count else { - logger.fault( - """ - UTF-8 offset \(utf8Offset) is past the end of the file while converting it to UTF-16 line:column \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineAndUTF16ColumnOf(content.endIndex) - } - return lineAndUTF16ColumnOf(content.utf8.index(content.startIndex, offsetBy: utf8Offset)) - } - - /// Converts the given UTF-8-based line:column position to the UTF-8 offset of that position within the source file. - /// - /// If the position does not refer to a valid position with in the snapshot, returns the closest valid line:colum pair - /// and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - @inlinable func lineAndUTF8ColumnOf( - utf8Offset: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> (line: Int, utf8Column: Int) { - let (line, utf16Column) = lineAndUTF16ColumnOf( - utf8Offset: utf8Offset, - callerFile: callerFile, - callerLine: callerLine - ) - let utf8Column = utf8ColumnAt( - line: line, - utf16Column: utf16Column, - callerFile: callerFile, - callerLine: callerLine - ) - return (line, utf8Column) - } - - // MARK: UTF-8 line:column <-> UTF-16 line:column - - /// Returns UTF-16 column offset at UTF-8 based `line:column` position. - /// - /// If the position does not refer to a valid position with in the snapshot, performs a best-effort recovery and logs - /// a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf8Column: UTF-8 column offset (zero-based). - @inlinable - package func utf16ColumnAt( - line: Int, - utf8Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Int { - let lineSlice: Substring - switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { - case .beforeFirstLine, .afterLastLine: - // This line is out-of-bounds. `lineSlice(at:)` already logged a fault. - // Recovery by assuming that UTF-8 and UTF-16 columns are similar. - return utf8Column - case .line(let line): - lineSlice = line - } - guard - let stringIndex = lineSlice.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex) - else { - logger.fault( - """ - UTF-8 column is past the end of the line while getting UTF-16 column of \(line):\(utf8Column) \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.utf16.count - } - return lineSlice.utf16.distance(from: lineSlice.startIndex, to: stringIndex) - } - - /// Returns UTF-8 column offset at UTF-16 based `line:column` position. - /// - /// If the position does not refer to a valid position with in the snapshot, performs a bets-effort recovery and logs - /// a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - /// - /// - parameter line: Line number (zero-based). - /// - parameter utf16Column: UTF-16 column offset (zero-based). - @inlinable - package func utf8ColumnAt( - line: Int, - utf16Column: Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Int { - let lineSlice: Substring - switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { - case .beforeFirstLine, .afterLastLine: - // This line is out-of-bounds. `lineSlice` already logged a fault. - // Recovery by assuming that UTF-8 and UTF-16 columns are similar. - return utf16Column - case .line(let line): - lineSlice = line - } - guard - let stringIndex = lineSlice.utf16.index( - lineSlice.startIndex, - offsetBy: utf16Column, - limitedBy: lineSlice.endIndex - ) - else { - logger.fault( - """ - UTF-16 column is past the end of the line while getting UTF-8 column of \(line):\(utf16Column) \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return lineSlice.utf8.count - } - return lineSlice.utf8.distance(from: lineSlice.startIndex, to: stringIndex) - } -} diff --git a/Sources/SKUtilitiesForPlugin b/Sources/SKUtilitiesForPlugin deleted file mode 120000 index 90700c1d9..000000000 --- a/Sources/SKUtilitiesForPlugin +++ /dev/null @@ -1 +0,0 @@ -SKUtilities \ No newline at end of file diff --git a/Sources/SemanticIndex/CMakeLists.txt b/Sources/SemanticIndex/CMakeLists.txt deleted file mode 100644 index 6f72105cd..000000000 --- a/Sources/SemanticIndex/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ - -add_library(SemanticIndex STATIC - CheckedIndex.swift - IndexTaskDescription.swift - IndexHooks.swift - PreparationTaskDescription.swift - SemanticIndexManager.swift - TaskScheduler.swift - UpdateIndexStoreTaskDescription.swift - UpToDateTracker.swift -) -set_target_properties(SemanticIndex PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(SemanticIndex PRIVATE - BuildServerIntegration - LanguageServerProtocol - SKLogging - SwiftExtensions - ToolchainRegistry - TSCExtensions - IndexStoreDB - $<$>:Foundation>) diff --git a/Sources/SemanticIndex/CheckedIndex.swift b/Sources/SemanticIndex/CheckedIndex.swift deleted file mode 100644 index 4077cb482..000000000 --- a/Sources/SemanticIndex/CheckedIndex.swift +++ /dev/null @@ -1,583 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -package import Foundation -@preconcurrency package import IndexStoreDB -package import LanguageServerProtocol -import SKLogging -import SwiftExtensions - -/// Essentially a `DocumentManager` from the `SourceKitLSP` module. -/// -/// Protocol is needed because the `SemanticIndex` module is lower-level than the `SourceKitLSP` module. -package protocol InMemoryDocumentManager { - /// Returns true if the file at the given URI has a different content in the document manager than on-disk. This is - /// the case if the user made edits to the file but didn't save them yet. - func fileHasInMemoryModifications(_ uri: DocumentURI) -> Bool -} - -package enum IndexCheckLevel { - /// Consider the index out-of-date only if the source file has been deleted on disk. - /// - /// This is usually a good default because: When a file gets modified, it's likely that some of the line:column - /// locations in it are still correct – eg. if only one line is modified and if lines are inserted/deleted all - /// locations above are still correct. - /// For locations that are out of date, showing stale results is one of the best ways of communicating to the user - /// that the index is out-of-date and that they need to rebuild. We might want to reconsider this default when we have - /// background indexing. - case deletedFiles - - /// Consider the index out-of-date if the source file has been deleted or modified on disk. - case modifiedFiles - - /// Consider the index out-of-date if the source file has been deleted or modified on disk or if there are - /// in-memory modifications in the given `DocumentManager`. - case inMemoryModifiedFiles(InMemoryDocumentManager) -} - -/// A wrapper around `IndexStoreDB` that checks if returned symbol occurrences are up-to-date with regard to a -/// `IndexCheckLevel`. -/// -/// - SeeAlso: Comment on `IndexOutOfDateChecker` -package final class CheckedIndex { - private var checker: IndexOutOfDateChecker - package let unchecked: UncheckedIndex - private var index: IndexStoreDB { unchecked.underlyingIndexStoreDB } - - /// Maps the USR of a symbol to its name and the name of all its containers, from outermost to innermost. - /// - /// It is important that we cache this because we might find a lot of symbols in the same container for eg. workspace - /// symbols (eg. consider many symbols in the same C++ namespace). If we didn't cache this value, then we would need - /// to perform a `primaryDefinitionOrDeclarationOccurrence` lookup for all of these containers, which is expensive. - /// - /// Since we don't expect `CheckedIndex` to be outlive a single request it is acceptable to cache these results - /// without having any invalidation logic (similar to how we don't invalide results cached in - /// `IndexOutOfDateChecker`). - /// - /// ### Examples - /// If we have - /// ```swift - /// struct Foo {} - /// ``` then - /// `containerNamesCache[]` will be `["Foo"]`. - /// - /// If we have - /// ```swift - /// struct Bar { - /// struct Foo {} - /// } - /// ```, then - /// `containerNamesCache[]` will be `["Bar", "Foo"]`. - private var containerNamesCache: [String: [String]] = [:] - - fileprivate init(unchecked: UncheckedIndex, checkLevel: IndexCheckLevel) { - self.unchecked = unchecked - self.checker = IndexOutOfDateChecker(checkLevel: checkLevel) - } - - @discardableResult - package func forEachSymbolOccurrence( - byUSR usr: String, - roles: SymbolRole, - _ body: (SymbolOccurrence) -> Bool - ) -> Bool { - index.forEachSymbolOccurrence(byUSR: usr, roles: roles) { occurrence in - guard self.checker.isUpToDate(occurrence.location) else { - return true // continue - } - return body(occurrence) - } - } - - package func occurrences(ofUSR usr: String, roles: SymbolRole) -> [SymbolOccurrence] { - return index.occurrences(ofUSR: usr, roles: roles).filter { checker.isUpToDate($0.location) } - } - - package func occurrences(relatedToUSR usr: String, roles: SymbolRole) -> [SymbolOccurrence] { - return index.occurrences(relatedToUSR: usr, roles: roles).filter { checker.isUpToDate($0.location) } - } - - @discardableResult package func forEachCanonicalSymbolOccurrence( - containing pattern: String, - anchorStart: Bool, - anchorEnd: Bool, - subsequence: Bool, - ignoreCase: Bool, - body: (SymbolOccurrence) -> Bool - ) -> Bool { - index.forEachCanonicalSymbolOccurrence( - containing: pattern, - anchorStart: anchorStart, - anchorEnd: anchorEnd, - subsequence: subsequence, - ignoreCase: ignoreCase - ) { occurrence in - guard self.checker.isUpToDate(occurrence.location) else { - return true // continue - } - return body(occurrence) - } - } - - @discardableResult package func forEachCanonicalSymbolOccurrence( - byName name: String, - body: (SymbolOccurrence) -> Bool - ) -> Bool { - index.forEachCanonicalSymbolOccurrence(byName: name) { occurrence in - guard self.checker.isUpToDate(occurrence.location) else { - return true // continue - } - return body(occurrence) - } - } - - package func symbols(inFilePath path: String) -> [Symbol] { - guard self.hasAnyUpToDateUnit(for: DocumentURI(filePath: path, isDirectory: false)) else { - return [] - } - return index.symbols(inFilePath: path) - } - - /// Returns all unit test symbol in unit files that reference one of the main files in `mainFilePaths`. - package func unitTests(referencedByMainFiles mainFilePaths: [String]) -> [SymbolOccurrence] { - return index.unitTests(referencedByMainFiles: mainFilePaths).filter { checker.isUpToDate($0.location) } - } - - /// Returns all unit test symbols in the index. - package func unitTests() -> [SymbolOccurrence] { - return index.unitTests().filter { checker.isUpToDate($0.location) } - } - - /// Return `true` if a unit file has been indexed for the given file path after its last modification date. - /// - /// If `mainFile` is passed, then `url` is a header file that won't have a unit associated with it. `mainFile` is - /// - /// This means that at least a single build configuration of this file has been indexed since its last modification. - /// This method does not care about which target (identified by output path in the index) produced the up-to-date - /// unit. - package func hasAnyUpToDateUnit(for uri: DocumentURI, mainFile: DocumentURI? = nil) -> Bool { - return checker.indexHasUpToDateUnit(for: uri, mainFile: mainFile, index: index) - } - - /// Return `true` if a unit file with the given output path has been indexed after its last modification date of - /// `uri`. - /// - /// If `outputPath` is `notSupported`, this behaves the same as `hasAnyUpToDateUnit`. - package func hasUpToDateUnit(for uri: DocumentURI, mainFile: DocumentURI? = nil, outputPath: OutputPath) -> Bool { - switch outputPath { - case .path(let outputPath): - return checker.indexHasUpToDateUnit(for: uri, outputPath: outputPath, index: index) - case .notSupported: - return self.hasAnyUpToDateUnit(for: uri, mainFile: mainFile) - } - } - - /// Returns true if the file at the given URI has a different content in the document manager than on-disk. This is - /// the case if the user made edits to the file but didn't save them yet. - /// - /// - Important: This must only be called on a `CheckedIndex` with a `checkLevel` of `inMemoryModifiedFiles` - package func fileHasInMemoryModifications(_ uri: DocumentURI) -> Bool { - return checker.fileHasInMemoryModifications(uri) - } - - /// Determine the modification date of the file at the given location or, if it is a symlink, the maximum modification - /// time of any hop in the symlink change until the real file. - /// - /// This uses the `CheckedIndex`'s mod date cache, so it doesn't require disk access if the modification date of the - /// file has already been computed. - /// - /// Returns `nil` if the modification date of the file could not be determined. - package func modificationDate(of uri: DocumentURI) -> Date? { - switch try? checker.modificationDate(of: uri) { - case nil, .fileDoesNotExist: return nil - case .date(let date): return date - } - } - - /// If there are any definition occurrences of the given USR, return these. - /// Otherwise return declaration occurrences. - package func definitionOrDeclarationOccurrences(ofUSR usr: String) -> [SymbolOccurrence] { - let definitions = occurrences(ofUSR: usr, roles: [.definition]) - if !definitions.isEmpty { - return definitions - } - return occurrences(ofUSR: usr, roles: [.declaration]) - } - - /// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given USR. - /// - /// If the USR has an ambiguous definition, the most important role of this function is to deterministically return - /// the same result every time. - package func primaryDefinitionOrDeclarationOccurrence(ofUSR usr: String) -> SymbolOccurrence? { - let result = definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first - if result == nil { - logger.error("Failed to find definition of \(usr) in index") - } - return result - } - - /// The names of all containers the symbol is contained in, from outermost to innermost. - /// - /// ### Examples - /// In the following, the container names of `test` are `["Foo"]`. - /// ```swift - /// struct Foo { - /// func test() {} - /// } - /// ``` - /// - /// In the following, the container names of `test` are `["Bar", "Foo"]`. - /// ```swift - /// struct Bar { - /// struct Foo { - /// func test() {} - /// } - /// } - /// ``` - package func containerNames(of symbol: SymbolOccurrence) -> [String] { - // The container name of accessors is the container of the surrounding variable. - let accessorOf = symbol.relations.filter { $0.roles.contains(.accessorOf) } - if let primaryVariable = accessorOf.sorted().first { - if accessorOf.count > 1 { - logger.fault("Expected an occurrence to an accessor of at most one symbol, not multiple") - } - if let primaryVariable = primaryDefinitionOrDeclarationOccurrence(ofUSR: primaryVariable.symbol.usr) { - return containerNames(of: primaryVariable) - } - } - - let containers = symbol.relations.filter { $0.roles.contains(.childOf) } - if containers.count > 1 { - logger.fault("Expected an occurrence to a child of at most one symbol, not multiple") - } - let container = containers.filter { - switch $0.symbol.kind { - case .module, .namespace, .enum, .struct, .class, .protocol, .extension, .union: - return true - case .unknown, .namespaceAlias, .macro, .typealias, .function, .variable, .field, .enumConstant, - .instanceMethod, .classMethod, .staticMethod, .instanceProperty, .classProperty, .staticProperty, .constructor, - .destructor, .conversionFunction, .parameter, .using, .concept, .commentTag: - return false - } - }.sorted().first - - guard var containerSymbol = container?.symbol else { - return [] - } - if let cached = containerNamesCache[containerSymbol.usr] { - return cached - } - - if containerSymbol.kind == .extension, - let extendedSymbol = self.occurrences(relatedToUSR: containerSymbol.usr, roles: .extendedBy).first?.symbol - { - containerSymbol = extendedSymbol - } - let result: [String] - - // Use `forEachSymbolOccurrence` instead of `primaryDefinitionOrDeclarationOccurrence` to get a symbol occurrence - // for the container because it can be significantly faster: Eg. when searching for a C++ namespace (such as `llvm`), - // it may be declared in many files. Finding the canonical definition means that we would need to scan through all - // of these files. But we expect all all of these declarations to have the same parent container names and we don't - // care about locations here. - var containerDefinition: SymbolOccurrence? - forEachSymbolOccurrence(byUSR: containerSymbol.usr, roles: [.definition, .declaration]) { occurrence in - containerDefinition = occurrence - return false // stop iteration - } - if let containerDefinition { - result = self.containerNames(of: containerDefinition) + [containerSymbol.name] - } else { - result = [containerSymbol.name] - } - containerNamesCache[containerSymbol.usr] = result - return result - } -} - -/// A wrapper around `IndexStoreDB` that allows the retrieval of a `CheckedIndex` with a specified check level or the -/// access of the underlying `IndexStoreDB`. This makes sure that accesses to the raw `IndexStoreDB` are explicit (by -/// calling `underlyingIndexStoreDB`) and we don't accidentally call into the `IndexStoreDB` when we wanted a -/// `CheckedIndex`. -package final actor UncheckedIndex: Sendable { - package nonisolated let underlyingIndexStoreDB: IndexStoreDB - - /// Whether the underlying `IndexStoreDB` uses has `useExplicitOutputUnits` enabled and thus needs to receive updates - /// updates as output paths are added or removed from the project. - package let usesExplicitOutputPaths: Bool - - /// The set of unit output paths that are currently registered in the underlying `IndexStoreDB`. - private var unitOutputPaths: Set = [] - - package init?(_ index: IndexStoreDB?, usesExplicitOutputPaths: Bool) { - guard let index else { - return nil - } - self.usesExplicitOutputPaths = usesExplicitOutputPaths - self.underlyingIndexStoreDB = index - } - - /// Update the set of output paths that should be considered visible in the project. For example, if a source file is - /// removed from all targets in the project but remains on disk, this allows the index to start excluding it. - package func setUnitOutputPaths(_ paths: Set) { - guard usesExplicitOutputPaths else { - return - } - let addedPaths = paths.filter { !unitOutputPaths.contains($0) } - let removedPaths = unitOutputPaths.filter { !paths.contains($0) } - underlyingIndexStoreDB.addUnitOutFilePaths(Array(addedPaths), waitForProcessing: false) - underlyingIndexStoreDB.removeUnitOutFilePaths(Array(removedPaths), waitForProcessing: false) - self.unitOutputPaths = paths - } - - package nonisolated func checked(for checkLevel: IndexCheckLevel) -> CheckedIndex { - return CheckedIndex(unchecked: self, checkLevel: checkLevel) - } - - /// Wait for IndexStoreDB to be updated based on new unit files written to disk. - package nonisolated func pollForUnitChangesAndWait() { - self.underlyingIndexStoreDB.pollForUnitChangesAndWait() - } - - /// Import the units for the given output paths into indexstore-db. Returns after the import has finished. - package nonisolated func processUnitsForOutputPathsAndWait(_ outputPaths: some Collection) { - self.underlyingIndexStoreDB.processUnitsForOutputPathsAndWait(outputPaths) - } -} - -/// Helper class to check if symbols from the index are up-to-date or if the source file has been modified after it was -/// indexed. Modifications include both changes to the file on disk as well as modifications to the file that have not -/// been saved to disk (ie. changes that only live in `DocumentManager`). -/// -/// The checker caches mod dates of source files. It should thus not be long lived. Its intended lifespan is the -/// evaluation of a single request. -private struct IndexOutOfDateChecker { - private let checkLevel: IndexCheckLevel - - /// The last modification time of a file. Can also represent the fact that the file does not exist. - enum ModificationTime { - case fileDoesNotExist - case date(Date) - } - - private enum Error: Swift.Error, CustomStringConvertible { - case fileAttributesDontHaveModificationDate - case circularSymlink(URL) - - var description: String { - switch self { - case .fileAttributesDontHaveModificationDate: - return "File attributes don't contain a modification date" - case .circularSymlink(let url): - return "Circular symlink at \(url)" - } - } - } - - /// Caches whether a document has modifications in `documentManager` that haven't been saved to disk yet. - private var fileHasInMemoryModificationsCache: [DocumentURI: Bool] = [:] - - /// Document URIs to modification times that have already been computed. - private var modTimeCache: [DocumentURI: ModificationTime] = [:] - - /// Document URIs to whether they exist on the file system - private var fileExistsCache: [DocumentURI: Bool] = [:] - - init(checkLevel: IndexCheckLevel) { - self.checkLevel = checkLevel - } - - // MARK: - Public interface - - /// Returns `true` if the source file for the given symbol location exists and has not been modified after it has been - /// indexed. - mutating func isUpToDate(_ symbolLocation: SymbolLocation) -> Bool { - let uri = DocumentURI(filePath: symbolLocation.path, isDirectory: false) - switch checkLevel { - case .inMemoryModifiedFiles(let documentManager): - if fileHasInMemoryModifications(uri, documentManager: documentManager) { - return false - } - fallthrough - case .modifiedFiles: - do { - let sourceFileModificationDate = try modificationDate(of: uri) - switch sourceFileModificationDate { - case .fileDoesNotExist: - return false - case .date(let sourceFileModificationDate): - return sourceFileModificationDate <= symbolLocation.timestamp - } - } catch { - logger.fault("Unable to determine if SymbolLocation is up-to-date: \(error.forLogging)") - return true - } - case .deletedFiles: - return fileExists(at: uri) - } - } - - /// Checks if we have a unit that's up to date for the given source file, assuming that the unit in question has been - /// modified at the date returned by `unitModificationDate`. - private mutating func unitIsUpToDate(for filePath: DocumentURI, unitModificationDate: () -> Date?) -> Bool { - switch checkLevel { - case .inMemoryModifiedFiles(let documentManager): - if fileHasInMemoryModifications(filePath, documentManager: documentManager) { - // If there are in-memory modifications to the file, we can't have an up-to-date unit since we only index files - // on disk. - return false - } - // If there are no in-memory modifications check if there are on-disk modifications. - fallthrough - case .modifiedFiles: - guard let lastUnitDate = unitModificationDate() else { - return false - } - do { - let sourceModificationDate = try modificationDate(of: filePath) - switch sourceModificationDate { - case .fileDoesNotExist: - return false - case .date(let sourceModificationDate): - return sourceModificationDate <= lastUnitDate - } - } catch { - logger.fault("Unable to determine if source file has up-to-date unit: \(error.forLogging)") - return true - } - case .deletedFiles: - // If we are asked if the index has an up-to-date unit for a source file, we can reasonably assume that this - // source file exists (otherwise, why are we doing the query at all). Thus, there's nothing to check here. - return true - } - } - - /// Return `true` if a unit file has been indexed for the given file path after its last modification date. - /// - /// This means that at least a single build configuration of this file has been indexed since its last modification. - mutating func indexHasUpToDateUnit(for filePath: DocumentURI, mainFile: DocumentURI?, index: IndexStoreDB) -> Bool { - return unitIsUpToDate( - for: filePath, - unitModificationDate: { - let filePathStr = orLog("Realpath for up-to-date", { try (mainFile ?? filePath).fileURL?.realpath.filePath }) - guard let filePathStr else { - return nil - } - return index.dateOfLatestUnitFor(filePath: filePathStr) - } - ) - } - - /// Return `true` if a unit file has been indexed for the given file path after its last modification date. - /// - /// This means that at least a single build configuration of this file has been indexed since its last modification. - /// - /// If `mainFile` is passed, then `filePath` is a header file that won't have a unit associated with it. `mainFile` is - /// assumed to be a file that imports `url`. To check that `url` has an up-to-date unit, check that the latest unit - /// for `mainFile` is newer than the mtime of the header file at `url`. - mutating func indexHasUpToDateUnit(for filePath: DocumentURI, outputPath: String, index: IndexStoreDB) -> Bool { - return unitIsUpToDate(for: filePath, unitModificationDate: { index.dateOfUnitFor(outputPath: outputPath) }) - } - - // MARK: - Cached check primitives - - /// `documentManager` must always be the same between calls to `hasFileInMemoryModifications` since it is not part of - /// the cache key. This is fine because we always assume the `documentManager` to come from the associated value of - /// `CheckLevel.imMemoryModifiedFiles`, which is constant. - private mutating func fileHasInMemoryModifications( - _ uri: DocumentURI, - documentManager: InMemoryDocumentManager - ) -> Bool { - if let cached = fileHasInMemoryModificationsCache[uri] { - return cached - } - let hasInMemoryModifications = documentManager.fileHasInMemoryModifications(uri) - fileHasInMemoryModificationsCache[uri] = hasInMemoryModifications - return hasInMemoryModifications - } - - /// Returns true if the file at the given URI has a different content in the document manager than on-disk. This is - /// the case if the user made edits to the file but didn't save them yet. - /// - /// - Important: This must only be called on an `IndexOutOfDateChecker` with a `checkLevel` of `inMemoryModifiedFiles` - mutating func fileHasInMemoryModifications(_ uri: DocumentURI) -> Bool { - switch checkLevel { - case .inMemoryModifiedFiles(let documentManager): - return fileHasInMemoryModifications(uri, documentManager: documentManager) - case .modifiedFiles, .deletedFiles: - logger.fault( - "fileHasInMemoryModifications(at:) must only be called on an `IndexOutOfDateChecker` with check level .inMemoryModifiedFiles" - ) - return false - } - } - - private static func modificationDate(atPath path: String) throws -> Date { - let attributes = try FileManager.default.attributesOfItem(atPath: path) - guard let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date else { - throw Error.fileAttributesDontHaveModificationDate - } - return modificationDate - } - - private func modificationDateUncached(of uri: DocumentURI) throws -> ModificationTime { - do { - guard var fileURL = uri.fileURL else { - return .fileDoesNotExist - } - var modificationDate = try Self.modificationDate(atPath: fileURL.filePath) - - var visited: Set = [fileURL] - - // Get the maximum mtime in the symlink chain as the modification date of the URI. That way if either the symlink - // is changed to point to a different file or if the underlying file is modified, the modification time is - // updated. - while let relativeSymlinkDestination = try? FileManager.default.destinationOfSymbolicLink( - atPath: fileURL.filePath - ) { - fileURL = URL(fileURLWithPath: relativeSymlinkDestination, relativeTo: fileURL) - if !visited.insert(fileURL).inserted { - throw Error.circularSymlink(fileURL) - } - modificationDate = max(modificationDate, try Self.modificationDate(atPath: fileURL.filePath)) - } - - return .date(modificationDate) - } catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError { - return .fileDoesNotExist - } - } - - mutating func modificationDate(of uri: DocumentURI) throws -> ModificationTime { - if let cached = modTimeCache[uri] { - return cached - } - let modTime = try modificationDateUncached(of: uri) - modTimeCache[uri] = modTime - return modTime - } - - private mutating func fileExists(at uri: DocumentURI) -> Bool { - if let cached = fileExistsCache[uri] { - return cached - } - let fileExists = - if let fileUrl = uri.fileURL { - FileManager.default.fileExists(at: fileUrl) - } else { - false - } - fileExistsCache[uri] = fileExists - return fileExists - } -} diff --git a/Sources/SemanticIndex/IndexHooks.swift b/Sources/SemanticIndex/IndexHooks.swift deleted file mode 100644 index 403a5b3dc..000000000 --- a/Sources/SemanticIndex/IndexHooks.swift +++ /dev/null @@ -1,66 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import IndexStoreDB - -/// When running SourceKit-LSP in-process, allows the creator of `SourceKitLSPServer` to provide the `IndexStoreDB` -/// instead of SourceKit-LSP creating the instance when needed. -package protocol IndexInjector: Sendable { - func createIndex( - storePath: URL, - databasePath: URL, - indexStoreLibraryPath: URL, - delegate: IndexDelegate, - prefixMappings: [PathMapping] - ) async throws -> IndexStoreDB - - /// Whether the injected index uses explicit output paths and whether SourceKit-LSP should thus set the unit output - /// paths on the index as they change. - var usesExplicitOutputPaths: Bool { get async } -} - -/// Callbacks that allow inspection of internal state modifications during testing. -package struct IndexHooks: Sendable { - package var indexInjector: IndexInjector? - - package var buildGraphGenerationDidStart: (@Sendable () async -> Void)? - - package var buildGraphGenerationDidFinish: (@Sendable () async -> Void)? - - package var preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)? - - package var preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? - - package var updateIndexStoreTaskDidStart: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? - - /// A callback that is called when an index task finishes. - package var updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? - - package init( - indexInjector: IndexInjector? = nil, - buildGraphGenerationDidStart: (@Sendable () async -> Void)? = nil, - buildGraphGenerationDidFinish: (@Sendable () async -> Void)? = nil, - preparationTaskDidStart: (@Sendable (PreparationTaskDescription) async -> Void)? = nil, - preparationTaskDidFinish: (@Sendable (PreparationTaskDescription) async -> Void)? = nil, - updateIndexStoreTaskDidStart: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil, - updateIndexStoreTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) async -> Void)? = nil - ) { - self.indexInjector = indexInjector - self.buildGraphGenerationDidStart = buildGraphGenerationDidStart - self.buildGraphGenerationDidFinish = buildGraphGenerationDidFinish - self.preparationTaskDidStart = preparationTaskDidStart - self.preparationTaskDidFinish = preparationTaskDidFinish - self.updateIndexStoreTaskDidStart = updateIndexStoreTaskDidStart - self.updateIndexStoreTaskDidFinish = updateIndexStoreTaskDidFinish - } -} diff --git a/Sources/SemanticIndex/IndexTaskDescription.swift b/Sources/SemanticIndex/IndexTaskDescription.swift deleted file mode 100644 index d687561ec..000000000 --- a/Sources/SemanticIndex/IndexTaskDescription.swift +++ /dev/null @@ -1,83 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SKLogging - -/// Protocol of tasks that are executed on the index task scheduler. -/// -/// It is assumed that `IndexTaskDescription` of different types are allowed to execute in parallel. -protocol IndexTaskDescription: TaskDescriptionProtocol { - /// A string that is unique to this type of `IndexTaskDescription`. It is used to produce unique IDs for tasks of - /// different types in `AnyIndexTaskDescription` - static var idPrefix: String { get } - - var id: UInt32 { get } -} - -extension IndexTaskDescription { - func dependencies( - to currentlyExecutingTasks: [AnyIndexTaskDescription] - ) -> [TaskDependencyAction] { - return self.dependencies(to: currentlyExecutingTasks.compactMap { $0.wrapped as? Self }) - .map { - switch $0 { - case .cancelAndRescheduleDependency(let td): - return .cancelAndRescheduleDependency(AnyIndexTaskDescription(td)) - case .waitAndElevatePriorityOfDependency(let td): - return .waitAndElevatePriorityOfDependency(AnyIndexTaskDescription(td)) - } - } - - } -} - -/// Type-erased wrapper of an `IndexTaskDescription`. -package struct AnyIndexTaskDescription: TaskDescriptionProtocol { - let wrapped: any IndexTaskDescription - - init(_ wrapped: any IndexTaskDescription) { - self.wrapped = wrapped - } - - package var isIdempotent: Bool { - return wrapped.isIdempotent - } - - package var estimatedCPUCoreCount: Int { - return wrapped.estimatedCPUCoreCount - } - - package var id: String { - return "\(type(of: wrapped).idPrefix)-\(wrapped.id)" - } - - package var description: String { - return wrapped.description - } - - package var redactedDescription: String { - return wrapped.redactedDescription - } - - package func execute() async { - return await wrapped.execute() - } - - /// Forward to the underlying task to compute the dependencies. Preparation and index tasks don't have any - /// dependencies that are managed by `TaskScheduler`. `SemanticIndexManager` awaits the preparation of a target before - /// indexing files within it. - package func dependencies( - to currentlyExecutingTasks: [AnyIndexTaskDescription] - ) -> [TaskDependencyAction] { - return wrapped.dependencies(to: currentlyExecutingTasks) - } -} diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift deleted file mode 100644 index 5e5d932b1..000000000 --- a/Sources/SemanticIndex/PreparationTaskDescription.swift +++ /dev/null @@ -1,134 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -package import BuildServerProtocol -import Foundation -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process - -private let preparationIDForLogging = AtomicUInt32(initialValue: 1) - -/// Describes a task to prepare a set of targets. -/// -/// This task description can be scheduled in a `TaskScheduler`. -package struct PreparationTaskDescription: IndexTaskDescription { - package static let idPrefix = "prepare" - - package let id = preparationIDForLogging.fetchAndIncrement() - - /// The targets that should be prepared. - package let targetsToPrepare: [BuildTargetIdentifier] - - /// The build server manager that is used to get the toolchain and build settings for the files to index. - private let buildServerManager: BuildServerManager - - private let preparationUpToDateTracker: UpToDateTracker - - /// See `SemanticIndexManager.logMessageToIndexLog`. - private let logMessageToIndexLog: - @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void - - /// Hooks that should be called when the preparation task finishes. - private let hooks: IndexHooks - - /// The task is idempotent because preparing the same target twice produces the same result as preparing it once. - package var isIdempotent: Bool { true } - - package var estimatedCPUCoreCount: Int { 1 } - - package var description: String { - return self.redactedDescription - } - - package var redactedDescription: String { - return "preparation-\(id)" - } - - init( - targetsToPrepare: [BuildTargetIdentifier], - buildServerManager: BuildServerManager, - preparationUpToDateTracker: UpToDateTracker, - logMessageToIndexLog: - @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void, - hooks: IndexHooks - ) { - self.targetsToPrepare = targetsToPrepare - self.buildServerManager = buildServerManager - self.preparationUpToDateTracker = preparationUpToDateTracker - self.logMessageToIndexLog = logMessageToIndexLog - self.hooks = hooks - } - - package func execute() async { - // Only use the last two digits of the preparation ID for the logging scope to avoid creating too many scopes. - // See comment in `withLoggingScope`. - // The last 2 digits should be sufficient to differentiate between multiple concurrently running preparation operations - await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "preparation-\(id % 100)") { - let targetsToPrepare = await targetsToPrepare.asyncFilter { await !preparationUpToDateTracker.isUpToDate($0) } - // Sort targets to get deterministic ordering. The actual order does not matter. - .sorted { $0.uri.stringValue < $1.uri.stringValue } - if targetsToPrepare.isEmpty { - return - } - await hooks.preparationTaskDidStart?(self) - - let targetsToPrepareDescription = targetsToPrepare.map(\.uri.stringValue).joined(separator: ", ") - logger.log( - "Starting preparation with priority \(Task.currentPriority.rawValue, privacy: .public): \(targetsToPrepareDescription)" - ) - let signposter = Logger(subsystem: LoggingScope.subsystem, category: "preparation").makeSignposter() - let signpostID = signposter.makeSignpostID() - let state = signposter.beginInterval("Preparing", id: signpostID, "Preparing \(targetsToPrepareDescription)") - let startDate = Date() - defer { - logger.log( - "Finished preparation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(targetsToPrepareDescription)" - ) - signposter.endInterval("Preparing", state) - } - do { - try await buildServerManager.prepare(targets: Set(targetsToPrepare)) - } catch { - logger.error("Preparation failed: \(error.forLogging)") - } - await hooks.preparationTaskDidFinish?(self) - if !Task.isCancelled { - await preparationUpToDateTracker.markUpToDate(targetsToPrepare, updateOperationStartDate: startDate) - } - } - } - - package func dependencies( - to currentlyExecutingTasks: [PreparationTaskDescription] - ) -> [TaskDependencyAction] { - return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction? in - if other.targetsToPrepare.count > self.targetsToPrepare.count { - // If there is an prepare operation with more targets already running, suspend it. - // The most common use case for this is if we prepare all targets simultaneously during the initial preparation - // when a project is opened and need a single target indexed for user interaction. We should suspend the - // workspace-wide preparation and just prepare the currently needed target. - return .cancelAndRescheduleDependency(other) - } - return .waitAndElevatePriorityOfDependency(other) - } - } -} diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift deleted file mode 100644 index 49c43bae1..000000000 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ /dev/null @@ -1,1013 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -package import BuildServerProtocol -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions - -/// The logging subsystem that should be used for all index-related logging. -let indexLoggingSubsystem = "org.swift.sourcekit-lsp.indexing" - -/// A wrapper around `QueuedTask` that only allows equality comparison and inspection whether the `QueuedTask` is -/// currently executing. -/// -/// This way we can store `QueuedTask` in the `inProgress` dictionaries while guaranteeing that whoever created the -/// queued task still has exclusive ownership of the task and can thus control the task's cancellation. -private struct OpaqueQueuedIndexTask: Equatable { - private let task: QueuedTask - - var isExecuting: Bool { - task.isExecuting - } - - init(_ task: QueuedTask) { - self.task = task - } - - static func == (lhs: OpaqueQueuedIndexTask, rhs: OpaqueQueuedIndexTask) -> Bool { - return lhs.task === rhs.task - } -} - -private struct InProgressIndexStore { - enum State { - /// We know that we need to index the file but and are currently gathering all information to create the `indexTask` - /// that will index it. - /// - /// This is needed to avoid the following race: We request indexing of file A. Getting the canonical target for A - /// takes a bit and before that finishes, we request another index of A. In this case, we don't want to kick off - /// two tasks to update the index store. - case creatingIndexTask - - /// We are waiting for preparation of the file's target to be scheduled. The next step is that we wait for - /// preparation to finish before we can update the index store for this file. - /// - /// `preparationTaskID` identifies the preparation task so that we can transition a file's index state to - /// `updatingIndexStore` when its preparation task has finished. - /// - /// `indexTask` is a task that finishes after both preparation and index store update are done. Whoever owns the index - /// task is still the sole owner of it and responsible for its cancellation. - case waitingForPreparation(preparationTaskID: UUID, indexTask: Task) - - /// We have started preparing this file and are waiting for preparation to finish before we can update the index - /// store for this file. - /// - /// `preparationTaskID` identifies the preparation task so that we can transition a file's index state to - /// `updatingIndexStore` when its preparation task has finished. - /// - /// `indexTask` is a task that finishes after both preparation and index store update are done. Whoever owns the index - /// task is still the sole owner of it and responsible for its cancellation. - case preparing(preparationTaskID: UUID, indexTask: Task) - - /// The file's target has been prepared and we are updating the file's index store. - /// - /// `updateIndexStoreTask` is the task that updates the index store itself. - /// - /// `indexTask` is a task that finishes after both preparation and index store update are done. Whoever owns the index - /// task is still the sole owner of it and responsible for its cancellation. - case updatingIndexStore(updateIndexStoreTask: OpaqueQueuedIndexTask, indexTask: Task) - } - - var state: State - - /// The modification time of the time of `FileToIndex.sourceFile` at the time that indexing was scheduled. This allows - /// us to avoid scheduling another indexing operation for the file if the file hasn't been modified since an - /// in-progress indexing operation was scheduled. - /// - /// `nil` if the modification date of the file could not be determined. - var fileModificationDate: Date? -} - -/// The information that's needed to index a file within a given target. -package struct FileIndexInfo: Sendable, Hashable { - package let file: FileToIndex - package let language: Language - package let target: BuildTargetIdentifier - package let outputPath: OutputPath -} - -/// Status of document indexing / target preparation in `inProgressIndexAndPreparationTasks`. -package enum IndexTaskStatus: Comparable { - case scheduled - case executing -} - -/// The current index status that should be displayed to the editor. -/// -/// In reality, these status are not exclusive. Eg. the index might be preparing one target for editor functionality, -/// re-generating the build graph and indexing files at the same time. To avoid showing too many concurrent status -/// messages to the user, we only show the highest priority task. -package enum IndexProgressStatus: Sendable, Equatable { - case preparingFileForEditorFunctionality - case generatingBuildGraph - case indexing( - preparationTasks: [BuildTargetIdentifier: IndexTaskStatus], - indexTasks: [FileIndexInfo: IndexTaskStatus] - ) - case upToDate - - package func merging(with other: IndexProgressStatus) -> IndexProgressStatus { - switch (self, other) { - case (_, .preparingFileForEditorFunctionality), (.preparingFileForEditorFunctionality, _): - return .preparingFileForEditorFunctionality - case (_, .generatingBuildGraph), (.generatingBuildGraph, _): - return .generatingBuildGraph - case ( - .indexing(let selfPreparationTasks, let selfIndexTasks), - .indexing(let otherPreparationTasks, let otherIndexTasks) - ): - return .indexing( - preparationTasks: selfPreparationTasks.merging(otherPreparationTasks) { max($0, $1) }, - indexTasks: selfIndexTasks.merging(otherIndexTasks) { max($0, $1) } - ) - case (.indexing, .upToDate): return self - case (.upToDate, .indexing): return other - case (.upToDate, .upToDate): return .upToDate - } - } -} - -/// See `SemanticIndexManager.inProgressPrepareForEditorTask`. -private struct InProgressPrepareForEditorTask { - /// A unique ID that identifies the preparation task and is used to set - /// `SemanticIndexManager.inProgressPrepareForEditorTask` to `nil` when the current in progress task finishes. - let id: UUID - - /// The document that is being prepared. - let document: DocumentURI - - /// The task that prepares the document. Should never be awaited and only be used to cancel the task. - let task: Task -} - -/// The reason why a target is being prepared. This is used to determine the `IndexProgressStatus`. -private enum TargetPreparationPurpose: Comparable { - /// We are preparing the target so we can index files in it. - case forIndexing - - /// We are preparing the target to provide semantic functionality in one of its files. - case forEditorFunctionality -} - -/// An entry in `SemanticIndexManager.inProgressPreparationTasks`. -private struct InProgressPreparationTask { - let task: OpaqueQueuedIndexTask - let purpose: TargetPreparationPurpose -} - -/// Schedules index tasks and keeps track of the index status of files. -package final actor SemanticIndexManager { - /// The underlying index. This is used to check if the index of a file is already up-to-date, in which case it doesn't - /// need to be indexed again. - private let index: UncheckedIndex - - /// The build server manager that is used to get compiler arguments for a file. - private let buildServerManager: BuildServerManager - - /// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all - /// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on - /// an expression for a long time. - package var updateIndexStoreTimeout: Duration - - private let hooks: IndexHooks - - /// The tasks to generate the build graph (resolving package dependencies, generating the build description, - /// ...) and to schedule indexing of modified tasks. - private var buildGraphGenerationTasks: [UUID: Task] = [:] - - /// Tasks that were created from `workspace/didChangeWatchedFiles` or `buildTarget/didChange` notifications. Needed - /// so that we can wait for these tasks to be handled in the poll index request. - private var fileOrBuildTargetChangedTasks: [UUID: Task] = [:] - - private let preparationUpToDateTracker = UpToDateTracker() - - private let indexStoreUpToDateTracker = UpToDateTracker() - - /// The preparation tasks that have been started and are either scheduled in the task scheduler or currently - /// executing. - /// - /// After a preparation task finishes, it is removed from this dictionary. - private var inProgressPreparationTasks: [BuildTargetIdentifier: InProgressPreparationTask] = [:] - - /// The files that are currently being index, either waiting for their target to be prepared, waiting for the index - /// store update task to be scheduled in the task scheduler or which currently have an index store update running. - /// - /// After the file is indexed, it is removed from this dictionary. - private var inProgressIndexTasks: [FileIndexInfo: InProgressIndexStore] = [:] - - /// The currently running task that prepares a document for editor functionality. - /// - /// This is used so we can cancel preparation tasks for documents that the user is no longer interacting with and - /// avoid the following scenario: The user browses through documents from targets A, B, and C in quick succession. We - /// don't want stack preparation of A, B, and C. Instead we want to only prepare target C - and also finish - /// preparation of A if it has already started when the user opens C. - private var inProgressPrepareForEditorTask: InProgressPrepareForEditorTask? = nil - - /// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s - /// in the process, to ensure that we don't schedule more index operations than processor cores from multiple - /// workspaces. - private let indexTaskScheduler: TaskScheduler - - /// Callback that is called when an indexing task produces output it wants to log to the index log. - private let logMessageToIndexLog: - @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void - - /// Called when files are scheduled to be indexed. - /// - /// The parameter is the number of files that were scheduled to be indexed. - private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void - - /// Callback that is called when `progressStatus` might have changed. - private let indexProgressStatusDidChange: @Sendable () -> Void - - // MARK: - Public API - - /// A summary of the tasks that this `SemanticIndexManager` has currently scheduled or is currently indexing. - package var progressStatus: IndexProgressStatus { - if inProgressPreparationTasks.values.contains(where: { $0.purpose == .forEditorFunctionality }) { - return .preparingFileForEditorFunctionality - } - let preparationTasks = inProgressPreparationTasks.mapValues { inProgressTask in - return inProgressTask.task.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled - } - let indexTasks = inProgressIndexTasks.mapValues { inProgress in - switch inProgress.state { - case .creatingIndexTask, .waitingForPreparation, .preparing: - return IndexTaskStatus.scheduled - case .updatingIndexStore(let updateIndexStoreTask, indexTask: _): - return updateIndexStoreTask.isExecuting ? IndexTaskStatus.executing : IndexTaskStatus.scheduled - } - } - if !preparationTasks.isEmpty || !indexTasks.isEmpty { - return .indexing(preparationTasks: preparationTasks, indexTasks: indexTasks) - } - // Only report the `schedulingIndexing` status when we don't have any in-progress indexing tasks. This way we avoid - // flickering between indexing progress and `Scheduling indexing` if we trigger an index schedule task while - // indexing is already in progress - if !buildGraphGenerationTasks.isEmpty { - return .generatingBuildGraph - } - return .upToDate - } - - package init( - index: UncheckedIndex, - buildServerManager: BuildServerManager, - updateIndexStoreTimeout: Duration, - hooks: IndexHooks, - indexTaskScheduler: TaskScheduler, - logMessageToIndexLog: - @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void, - indexTasksWereScheduled: @escaping @Sendable (Int) -> Void, - indexProgressStatusDidChange: @escaping @Sendable () -> Void - ) { - self.index = index - self.buildServerManager = buildServerManager - self.updateIndexStoreTimeout = updateIndexStoreTimeout - self.hooks = hooks - self.indexTaskScheduler = indexTaskScheduler - self.logMessageToIndexLog = logMessageToIndexLog - self.indexTasksWereScheduled = indexTasksWereScheduled - self.indexProgressStatusDidChange = indexProgressStatusDidChange - } - - /// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the - /// build server that don't currently have a unit with a timestamp that matches the mtime of the file. - /// - /// This is a costly operation since it iterates through all the unit files on the file - /// system but if existing unit files are not known to the index, we might re-index those files even if they are - /// up-to-date. Generally this should be set to `true` during the initial indexing (in which case we might be need to - /// build the indexstore-db) and `false` for all subsequent indexing. - package func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: Bool) async { - let taskId = UUID() - let generateBuildGraphTask = Task(priority: .low) { - await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "build-graph-generation") { - await hooks.buildGraphGenerationDidStart?() - await self.buildServerManager.waitForUpToDateBuildGraph() - // Polling for unit changes is a costly operation since it iterates through all the unit files on the file - // system but if existing unit files are not known to the index, we might re-index those files even if they are - // up-to-date. This operation is worth the cost during initial indexing and during the manual re-index command. - index.pollForUnitChangesAndWait() - await hooks.buildGraphGenerationDidFinish?() - // TODO: Ideally this would be a type like any Collection & Sendable but that doesn't work due to - // https://github.com/swiftlang/swift/issues/75602 - let filesToIndex: [DocumentURI] = - await orLog("Getting files to index") { - try await self.buildServerManager.buildableSourceFiles().sorted { $0.stringValue < $1.stringValue } - } ?? [] - await self.scheduleIndexing( - of: filesToIndex, - waitForBuildGraphGenerationTasks: false, - indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit, - priority: .low - ) - buildGraphGenerationTasks[taskId] = nil - } - } - buildGraphGenerationTasks[taskId] = generateBuildGraphTask - indexProgressStatusDidChange() - } - - /// Causes all files to be re-indexed even if the unit file for the source file is up to date. - /// See `TriggerReindexRequest`. - package func scheduleReindex() async { - await indexStoreUpToDateTracker.markAllKnownOutOfDate() - await preparationUpToDateTracker.markAllKnownOutOfDate() - await scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(indexFilesWithUpToDateUnit: true) - } - - private func waitForBuildGraphGenerationTasks() async { - await buildGraphGenerationTasks.values.concurrentForEach { await $0.value } - } - - /// Wait for all in-progress index tasks to finish. - package func waitForUpToDateIndex() async { - logger.info("Waiting for up-to-date index") - // Wait for a build graph update first, if one is in progress. This will add all index tasks to `indexStatus`, so we - // can await the index tasks below. - await waitForBuildGraphGenerationTasks() - - await fileOrBuildTargetChangedTasks.concurrentForEach { await $0.value.value } - - await inProgressIndexTasks.concurrentForEach { (_, inProgress) in - switch inProgress.state { - case .creatingIndexTask: - break - case .waitingForPreparation(preparationTaskID: _, let indexTask), - .preparing(preparationTaskID: _, let indexTask), - .updatingIndexStore(updateIndexStoreTask: _, let indexTask): - await indexTask.value - } - } - index.pollForUnitChangesAndWait() - logger.debug("Done waiting for up-to-date index") - } - - /// Ensure that the index for the given files is up-to-date. - /// - /// This tries to produce an up-to-date index for the given files as quickly as possible. To achieve this, it might - /// suspend previous target-wide index tasks in favor of index tasks that index a fewer files. - package func waitForUpToDateIndex(for uris: some Collection & Sendable) async { - logger.info( - "Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))" - ) - - // Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will - // - Wait for the existing index operations to finish if they have the same number of files. - // - Reschedule the background index task in favor of an index task with fewer source files. - await self.scheduleIndexing( - of: uris, - waitForBuildGraphGenerationTasks: true, - indexFilesWithUpToDateUnit: false, - priority: nil - ).value - index.pollForUnitChangesAndWait() - logger.debug("Done waiting for up-to-date index") - } - - package func filesDidChange(_ events: [FileEvent]) async { - // We only re-index the files that were changed and don't re-index any of their dependencies. See the - // `Documentation/Files_To_Reindex.md` file. - let changedFiles = events.filter { $0.type != .deleted }.map(\.uri) - await indexStoreUpToDateTracker.markOutOfDate(changedFiles) - - var outOfDateTargets = Set() - var targetsOfChangedFiles = Set() - for file in changedFiles { - let changedTargets = await buildServerManager.targets(for: file) - targetsOfChangedFiles.formUnion(changedTargets) - if Language(inferredFromFileExtension: file) == nil { - // Preparation tracking should be per file. For now consider any non-known-language change as having to - // re-prepare the target itself so that we re-prepare potential input files to plugins. - // https://github.com/swiftlang/sourcekit-lsp/issues/1975 - outOfDateTargets.formUnion(changedTargets) - } - } - outOfDateTargets.formUnion(await buildServerManager.targets(dependingOn: targetsOfChangedFiles)) - if !outOfDateTargets.isEmpty { - logger.info( - """ - Marking dependent targets as out-of-date: \ - \(String(outOfDateTargets.map(\.uri.stringValue).joined(separator: ", "))) - """ - ) - await preparationUpToDateTracker.markOutOfDate(outOfDateTargets) - } - - // We need to invalidate the preparation status of the changed files immediately so that we re-prepare its target - // eg. on a `workspace/sourceKitOptions` request. But the actual indexing can happen using a background task. - // We don't need a queue here because we don't care about the order in which we schedule re-indexing of files. - let uuid = UUID() - fileOrBuildTargetChangedTasks[uuid] = Task { - defer { fileOrBuildTargetChangedTasks[uuid] = nil } - await scheduleIndexing( - of: changedFiles, - waitForBuildGraphGenerationTasks: true, - indexFilesWithUpToDateUnit: false, - priority: .low - ) - } - } - - package func buildTargetsChanged(_ targets: Set?) async { - if let targets { - var targetsAndDependencies = targets - targetsAndDependencies.formUnion(await buildServerManager.targets(dependingOn: targets)) - if !targetsAndDependencies.isEmpty { - logger.info( - """ - Marking dependent targets as out-of-date: \ - \(String(targetsAndDependencies.map(\.uri.stringValue).joined(separator: ", "))) - """ - ) - await preparationUpToDateTracker.markOutOfDate(targetsAndDependencies) - } - } else { - await preparationUpToDateTracker.markAllKnownOutOfDate() - } - - // We need to invalidate the preparation status of the changed files immediately so that we re-prepare its target - // eg. on a `workspace/sourceKitOptions` request. But the actual indexing can happen using a background task. - // We don't need a queue here because we don't care about the order in which we schedule re-indexing of files. - let uuid = UUID() - fileOrBuildTargetChangedTasks[uuid] = Task { - await orLog("Scheduling re-indexing of changed targets") { - defer { fileOrBuildTargetChangedTasks[uuid] = nil } - var sourceFiles = try await self.buildServerManager.sourceFiles(includeNonBuildableFiles: false) - if let targets { - sourceFiles = sourceFiles.filter { !targets.isDisjoint(with: $0.value.targets) } - } - _ = await scheduleIndexing( - of: sourceFiles.keys, - waitForBuildGraphGenerationTasks: true, - indexFilesWithUpToDateUnit: false, - priority: .low - ) - } - } - } - - /// Returns the files that should be indexed to get up-to-date index information for the given files. - /// - /// If `files` contains a header file, this will return a `FileToIndex` that re-indexes a main file which includes the - /// header file to update the header file's index. - /// - /// The returned results will not include files that are known to be up-to-date based on `indexStoreUpToDateTracker` - /// or the unit timestamp will not be re-indexed unless `indexFilesWithUpToDateUnit` is `true`. - private func filesToIndex( - toCover files: some Collection & Sendable, - indexFilesWithUpToDateUnits: Bool - ) async -> [(file: FileIndexInfo, fileModificationDate: Date?)] { - let sourceFiles = await orLog("Getting source files in project") { - try await buildServerManager.buildableSourceFiles() - } - guard let sourceFiles else { - return [] - } - let modifiedFilesIndex = index.checked(for: .modifiedFiles) - - var filesToReIndex: [(FileIndexInfo, Date?)] = [] - for uri in files { - var didFindTargetToIndex = false - for (target, outputPath) in await buildServerManager.sourceFileInfo(for: uri)?.targetsToOutputPath ?? [:] { - // First, check if we know that the file is up-to-date, in which case we don't need to hit the index or file - // system at all - if !indexFilesWithUpToDateUnits, await indexStoreUpToDateTracker.isUpToDate(uri, target) { - continue - } - if sourceFiles.contains(uri) { - guard let outputPath else { - logger.info("Not indexing \(uri.forLogging) because its output file could not be determined") - continue - } - if !indexFilesWithUpToDateUnits, modifiedFilesIndex.hasUpToDateUnit(for: uri, outputPath: outputPath) { - continue - } - // If this is a source file, just index it. - guard let language = await buildServerManager.defaultLanguage(for: uri, in: target) else { - continue - } - didFindTargetToIndex = true - filesToReIndex.append( - ( - FileIndexInfo(file: .indexableFile(uri), language: language, target: target, outputPath: outputPath), - modifiedFilesIndex.modificationDate(of: uri) - ) - ) - } - } - if didFindTargetToIndex { - continue - } - // If we haven't found any ways to index the file, see if it is a header file. If so, index a main file that - // that imports it to update header file's index. - // Deterministically pick a main file. This ensures that we always pick the same main file for a header. This way, - // if we request the same header to be indexed twice, we'll pick the same unit file the second time around, - // realize that its timestamp is later than the modification date of the header and we don't need to re-index. - let mainFile = await buildServerManager.mainFiles(containing: uri) - .filter { sourceFiles.contains($0) } - .sorted(by: { $0.stringValue < $1.stringValue }).first - guard let mainFile else { - logger.info("Not indexing \(uri.forLogging) because its main file could not be inferred") - continue - } - let targetAndOutputPath = (await buildServerManager.sourceFileInfo(for: mainFile)?.targetsToOutputPath ?? [:]) - .sorted(by: { $0.key.uri.stringValue < $1.key.uri.stringValue }).first - guard let targetAndOutputPath else { - logger.info( - "Not indexing \(uri.forLogging) because the target file of its main file \(mainFile.forLogging) could not be determined" - ) - continue - } - guard let outputPath = targetAndOutputPath.value else { - logger.info( - "Not indexing \(uri.forLogging) because the output file of its main file \(mainFile.forLogging) could not be determined" - ) - continue - } - if !indexFilesWithUpToDateUnits, - modifiedFilesIndex.hasUpToDateUnit(for: uri, mainFile: mainFile, outputPath: outputPath) - { - continue - } - guard let language = await buildServerManager.defaultLanguage(for: uri, in: targetAndOutputPath.key) else { - continue - } - filesToReIndex.append( - ( - FileIndexInfo( - file: .headerFile(header: uri, mainFile: mainFile), - language: language, - target: targetAndOutputPath.key, - outputPath: outputPath - ), - modifiedFilesIndex.modificationDate(of: uri) - ) - ) - } - return filesToReIndex - } - - /// Schedule preparation of the target that contains the given URI, building all modules that the file depends on. - /// - /// This is intended to be called when the user is interacting with the document at the given URI. - package func schedulePreparationForEditorFunctionality(of uri: DocumentURI, priority: TaskPriority? = nil) { - if inProgressPrepareForEditorTask?.document == uri { - // We are already preparing this document, so nothing to do. This is necessary to avoid the following scenario: - // Determining the canonical target for a document takes 1s and we get a new document request for the - // document ever 0.5s, which would cancel the previous in-progress preparation task, cancelling the canonical - // target configuration, never actually getting to the actual preparation. - return - } - let id = UUID() - let task = Task(priority: priority) { - await withLoggingScope("preparation") { - await prepareFileForEditorFunctionality(uri) - if inProgressPrepareForEditorTask?.id == id { - inProgressPrepareForEditorTask = nil - self.indexProgressStatusDidChange() - } - } - } - - markPreparationForEditorFunctionalityTaskAsIrrelevant() - inProgressPrepareForEditorTask = InProgressPrepareForEditorTask( - id: id, - document: uri, - task: task - ) - self.indexProgressStatusDidChange() - } - - /// If there is an in-progress prepare for editor task, cancel it to indicate that we are no longer interested in it. - /// This will cancel the preparation of `inProgressPrepareForEditorTask`'s target if it hasn't started yet. If the - /// preparation has already started, we don't cancel it to guarantee forward progress (see comment at the end of - /// `SemanticIndexManager.prepare`). - package func markPreparationForEditorFunctionalityTaskAsIrrelevant() { - guard let inProgressPrepareForEditorTask else { - return - } - logger.debug("Marking preparation of \(inProgressPrepareForEditorTask.document) as no longer relevant") - inProgressPrepareForEditorTask.task.cancel() - } - - /// Prepare the target that the given file is in, building all modules that the file depends on. Returns when - /// preparation has finished. - /// - /// If file's target is known to be up-to-date, this returns almost immediately. - package func prepareFileForEditorFunctionality(_ uri: DocumentURI) async { - guard let target = await buildServerManager.canonicalTarget(for: uri) else { - return - } - if Task.isCancelled { - return - } - if await preparationUpToDateTracker.isUpToDate(target) { - // If the target is up-to-date, there is nothing to prepare. - return - } - await self.prepare(targets: [target], purpose: .forEditorFunctionality, priority: nil) - } - - /// Prepare the given target. - /// - /// Returns `false` if the target was already up-to-date and `true` if this did actually cause the target to ger - /// prepared. - package func prepareTargetsForSourceKitOptions(target: BuildTargetIdentifier) async -> Bool { - if await preparationUpToDateTracker.isUpToDate(target) { - return false - } - await self.prepare(targets: [target], purpose: .forEditorFunctionality, priority: nil) - return true - } - - // MARK: - Helper functions - - private func schedulePreparation( - targets: [BuildTargetIdentifier], - purpose: TargetPreparationPurpose, - priority: TaskPriority?, - executionStatusChangedCallback: @escaping (QueuedTask, TaskExecutionState) async -> Void - ) async -> QueuedTask? { - // Perform a quick initial check whether the target is up-to-date, in which case we don't need to schedule a - // preparation operation at all. - // We will check the up-to-date status again in `PreparationTaskDescription.execute`. This ensures that if we - // schedule two preparations of the same target in quick succession, only the first one actually performs a prepare - // and the second one will be a no-op once it runs. - let targetsToPrepare = await targets.asyncFilter { - await !preparationUpToDateTracker.isUpToDate($0) - } - - guard !targetsToPrepare.isEmpty else { - return nil - } - - let taskDescription = AnyIndexTaskDescription( - PreparationTaskDescription( - targetsToPrepare: targetsToPrepare, - buildServerManager: self.buildServerManager, - preparationUpToDateTracker: preparationUpToDateTracker, - logMessageToIndexLog: logMessageToIndexLog, - hooks: hooks - ) - ) - if Task.isCancelled { - return nil - } - let preparationTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in - await executionStatusChangedCallback(task, newState) - guard case .finished = newState else { - self.indexProgressStatusDidChange() - return - } - for target in targetsToPrepare { - if self.inProgressPreparationTasks[target]?.task == OpaqueQueuedIndexTask(task) { - self.inProgressPreparationTasks[target] = nil - } - } - self.indexProgressStatusDidChange() - } - for target in targetsToPrepare { - // If we are preparing the same target for indexing and editor functionality, pick editor functionality as the - // purpose because it is more significant. - let mergedPurpose = - if let existingPurpose = inProgressPreparationTasks[target]?.purpose { - max(existingPurpose, purpose) - } else { - purpose - } - inProgressPreparationTasks[target] = InProgressPreparationTask( - task: OpaqueQueuedIndexTask(preparationTask), - purpose: mergedPurpose - ) - } - return preparationTask - } - - private func prepare( - targets: [BuildTargetIdentifier], - purpose: TargetPreparationPurpose, - priority: TaskPriority?, - executionStatusChangedCallback: @escaping (QueuedTask, TaskExecutionState) async -> Void = - { _, _ in } - ) async { - let preparationTask = await schedulePreparation( - targets: targets, - purpose: purpose, - priority: priority, - executionStatusChangedCallback: executionStatusChangedCallback - ) - guard let preparationTask else { - return - } - await withTaskCancellationHandler { - return await preparationTask.waitToFinish() - } onCancel: { - // Only cancel the preparation task if it hasn't started executing yet. This ensures that we always make progress - // during preparation and can't get into the following scenario: The user has two target A and B that both take - // 10s to prepare. The user is now switching between the files every 5 seconds, which would always cause - // preparation for one target to get cancelled, never resulting in an up-to-date preparation status. - if !preparationTask.isExecuting { - preparationTask.cancel() - } - } - } - - /// Update the index store for the given files, assuming that their targets has already been prepared. - private func updateIndexStore( - for fileAndOutputPaths: [FileAndOutputPath], - target: BuildTargetIdentifier, - language: Language, - indexFilesWithUpToDateUnit: Bool, - preparationTaskID: UUID, - priority: TaskPriority? - ) async { - let taskDescription = AnyIndexTaskDescription( - UpdateIndexStoreTaskDescription( - filesToIndex: fileAndOutputPaths, - target: target, - language: language, - buildServerManager: self.buildServerManager, - index: index, - indexStoreUpToDateTracker: indexStoreUpToDateTracker, - indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit, - logMessageToIndexLog: logMessageToIndexLog, - timeout: updateIndexStoreTimeout, - hooks: hooks - ) - ) - - let updateIndexTask = await indexTaskScheduler.schedule(priority: priority, taskDescription) { task, newState in - guard case .finished = newState else { - self.indexProgressStatusDidChange() - return - } - for fileAndOutputPath in fileAndOutputPaths { - let fileAndTarget = FileIndexInfo( - file: fileAndOutputPath.file, - language: language, - target: target, - outputPath: fileAndOutputPath.outputPath - ) - switch self.inProgressIndexTasks[fileAndTarget]?.state { - case .updatingIndexStore(let registeredTask, _): - if registeredTask == OpaqueQueuedIndexTask(task) { - self.inProgressIndexTasks[fileAndTarget] = nil - } - case .waitingForPreparation(let registeredTask, _), .preparing(let registeredTask, _): - if registeredTask == preparationTaskID { - self.inProgressIndexTasks[fileAndTarget] = nil - } - case .creatingIndexTask, nil: - break - } - } - self.indexProgressStatusDidChange() - } - for fileAndOutputPath in fileAndOutputPaths { - let fileAndTarget = FileIndexInfo( - file: fileAndOutputPath.file, - language: language, - target: target, - outputPath: fileAndOutputPath.outputPath - ) - switch inProgressIndexTasks[fileAndTarget]?.state { - case .waitingForPreparation(preparationTaskID, let indexTask), .preparing(preparationTaskID, let indexTask): - inProgressIndexTasks[fileAndTarget]?.state = .updatingIndexStore( - updateIndexStoreTask: OpaqueQueuedIndexTask(updateIndexTask), - indexTask: indexTask - ) - default: break - } - } - return await updateIndexTask.waitToFinishPropagatingCancellation() - } - - /// Index the given set of files at the given priority, preparing their targets beforehand, if needed. Files that are - /// known to be up-to-date based on `indexStoreUpToDateTracker` or the unit timestamp will not be re-indexed unless - /// `indexFilesWithUpToDateUnit` is `true`. - /// - /// If `waitForBuildGraphGenerationTasks` is `true` and there are any tasks in progress that wait for an up-to-date - /// build graph, wait for those to finish. This is helpful so to avoid the following: We receive a build target change - /// notification before the initial background indexing has finished. If indexstore-db hasn't been initialized with - /// `pollForUnitChangesAndWait` yet, we might not know that the changed targets' files' index is actually up-to-date - /// and would thus schedule an unnecessary re-index of the file. - /// - /// The returned task finishes when all files are indexed. - @discardableResult - package func scheduleIndexing( - of files: some Collection & Sendable, - waitForBuildGraphGenerationTasks: Bool, - indexFilesWithUpToDateUnit: Bool, - priority: TaskPriority? - ) async -> Task { - if waitForBuildGraphGenerationTasks { - await self.waitForBuildGraphGenerationTasks() - } - // Perform a quick initial check to whether the files is up-to-date, in which case we don't need to schedule a - // prepare and index operation at all. - // We will check the up-to-date status again in `IndexTaskDescription.execute`. This ensures that if we schedule - // schedule two indexing jobs for the same file in quick succession, only the first one actually updates the index - // store and the second one will be a no-op once it runs. - var filesToIndex = await filesToIndex(toCover: files, indexFilesWithUpToDateUnits: indexFilesWithUpToDateUnit) - // sort files to get deterministic indexing order - .sorted(by: { - if $0.file.file.sourceFile.stringValue != $1.file.file.sourceFile.stringValue { - return $0.file.file.sourceFile.stringValue < $1.file.file.sourceFile.stringValue - } - return $0.file.target.uri.stringValue < $1.file.target.uri.stringValue - }) - - filesToIndex = - filesToIndex - .filter { file in - let inProgress = inProgressIndexTasks[file.file] - - switch inProgress?.state { - case nil: - return true - case .creatingIndexTask, .waitingForPreparation: - // We already have a task that indexes the file but hasn't started preparation yet. Indexing the file again - // won't produce any new results. - return false - case .preparing(_, _), .updatingIndexStore(_, _): - // We have started indexing of the file and are now requesting to index it again. Unless we know that the file - // hasn't been modified since the last request for indexing, we need to schedule it to get re-indexed again. - if let modDate = file.fileModificationDate, inProgress?.fileModificationDate == modDate { - return false - } else { - return true - } - } - } - - if filesToIndex.isEmpty { - indexProgressStatusDidChange() - // Early exit if there are no files to index. - return Task {} - } - - logger.debug( - "Scheduling indexing of \(filesToIndex.map(\.file.file.sourceFile.stringValue).joined(separator: ", "))" - ) - - // Sort the targets in topological order so that low-level targets get built before high-level targets, allowing us - // to index the low-level targets ASAP. - var filesByTarget: [BuildTargetIdentifier: [FileIndexInfo]] = [:] - - // The number of index tasks that don't currently have an in-progress task associated with it. - // The denominator in the index progress should get incremented by this amount. - // We don't want to increment the denominator for tasks that already have an index in progress. - var newIndexTasks = 0 - - for (fileIndexInfo, fileModificationDate) in filesToIndex { - guard UpdateIndexStoreTaskDescription.canIndex(language: fileIndexInfo.language) else { - continue - } - - if inProgressIndexTasks[fileIndexInfo] == nil { - // If `inProgressIndexTasks[fileToIndex]` is not `nil`, this new index task is replacing another index task. - // We are thus not indexing a new file and thus shouldn't increment the denominator of the indexing status. - newIndexTasks += 1 - } - inProgressIndexTasks[fileIndexInfo] = InProgressIndexStore( - state: .creatingIndexTask, - fileModificationDate: fileModificationDate - ) - - filesByTarget[fileIndexInfo.target, default: []].append(fileIndexInfo) - } - if newIndexTasks > 0 { - indexTasksWereScheduled(newIndexTasks) - } - - // The targets sorted in reverse topological order, low-level targets before high-level targets. If topological - // sorting fails, sorted in another deterministic way where the actual order doesn't matter. - var sortedTargets: [BuildTargetIdentifier] = - await orLog("Sorting targets") { try await buildServerManager.topologicalSort(of: Array(filesByTarget.keys)) } - ?? Array(filesByTarget.keys).sorted { $0.uri.stringValue < $1.uri.stringValue } - - if Set(sortedTargets) != Set(filesByTarget.keys) { - logger.fault( - """ - Sorting targets topologically changed set of targets: - \(sortedTargets.map(\.uri.stringValue).joined(separator: ", ")) != \(filesByTarget.keys.map(\.uri.stringValue).joined(separator: ", ")) - """ - ) - sortedTargets = Array(filesByTarget.keys).sorted { $0.uri.stringValue < $1.uri.stringValue } - } - - var indexTasks: [Task] = [] - - // TODO: When we can index multiple targets concurrently in SwiftPM, increase the batch size to half the - // processor count, so we can get parallelism during preparation. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) - for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) { - let preparationTaskID = UUID() - let filesToIndex = targetsBatch.flatMap { (target) -> [FileIndexInfo] in - guard let files = filesByTarget[target] else { - logger.fault("Unexpectedly found no files for target in target batch") - return [] - } - return files - } - - // First schedule preparation of the targets. We schedule the preparation outside of `indexTask` so that we - // deterministically prepare targets in the topological order for indexing. If we triggered preparation inside the - // indexTask, we would get nondeterministic ordering since Tasks may start executing in any order. - let preparationTask = await schedulePreparation( - targets: targetsBatch, - purpose: .forIndexing, - priority: priority - ) { task, newState in - if case .executing = newState { - for file in filesToIndex { - if case .waitingForPreparation(preparationTaskID: preparationTaskID, indexTask: let indexTask) = - self.inProgressIndexTasks[file]?.state - { - self.inProgressIndexTasks[file]?.state = .preparing( - preparationTaskID: preparationTaskID, - indexTask: indexTask - ) - } - } - } - } - - let indexTask = Task(priority: priority) { - await preparationTask?.waitToFinishPropagatingCancellation() - - // And after preparation is done, index the files in the targets. - await withTaskGroup(of: Void.self) { taskGroup in - let fileInfos = targetsBatch.flatMap { (target) -> [FileIndexInfo] in - guard let files = filesByTarget[target] else { - logger.fault("Unexpectedly found no files for target in target batch") - return [] - } - return files - } - let batches = await UpdateIndexStoreTaskDescription.batches( - toIndex: fileInfos, - buildServerManager: buildServerManager - ) - for (target, language, fileBatch) in batches { - taskGroup.addTask { - let fileAndOutputPaths: [FileAndOutputPath] = fileBatch.compactMap { - guard $0.target == target else { - logger.fault( - "FileIndexInfo refers to different target than should be indexed: \($0.target.forLogging) vs \(target.forLogging)" - ) - return nil - } - return FileAndOutputPath(file: $0.file, outputPath: $0.outputPath) - } - await self.updateIndexStore( - for: fileAndOutputPaths, - target: target, - language: language, - indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit, - preparationTaskID: preparationTaskID, - priority: priority - ) - } - } - await taskGroup.waitForAll() - } - } - indexTasks.append(indexTask) - - for file in filesToIndex { - // The state of `inProgressIndexTasks` will get pushed on from `updateIndexStore`. - // The updates to `inProgressIndexTasks` from `updateIndexStore` cannot race with setting it to - // `.waitingForPreparation` here because we don't have an `await` call between the creation of `indexTask` and - // this loop, so we still have exclusive access to the `SemanticIndexManager` actor and hence `updateIndexStore` - // can't execute until we have set all index statuses to `.waitingForPreparation`. - inProgressIndexTasks[file]?.state = .waitingForPreparation( - preparationTaskID: preparationTaskID, - indexTask: indexTask - ) - } - } - - return Task(priority: priority) { - await indexTasks.concurrentForEach { await $0.value } - } - } -} diff --git a/Sources/SemanticIndex/TaskScheduler.swift b/Sources/SemanticIndex/TaskScheduler.swift deleted file mode 100644 index b50dcbfd7..000000000 --- a/Sources/SemanticIndex/TaskScheduler.swift +++ /dev/null @@ -1,692 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocolExtensions -package import SKLogging -import SwiftExtensions - -/// See comment on ``TaskDescriptionProtocol/dependencies(to:taskPriority:)`` -package enum TaskDependencyAction { - case waitAndElevatePriorityOfDependency(TaskDescription) - case cancelAndRescheduleDependency(TaskDescription) -} - -private let taskSchedulerSubsystem = "org.swift.sourcekit-lsp.task-scheduler" - -package protocol TaskDescriptionProtocol: Identifiable, Sendable, CustomLogStringConvertible { - /// Execute the task. - /// - /// - Important: This should only be called from `TaskScheduler` and never be called manually. - func execute() async - - /// When a new task is picked for execution, this determines how the task should behave with respect to the tasks that - /// are already running. - /// - /// Options are the following (see doc comment on `TaskScheduler` for examples): - /// 1. Not add any `TaskDependencyAction` for a currently executing task. This means that the two tasks can run in - /// parallel. - /// 2. Declare a `waitAndElevatePriorityOfDependency` dependency. This will prevent execution of this task until - /// the other task has finished executing. It will elevate the priority of the dependency to the same priority as - /// this task. This ensures that we don't get into a priority inversion problem where a high-priority task is - /// waiting for a low-priority task. - /// 3. Declare a `cancelAndRescheduleDependency`. If the task dependency is idempotent and has a priority that's not - /// higher than the this task's priority, this causes the task dependency to be cancelled, so that this task can - /// execute. The canceled task will be scheduled to re-run at a later point. - /// - Declaring a `cancelAndRescheduleDependency` dependency on a task that is not idempotent will change the - /// dependency to a `waitAndElevatePriorityOfDependency` dependency and log a fault. - /// A `cancelAndRescheduleDependency` dependency should never be emitted for a task that's not idempotent. - /// - If the task that should be canceled and re-scheduled has a higher priority than this task, the - /// `waitAndElevatePriorityOfDependency` dependency is changed to a `waitAndElevatePriorityOfDependency` - /// dependency. This is done to ensure that low-priority tasks can't interfere with the execution of - /// high-priority tasks. - /// - **Important**: The task that is canceled to be rescheduled must depend on this task, otherwise the two tasks - /// will fight each other for execution priority. - func dependencies(to currentlyExecutingTasks: [Self]) -> [TaskDependencyAction] - - /// Whether executing this task twice produces the same results. - /// - /// This is required for the task to be canceled and re-scheduled (`TaskDependencyAction.cancelAndRescheduleDependency`) - /// - /// Tasks that are not idempotent should never be cancelled and rescheduled in the first place. This variable is just - /// a safety net in case non-idempotent tasks are cancelled and rescheduled. It also ensures that tasks conforming to - /// `TaskDescriptionProtocol` think about idempotency. - var isIdempotent: Bool { get } - - /// The number of CPU cores this task is expected to use. - /// - /// If the `TaskScheduler` only allows 4 concurrent tasks and a task has `estimatedCPUCoreCount == 4`, this means that - /// no other tasks will be scheduled while this task is executing. Note that the `TaskScheduler` might over-subscribe - /// itself to start executing this task though, ie. it only needs to have one available execution slot even if this - /// task will use 4 CPU cores. This ensures that we get to schedule a 4-core high-priority task in a 4 core scheduler - /// if there are 100 low-priority 1-core tasks in the queue. Otherwise we would just keep executing those whenever a - /// slot opens up and only have enough available slots to execute the 4-core high-priority task when all the - /// low-priority tasks are done. - /// - /// For example, this is used by preparation tasks that are known to prepare multiple targets (or source files within - /// one target) in parallel. - var estimatedCPUCoreCount: Int { get } -} - -/// Parameter that's passed to `executionStateChangedCallback` to indicate the new state of a scheduled task. -package enum TaskExecutionState { - /// The task started executing. - case executing - - /// The task was cancelled and will be re-scheduled for execution later. Will be followed by another call with - /// `executing`. - case cancelledToBeRescheduled - - /// The task has finished executing. Now more state updates will come after this one. - case finished -} - -package actor QueuedTask { - /// Result of `executionTask` / the tasks in `executionTaskCreatedContinuation`. - /// See doc comment on `executionTask`. - enum ExecutionTaskFinishStatus { - case terminated - case cancelledToBeRescheduled - } - - /// The `TaskDescription` that defines what the queued task does. - /// - /// This is also used to determine dependencies between running tasks. - nonisolated let description: TaskDescription - - /// The `Task` that produces the actual result of the `QueuedTask`. This is the task that is visible to clients. - /// - /// See initialization of this task to see how it works. - /// - /// - Note: Implicitly unwrapped optional so the task's closure can access `self`. - /// - Note: `nonisolated(unsafe)` is fine because it will never get modified after being set in the initializer. - nonisolated(unsafe) private(set) var resultTask: Task! = nil - - /// After `execute` is called, the `executionTask` is a task that performs the computation defined by - /// `description.execute`. - /// - /// The `resultTask` effectively waits for this task to be set (by watching for new values produced by - /// `executionTaskCreatedContinuation`) and awaits its result. The task can terminate with two different statuses: - /// - `terminated`: The task has finished executing and the `resultTask` is done. - /// - `cancelledToBeRescheduled`: The `executionTask` was cancelled by calling `QueuedTask.cancelToBeRescheduled()`. - /// In this case the `TaskScheduler` is expected to call `execute` again, which will produce a new - /// `executionTask`. `resultTask` then awaits the creation of the new `executionTask` and then the result of that - /// `executionTask`. - private var executionTask: Task? - - /// Every time `execute` gets called, a new task is placed in this continuation. See comment on `executionTask`. - private let executionTaskCreatedContinuation: AsyncStream>.Continuation - - private let _priority: AtomicUInt8 - - /// The latest known priority of the task. - /// - /// This starts off as the priority with which the task is being created. If higher priority tasks start depending on - /// it, the priority may get elevated. - nonisolated var priority: TaskPriority { - get { - TaskPriority(rawValue: _priority.value) - } - set { - _priority.value = newValue.rawValue - } - } - - /// Whether `cancelToBeRescheduled` has been called on this `QueuedTask`. - /// - /// Gets reset every time `executionTask` finishes. - private var cancelledToBeRescheduled: Bool = false - - /// Whether `resultTask` has been cancelled. - private let resultTaskCancelled: AtomicBool = .init(initialValue: false) - - private let _isExecuting: AtomicBool = .init(initialValue: false) - - /// Whether the task is currently executing or still queued to be executed later. - package nonisolated var isExecuting: Bool { - return _isExecuting.value - } - - package nonisolated func cancel() { - resultTask.cancel() - } - - /// Wait for the task to finish. - /// - /// If the tasks that waits for this queued task to finished is cancelled, the QueuedTask will still continue - /// executing. - package func waitToFinish() async { - return await resultTask.value - } - - /// Wait for the task to finish. - /// - /// If the tasks that waits for this queued task to finished is cancelled, the QueuedTask will also be cancelled. - /// This assumes that the caller of this method has unique control over the task and is the only one interested in its - /// value. - package func waitToFinishPropagatingCancellation() async { - return await resultTask.valuePropagatingCancellation - } - - /// A callback that will be called when the task starts executing, is cancelled to be rescheduled, or when it finishes - /// execution. - private let executionStateChangedCallback: (@Sendable (QueuedTask, TaskExecutionState) async -> Void)? - - fileprivate init( - priority: TaskPriority, - description: TaskDescription, - taskPriorityChangedCallback: @escaping @Sendable (_ newPriority: TaskPriority) -> Void, - executionStateChangedCallback: (@Sendable (QueuedTask, TaskExecutionState) async -> Void)? - ) async { - self._priority = AtomicUInt8(initialValue: priority.rawValue) - self.description = description - self.executionStateChangedCallback = executionStateChangedCallback - - var executionTaskCreatedContinuation: AsyncStream>.Continuation! - let executionTaskCreatedStream = AsyncStream { - executionTaskCreatedContinuation = $0 - } - self.executionTaskCreatedContinuation = executionTaskCreatedContinuation - - self.resultTask = Task.detached(priority: priority) { - await withTaskCancellationHandler { - await withTaskPriorityChangedHandler(initialPriority: self.priority) { - for await task in executionTaskCreatedStream { - switch await task.valuePropagatingCancellation { - case .cancelledToBeRescheduled: - // Break the switch and wait for a new `executionTask` to be placed into `executionTaskCreatedStream`. - break - case .terminated: - // The task finished. We are done with this `QueuedTask` - return - } - } - } taskPriorityChanged: { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.debug( - "Updating priority of \(self.description.forLogging) from \(self.priority.rawValue) to \(Task.currentPriority.rawValue)" - ) - } - self.priority = Task.currentPriority - taskPriorityChangedCallback(self.priority) - } - } onCancel: { - self.resultTaskCancelled.value = true - } - } - } - - /// Start executing the task. - /// - /// Execution might be canceled to be rescheduled, in which case this returns `.cancelledToBeRescheduled`. In that - /// case the `TaskScheduler` is expected to call `execute` again. - func execute() async -> ExecutionTaskFinishStatus { - if cancelledToBeRescheduled { - // `QueuedTask.execute` is called from a detached task in `TaskScheduler.poke` but we insert it into the - // `currentlyExecutingTasks` queue beforehand. This leaves a short windows in which we could cancel the task to - // reschedule it before it actually starts executing. - // If this happens, we don't have to do anything in `execute` and can immediately return. `execute` will be called - // again when the task gets rescheduled. - cancelledToBeRescheduled = false - return .cancelledToBeRescheduled - } - precondition(executionTask == nil, "Task started twice") - let task = Task.detached(priority: self.priority) { - if !Task.isCancelled && !self.resultTaskCancelled.value { - await self.description.execute() - } - return await self.finalizeExecution() - } - _isExecuting.value = true - executionTask = task - executionTaskCreatedContinuation.yield(task) - if self.resultTaskCancelled.value { - // The queued task might have been cancelled after the execution ask was started but before the task was yielded - // to `executionTaskCreatedContinuation`. In that case the result task will simply cancel the await on the - // `executionTaskCreatedStream` and hence not call `valuePropagatingCancellation` on the execution task. This - // means that the queued task cancellation wouldn't be propagated to the execution task. To address this, check if - // `resultTaskCancelled` was set and, if so, explicitly cancel the execution task here. - task.cancel() - } - await executionStateChangedCallback?(self, .executing) - return await task.value - } - - /// Implementation detail of `execute` that is called after `self.description.execute()` finishes. - private func finalizeExecution() async -> ExecutionTaskFinishStatus { - self.executionTask = nil - _isExecuting.value = false - if Task.isCancelled && self.cancelledToBeRescheduled { - await executionStateChangedCallback?(self, .cancelledToBeRescheduled) - self.cancelledToBeRescheduled = false - return ExecutionTaskFinishStatus.cancelledToBeRescheduled - } else { - await executionStateChangedCallback?(self, .finished) - return ExecutionTaskFinishStatus.terminated - } - } - - /// Cancel the task to be rescheduled later. - /// - /// If the task has not been started yet or has already finished execution, this is a no-op. - func cancelToBeRescheduled() { - self.cancelledToBeRescheduled = true - guard let executionTask else { - return - } - executionTask.cancel() - self.executionTask = nil - } - - /// If the priority of this task is less than `targetPriority`, elevate the priority to `targetPriority` by spawning - /// a new task that depends on it. Otherwise a no-op. - nonisolated func elevatePriority(to targetPriority: TaskPriority) { - if priority < targetPriority { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.debug( - "Elevating priority of \(self.description.forLogging) from \(self.priority.rawValue) to \(targetPriority.rawValue)" - ) - } - // Awaiting the result task from a higher-priority task will eventually update `priority` through - // `withTaskPriorityChangedHandler` but that might take a while because `withTaskPriorityChangedHandler` polls. - // Since we know that the priority will be elevated, set it now. That way we don't try to elevate it again. - self.priority = targetPriority - Task(priority: targetPriority) { - await self.resultTask.value - } - } - } -} - -/// Schedules an unordered list of tasks for execution. -/// -/// The key features that `TaskScheduler` provides are: -/// - It allows the dynamic declaration of dependencies between tasks. A task can declare whether it can be executed -/// based on which other tasks are currently running. For example, this allows us to guarantee that only a single -/// preparation task is running at a time without enforcing any order in which the preparation tasks should run. -/// - It allows the maximum number of tasks to be limited at a given priority. This allows us to eg. only use half the -/// computer's cores for background indexing and using all cores if user interaction is depending on a set of files -/// being indexed without over-subscribing the CPU. -/// - It allows tasks to be canceled and rescheduled to make room for tasks that are faster to execute. For example, -/// this is used when we have a joint background index task for file `A`, `B` and `C` (which might be in the same -/// target) with low priority. We now request to index `A` with high priority separately because it's needed for user -/// interaction. This cancels the joint indexing of `A`, `B` and `C` so that `A` can be indexed as a standalone file -/// as quickly as possible. The joint indexing of `A`, `B` and `C` is then re-scheduled (again at low priority) and -/// will depend on `A` being indexed. -package actor TaskScheduler { - /// The tasks that are currently being executed. - /// - /// All tasks in this queue are guaranteed to trigger a call `poke` again once they finish. Thus, whenever there are - /// items left in this array, we are guaranteed to get another call to `poke` - private var currentlyExecutingTasks: [QueuedTask] = [] - - /// The queue of pending tasks that haven't been scheduled for execution yet. - private var pendingTasks: [QueuedTask] = [] - - /// Whether `shutDown` has been called on the `TaskScheduler` and it should thus schedule any new tasks. - private var isShutDown: Bool = false - - /// An ordered list of task priorities to the number of tasks that might execute concurrently at that or a lower - /// priority. As the task priority is decreased, the number of current tasks becomes more restricted. - /// - /// This list is normalized according to `normalize(maxConcurrentTasksByPriority:)`. - /// - /// The highest priority entry in this list restricts the total number of tasks that can be executed at any priority - /// (including priorities higher than its entry). - /// - /// For example if you have - /// ```swift - /// [ - /// (.medium, 4), - /// (.low, 2) - /// ] - /// ``` - /// - /// Then we allow the following number of concurrent tasks at the following priorities - /// - `.high`: 4 (because `.medium: 4` restricts the total number of tasks to 4) - /// - `.medium`: 4 - /// - `.low`: 2 - /// - `.background`: 2 - /// - /// When combining tasks with different priorities: - /// - If we have 3 medium priority tasks, we can have at most 1 low priority task - /// - If we have 1 medium priority task, we can still have 2 low priority tasks, but no more - private var maxConcurrentTasksByPriority: [(priority: TaskPriority, maxConcurrentTasks: Int)] { - didSet { - maxConcurrentTasksByPriority = Self.normalize(maxConcurrentTasksByPriority: maxConcurrentTasksByPriority) - - if maxConcurrentTasksByPriority.count == oldValue.count, - zip(maxConcurrentTasksByPriority, oldValue).allSatisfy(==) - { - // We didn't actually change anything, so we don't need to perform any validation or task processing. - return - } - - // Check we are over-subscribed in currently executing tasks by walking through all currently executing tasks and - // checking if we could schedule them within the new execution limits. Cancel any tasks that do not fit within the - // new limit to be rescheduled when we are within the limit again. - var currentlyExecutingTaskDetails: [(priority: TaskPriority, estimatedCPUCoreCount: Int)] = [] - var tasksToCancelAndReschedule: [QueuedTask] = [] - for task in currentlyExecutingTasks.sorted(by: { $0.priority > $1.priority }) { - let taskPriority = task.priority - if Self.canScheduleTask( - withPriority: taskPriority, - maxConcurrentTasksByPriority: maxConcurrentTasksByPriority, - currentlyExecutingTaskDetails: currentlyExecutingTaskDetails - ) { - currentlyExecutingTaskDetails.append((taskPriority, task.description.estimatedCPUCoreCount)) - } else { - tasksToCancelAndReschedule.append(task) - } - } - - // Poke the scheduler to schedule new jobs if new execution slots became available. - poke() - - // Cancel any tasks that didn't fit into the new execution slots anymore. Do this on a separate task is fine - // because even if we extend the number of execution slots before the task gets executed (which is unlikely), we - // would cancel the tasks and then immediately reschedule it – while that's doing unnecessary work, it's still - // correct. - Task.detached(priority: .high) { - for tasksToReschedule in tasksToCancelAndReschedule { - await tasksToReschedule.cancelToBeRescheduled() - } - } - } - } - - /// Modify the number of tasks that are allowed to run concurrently at each priority level. - /// - /// If there are more tasks executing currently that fit within the new execution limits, tasks will be cancelled and - /// rescheduled again when execution slots become available. - package func setMaxConcurrentTasksByPriority(_ newValue: [(priority: TaskPriority, maxConcurrentTasks: Int)]) { - self.maxConcurrentTasksByPriority = newValue - } - - package init(maxConcurrentTasksByPriority: [(priority: TaskPriority, maxConcurrentTasks: Int)]) { - self.maxConcurrentTasksByPriority = Self.normalize(maxConcurrentTasksByPriority: maxConcurrentTasksByPriority) - } - - /// Enqueue a new task to be executed. - /// - /// - Important: A task that is scheduled by `TaskScheduler` must never be awaited from a task that runs on - /// `TaskScheduler`. Otherwise we might end up in deadlocks, eg. if the inner task cannot be scheduled because the - /// outer task is claiming all execution slots in the `TaskScheduler`. - @discardableResult - package func schedule( - priority: TaskPriority? = nil, - _ taskDescription: TaskDescription, - @_inheritActorContext executionStateChangedCallback: ( - @Sendable (QueuedTask, TaskExecutionState) async -> Void - )? = nil - ) async -> QueuedTask { - let queuedTask = await QueuedTask( - priority: priority ?? Task.currentPriority, - description: taskDescription, - taskPriorityChangedCallback: { [weak self] (newPriority) in - Task.detached(priority: newPriority) { - // If the task's priority got elevated, there might be an execution slot for it now. Poke the scheduler - // to run the task if possible. - await self?.poke() - } - }, - executionStateChangedCallback: executionStateChangedCallback - ) - pendingTasks.append(queuedTask) - Task.detached(priority: priority ?? Task.currentPriority) { - // Poke the `TaskScheduler` to execute a new task. If the `TaskScheduler` is already working at its capacity - // limit, this will not do anything. If there are execution slots available, this will start executing the freshly - // queued task. - await self.poke() - } - return queuedTask - } - - /// Cancel all in-progress tasks and wait for them to finish, either by honoring the cancellation or finishing their - /// work. - /// - /// After `shutDown` has been called, no more tasks will be executed on this `TaskScheduler`. - package func shutDown() async { - self.isShutDown = true - await self.currentlyExecutingTasks.concurrentForEach { task in - task.cancel() - await task.waitToFinish() - } - } - - private static func normalize( - maxConcurrentTasksByPriority: [(priority: TaskPriority, maxConcurrentTasks: Int)] - ) -> [(priority: TaskPriority, maxConcurrentTasks: Int)] { - var maxConcurrentTasksByPriority = maxConcurrentTasksByPriority - - // Ensure elements are sorted decreasingly by priority. - maxConcurrentTasksByPriority = maxConcurrentTasksByPriority.sorted(by: { $0.priority > $1.priority }) - - // Ensure array is not empty. - if maxConcurrentTasksByPriority.isEmpty { - logger.fault("Received empty maxConcurrentTasksByPriority. Allowing as many tasks as there are processor cores.") - maxConcurrentTasksByPriority = [(.medium, ProcessInfo.processInfo.processorCount)] - } - - // Ensure `maxConcurrentTasks` is not increasing with lower priority tasks. - var lastMaxConcurrentTasks = maxConcurrentTasksByPriority.first!.maxConcurrentTasks - for i in 1.. lastMaxConcurrentTasks { - logger.fault("More tasks allowed for lower priority than for higher priority") - maxConcurrentTasksByPriority[i].maxConcurrentTasks = lastMaxConcurrentTasks - } else { - lastMaxConcurrentTasks = maxConcurrentTasksByPriority[i].maxConcurrentTasks - } - } - - return maxConcurrentTasksByPriority - } - - /// Returns `true` if we can schedule a task with the given priority, assuming that the currently executing tasks have - /// the given priorities. - package static func canScheduleTask( - withPriority newTaskPriority: TaskPriority, - maxConcurrentTasksByPriority: [(priority: TaskPriority, maxConcurrentTasks: Int)], - currentlyExecutingTaskDetails: [(priority: TaskPriority, estimatedCPUCoreCount: Int)] - ) -> Bool { - if currentlyExecutingTaskDetails.sum(of: \.estimatedCPUCoreCount) - >= maxConcurrentTasksByPriority.first!.maxConcurrentTasks - { - return false - } - for (priority, maxConcurrentTasks) in maxConcurrentTasksByPriority { - guard priority >= newTaskPriority else { - // This limit does not affect the new task - continue - } - if currentlyExecutingTaskDetails.filter({ $0.priority <= priority }).sum(of: \.estimatedCPUCoreCount) - >= maxConcurrentTasks - { - return false - } - } - return true - } - - /// Poke the execution of more tasks in the queue. - /// - /// This will continue calling itself until the queue is empty. - private func poke() { - if isShutDown { - return - } - pendingTasks.sort(by: { $0.priority > $1.priority }) - for task in pendingTasks { - guard - Self.canScheduleTask( - withPriority: task.priority, - maxConcurrentTasksByPriority: maxConcurrentTasksByPriority, - currentlyExecutingTaskDetails: currentlyExecutingTasks.map({ - ($0.priority, $0.description.estimatedCPUCoreCount) - }) - ) - else { - // We don't have any execution slots left. Thus, this poker has nothing to do and is done. - // When the next task finishes, it calls `poke` again. - // If a low priority task's priority gets elevated that task's priority will get elevated, which will call - // `poke`. - return - } - let dependencies = task.description.dependencies(to: currentlyExecutingTasks.map(\.description)) - let waitForTasks = dependencies.compactMap { (taskDependency) -> QueuedTask? in - switch taskDependency { - case .cancelAndRescheduleDependency(let taskDescription): - guard let dependency = self.currentlyExecutingTasks.first(where: { $0.description.id == taskDescription.id }) - else { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.fault( - "Cannot find task to wait for \(taskDescription.forLogging) in list of currently executing tasks" - ) - } - return nil - } - if !taskDescription.isIdempotent { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.fault("Cannot reschedule task '\(taskDescription.forLogging)' since it is not idempotent") - } - return dependency - } - if dependency.priority > task.priority { - // Don't reschedule tasks that are more important than the new task we would like to schedule. - return dependency - } - return nil - case .waitAndElevatePriorityOfDependency(let taskDescription): - guard let dependency = self.currentlyExecutingTasks.first(where: { $0.description.id == taskDescription.id }) - else { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.fault( - "Cannot find task to wait for '\(taskDescription.forLogging)' in list of currently executing tasks" - ) - } - return nil - } - return dependency - } - } - if !waitForTasks.isEmpty { - // This task is blocked by a task that's currently executing. Elevate the priorities of those tasks and continue - // looking in the queue if there is another task we can execute. - for waitForTask in waitForTasks { - waitForTask.elevatePriority(to: task.priority) - } - continue - } - let rescheduleTasks = dependencies.compactMap { (taskDependency) -> QueuedTask? in - switch taskDependency { - case .cancelAndRescheduleDependency(let taskDescription): - guard let task = self.currentlyExecutingTasks.first(where: { $0.description.id == taskDescription.id }) else { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.fault( - "Cannot find task to reschedule \(taskDescription.forLogging) in list of currently executing tasks" - ) - } - return nil - } - return task - default: - return nil - } - } - if !rescheduleTasks.isEmpty { - Task.detached(priority: task.priority) { - for task in rescheduleTasks { - withLoggingSubsystemAndScope(subsystem: taskSchedulerSubsystem, scope: nil) { - logger.debug("Suspending \(task.description.forLogging)") - } - await task.cancelToBeRescheduled() - } - } - // Don't go looking for other tasks to execute in this poker because we should be waiting for the rescheduled - // tasks to finish (which will call `poke` again), and then actually schedule `task`. - // If we did enqueue another task from the pending queue, that new task might introduce a new dependency `task`, - // which could delay its execution and render the suspension of previous tasks useless. - return - } - - currentlyExecutingTasks.append(task) - pendingTasks.removeAll(where: { $0 === task }) - Task.detached(priority: task.priority) { - // Await the task's return in a task so that this poker can continue checking if there are more execution - // slots that can be filled with queued tasks. - let finishStatus = await task.execute() - await self.finalizeTaskExecution(task: task, finishStatus: finishStatus) - } - } - } - - /// Implementation detail of `poke` to be called after `task.execute()` to ensure that `task.execute()` executes in - /// a different isolation domain then `TaskScheduler`. - private func finalizeTaskExecution( - task: QueuedTask, - finishStatus: QueuedTask.ExecutionTaskFinishStatus - ) async { - currentlyExecutingTasks.removeAll(where: { $0.description.id == task.description.id }) - switch finishStatus { - case .terminated: break - case .cancelledToBeRescheduled: pendingTasks.append(task) - } - self.poke() - } -} - -// MARK: - Collection utilities - -fileprivate extension Collection where Element: Comparable { - func isSorted(descending: Bool) -> Bool { - var previous = self.first - for element in self { - if (previous! < element) == descending { - return false - } - previous = element - } - return true - } -} - -fileprivate extension Collection { - func sum(of transform: (Self.Element) -> Int) -> Int { - var result = 0 - for element in self { - result += transform(element) - } - return result - } -} - -/// Version of the `withTaskPriorityChangedHandler` where the body doesn't throw. -private func withTaskPriorityChangedHandler( - initialPriority: TaskPriority = Task.currentPriority, - pollingInterval: Duration = .seconds(0.1), - @_inheritActorContext operation: @escaping @Sendable () async -> Void, - taskPriorityChanged: @escaping @Sendable () -> Void -) async { - do { - try await withTaskPriorityChangedHandler( - initialPriority: initialPriority, - pollingInterval: pollingInterval, - operation: operation as @Sendable () async throws -> Void, - taskPriorityChanged: taskPriorityChanged - ) - } catch is CancellationError { - } catch { - // Since `operation` does not throw, the only error we expect `withTaskPriorityChangedHandler` to throw is a - // `CancellationError`, in which case we can just return. - logger.fault("Unexpected error thrown from withTaskPriorityChangedHandler: \(error.forLogging)") - } -} diff --git a/Sources/SemanticIndex/UpToDateTracker.swift b/Sources/SemanticIndex/UpToDateTracker.swift deleted file mode 100644 index 0aa897e3f..000000000 --- a/Sources/SemanticIndex/UpToDateTracker.swift +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -struct DummySecondaryKey: Hashable {} - -/// Keeps track of whether an item (a target or file to index) is up-to-date. -/// -/// The `UpToDateTracker` has two sets of keys. The primary key is the key by which items in the `UpToDateTracker` can -/// be marked as out of date, but only a combination of primary key and secondary key can be marked as up-to-date. -/// This is useful so that we invalidate the index status for a source file's URI (the primary key) when the file -/// receives an update but only mark it up-to-date in with respect to a target (secondary key). -/// -/// `DummySecondaryKey` can be used as `SecondaryKey` if the secondary key tracking is not needed. -actor UpToDateTracker { - private struct Status { - /// The date at which this primary key has last been marked out-of-date or `nil` if it has never been marked - /// out-of-date. - /// - /// Keeping track of the date is necessary so that we don't mark a target as up-to-date if we have the following - /// ordering of events: - /// - Preparation started - /// - Target marked out of date - /// - Preparation finished - var lastOutOfDate: Date? - - /// The secondary keys for which the item is considered up-to-date. - var secondaryKeys: Set - - internal init(lastOutOfDate: Date? = nil, secondaryKeys: Set = []) { - self.lastOutOfDate = lastOutOfDate - self.secondaryKeys = secondaryKeys - } - } - - private var status: [PrimaryKey: Status] = [:] - - /// Mark the target or file as up-to-date from a preparation/update-indexstore operation started at - /// `updateOperationStartDate`. - /// - /// See comment on `Status.outOfDate` why `updateOperationStartDate` needs to be passed. - func markUpToDate(_ items: some Sequence<(PrimaryKey, SecondaryKey)>, updateOperationStartDate: Date) { - for (primaryKey, secondaryKey) in items { - if let status = status[primaryKey] { - if let lastOutOfDate = status.lastOutOfDate, lastOutOfDate > updateOperationStartDate { - // The key was marked as out-of-date after the operation started. We thus can't mark it as up-to-date again. - continue - } - } - status[primaryKey, default: Status()].secondaryKeys.insert(secondaryKey) - } - } - - func markOutOfDate(_ items: some Sequence) { - let date = Date() - for item in items { - status[item] = Status(lastOutOfDate: date) - } - } - - func markAllKnownOutOfDate() { - markOutOfDate(status.keys) - } - - func isUpToDate(_ primaryKey: PrimaryKey, _ secondaryKey: SecondaryKey) -> Bool { - return status[primaryKey]?.secondaryKeys.contains(secondaryKey) ?? false - } -} - -extension UpToDateTracker where SecondaryKey == DummySecondaryKey { - func markUpToDate(_ items: [PrimaryKey], updateOperationStartDate: Date) { - self.markUpToDate(items.lazy.map { ($0, DummySecondaryKey()) }, updateOperationStartDate: updateOperationStartDate) - } - - func isUpToDate(_ primaryKey: PrimaryKey) -> Bool { - self.isUpToDate(primaryKey, DummySecondaryKey()) - } -} diff --git a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift deleted file mode 100644 index 0d6b91e00..000000000 --- a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift +++ /dev/null @@ -1,781 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -package import BuildServerProtocol -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions -import TSCExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import class TSCBasic.Process -import struct TSCBasic.ProcessResult -import enum TSCBasic.SystemError - -#if os(Windows) -import WinSDK -#endif - -private let updateIndexStoreIDForLogging = AtomicUInt32(initialValue: 1) - -package enum FileToIndex: CustomLogStringConvertible, Hashable { - /// A non-header file - case indexableFile(DocumentURI) - - /// A header file where `mainFile` should be indexed to update the index of `header`. - case headerFile(header: DocumentURI, mainFile: DocumentURI) - - /// The file whose index store should be updated. - /// - /// This file might be a header file that doesn't have build settings associated with it. For the actual compiler - /// invocation that updates the index store, the `mainFile` should be used. - package var sourceFile: DocumentURI { - switch self { - case .indexableFile(let uri): return uri - case .headerFile(let header, mainFile: _): return header - } - } - - /// The file that should be used for compiler invocations that update the index. - /// - /// If the `sourceFile` is a header file, this will be a main file that includes the header. Otherwise, it will be the - /// same as `sourceFile`. - var mainFile: DocumentURI { - switch self { - case .indexableFile(let uri): return uri - case .headerFile(header: _, let mainFile): return mainFile - } - } - - package var description: String { - switch self { - case .indexableFile(let uri): - return uri.description - case .headerFile(let header, let mainFile): - return "\(header.description) using main file \(mainFile.description)" - } - } - - package var redactedDescription: String { - switch self { - case .indexableFile(let uri): - return uri.redactedDescription - case .headerFile(let header, let mainFile): - return "\(header.redactedDescription) using main file \(mainFile.redactedDescription)" - } - } -} - -/// A source file to index and the output path that should be used for indexing. -package struct FileAndOutputPath: Sendable, Hashable { - package let file: FileToIndex - package let outputPath: OutputPath - - fileprivate var mainFile: DocumentURI { file.mainFile } - fileprivate var sourceFile: DocumentURI { file.sourceFile } -} - -/// A single file or a list of files that should be indexed in a single compiler invocation. -private enum UpdateIndexStorePartition { - case multipleFiles(filesAndOutputPaths: [(file: FileToIndex, outputPath: String)], buildSettings: FileBuildSettings) - case singleFile(file: FileAndOutputPath, buildSettings: FileBuildSettings) - - var buildSettings: FileBuildSettings { - switch self { - case .multipleFiles(_, let buildSettings): return buildSettings - case .singleFile(_, let buildSettings): return buildSettings - } - } - - var files: [FileToIndex] { - switch self { - case .multipleFiles(let filesAndOutputPaths, _): return filesAndOutputPaths.map(\.file) - case .singleFile(let file, _): return [file.file] - } - } -} - -/// Describes a task to index a set of source files. -/// -/// This task description can be scheduled in a `TaskScheduler`. -package struct UpdateIndexStoreTaskDescription: IndexTaskDescription { - package static let idPrefix = "update-indexstore" - package let id = updateIndexStoreIDForLogging.fetchAndIncrement() - - /// The files that should be indexed. - package let filesToIndex: [FileAndOutputPath] - - /// The target in whose context the files should be indexed. - package let target: BuildTargetIdentifier - - /// The common language of all main files in `filesToIndex`. - package let language: Language - - /// The build server manager that is used to get the toolchain and build settings for the files to index. - private let buildServerManager: BuildServerManager - - /// A reference to the underlying index store. Used to check if the index is already up-to-date for a file, in which - /// case we don't need to index it again. - private let index: UncheckedIndex - - private let indexStoreUpToDateTracker: UpToDateTracker - - /// Whether files that have an up-to-date unit file should be indexed. - /// - /// In general, this should be `false`. The only situation when this should be set to `true` is when the user - /// explicitly requested a re-index of all files. - private let indexFilesWithUpToDateUnit: Bool - - /// See `SemanticIndexManager.logMessageToIndexLog`. - private let logMessageToIndexLog: - @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void - - /// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all - /// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on - /// an expression for a long time. - private let timeout: Duration - - /// Test hooks that should be called when the index task finishes. - private let hooks: IndexHooks - - /// The task is idempotent because indexing the same file twice produces the same result as indexing it once. - package var isIdempotent: Bool { true } - - package var estimatedCPUCoreCount: Int { 1 } - - package var description: String { - return self.redactedDescription - } - - package var redactedDescription: String { - return "update-indexstore-\(id)" - } - - static func canIndex(language: Language) -> Bool { - return language.semanticKind != nil - } - - init( - filesToIndex: [FileAndOutputPath], - target: BuildTargetIdentifier, - language: Language, - buildServerManager: BuildServerManager, - index: UncheckedIndex, - indexStoreUpToDateTracker: UpToDateTracker, - indexFilesWithUpToDateUnit: Bool, - logMessageToIndexLog: - @escaping @Sendable ( - _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind - ) -> Void, - timeout: Duration, - hooks: IndexHooks - ) { - self.filesToIndex = filesToIndex - self.target = target - self.language = language - self.buildServerManager = buildServerManager - self.index = index - self.indexStoreUpToDateTracker = indexStoreUpToDateTracker - self.indexFilesWithUpToDateUnit = indexFilesWithUpToDateUnit - self.logMessageToIndexLog = logMessageToIndexLog - self.timeout = timeout - self.hooks = hooks - } - - package func execute() async { - // Only use the last two digits of the indexing ID for the logging scope to avoid creating too many scopes. - // See comment in `withLoggingScope`. - // The last 2 digits should be sufficient to differentiate between multiple concurrently running indexing operation. - await withLoggingSubsystemAndScope(subsystem: indexLoggingSubsystem, scope: "update-indexstore-\(id % 100)") { - let startDate = Date() - - await hooks.updateIndexStoreTaskDidStart?(self) - - let filesToIndexDescription = filesToIndex.map { - $0.file.sourceFile.fileURL?.lastPathComponent ?? $0.file.sourceFile.stringValue - } - .joined(separator: ", ") - logger.log( - "Starting updating index store with priority \(Task.currentPriority.rawValue, privacy: .public): \(filesToIndexDescription)" - ) - let filesToIndex = filesToIndex.sorted(by: { $0.file.sourceFile.stringValue < $1.file.sourceFile.stringValue }) - await updateIndexStore(forFiles: filesToIndex) - // If we know the output paths, make sure that we load their units into indexstore-db. We would eventually also - // pick the units up through file watching but that would leave a short time period in which we think that - // indexing has finished (because the index process has terminated) but when the new symbols aren't present in - // indexstore-db. - let outputPaths = filesToIndex.compactMap { fileToIndex in - switch fileToIndex.outputPath { - case .path(let string): return string - case .notSupported: return nil - } - } - index.processUnitsForOutputPathsAndWait(outputPaths) - await hooks.updateIndexStoreTaskDidFinish?(self) - logger.log( - "Finished updating index store in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(filesToIndexDescription)" - ) - } - } - - package func dependencies( - to currentlyExecutingTasks: [UpdateIndexStoreTaskDescription] - ) -> [TaskDependencyAction] { - let selfMainFiles = Set(filesToIndex.map(\.file.mainFile)) - return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction? in - if !other.filesToIndex.lazy.map(\.file.mainFile).contains(where: { selfMainFiles.contains($0) }) { - // Disjoint sets of files can be indexed concurrently. - return nil - } - return .waitAndElevatePriorityOfDependency(other) - } - } - - private func updateIndexStore(forFiles fileInfos: [FileAndOutputPath]) async { - let fileInfos = await fileInfos.asyncFilter { fileInfo in - // If we know that the file is up-to-date without having ot hit the index, do that because it's fastest. - if await indexStoreUpToDateTracker.isUpToDate(fileInfo.file.sourceFile, target) { - return false - } - if indexFilesWithUpToDateUnit { - return true - } - let hasUpToDateUnit = index.checked(for: .modifiedFiles).hasUpToDateUnit( - for: fileInfo.sourceFile, - mainFile: fileInfo.mainFile, - outputPath: fileInfo.outputPath - ) - if !hasUpToDateUnit { - logger.debug("Not indexing \(fileInfo.file.forLogging) because index has an up-to-date unit") - // We consider a file's index up-to-date if we have any up-to-date unit. Changing build settings does not - // invalidate the up-to-date status of the index. - } - return !hasUpToDateUnit - } - if fileInfos.isEmpty { - return - } - for fileInfo in fileInfos where fileInfo.mainFile != fileInfo.sourceFile { - logger.log( - "Updating index store of \(fileInfo.file.forLogging) using main file \(fileInfo.mainFile.forLogging)" - ) - } - - // Compute the partitions within which we can perform multi-file indexing. For this we gather all the files that may - // only be indexed by themselves in `partitions` an collect all other files in `fileInfosByBuildSettings` so that we - // can create a partition for all files that share the same build settings. - // In most cases, we will only end up with a single partition for each `UpdateIndexStoreTaskDescription` since - // `UpdateIndexStoreTaskDescription.batches(toIndex:)` tries to batch the files in a way such that all files within - // the batch can be indexed by a single compiler invocation. However, we might discover that two Swift files within - // the same target have different build settings in the build server. In that case, the best thing we can do is - // trigger two compiler invocations. - var swiftFileInfosByBuildSettings: [FileBuildSettings: [(file: FileToIndex, outputPath: String)]] = [:] - var partitions: [UpdateIndexStorePartition] = [] - for fileInfo in fileInfos { - let buildSettings = await buildServerManager.buildSettings( - for: fileInfo.mainFile, - in: target, - language: language, - fallbackAfterTimeout: false - ) - guard var buildSettings else { - logger.error("Not indexing \(fileInfo.file.forLogging) because it has no compiler arguments") - continue - } - if buildSettings.isFallback { - // Fallback build settings don’t even have an indexstore path set, so they can't generate index data that we - // would pick up. Also, indexing with fallback args has some other problems: - // - If it did generate a unit file, we would consider the file’s index up-to-date even if the compiler - // arguments change, which means that we wouldn't get any up-to-date-index even when we have build settings - // for the file. - // - It's unlikely that the index from a single file with fallback arguments will be very useful as it can't tie - // into the rest of the project. - // So, don't index the file. - logger.error("Not indexing \(fileInfo.file.forLogging) because it has fallback compiler arguments") - continue - } - - guard buildSettings.language == .swift else { - // We only support multi-file indexing for Swift files. Do not try to batch or normalize - // `-index-unit-output-path` for clang files. - partitions.append(.singleFile(file: fileInfo, buildSettings: buildSettings)) - continue - } - - guard - await buildServerManager.toolchain(for: target, language: language)? - .canIndexMultipleSwiftFilesInSingleInvocation ?? false - else { - partitions.append(.singleFile(file: fileInfo, buildSettings: buildSettings)) - continue - } - - // If the build settings contain `-index-unit-output-path`, remove it. We add the index unit output path back in - // using an `-output-file-map`. Removing it from the build settings allows us to index multiple Swift files in a - // single compiler invocation if they share all build settings and only differ in their `-index-unit-output-path`. - let indexUnitOutputPathFromSettings = removeIndexUnitOutputPath( - from: &buildSettings, - for: fileInfo.mainFile - ) - - switch (fileInfo.outputPath, indexUnitOutputPathFromSettings) { - case (.notSupported, nil): - partitions.append(.singleFile(file: fileInfo, buildSettings: buildSettings)) - case (.notSupported, let indexUnitOutputPathFromSettings?): - swiftFileInfosByBuildSettings[buildSettings, default: []].append( - (fileInfo.file, indexUnitOutputPathFromSettings) - ) - case (.path(let indexUnitOutputPath), nil): - swiftFileInfosByBuildSettings[buildSettings, default: []].append((fileInfo.file, indexUnitOutputPath)) - case (.path(let indexUnitOutputPath), let indexUnitOutputPathFromSettings?): - if indexUnitOutputPathFromSettings != indexUnitOutputPath { - logger.error( - "Output path reported by BSP server does not match -index-unit-output path in compiler arguments: \(indexUnitOutputPathFromSettings) vs \(indexUnitOutputPath)" - ) - } - swiftFileInfosByBuildSettings[buildSettings, default: []].append((fileInfo.file, indexUnitOutputPath)) - } - } - for (buildSettings, fileInfos) in swiftFileInfosByBuildSettings { - partitions.append(.multipleFiles(filesAndOutputPaths: fileInfos, buildSettings: buildSettings)) - } - - for partition in partitions { - guard let toolchain = await buildServerManager.toolchain(for: target, language: partition.buildSettings.language) - else { - logger.fault( - "Unable to determine toolchain to index \(partition.buildSettings.language.description, privacy: .public) files in \(target.forLogging)" - ) - continue - } - let startDate = Date() - switch partition.buildSettings.language.semanticKind { - case .swift: - do { - try await updateIndexStore( - forSwiftFilesInPartition: partition, - toolchain: toolchain - ) - } catch { - logger.error( - """ - Updating index store failed: \(error.forLogging). - Files: \(partition.files) - """ - ) - BuildSettingsLogger.log(settings: partition.buildSettings, for: partition.files.map(\.mainFile)) - } - case .clang: - for fileInfo in partition.files { - do { - try await updateIndexStore( - forClangFile: fileInfo.mainFile, - buildSettings: partition.buildSettings, - toolchain: toolchain - ) - } catch { - logger.error("Updating index store for \(fileInfo.mainFile.forLogging) failed: \(error.forLogging)") - BuildSettingsLogger.log(settings: partition.buildSettings, for: fileInfo.mainFile) - } - } - case nil: - logger.error( - """ - Not updating index store because \(partition.buildSettings.language.rawValue, privacy: .public) is not \ - supported by background indexing. - Files: \(partition.files) - """ - ) - } - await indexStoreUpToDateTracker.markUpToDate( - partition.files.map { ($0.sourceFile, target) }, - updateOperationStartDate: startDate - ) - } - } - - /// If `args` does not contain an `-index-store-path` argument, add it, pointing to the build server's index store - /// path. If an `-index-store-path` already exists, validate that it matches the build server's index store path and - /// replace it by the build server's index store path if they don't match. - private func addOrReplaceIndexStorePath(in args: [String], for uris: [DocumentURI]) async throws -> [String] { - var args = args - guard let buildServerIndexStorePath = await self.buildServerManager.initializationData?.indexStorePath else { - struct NoIndexStorePathError: Error {} - throw NoIndexStorePathError() - } - if let indexStorePathIndex = args.lastIndex(of: "-index-store-path"), indexStorePathIndex + 1 < args.count { - let indexStorePath = args[indexStorePathIndex + 1] - if indexStorePath != buildServerIndexStorePath { - logger.error( - """ - Compiler arguments specify index store path \(indexStorePath) but build server specified an \ - incompatible index store path \(buildServerIndexStorePath). Overriding with the path specified by the build \ - system. For \(uris) - """ - ) - args[indexStorePathIndex + 1] = buildServerIndexStorePath - } - } else { - args += ["-index-store-path", buildServerIndexStorePath] - } - return args - } - - /// If the build settings contain an `-index-unit-output-path` argument, remove it and return the index unit output - /// path. Otherwise don't modify `buildSettings` and return `nil`. - private func removeIndexUnitOutputPath(from buildSettings: inout FileBuildSettings, for uri: DocumentURI) -> String? { - guard let indexUnitOutputPathIndex = buildSettings.compilerArguments.lastIndex(of: "-index-unit-output-path"), - indexUnitOutputPathIndex + 1 < buildSettings.compilerArguments.count - else { - return nil - } - let indexUnitOutputPath = buildSettings.compilerArguments[indexUnitOutputPathIndex + 1] - buildSettings.compilerArguments.removeSubrange(indexUnitOutputPathIndex...(indexUnitOutputPathIndex + 1)) - if buildSettings.compilerArguments.contains("-index-unit-output-path") { - logger.error("Build settings contained two -index-unit-output-path arguments") - BuildSettingsLogger.log(settings: buildSettings, for: uri) - } - return indexUnitOutputPath - } - - private func updateIndexStore( - forSwiftFilesInPartition partition: UpdateIndexStorePartition, - toolchain: Toolchain - ) async throws { - guard let swiftc = toolchain.swiftc else { - logger.error( - "Not updating index store for \(partition.files) because toolchain \(toolchain.identifier) does not contain a Swift compiler" - ) - return - } - - var args = - try [swiftc.filePath] + partition.buildSettings.compilerArguments + [ - "-index-file", - // batch mode is not compatible with -index-file - "-disable-batch-mode", - ] - args = try await addOrReplaceIndexStorePath(in: args, for: partition.files.map(\.mainFile)) - - switch partition { - case .multipleFiles(let filesAndOutputPaths, let buildSettings): - if await !toolchain.canIndexMultipleSwiftFilesInSingleInvocation { - // We should never get here because we shouldn't create `multipleFiles` batches if the toolchain doesn't support - // indexing multiple files in a single compiler invocation. - logger.fault("Cannot index multiple files in a single compiler invocation.") - } - - struct OutputFileMapEntry: Encodable { - let indexUnitOutputPath: String - - private enum CodingKeys: String, CodingKey { - case indexUnitOutputPath = "index-unit-output-path" - } - } - var outputFileMap: [String: OutputFileMapEntry] = [:] - for (fileInfo, outputPath) in filesAndOutputPaths { - guard let filePath = try? fileInfo.mainFile.fileURL?.filePath else { - logger.error("Failed to determine file path of file to index \(fileInfo.mainFile.forLogging)") - continue - } - outputFileMap[filePath] = .init(indexUnitOutputPath: outputPath) - } - let tempFileUri = FileManager.default.temporaryDirectory - .appending(component: "sourcekit-lsp-output-file-map-\(UUID().uuidString).json") - try JSONEncoder().encode(outputFileMap).write(to: tempFileUri) - defer { - orLog("Delete output file map") { - try FileManager.default.removeItem(at: tempFileUri) - } - } - - let indexFiles = filesAndOutputPaths.map(\.file.mainFile) - // If the compiler arguments already contain an `-output-file-map` argument, we override it by adding a second one - // This is fine because we shouldn't be generating any outputs except for the index. - args += ["-output-file-map", try tempFileUri.filePath] - args += indexFiles.flatMap { (indexFile) -> [String] in - guard let filePath = try? indexFile.fileURL?.filePath else { - logger.error("Failed to determine file path of file to index \(indexFile.forLogging)") - return [] - } - return ["-index-file-path", filePath] - } - - try await runIndexingProcess( - indexFiles: indexFiles, - buildSettings: buildSettings, - processArguments: args, - workingDirectory: buildSettings.workingDirectory.map(AbsolutePath.init(validating:)) - ) - case .singleFile(let file, let buildSettings): - // We only end up in this case if the file's build settings didn't contain `-index-unit-output-path` and the build - // server is not a `outputPathsProvider`. - args += ["-index-file-path", file.mainFile.pseudoPath] - - try await runIndexingProcess( - indexFiles: [file.mainFile], - buildSettings: buildSettings, - processArguments: args, - workingDirectory: buildSettings.workingDirectory.map(AbsolutePath.init(validating:)) - ) - } - } - - private func updateIndexStore( - forClangFile uri: DocumentURI, - buildSettings: FileBuildSettings, - toolchain: Toolchain - ) async throws { - guard let clang = toolchain.clang else { - logger.error( - "Not updating index store for \(uri.forLogging) because toolchain \(toolchain.identifier) does not contain clang" - ) - return - } - - var args = [try clang.filePath] + buildSettings.compilerArguments - args = try await addOrReplaceIndexStorePath(in: args, for: [uri]) - - try await runIndexingProcess( - indexFiles: [uri], - buildSettings: buildSettings, - processArguments: args, - workingDirectory: buildSettings.workingDirectory.map(AbsolutePath.init(validating:)) - ) - } - - private func runIndexingProcess( - indexFiles: [DocumentURI], - buildSettings: FileBuildSettings, - processArguments: [String], - workingDirectory: AbsolutePath? - ) async throws { - if Task.isCancelled { - return - } - let start = ContinuousClock.now - let signposter = Logger(subsystem: LoggingScope.subsystem, category: "indexing").makeSignposter() - let signpostID = signposter.makeSignpostID() - let state = signposter.beginInterval( - "Indexing", - id: signpostID, - "Indexing \(indexFiles.map { $0.fileURL?.lastPathComponent ?? $0.pseudoPath })" - ) - defer { - signposter.endInterval("Indexing", state) - } - let taskId = "indexing-\(id)" - logMessageToIndexLog( - processArguments.joined(separator: " "), - .info, - .begin( - StructuredLogBegin(title: "Indexing \(indexFiles.map(\.pseudoPath).joined(separator: ", "))", taskID: taskId) - ) - ) - - let stdoutHandler = PipeAsStringHandler { - logMessageToIndexLog($0, .info, .report(StructuredLogReport(taskID: taskId))) - } - let stderrHandler = PipeAsStringHandler { - logMessageToIndexLog($0, .info, .report(StructuredLogReport(taskID: taskId))) - } - - let result: ProcessResult - do { - result = try await withTimeout(timeout) { - try await Process.runUsingResponseFileIfTooManyArguments( - arguments: processArguments, - workingDirectory: workingDirectory, - outputRedirection: .stream( - stdout: { @Sendable bytes in stdoutHandler.handleDataFromPipe(Data(bytes)) }, - stderr: { @Sendable bytes in stderrHandler.handleDataFromPipe(Data(bytes)) } - ) - ) - } - } catch { - logMessageToIndexLog( - "Finished with error in \(start.duration(to: .now)): \(error)", - .error, - .end(StructuredLogEnd(taskID: taskId)) - ) - throw error - } - let exitStatus = result.exitStatus.exhaustivelySwitchable - logMessageToIndexLog( - "Finished with \(exitStatus.description) in \(start.duration(to: .now))", - exitStatus.isSuccess ? .info : .error, - .end(StructuredLogEnd(taskID: taskId)) - ) - switch exitStatus { - case .terminated(code: 0): - break - case .terminated(let code): - // This most likely happens if there are compilation errors in the source file. This is nothing to worry about. - let stdout = (try? String(bytes: result.output.get(), encoding: .utf8)) ?? "" - let stderr = (try? String(bytes: result.stderrOutput.get(), encoding: .utf8)) ?? "" - // Indexing will frequently fail if the source code is in an invalid state. Thus, log the failure at a low level. - logger.debug( - """ - Updating index store for terminated with non-zero exit code \(code) for \(indexFiles) - Stderr: - \(stderr) - Stdout: - \(stdout) - """ - ) - BuildSettingsLogger.log(level: .debug, settings: buildSettings, for: indexFiles) - case .signalled(let signal): - if !Task.isCancelled { - // The indexing job finished with a signal. Could be because the compiler crashed. - // Ignore signal exit codes if this task has been cancelled because the compiler exits with SIGINT if it gets - // interrupted. - logger.error("Updating index store for signaled \(signal) for \(indexFiles)") - BuildSettingsLogger.log(level: .error, settings: buildSettings, for: indexFiles) - } - case .abnormal(let exception): - if !Task.isCancelled { - logger.error("Updating index store exited abnormally \(exception) for \(indexFiles)") - BuildSettingsLogger.log(level: .error, settings: buildSettings, for: indexFiles) - } - } - } - - /// Partition the given `FileIndexInfos` into batches so that a single `UpdateIndexStoreTaskDescription` should be - /// created for every one of these batches, taking advantage of multi-file indexing when it is supported. - static func batches( - toIndex fileIndexInfos: [FileIndexInfo], - buildServerManager: BuildServerManager - ) async -> [(target: BuildTargetIdentifier, language: Language, files: [FileIndexInfo])] { - struct TargetAndLanguage: Hashable, Comparable { - let target: BuildTargetIdentifier - let language: Language - - static func < (lhs: TargetAndLanguage, rhs: TargetAndLanguage) -> Bool { - if lhs.target.uri.stringValue < rhs.target.uri.stringValue { - return true - } else if lhs.target.uri.stringValue > rhs.target.uri.stringValue { - return false - } - if lhs.language.rawValue < rhs.language.rawValue { - return true - } else if lhs.language.rawValue > rhs.language.rawValue { - return false - } - return false - } - } - - var partitions: [(target: BuildTargetIdentifier, language: Language, files: [FileIndexInfo])] = [] - var fileIndexInfosToBatch: [TargetAndLanguage: [FileIndexInfo]] = [:] - for fileIndexInfo in fileIndexInfos { - guard fileIndexInfo.language == .swift, - await buildServerManager.toolchain(for: fileIndexInfo.target, language: fileIndexInfo.language)? - .canIndexMultipleSwiftFilesInSingleInvocation ?? false - else { - // Only Swift supports indexing multiple files in a single compiler invocation, so don't batch files of other - // languages. - partitions.append((fileIndexInfo.target, fileIndexInfo.language, [fileIndexInfo])) - continue - } - // Even for Swift files, we can only index files in a single compiler invocation if they have the same build - // settings (modulo some normalization in `updateIndexStore(forFiles:)`). We can't know that all the files do - // indeed have the same compiler arguments but loading build settings during scheduling from the build server - // is not desirable because it slows down a bottleneck. - // Since Swift files within the same target should build a single module, it is reasonable to assume that they all - // share the same build settings. - let languageAndTarget = TargetAndLanguage(target: fileIndexInfo.target, language: fileIndexInfo.language) - fileIndexInfosToBatch[languageAndTarget, default: []].append(fileIndexInfo) - } - // Create one partition per processor core but limit the partition size to 25 primary files. This matches the - // driver's behavior in `numberOfBatchPartitions` - // https://github.com/swiftlang/swift-driver/blob/df3d0796ed5e533d82accd7baac43d15e97b5671/Sources/SwiftDriver/Jobs/Planning.swift#L917-L1022 - let partitionSize = max(fileIndexInfosToBatch.count / ProcessInfo.processInfo.activeProcessorCount, 25) - let batchedPartitions = - fileIndexInfosToBatch - .sorted { $0.key < $1.key } // Ensure we get a deterministic partition order - .flatMap { targetAndLanguage, files in - files.partition(intoBatchesOfSize: partitionSize).map { - (targetAndLanguage.target, targetAndLanguage.language, $0) - } - } - return partitions + batchedPartitions - } -} - -fileprivate extension Process { - /// Run a process with the given arguments. If the number of arguments exceeds the maximum number of arguments allows, - /// create a response file and use it to pass the arguments. - static func runUsingResponseFileIfTooManyArguments( - arguments: [String], - workingDirectory: AbsolutePath?, - outputRedirection: OutputRedirection = .collect(redirectStderr: false) - ) async throws -> ProcessResult { - do { - return try await Process.run( - arguments: arguments, - workingDirectory: workingDirectory, - outputRedirection: outputRedirection - ) - } catch { - let argumentListTooLong: Bool - #if os(Windows) - if let error = error as? CocoaError { - argumentListTooLong = - error.underlyingErrors.contains(where: { - return ($0 as NSError).domain == "org.swift.Foundation.WindowsError" - && ($0 as NSError).code == ERROR_FILENAME_EXCED_RANGE - }) - } else { - argumentListTooLong = false - } - #else - if case SystemError.posix_spawn(E2BIG, _) = error { - argumentListTooLong = true - } else { - argumentListTooLong = false - } - #endif - - guard argumentListTooLong else { - throw error - } - - logger.debug("Argument list is too long. Using response file.") - let responseFile = FileManager.default.temporaryDirectory.appending( - component: "index-response-file-\(UUID()).txt" - ) - defer { - orLog("Failed to remove temporary response file") { - try FileManager.default.removeItem(at: responseFile) - } - } - try FileManager.default.createFile(at: responseFile, contents: nil) - let handle = try FileHandle(forWritingTo: responseFile) - for argument in arguments.dropFirst() { - handle.write(Data((argument.spm_shellEscaped() + "\n").utf8)) - } - try handle.close() - - return try await Process.run( - arguments: arguments.prefix(1) + ["@\(responseFile.filePath)"], - workingDirectory: workingDirectory, - outputRedirection: outputRedirection - ) - } - } -} diff --git a/Sources/SourceKitD/CMakeLists.txt b/Sources/SourceKitD/CMakeLists.txt deleted file mode 100644 index 3fad8d47a..000000000 --- a/Sources/SourceKitD/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -set(sources - dlopen.swift - SKDRequestArray.swift - SKDRequestDictionary.swift - SKDResponse.swift - SKDResponseArray.swift - SKDResponseDictionary.swift - SourceKitD.swift - SourceKitDRegistry.swift - sourcekitd_functions.swift - sourcekitd_uids.swift) - -add_library(SourceKitD STATIC ${sources}) -set_target_properties(SourceKitD PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(SourceKitD PUBLIC - Csourcekitd) -target_link_libraries(SourceKitD PRIVATE - SKLogging - SwiftExtensions - $<$>:Foundation>) - - -add_library(SourceKitDForPlugin STATIC ${sources}) -set_target_properties(SourceKitDForPlugin PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_compile_options(SourceKitDForPlugin PRIVATE - $<$: - "SHELL:-module-alias SKLogging=SKLoggingForPlugin" - "SHELL:-module-alias SwiftExtensions=SwiftExtensionsForPlugin" - >) -target_link_libraries(SourceKitDForPlugin PUBLIC - Csourcekitd) -target_link_libraries(SourceKitDForPlugin PRIVATE - SKLoggingForPlugin - SwiftExtensionsForPlugin - $<$>:Foundation>) diff --git a/Sources/SourceKitD/SKDRequestArray.swift b/Sources/SourceKitD/SKDRequestArray.swift deleted file mode 100644 index 515bc1fdc..000000000 --- a/Sources/SourceKitD/SKDRequestArray.swift +++ /dev/null @@ -1,89 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(CRT) -import CRT -#elseif canImport(Bionic) -import Bionic -#endif - -extension SourceKitD { - /// Create a `SKDRequestArray` from the given array. - nonisolated package func array(_ array: [SKDRequestValue]) -> SKDRequestArray { - let result = SKDRequestArray(sourcekitd: self) - for element in array { - result.append(element) - } - return result - } -} - -package final class SKDRequestArray: Sendable { - nonisolated(unsafe) let array: sourcekitd_api_object_t - private let sourcekitd: SourceKitD - - package init(_ array: sourcekitd_api_object_t? = nil, sourcekitd: SourceKitD) { - self.array = array ?? sourcekitd.api.request_array_create(nil, 0)! - self.sourcekitd = sourcekitd - } - - deinit { - sourcekitd.api.request_release(array) - } - - package func append(_ newValue: SKDRequestValue) { - switch newValue { - case let newValue as String: - sourcekitd.api.request_array_set_string(array, -1, newValue) - case let newValue as Int: - sourcekitd.api.request_array_set_int64(array, -1, Int64(newValue)) - case let newValue as sourcekitd_api_uid_t: - sourcekitd.api.request_array_set_uid(array, -1, newValue) - case let newValue as SKDRequestDictionary: - sourcekitd.api.request_array_set_value(array, -1, newValue.dict) - case let newValue as SKDRequestArray: - sourcekitd.api.request_array_set_value(array, -1, newValue.array) - case let newValue as [SKDRequestValue]: - self.append(sourcekitd.array(newValue)) - case let newValue as [sourcekitd_api_uid_t: SKDRequestValue]: - self.append(sourcekitd.dictionary(newValue)) - case let newValue as SKDRequestValue?: - if let newValue { - self.append(newValue) - } - default: - preconditionFailure("Unknown type conforming to SKDRequestValue") - } - } - - package static func += (array: SKDRequestArray, other: some Sequence) { - for item in other { - array.append(item) - } - } -} - -extension SKDRequestArray: CustomStringConvertible { - package var description: String { - let ptr = sourcekitd.api.request_description_copy(array)! - defer { free(ptr) } - return String(cString: ptr) - } -} diff --git a/Sources/SourceKitD/SKDRequestDictionary.swift b/Sources/SourceKitD/SKDRequestDictionary.swift deleted file mode 100644 index 6ae563766..000000000 --- a/Sources/SourceKitD/SKDRequestDictionary.swift +++ /dev/null @@ -1,110 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd -import SKLogging - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(CRT) -import CRT -#elseif canImport(Bionic) -import Bionic -#endif - -/// Values that can be stored in a `SKDRequestDictionary`. -/// -/// - Warning: `SKDRequestDictionary.subscript` and `SKDRequestArray.append` -/// switch exhaustively over this protocol. -/// Do not add new conformances without adding a new case in the subscript and -/// `append` function. -package protocol SKDRequestValue {} - -extension String: SKDRequestValue {} -extension Int: SKDRequestValue {} -extension sourcekitd_api_uid_t: SKDRequestValue {} -extension SKDRequestDictionary: SKDRequestValue {} -extension SKDRequestArray: SKDRequestValue {} -extension [SKDRequestValue]: SKDRequestValue {} -extension [sourcekitd_api_uid_t: SKDRequestValue]: SKDRequestValue {} -extension Optional: SKDRequestValue where Wrapped: SKDRequestValue {} - -extension SourceKitD { - /// Create a `SKDRequestDictionary` from the given dictionary. - nonisolated package func dictionary(_ dict: [sourcekitd_api_uid_t: SKDRequestValue]) -> SKDRequestDictionary { - let result = SKDRequestDictionary(sourcekitd: self) - for (key, value) in dict { - result.set(key, to: value) - } - return result - } -} - -package final class SKDRequestDictionary: Sendable { - nonisolated(unsafe) let dict: sourcekitd_api_object_t - private let sourcekitd: SourceKitD - - package init(_ dict: sourcekitd_api_object_t? = nil, sourcekitd: SourceKitD) { - self.dict = dict ?? sourcekitd.api.request_dictionary_create(nil, nil, 0)! - self.sourcekitd = sourcekitd - } - - deinit { - sourcekitd.api.request_release(dict) - } - - package func set(_ key: sourcekitd_api_uid_t, to newValue: SKDRequestValue) { - switch newValue { - case let newValue as String: - sourcekitd.api.request_dictionary_set_string(dict, key, newValue) - case let newValue as Int: - sourcekitd.api.request_dictionary_set_int64(dict, key, Int64(newValue)) - case let newValue as sourcekitd_api_uid_t: - sourcekitd.api.request_dictionary_set_uid(dict, key, newValue) - case let newValue as SKDRequestDictionary: - sourcekitd.api.request_dictionary_set_value(dict, key, newValue.dict) - case let newValue as SKDRequestArray: - sourcekitd.api.request_dictionary_set_value(dict, key, newValue.array) - case let newValue as [SKDRequestValue]: - self.set(key, to: sourcekitd.array(newValue)) - case let newValue as [sourcekitd_api_uid_t: SKDRequestValue]: - self.set(key, to: sourcekitd.dictionary(newValue)) - case let newValue as SKDRequestValue?: - if let newValue { - self.set(key, to: newValue) - } - default: - preconditionFailure("Unknown type conforming to SKDRequestValue") - } - } -} - -extension SKDRequestDictionary: CustomStringConvertible { - package var description: String { - let ptr = sourcekitd.api.request_description_copy(dict)! - defer { free(ptr) } - return String(cString: ptr) - } -} - -extension SKDRequestDictionary: CustomLogStringConvertible { - package var redactedDescription: String { - // TODO: Implement a better redacted log that contains keys, number of - // elements in an array but not the data itself. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1598) - return "<\(description.filter(\.isNewline).count) lines>" - } -} diff --git a/Sources/SourceKitD/SKDResponse.swift b/Sources/SourceKitD/SKDResponse.swift deleted file mode 100644 index a7a290f15..000000000 --- a/Sources/SourceKitD/SKDResponse.swift +++ /dev/null @@ -1,81 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd -import SKLogging - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(CRT) -import CRT -#elseif canImport(Bionic) -import Bionic -#endif - -package final class SKDResponse: Sendable { - private nonisolated(unsafe) let response: sourcekitd_api_response_t - let sourcekitd: SourceKitD - - /// Creates a new `SKDResponse` that exclusively manages the raw `sourcekitd_api_response_t`. - /// - /// - Important: When this `SKDResponse` object gets destroyed, it will dispose the response. It is thus illegal to - /// have two `SDKResponse` objects managing the same `sourcekitd_api_response_t`. - package init(_ response: sourcekitd_api_response_t, sourcekitd: SourceKitD) { - self.response = response - self.sourcekitd = sourcekitd - } - - deinit { - sourcekitd.api.response_dispose(response) - } - - package var error: SKDError? { - if !sourcekitd.api.response_is_error(response) { - return nil - } - switch sourcekitd.api.response_error_get_kind(response) { - case SOURCEKITD_API_ERROR_CONNECTION_INTERRUPTED: return .connectionInterrupted - case SOURCEKITD_API_ERROR_REQUEST_INVALID: return .requestInvalid(description) - case SOURCEKITD_API_ERROR_REQUEST_FAILED: return .requestFailed(description) - case SOURCEKITD_API_ERROR_REQUEST_CANCELLED: return .requestCancelled - default: return .requestFailed(description) - } - } - - package var value: SKDResponseDictionary? { - if sourcekitd.api.response_is_error(response) { - return nil - } - return SKDResponseDictionary(sourcekitd.api.response_get_value(response), response: self) - } -} - -extension SKDResponse: CustomStringConvertible { - package var description: String { - let ptr = sourcekitd.api.response_description_copy(response)! - defer { free(ptr) } - return String(cString: ptr) - } -} - -extension SKDResponse: CustomLogStringConvertible { - package var redactedDescription: String { - // TODO: Implement a better redacted log that contains keys, number of - // elements in an array but not the data itself. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1598) - return "<\(description.filter(\.isNewline).count) lines>" - } -} diff --git a/Sources/SourceKitD/SKDResponseArray.swift b/Sources/SourceKitD/SKDResponseArray.swift deleted file mode 100644 index 62784d8d5..000000000 --- a/Sources/SourceKitD/SKDResponseArray.swift +++ /dev/null @@ -1,112 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd - -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(CRT) -import CRT -#elseif canImport(Bionic) -import Bionic -#endif - -package final class SKDResponseArray: Sendable { - private let array: sourcekitd_api_variant_t - private let resp: SKDResponse - - package var sourcekitd: SourceKitD { return resp.sourcekitd } - - package init(_ array: sourcekitd_api_variant_t, response: SKDResponse) { - self.array = array - self.resp = response - } - - package var count: Int { return sourcekitd.api.variant_array_get_count(array) } - - /// If the `applier` returns `false`, iteration terminates. - @discardableResult - package func forEach(_ applier: (Int, SKDResponseDictionary) throws -> Bool) rethrows -> Bool { - for i in 0.. Bool) rethrows -> Bool { - for i in 0..(_ transform: (SKDResponseDictionary) throws -> T) rethrows -> [T] { - var result: [T] = [] - result.reserveCapacity(self.count) - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - try self.forEach { _, element in - result.append(try transform(element)) - return true - } - return result - } - - package func compactMap(_ transform: (SKDResponseDictionary) throws -> T?) rethrows -> [T] { - var result: [T] = [] - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - try self.forEach { _, element in - if let transformed = try transform(element) { - result.append(transformed) - } - return true - } - return result - } - - /// Attempt to access the item at `index` as a string. - package subscript(index: Int) -> String? { - if let cstr = sourcekitd.api.variant_array_get_string(array, index) { - return String(cString: cstr) - } - return nil - } - - public var asStringArray: [String] { - var result: [String] = [] - for i in 0.. String? { - return sourcekitd.api.variant_dictionary_get_string(dict, key).map(String.init(cString:)) - } - - package subscript(key: sourcekitd_api_uid_t) -> Int? { - let value = sourcekitd.api.variant_dictionary_get_value(dict, key) - if sourcekitd.api.variant_get_type(value) == SOURCEKITD_API_VARIANT_TYPE_INT64 { - return Int(sourcekitd.api.variant_int64_get_value(value)) - } else { - return nil - } - } - - package subscript(key: sourcekitd_api_uid_t) -> Bool? { - let value = sourcekitd.api.variant_dictionary_get_value(dict, key) - if sourcekitd.api.variant_get_type(value) == SOURCEKITD_API_VARIANT_TYPE_BOOL { - return sourcekitd.api.variant_bool_get_value(value) - } else { - return nil - } - } - - public subscript(key: sourcekitd_api_uid_t) -> Double? { - let value = sourcekitd.api.variant_dictionary_get_value(dict, key) - if sourcekitd.api.variant_get_type(value) == SOURCEKITD_API_VARIANT_TYPE_DOUBLE { - return sourcekitd.api.variant_double_get_value!(value) - } else { - return nil - } - } - - package subscript(key: sourcekitd_api_uid_t) -> sourcekitd_api_uid_t? { - return sourcekitd.api.variant_dictionary_get_uid(dict, key) - } - - package subscript(key: sourcekitd_api_uid_t) -> SKDResponseArray? { - let value = sourcekitd.api.variant_dictionary_get_value(dict, key) - if sourcekitd.api.variant_get_type(value) == SOURCEKITD_API_VARIANT_TYPE_ARRAY { - return SKDResponseArray(value, response: resp) - } else { - return nil - } - } -} - -extension SKDResponseDictionary: CustomStringConvertible { - package var description: String { - let ptr = sourcekitd.api.variant_description_copy(dict)! - defer { free(ptr) } - return String(cString: ptr) - } -} diff --git a/Sources/SourceKitD/SourceKitD.swift b/Sources/SourceKitD/SourceKitD.swift deleted file mode 100644 index f76c0e34b..000000000 --- a/Sources/SourceKitD/SourceKitD.swift +++ /dev/null @@ -1,481 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Csourcekitd -package import Foundation -import SKLogging -import SwiftExtensions - -extension sourcekitd_api_keys: @unchecked Sendable {} -extension sourcekitd_api_requests: @unchecked Sendable {} -extension sourcekitd_api_values: @unchecked Sendable {} - -fileprivate extension ThreadSafeBox { - /// If the wrapped value is `nil`, run `compute` and store the computed value. If it is not `nil`, return the stored - /// value. - func computeIfNil(compute: () -> WrappedValue) -> WrappedValue where T == WrappedValue? { - return withLock { value in - if let value { - return value - } - let computed = compute() - value = computed - return computed - } - } -} - -#if canImport(Darwin) -private func setenv(name: String, value: String, override: Bool) throws { - struct FailedToSetEnvError: Error { - let errorCode: Int32 - } - try name.withCString { name in - try value.withCString { value in - let result = setenv(name, value, override ? 0 : 1) - if result != 0 { - throw FailedToSetEnvError(errorCode: result) - } - } - } -} -#endif - -private struct SourceKitDRequestHandle: Sendable { - /// `nonisolated(unsafe)` is fine because we just use the handle as an opaque value. - nonisolated(unsafe) let handle: sourcekitd_api_request_handle_t -} - -package struct PluginPaths: Equatable, CustomLogStringConvertible { - package let clientPlugin: URL - package let servicePlugin: URL - - package init(clientPlugin: URL, servicePlugin: URL) { - self.clientPlugin = clientPlugin - self.servicePlugin = servicePlugin - } - - package var description: String { - "(client: \(clientPlugin), service: \(servicePlugin))" - } - - var redactedDescription: String { - "(client: \(clientPlugin.description.hashForLogging), service: \(servicePlugin.description.hashForLogging))" - } -} - -package enum SKDError: Error, Equatable { - /// The service has crashed. - case connectionInterrupted - - /// The request was unknown or had an invalid or missing parameter. - case requestInvalid(String) - - /// The request failed. - case requestFailed(String) - - /// The request was cancelled. - case requestCancelled - - /// The request exceeded the maximum allowed duration. - case timedOut - - /// Loading a required symbol from the sourcekitd library failed. - case missingRequiredSymbol(String) -} - -/// Wrapper for sourcekitd, taking care of initialization, shutdown, and notification handler -/// multiplexing. -/// -/// Users of this class should not call the api functions `initialize`, `shutdown`, or -/// `set_notification_handler`, which are global state managed internally by this class. -package actor SourceKitD { - /// The path to the sourcekitd dylib. - nonisolated package let path: URL - - /// The handle to the dylib. - private let dylib: DLHandle - - /// The sourcekitd API functions. - nonisolated package let api: sourcekitd_api_functions_t - - /// General API for the SourceKit service and client framework, eg. for plugin initialization and to set up custom - /// variant functions. - /// - /// This must not be referenced outside of `SwiftSourceKitPlugin`, `SwiftSourceKitPluginCommon`, or - /// `SwiftSourceKitClientPlugin`. - package nonisolated var pluginApi: sourcekitd_plugin_api_functions_t { try! pluginApiResult.get() } - private let pluginApiResult: Result - - /// The API with which the SourceKit plugin handles requests. - /// - /// This must not be referenced outside of `SwiftSourceKitPlugin`. - package nonisolated var servicePluginApi: sourcekitd_service_plugin_api_functions_t { - try! servicePluginApiResult.get() - } - private let servicePluginApiResult: Result - - /// The API with which the SourceKit plugin communicates with the type-checker in-process. - /// - /// This must not be referenced outside of `SwiftSourceKitPlugin`. - package nonisolated var ideApi: sourcekitd_ide_api_functions_t { try! ideApiResult.get() } - private let ideApiResult: Result - - /// Convenience for accessing known keys. - /// - /// These need to be computed dynamically so that a client has the chance to register a UID handler between the - /// initialization of the SourceKit plugin and the first request being handled by it. - private let _keys: ThreadSafeBox = ThreadSafeBox(initialValue: nil) - package nonisolated var keys: sourcekitd_api_keys { - _keys.computeIfNil { sourcekitd_api_keys(api: self.api) } - } - - /// Convenience for accessing known request names. - /// - /// These need to be computed dynamically so that a client has the chance to register a UID handler between the - /// initialization of the SourceKit plugin and the first request being handled by it. - private let _requests: ThreadSafeBox = ThreadSafeBox(initialValue: nil) - package nonisolated var requests: sourcekitd_api_requests { - _requests.computeIfNil { sourcekitd_api_requests(api: self.api) } - } - - /// Convenience for accessing known request/response values. - /// - /// These need to be computed dynamically so that a client has the chance to register a UID handler between the - /// initialization of the SourceKit plugin and the first request being handled by it. - private let _values: ThreadSafeBox = ThreadSafeBox(initialValue: nil) - package nonisolated var values: sourcekitd_api_values { - _values.computeIfNil { sourcekitd_api_values(api: self.api) } - } - - private nonisolated let notificationHandlingQueue = AsyncQueue() - - /// List of notification handlers that will be called for each notification. - private var notificationHandlers: [WeakSKDNotificationHandler] = [] - - /// List of hooks that should be executed and that need to finish executing before a request is sent to sourcekitd. - private var preRequestHandlingHooks: [UUID: @Sendable (SKDRequestDictionary) async -> Void] = [:] - - /// List of hooks that should be executed after a request sent to sourcekitd. - private var requestHandlingHooks: [UUID: (SKDRequestDictionary) -> Void] = [:] - - package static func getOrCreate( - dylibPath: URL, - pluginPaths: PluginPaths? - ) async throws -> SourceKitD { - try await SourceKitDRegistry.shared.getOrAdd(dylibPath, pluginPaths: pluginPaths) { - return try SourceKitD(dylib: dylibPath, pluginPaths: pluginPaths) - } - } - - package init(dylib path: URL, pluginPaths: PluginPaths?, initialize: Bool = true) throws { - #if os(Windows) - let dlopenModes: DLOpenFlags = [] - #else - let dlopenModes: DLOpenFlags = [.lazy, .local, .first] - #endif - let dlhandle = try dlopen(path.filePath, mode: dlopenModes) - try self.init( - dlhandle: dlhandle, - path: path, - pluginPaths: pluginPaths, - initialize: initialize - ) - } - - /// Create a `SourceKitD` instance from an existing `DLHandle`. `SourceKitD` takes over ownership of the `DLHandler` - /// and will close it when the `SourceKitD` instance gets deinitialized or if the initializer throws. - package init(dlhandle: DLHandle, path: URL, pluginPaths: PluginPaths?, initialize: Bool) throws { - do { - self.path = path - self.dylib = dlhandle - let api = try sourcekitd_api_functions_t(dlhandle) - self.api = api - - // We load the plugin-related functions eagerly so the members are initialized and we don't have data races on first - // access to eg. `pluginApi`. But if one of the functions is missing, we will only emit that error when that family - // of functions is being used. For example, it is expected that the plugin functions are not available in - // SourceKit-LSP. - self.ideApiResult = Result(catching: { try sourcekitd_ide_api_functions_t(dlhandle) }) - self.pluginApiResult = Result(catching: { try sourcekitd_plugin_api_functions_t(dlhandle) }) - self.servicePluginApiResult = Result(catching: { try sourcekitd_service_plugin_api_functions_t(dlhandle) }) - - if let pluginPaths { - api.register_plugin_path?(pluginPaths.clientPlugin.path, pluginPaths.servicePlugin.path) - } - if initialize { - self.api.initialize() - } - - if initialize { - self.api.set_notification_handler { [weak self] rawResponse in - guard let self, let rawResponse else { return } - let response = SKDResponse(rawResponse, sourcekitd: self) - self.notificationHandlingQueue.async { - let handlers = await self.notificationHandlers.compactMap(\.value) - - for handler in handlers { - handler.notification(response) - } - } - } - } - } catch { - orLog("Closing dlhandle after opening sourcekitd failed") { - try? dlhandle.close() - } - throw error - } - } - - deinit { - self.api.set_notification_handler(nil) - self.api.shutdown() - Task.detached(priority: .background) { [dylib, path] in - orLog("Closing dylib \(path)") { try dylib.close() } - } - } - - /// Adds a new notification handler (referenced weakly). - package func addNotificationHandler(_ handler: SKDNotificationHandler) { - notificationHandlers.removeAll(where: { $0.value == nil }) - notificationHandlers.append(.init(handler)) - } - - /// Removes a previously registered notification handler. - package func removeNotificationHandler(_ handler: SKDNotificationHandler) { - notificationHandlers.removeAll(where: { $0.value == nil || $0.value === handler }) - } - - /// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`. - /// - /// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd - /// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to - /// send a request during that time. - /// - /// This is intended for testing only. - package func withPreRequestHandlingHook( - body: () async throws -> Void, - hook: @escaping @Sendable (SKDRequestDictionary) async -> Void - ) async rethrows { - let id = UUID() - preRequestHandlingHooks[id] = hook - defer { preRequestHandlingHooks[id] = nil } - try await body() - } - - func willSend(request: SKDRequestDictionary) async { - let request = request - for hook in preRequestHandlingHooks.values { - await hook(request) - } - } - - /// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`. - /// - /// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd - /// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to - /// send a request during that time. - /// - /// This is intended for testing only. - package func withRequestHandlingHook( - body: () async throws -> Void, - hook: @escaping (SKDRequestDictionary) -> Void - ) async rethrows { - let id = UUID() - requestHandlingHooks[id] = hook - defer { requestHandlingHooks[id] = nil } - try await body() - } - - func didSend(request: SKDRequestDictionary) { - for hook in requestHandlingHooks.values { - hook(request) - } - } - - private struct ContextualRequest { - enum Kind { - case editorOpen - case codeCompleteOpen - } - let kind: Kind - let request: SKDRequestDictionary - } - - private var contextualRequests: [URL: [ContextualRequest]] = [:] - - private func recordContextualRequest( - requestUid: sourcekitd_api_uid_t, - request: SKDRequestDictionary, - documentUrl: URL? - ) { - guard let documentUrl else { - return - } - switch requestUid { - case requests.editorOpen: - contextualRequests[documentUrl] = [ContextualRequest(kind: .editorOpen, request: request)] - case requests.editorClose: - contextualRequests[documentUrl] = nil - case requests.codeCompleteOpen: - contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) - contextualRequests[documentUrl, default: []].append(ContextualRequest(kind: .codeCompleteOpen, request: request)) - case requests.codeCompleteClose: - contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) - if contextualRequests[documentUrl]?.isEmpty ?? false { - // This should never happen because we should still have an active `.editorOpen` contextual request but just be - // safe in case we don't. - contextualRequests[documentUrl] = nil - } - default: - break - } - } - - /// - Parameters: - /// - request: The request to send to sourcekitd. - /// - timeout: The maximum duration how long to wait for a response. If no response is returned within this time, - /// declare the request as having timed out. - /// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents - /// will be logged. - package func send( - _ requestUid: KeyPath, - _ request: SKDRequestDictionary, - timeout: Duration, - restartTimeout: Duration, - documentUrl: URL?, - fileContents: String? - ) async throws -> SKDResponseDictionary { - request.set(keys.request, to: requests[keyPath: requestUid]) - recordContextualRequest(requestUid: requests[keyPath: requestUid], request: request, documentUrl: documentUrl) - - let sourcekitdResponse = try await withTimeout(timeout) { - let restartTimeoutHandle = TimeoutHandle() - do { - return try await withTimeout(restartTimeout, handle: restartTimeoutHandle) { - await self.willSend(request: request) - return try await withCancellableCheckedThrowingContinuation { (continuation) -> SourceKitDRequestHandle? in - logger.info( - """ - Sending sourcekitd request: - \(request.forLogging) - """ - ) - var handle: sourcekitd_api_request_handle_t? = nil - self.api.send_request(request.dict, &handle) { response in - continuation.resume(returning: SKDResponse(response!, sourcekitd: self)) - } - Task { - await self.didSend(request: request) - } - if let handle { - return SourceKitDRequestHandle(handle: handle) - } - return nil - } cancel: { (handle: SourceKitDRequestHandle?) in - if let handle { - logger.info( - """ - Cancelling sourcekitd request: - \(request.forLogging) - """ - ) - self.api.cancel_request(handle.handle) - } - } - } - } catch let error as TimeoutError where error.handle == restartTimeoutHandle { - if !self.path.lastPathComponent.contains("InProc") { - logger.fault( - "Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Terminating and restarting sourcekitd." - ) - await self.crash() - } else { - logger.fault( - "Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Not terminating sourcekitd because it is run in-process." - ) - } - throw error - } - } - - logger.log( - level: (sourcekitdResponse.error == nil || sourcekitdResponse.error == .requestCancelled) ? .debug : .error, - """ - Received sourcekitd response: - \(sourcekitdResponse.forLogging) - """ - ) - - guard let dict = sourcekitdResponse.value else { - if sourcekitdResponse.error == .connectionInterrupted { - var log = """ - Request: - \(request.description) - - File contents: - \(fileContents ?? "") - """ - - if let documentUrl { - let contextualRequests = (contextualRequests[documentUrl] ?? []).filter { $0.request !== request } - for (index, contextualRequest) in contextualRequests.enumerated() { - log += """ - - Contextual request \(index + 1) / \(contextualRequests.count): - \(contextualRequest.request.description) - """ - } - } - let chunks = splitLongMultilineMessage(message: log) - for (index, chunk) in chunks.enumerated() { - logger.fault( - """ - sourcekitd crashed (\(index + 1)/\(chunks.count)) - \(chunk) - """ - ) - } - } - if sourcekitdResponse.error == .requestCancelled && !Task.isCancelled { - throw SKDError.timedOut - } - throw sourcekitdResponse.error! - } - - return dict - } - - package func crash() async { - _ = try? await send( - \.crashWithExit, - dictionary([:]), - timeout: .seconds(60), - restartTimeout: .seconds(24 * 60 * 60), - documentUrl: nil, - fileContents: nil - ) - } -} - -/// A sourcekitd notification handler in a class to allow it to be uniquely referenced. -package protocol SKDNotificationHandler: AnyObject, Sendable { - func notification(_: SKDResponse) -} - -struct WeakSKDNotificationHandler: Sendable { - weak private(set) var value: SKDNotificationHandler? - init(_ value: SKDNotificationHandler) { - self.value = value - } -} diff --git a/Sources/SourceKitD/SourceKitDRegistry.swift b/Sources/SourceKitD/SourceKitDRegistry.swift deleted file mode 100644 index f67e3a437..000000000 --- a/Sources/SourceKitD/SourceKitDRegistry.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -import SKLogging - -/// The set of known SourceKitD instances, uniqued by path. -/// -/// It is not generally safe to have two instances of SourceKitD for the same libsourcekitd, so -/// care is taken to ensure that there is only ever one instance per path. -/// -/// * To get a new instance, use `getOrAdd("path", create: { NewSourceKitD() })`. -/// * To remove an existing instance, use `remove("path")`, but be aware that if there are any other -/// references to the instances in the program, it can be resurrected if `getOrAdd` is called with -/// the same path. See note on `remove(_:)` -/// -/// `SourceKitDType` is usually `SourceKitD` but can be substituted for a different type for testing purposes. -package actor SourceKitDRegistry { - - /// Mapping from path to active SourceKitD instance. - private var active: [URL: (pluginPaths: PluginPaths?, sourcekitd: SourceKitDType)] = [:] - - /// Instances that have been unregistered, but may be resurrected if accessed before destruction. - private var cemetery: [URL: (pluginPaths: PluginPaths?, sourcekitd: WeakSourceKitD)] = [:] - - /// Initialize an empty registry. - package init() {} - - /// Returns the existing SourceKitD for the given path, or creates it and registers it. - package func getOrAdd( - _ key: URL, - pluginPaths: PluginPaths?, - create: () throws -> SourceKitDType - ) async rethrows -> SourceKitDType { - if let existing = active[key] { - if existing.pluginPaths != pluginPaths { - logger.fault( - "Already created SourceKitD with plugin paths \(existing.pluginPaths?.forLogging), now requesting incompatible plugin paths \(pluginPaths.forLogging)" - ) - } - return existing.sourcekitd - } - if let resurrected = cemetery[key], let resurrectedSourcekitD = resurrected.sourcekitd.value { - cemetery[key] = nil - if resurrected.pluginPaths != pluginPaths { - logger.fault( - "Already created SourceKitD with plugin paths \(resurrected.pluginPaths?.forLogging), now requesting incompatible plugin paths \(pluginPaths.forLogging)" - ) - } - active[key] = (resurrected.pluginPaths, resurrectedSourcekitD) - return resurrectedSourcekitD - } - let newValue = try create() - active[key] = (pluginPaths, newValue) - return newValue - } - - /// Removes the SourceKitD instance registered for the given path, if any, from the set of active - /// instances. - /// - /// Since it is not generally safe to have two sourcekitd connections at once, the existing value - /// is converted to a weak reference until it is no longer referenced anywhere by the program. If - /// the same path is looked up again before the original service is deinitialized, the original - /// service is resurrected rather than creating a new instance. - package func remove(_ key: URL) -> SourceKitDType? { - let existing = active.removeValue(forKey: key) - if let existing = existing { - assert(self.cemetery[key]?.sourcekitd.value == nil) - cemetery[key] = (existing.pluginPaths, WeakSourceKitD(value: existing.sourcekitd)) - } - return existing?.sourcekitd - } -} - -extension SourceKitDRegistry { - /// The global shared SourceKitD registry. - package static let shared: SourceKitDRegistry = SourceKitDRegistry() -} - -private struct WeakSourceKitD { - weak var value: SourceKitDType? -} diff --git a/Sources/SourceKitD/dlopen.swift b/Sources/SourceKitD/dlopen.swift deleted file mode 100644 index dd34e4f97..000000000 --- a/Sources/SourceKitD/dlopen.swift +++ /dev/null @@ -1,143 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SKLogging -import SwiftExtensions - -#if os(Windows) -import CRT -import WinSDK -#elseif os(iOS) || os(macOS) || os(tvOS) || os(watchOS) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#elseif canImport(Android) -import Android -#endif - -package final class DLHandle: Sendable { - fileprivate struct Handle: @unchecked Sendable { - #if os(Windows) - let handle: HMODULE - #else - let handle: UnsafeMutableRawPointer - #endif - } - - #if canImport(Darwin) - package static let rtldDefault = DLHandle(rawValue: Handle(handle: UnsafeMutableRawPointer(bitPattern: -2)!)) - #endif - - fileprivate let rawValue: ThreadSafeBox - - fileprivate init(rawValue: Handle) { - self.rawValue = .init(initialValue: rawValue) - } - - deinit { - if rawValue.value != nil { - logger.fault("DLHandle must be closed or explicitly leaked before destroying") - } - } - - /// The handle must not be used anymore after calling `close`. - package func close() throws { - try rawValue.withLock { rawValue in - if let handle = rawValue { - #if os(Windows) - guard FreeLibrary(handle.handle) else { - throw DLError.close("Failed to FreeLibrary: \(GetLastError())") - } - #else - guard dlclose(handle.handle) == 0 else { - throw DLError.close(dlerror() ?? "unknown error") - } - #endif - } - rawValue = nil - } - } - - /// The handle must not be used anymore after calling `leak`. - package func leak() { - rawValue.value = nil - } -} - -package struct DLOpenFlags: RawRepresentable, OptionSet, Sendable { - - #if !os(Windows) - package static let lazy: DLOpenFlags = DLOpenFlags(rawValue: RTLD_LAZY) - package static let now: DLOpenFlags = DLOpenFlags(rawValue: RTLD_NOW) - package static let local: DLOpenFlags = DLOpenFlags(rawValue: RTLD_LOCAL) - package static let global: DLOpenFlags = DLOpenFlags(rawValue: RTLD_GLOBAL) - - // Platform-specific flags. - #if os(macOS) - package static let first: DLOpenFlags = DLOpenFlags(rawValue: RTLD_FIRST) - #else - package static let first: DLOpenFlags = DLOpenFlags(rawValue: 0) - #endif - #endif - - package var rawValue: Int32 - - package init(rawValue: Int32) { - self.rawValue = rawValue - } -} - -package enum DLError: Swift.Error { - case `open`(String) - case close(String) -} - -package func dlopen(_ path: String?, mode: DLOpenFlags) throws -> DLHandle { - #if os(Windows) - guard let handle = path?.withCString(encodedAs: UTF16.self, LoadLibraryW) else { - throw DLError.open("LoadLibraryW failed: \(GetLastError())") - } - #else - guard let handle = dlopen(path, mode.rawValue) else { - throw DLError.open(dlerror() ?? "unknown error") - } - #endif - return DLHandle(rawValue: DLHandle.Handle(handle: handle)) -} - -package func dlsym(_ handle: DLHandle, symbol: String) -> T? { - #if os(Windows) - guard let ptr = GetProcAddress(handle.rawValue.value!.handle, symbol) else { - return nil - } - #else - guard let ptr = dlsym(handle.rawValue.value!.handle, symbol) else { - return nil - } - #endif - return unsafeBitCast(ptr, to: T.self) -} - -package func dlclose(_ handle: DLHandle) throws { - try handle.close() -} - -#if !os(Windows) -package func dlerror() -> String? { - if let err: UnsafeMutablePointer = dlerror() { - return String(cString: err) - } - return nil -} -#endif diff --git a/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift deleted file mode 100644 index c3d5a3b0e..000000000 --- a/Sources/SourceKitD/sourcekitd_functions.swift +++ /dev/null @@ -1,288 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd - -extension sourcekitd_api_functions_t { - package init(_ sourcekitd: DLHandle) throws { - func loadRequired(_ symbol: String) throws -> T { - guard let sym: T = dlsym(sourcekitd, symbol: symbol) else { - throw SKDError.missingRequiredSymbol(symbol) - } - return sym - } - - func loadOptional(_ symbol: String) -> T? { - return dlsym(sourcekitd, symbol: symbol) - } - - self.init( - initialize: try loadRequired("sourcekitd_initialize"), - shutdown: try loadRequired("sourcekitd_shutdown"), - register_plugin_path: loadOptional("sourcekitd_register_plugin_path"), - uid_get_from_cstr: try loadRequired("sourcekitd_uid_get_from_cstr"), - uid_get_from_buf: try loadRequired("sourcekitd_uid_get_from_buf"), - uid_get_length: try loadRequired("sourcekitd_uid_get_length"), - uid_get_string_ptr: try loadRequired("sourcekitd_uid_get_string_ptr"), - request_retain: try loadRequired("sourcekitd_request_retain"), - request_release: try loadRequired("sourcekitd_request_release"), - request_dictionary_create: try loadRequired("sourcekitd_request_dictionary_create"), - request_dictionary_set_value: try loadRequired("sourcekitd_request_dictionary_set_value"), - request_dictionary_set_string: try loadRequired("sourcekitd_request_dictionary_set_string"), - request_dictionary_set_stringbuf: try loadRequired("sourcekitd_request_dictionary_set_stringbuf"), - request_dictionary_set_int64: try loadRequired("sourcekitd_request_dictionary_set_int64"), - request_dictionary_set_uid: try loadRequired("sourcekitd_request_dictionary_set_uid"), - request_array_create: try loadRequired("sourcekitd_request_array_create"), - request_array_set_value: try loadRequired("sourcekitd_request_array_set_value"), - request_array_set_string: try loadRequired("sourcekitd_request_array_set_string"), - request_array_set_stringbuf: try loadRequired("sourcekitd_request_array_set_stringbuf"), - request_array_set_int64: try loadRequired("sourcekitd_request_array_set_int64"), - request_array_set_uid: try loadRequired("sourcekitd_request_array_set_uid"), - request_int64_create: try loadRequired("sourcekitd_request_int64_create"), - request_string_create: try loadRequired("sourcekitd_request_string_create"), - request_uid_create: try loadRequired("sourcekitd_request_uid_create"), - request_create_from_yaml: try loadRequired("sourcekitd_request_create_from_yaml"), - request_description_dump: try loadRequired("sourcekitd_request_description_dump"), - request_description_copy: try loadRequired("sourcekitd_request_description_copy"), - response_dispose: try loadRequired("sourcekitd_response_dispose"), - response_is_error: try loadRequired("sourcekitd_response_is_error"), - response_error_get_kind: try loadRequired("sourcekitd_response_error_get_kind"), - response_error_get_description: try loadRequired("sourcekitd_response_error_get_description"), - response_get_value: try loadRequired("sourcekitd_response_get_value"), - variant_get_type: try loadRequired("sourcekitd_variant_get_type"), - variant_dictionary_get_value: try loadRequired("sourcekitd_variant_dictionary_get_value"), - variant_dictionary_get_string: try loadRequired("sourcekitd_variant_dictionary_get_string"), - variant_dictionary_get_int64: try loadRequired("sourcekitd_variant_dictionary_get_int64"), - variant_dictionary_get_bool: try loadRequired("sourcekitd_variant_dictionary_get_bool"), - variant_dictionary_get_uid: try loadRequired("sourcekitd_variant_dictionary_get_uid"), - variant_array_get_count: try loadRequired("sourcekitd_variant_array_get_count"), - variant_array_get_value: try loadRequired("sourcekitd_variant_array_get_value"), - variant_array_get_string: try loadRequired("sourcekitd_variant_array_get_string"), - variant_array_get_int64: try loadRequired("sourcekitd_variant_array_get_int64"), - variant_array_get_bool: try loadRequired("sourcekitd_variant_array_get_bool"), - variant_array_get_uid: try loadRequired("sourcekitd_variant_array_get_uid"), - variant_int64_get_value: try loadRequired("sourcekitd_variant_int64_get_value"), - variant_bool_get_value: try loadRequired("sourcekitd_variant_bool_get_value"), - variant_double_get_value: loadOptional("sourcekitd_variant_double_get_value"), - variant_string_get_length: try loadRequired("sourcekitd_variant_string_get_length"), - variant_string_get_ptr: try loadRequired("sourcekitd_variant_string_get_ptr"), - variant_data_get_size: loadOptional("sourcekitd_variant_data_get_size"), - variant_data_get_ptr: loadOptional("sourcekitd_variant_data_get_ptr"), - variant_uid_get_value: try loadRequired("sourcekitd_variant_uid_get_value"), - response_description_dump: try loadRequired("sourcekitd_response_description_dump"), - response_description_dump_filedesc: try loadRequired("sourcekitd_response_description_dump_filedesc"), - response_description_copy: try loadRequired("sourcekitd_response_description_copy"), - variant_description_dump: try loadRequired("sourcekitd_variant_description_dump"), - variant_description_dump_filedesc: try loadRequired("sourcekitd_variant_description_dump_filedesc"), - variant_description_copy: try loadRequired("sourcekitd_variant_description_copy"), - send_request_sync: try loadRequired("sourcekitd_send_request_sync"), - send_request: try loadRequired("sourcekitd_send_request"), - cancel_request: try loadRequired("sourcekitd_cancel_request"), - set_notification_handler: try loadRequired("sourcekitd_set_notification_handler"), - set_uid_handlers: try loadRequired("sourcekitd_set_uid_handlers") - ) - - } -} - -extension sourcekitd_ide_api_functions_t { - package init(_ sourcekitd: DLHandle) throws { - func loadRequired(_ symbol: String) throws -> T { - guard let sym: T = dlsym(sourcekitd, symbol: symbol) else { - throw SKDError.missingRequiredSymbol(symbol) - } - return sym - } - - func loadOptional(_ symbol: String) -> T? { - return dlsym(sourcekitd, symbol: symbol) - } - self.init( - connection_create_with_inspection_instance: try loadRequired( - "swiftide_connection_create_with_inspection_instance" - ), - connection_dispose: try loadRequired("swiftide_connection_dispose"), - connection_mark_cached_compiler_instance_should_be_invalidated: try loadRequired( - "swiftide_connection_mark_cached_compiler_instance_should_be_invalidated" - ), - set_file_contents: try loadRequired("swiftide_set_file_contents"), - cancel_request: try loadRequired("swiftide_cancel_request"), - completion_request_create: try loadRequired("swiftide_completion_request_create"), - completion_request_dispose: try loadRequired("swiftide_completion_request_dispose"), - completion_request_set_annotate_result: try loadRequired("swiftide_completion_request_set_annotate_result"), - completion_request_set_include_objectliterals: try loadRequired( - "swiftide_completion_request_set_include_objectliterals" - ), - completion_request_set_add_inits_to_top_level: try loadRequired( - "swiftide_completion_request_set_add_inits_to_top_level" - ), - completion_request_set_add_call_with_no_default_args: try loadRequired( - "swiftide_completion_request_set_add_call_with_no_default_args" - ), - complete_cancellable: try loadRequired("swiftide_complete_cancellable"), - completion_result_dispose: try loadRequired("swiftide_completion_result_dispose"), - completion_result_is_error: try loadRequired("swiftide_completion_result_is_error"), - completion_result_get_error_description: try loadRequired("swiftide_completion_result_get_error_description"), - completion_result_is_cancelled: try loadRequired("swiftide_completion_result_is_cancelled"), - completion_result_description_copy: try loadRequired("swiftide_completion_result_description_copy"), - completion_result_get_completions: try loadRequired("swiftide_completion_result_get_completions"), - completion_result_get_completion_at_index: try loadRequired("swiftide_completion_result_get_completion_at_index"), - completion_result_get_kind: try loadRequired("swiftide_completion_result_get_kind"), - completion_result_foreach_baseexpr_typename: try loadRequired( - "swiftide_completion_result_foreach_baseexpr_typename" - ), - completion_result_is_reusing_astcontext: try loadRequired("swiftide_completion_result_is_reusing_astcontext"), - completion_item_description_copy: try loadRequired("swiftide_completion_item_description_copy"), - completion_item_get_label: try loadRequired("swiftide_completion_item_get_label"), - completion_item_get_source_text: try loadRequired("swiftide_completion_item_get_source_text"), - completion_item_get_type_name: try loadRequired("swiftide_completion_item_get_type_name"), - completion_item_get_doc_brief: try loadRequired("swiftide_completion_item_get_doc_brief"), - completion_item_get_doc_full_as_xml: loadOptional("swiftide_completion_item_get_doc_full_as_xml"), - completion_item_get_doc_raw: loadOptional("swiftide_completion_item_get_doc_raw"), - completion_item_get_associated_usrs: try loadRequired("swiftide_completion_item_get_associated_usrs"), - completion_item_get_kind: try loadRequired("swiftide_completion_item_get_kind"), - completion_item_get_associated_kind: try loadRequired("swiftide_completion_item_get_associated_kind"), - completion_item_get_semantic_context: try loadRequired("swiftide_completion_item_get_semantic_context"), - completion_item_get_flair: try loadRequired("swiftide_completion_item_get_flair"), - completion_item_is_not_recommended: try loadRequired("swiftide_completion_item_is_not_recommended"), - completion_item_not_recommended_reason: try loadRequired("swiftide_completion_item_not_recommended_reason"), - completion_item_has_diagnostic: try loadRequired("swiftide_completion_item_has_diagnostic"), - completion_item_get_diagnostic: try loadRequired("swiftide_completion_item_get_diagnostic"), - completion_item_is_system: try loadRequired("swiftide_completion_item_is_system"), - completion_item_get_module_name: try loadRequired("swiftide_completion_item_get_module_name"), - completion_item_get_num_bytes_to_erase: try loadRequired("swiftide_completion_item_get_num_bytes_to_erase"), - completion_item_get_type_relation: try loadRequired("swiftide_completion_item_get_type_relation"), - completion_item_import_depth: try loadRequired("swiftide_completion_item_import_depth"), - fuzzy_match_pattern_create: try loadRequired("swiftide_fuzzy_match_pattern_create"), - fuzzy_match_pattern_matches_candidate: try loadRequired("swiftide_fuzzy_match_pattern_matches_candidate"), - fuzzy_match_pattern_dispose: try loadRequired("swiftide_fuzzy_match_pattern_dispose") - ) - } -} - -extension sourcekitd_plugin_api_functions_t { - package init(_ sourcekitd: DLHandle) throws { - func loadRequired(_ symbol: String) throws -> T { - guard let sym: T = dlsym(sourcekitd, symbol: symbol) else { - throw SKDError.missingRequiredSymbol(symbol) - } - return sym - } - - func loadOptional(_ symbol: String) -> T? { - return dlsym(sourcekitd, symbol: symbol) - } - self.init( - variant_functions_create: try loadRequired("sourcekitd_variant_functions_create"), - variant_functions_set_get_type: try loadRequired("sourcekitd_variant_functions_set_get_type"), - variant_functions_set_array_apply: try loadRequired("sourcekitd_variant_functions_set_array_apply"), - variant_functions_set_array_get_bool: try loadRequired("sourcekitd_variant_functions_set_array_get_bool"), - variant_functions_set_array_get_double: try loadRequired("sourcekitd_variant_functions_set_array_get_double"), - variant_functions_set_array_get_count: try loadRequired("sourcekitd_variant_functions_set_array_get_count"), - variant_functions_set_array_get_int64: try loadRequired("sourcekitd_variant_functions_set_array_get_int64"), - variant_functions_set_array_get_string: try loadRequired("sourcekitd_variant_functions_set_array_get_string"), - variant_functions_set_array_get_uid: try loadRequired("sourcekitd_variant_functions_set_array_get_uid"), - variant_functions_set_array_get_value: try loadRequired("sourcekitd_variant_functions_set_array_get_value"), - variant_functions_set_bool_get_value: try loadRequired("sourcekitd_variant_functions_set_bool_get_value"), - variant_functions_set_double_get_value: try loadRequired("sourcekitd_variant_functions_set_double_get_value"), - variant_functions_set_dictionary_apply: try loadRequired("sourcekitd_variant_functions_set_dictionary_apply"), - variant_functions_set_dictionary_get_bool: try loadRequired( - "sourcekitd_variant_functions_set_dictionary_get_bool" - ), - variant_functions_set_dictionary_get_double: try loadRequired( - "sourcekitd_variant_functions_set_dictionary_get_double" - ), - variant_functions_set_dictionary_get_int64: try loadRequired( - "sourcekitd_variant_functions_set_dictionary_get_int64" - ), - variant_functions_set_dictionary_get_string: try loadRequired( - "sourcekitd_variant_functions_set_dictionary_get_string" - ), - variant_functions_set_dictionary_get_value: try loadRequired( - "sourcekitd_variant_functions_set_dictionary_get_value" - ), - variant_functions_set_dictionary_get_uid: try loadRequired("sourcekitd_variant_functions_set_dictionary_get_uid"), - variant_functions_set_string_get_length: try loadRequired("sourcekitd_variant_functions_set_string_get_length"), - variant_functions_set_string_get_ptr: try loadRequired("sourcekitd_variant_functions_set_string_get_ptr"), - variant_functions_set_int64_get_value: try loadRequired("sourcekitd_variant_functions_set_int64_get_value"), - variant_functions_set_uid_get_value: try loadRequired("sourcekitd_variant_functions_set_uid_get_value"), - variant_functions_set_data_get_size: try loadRequired("sourcekitd_variant_functions_set_data_get_size"), - variant_functions_set_data_get_ptr: try loadRequired("sourcekitd_variant_functions_set_data_get_ptr"), - plugin_initialize_is_client_only: try loadRequired("sourcekitd_plugin_initialize_is_client_only"), - plugin_initialize_custom_buffer_start: try loadRequired("sourcekitd_plugin_initialize_custom_buffer_start"), - plugin_initialize_uid_get_from_cstr: try loadRequired("sourcekitd_plugin_initialize_uid_get_from_cstr"), - plugin_initialize_uid_get_string_ptr: try loadRequired("sourcekitd_plugin_initialize_uid_get_string_ptr"), - plugin_initialize_register_custom_buffer: try loadRequired("sourcekitd_plugin_initialize_register_custom_buffer") - ) - } -} - -extension sourcekitd_service_plugin_api_functions_t { - package init(_ sourcekitd: DLHandle) throws { - func loadRequired(_ symbol: String) throws -> T { - guard let sym: T = dlsym(sourcekitd, symbol: symbol) else { - throw SKDError.missingRequiredSymbol(symbol) - } - return sym - } - - func loadOptional(_ symbol: String) -> T? { - return dlsym(sourcekitd, symbol: symbol) - } - self.init( - plugin_initialize_register_cancellable_request_handler: try loadRequired( - "sourcekitd_plugin_initialize_register_cancellable_request_handler" - ), - plugin_initialize_register_cancellation_handler: try loadRequired( - "sourcekitd_plugin_initialize_register_cancellation_handler" - ), - plugin_initialize_get_swift_ide_inspection_instance: try loadRequired( - "sourcekitd_plugin_initialize_get_swift_ide_inspection_instance" - ), - request_get_type: try loadRequired("sourcekitd_request_get_type"), - request_dictionary_get_value: try loadRequired("sourcekitd_request_dictionary_get_value"), - request_dictionary_get_string: try loadRequired("sourcekitd_request_dictionary_get_string"), - request_dictionary_get_int64: try loadRequired("sourcekitd_request_dictionary_get_int64"), - request_dictionary_get_bool: try loadRequired("sourcekitd_request_dictionary_get_bool"), - request_dictionary_get_uid: try loadRequired("sourcekitd_request_dictionary_get_uid"), - request_array_get_count: try loadRequired("sourcekitd_request_array_get_count"), - request_array_get_value: try loadRequired("sourcekitd_request_array_get_value"), - request_array_get_string: try loadRequired("sourcekitd_request_array_get_string"), - request_array_get_int64: try loadRequired("sourcekitd_request_array_get_int64"), - request_array_get_bool: try loadRequired("sourcekitd_request_array_get_bool"), - request_array_get_uid: try loadRequired("sourcekitd_request_array_get_uid"), - request_int64_get_value: try loadRequired("sourcekitd_request_int64_get_value"), - request_bool_get_value: try loadRequired("sourcekitd_request_bool_get_value"), - request_string_get_length: try loadRequired("sourcekitd_request_string_get_length"), - request_string_get_ptr: try loadRequired("sourcekitd_request_string_get_ptr"), - request_uid_get_value: try loadRequired("sourcekitd_request_uid_get_value"), - response_retain: try loadRequired("sourcekitd_response_retain"), - response_error_create: try loadRequired("sourcekitd_response_error_create"), - response_dictionary_create: try loadRequired("sourcekitd_response_dictionary_create"), - response_dictionary_set_value: try loadRequired("sourcekitd_response_dictionary_set_value"), - response_dictionary_set_string: try loadRequired("sourcekitd_response_dictionary_set_string"), - response_dictionary_set_stringbuf: try loadRequired("sourcekitd_response_dictionary_set_stringbuf"), - response_dictionary_set_int64: try loadRequired("sourcekitd_response_dictionary_set_int64"), - response_dictionary_set_bool: try loadRequired("sourcekitd_response_dictionary_set_bool"), - response_dictionary_set_double: try loadRequired("sourcekitd_response_dictionary_set_double"), - response_dictionary_set_uid: try loadRequired("sourcekitd_response_dictionary_set_uid"), - response_array_create: try loadRequired("sourcekitd_response_array_create"), - response_array_set_value: try loadRequired("sourcekitd_response_array_set_value"), - response_array_set_string: try loadRequired("sourcekitd_response_array_set_string"), - response_array_set_stringbuf: try loadRequired("sourcekitd_response_array_set_stringbuf"), - response_array_set_int64: try loadRequired("sourcekitd_response_array_set_int64"), - response_array_set_double: try loadRequired("sourcekitd_response_array_set_double"), - response_array_set_uid: try loadRequired("sourcekitd_response_array_set_uid"), - response_dictionary_set_custom_buffer: try loadRequired("sourcekitd_response_dictionary_set_custom_buffer") - ) - } -} diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift deleted file mode 100644 index 09a73cdb9..000000000 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ /dev/null @@ -1,1551 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -// Automatically Generated From UIDs.swift.gyb. -// Do Not Edit Directly! To regenerate run Utilities/generate-uids.py - -package import Csourcekitd - -// swift-format-ignore: TypeNamesShouldBeCapitalized -// Matching C style types -package struct sourcekitd_api_keys { - /// `key.version_major` - package let versionMajor: sourcekitd_api_uid_t - /// `key.version_minor` - package let versionMinor: sourcekitd_api_uid_t - /// `key.version_patch` - package let versionPatch: sourcekitd_api_uid_t - /// `key.results` - package let results: sourcekitd_api_uid_t - /// `key.request` - package let request: sourcekitd_api_uid_t - /// `key.notification` - package let notification: sourcekitd_api_uid_t - /// `key.kind` - package let kind: sourcekitd_api_uid_t - /// `key.accessibility` - package let accessLevel: sourcekitd_api_uid_t - /// `key.setter_accessibility` - package let setterAccessLevel: sourcekitd_api_uid_t - /// `key.keyword` - package let keyword: sourcekitd_api_uid_t - /// `key.name` - package let name: sourcekitd_api_uid_t - /// `key.usr` - package let usr: sourcekitd_api_uid_t - /// `key.original_usr` - package let originalUSR: sourcekitd_api_uid_t - /// `key.default_implementation_of` - package let defaultImplementationOf: sourcekitd_api_uid_t - /// `key.interested_usr` - package let interestedUSR: sourcekitd_api_uid_t - /// `key.generic_params` - package let genericParams: sourcekitd_api_uid_t - /// `key.generic_requirements` - package let genericRequirements: sourcekitd_api_uid_t - /// `key.doc.full_as_xml` - package let docFullAsXML: sourcekitd_api_uid_t - /// `key.doc_comment` - package let docComment: sourcekitd_api_uid_t - /// `key.line` - package let line: sourcekitd_api_uid_t - /// `key.column` - package let column: sourcekitd_api_uid_t - /// `key.receiver_usr` - package let receiverUSR: sourcekitd_api_uid_t - /// `key.receivers` - package let receivers: sourcekitd_api_uid_t - /// `key.is_dynamic` - package let isDynamic: sourcekitd_api_uid_t - /// `key.is_implicit` - package let isImplicit: sourcekitd_api_uid_t - /// `key.filepath` - package let filePath: sourcekitd_api_uid_t - /// `key.module_interface_name` - package let moduleInterfaceName: sourcekitd_api_uid_t - /// `key.hash` - package let hash: sourcekitd_api_uid_t - /// `key.severity` - package let severity: sourcekitd_api_uid_t - /// `key.offset` - package let offset: sourcekitd_api_uid_t - /// `key.length` - package let length: sourcekitd_api_uid_t - /// `key.sourcefile` - package let sourceFile: sourcekitd_api_uid_t - /// `key.primary_file` - package let primaryFile: sourcekitd_api_uid_t - /// `key.enablesyntaxmap` - package let enableSyntaxMap: sourcekitd_api_uid_t - /// `key.enablesubstructure` - package let enableStructure: sourcekitd_api_uid_t - /// `key.id` - package let id: sourcekitd_api_uid_t - /// `key.description` - package let description: sourcekitd_api_uid_t - /// `key.typename` - package let typeName: sourcekitd_api_uid_t - /// `key.runtime_name` - package let runtimeName: sourcekitd_api_uid_t - /// `key.selector_name` - package let selectorName: sourcekitd_api_uid_t - /// `key.annotated_decl` - package let annotatedDecl: sourcekitd_api_uid_t - /// `key.fully_annotated_decl` - package let fullyAnnotatedDecl: sourcekitd_api_uid_t - /// `key.fully_annotated_generic_signature` - package let fullyAnnotatedGenericSignature: sourcekitd_api_uid_t - /// `key.signatures` - package let signatures: sourcekitd_api_uid_t - /// `key.active_signature` - package let activeSignature: sourcekitd_api_uid_t - /// `key.parameters` - package let parameters: sourcekitd_api_uid_t - /// `key.active_parameter` - package let activeParameter: sourcekitd_api_uid_t - /// `key.doc.brief` - package let docBrief: sourcekitd_api_uid_t - /// `key.context` - package let context: sourcekitd_api_uid_t - /// `key.typerelation` - package let typeRelation: sourcekitd_api_uid_t - /// `key.moduleimportdepth` - package let moduleImportDepth: sourcekitd_api_uid_t - /// `key.num_bytes_to_erase` - package let numBytesToErase: sourcekitd_api_uid_t - /// `key.not_recommended` - package let notRecommended: sourcekitd_api_uid_t - /// `key.declarations` - package let declarations: sourcekitd_api_uid_t - /// `key.enabledeclarations` - package let enableDeclarations: sourcekitd_api_uid_t - /// `key.annotations` - package let annotations: sourcekitd_api_uid_t - /// `key.semantic_tokens` - package let semanticTokens: sourcekitd_api_uid_t - /// `key.diagnostic_stage` - package let diagnosticStage: sourcekitd_api_uid_t - /// `key.syntaxmap` - package let syntaxMap: sourcekitd_api_uid_t - /// `key.is_system` - package let isSystem: sourcekitd_api_uid_t - /// `key.related` - package let related: sourcekitd_api_uid_t - /// `key.inherits` - package let inherits: sourcekitd_api_uid_t - /// `key.conforms` - package let conforms: sourcekitd_api_uid_t - /// `key.extends` - package let extends: sourcekitd_api_uid_t - /// `key.dependencies` - package let dependencies: sourcekitd_api_uid_t - /// `key.entities` - package let entities: sourcekitd_api_uid_t - /// `key.nameoffset` - package let nameOffset: sourcekitd_api_uid_t - /// `key.namelength` - package let nameLength: sourcekitd_api_uid_t - /// `key.bodyoffset` - package let bodyOffset: sourcekitd_api_uid_t - /// `key.bodylength` - package let bodyLength: sourcekitd_api_uid_t - /// `key.docoffset` - package let docOffset: sourcekitd_api_uid_t - /// `key.doclength` - package let docLength: sourcekitd_api_uid_t - /// `key.is_active` - package let isActive: sourcekitd_api_uid_t - /// `key.is_local` - package let isLocal: sourcekitd_api_uid_t - /// `key.inheritedtypes` - package let inheritedTypes: sourcekitd_api_uid_t - /// `key.attributes` - package let attributes: sourcekitd_api_uid_t - /// `key.attribute` - package let attribute: sourcekitd_api_uid_t - /// `key.elements` - package let elements: sourcekitd_api_uid_t - /// `key.substructure` - package let subStructure: sourcekitd_api_uid_t - /// `key.ranges` - package let ranges: sourcekitd_api_uid_t - /// `key.fixits` - package let fixits: sourcekitd_api_uid_t - /// `key.generated_buffers` - package let generatedBuffers: sourcekitd_api_uid_t - /// `key.buffer_text` - package let bufferText: sourcekitd_api_uid_t - /// `key.original_location` - package let originalLocation: sourcekitd_api_uid_t - /// `key.diagnostics` - package let diagnostics: sourcekitd_api_uid_t - /// `key.educational_note_paths` - package let educationalNotePaths: sourcekitd_api_uid_t - /// `key.editor.format.options` - package let formatOptions: sourcekitd_api_uid_t - /// `key.codecomplete.options` - package let codeCompleteOptions: sourcekitd_api_uid_t - /// `key.typecontextinfo.options` - package let typeContextInfoOptions: sourcekitd_api_uid_t - /// `key.conformingmethods.options` - package let conformingMethodListOptions: sourcekitd_api_uid_t - /// `key.codecomplete.filterrules` - package let filterRules: sourcekitd_api_uid_t - /// `key.nextrequeststart` - package let nextRequestStart: sourcekitd_api_uid_t - /// `key.popular` - package let popular: sourcekitd_api_uid_t - /// `key.unpopular` - package let unpopular: sourcekitd_api_uid_t - /// `key.hide` - package let hide: sourcekitd_api_uid_t - /// `key.platform` - package let platform: sourcekitd_api_uid_t - /// `key.is_deprecated` - package let isDeprecated: sourcekitd_api_uid_t - /// `key.is_unavailable` - package let isUnavailable: sourcekitd_api_uid_t - /// `key.is_optional` - package let isOptional: sourcekitd_api_uid_t - /// `key.is_async` - package let isAsync: sourcekitd_api_uid_t - /// `key.message` - package let message: sourcekitd_api_uid_t - /// `key.introduced` - package let introduced: sourcekitd_api_uid_t - /// `key.deprecated` - package let deprecated: sourcekitd_api_uid_t - /// `key.obsoleted` - package let obsoleted: sourcekitd_api_uid_t - /// `key.cancel_builds` - package let cancelBuilds: sourcekitd_api_uid_t - /// `key.removecache` - package let removeCache: sourcekitd_api_uid_t - /// `key.typeusr` - package let typeUsr: sourcekitd_api_uid_t - /// `key.containertypeusr` - package let containerTypeUsr: sourcekitd_api_uid_t - /// `key.modulegroups` - package let moduleGroups: sourcekitd_api_uid_t - /// `key.basename` - package let baseName: sourcekitd_api_uid_t - /// `key.argnames` - package let argNames: sourcekitd_api_uid_t - /// `key.selectorpieces` - package let selectorPieces: sourcekitd_api_uid_t - /// `key.namekind` - package let nameKind: sourcekitd_api_uid_t - /// `key.localization_key` - package let localizationKey: sourcekitd_api_uid_t - /// `key.is_zero_arg_selector` - package let isZeroArgSelector: sourcekitd_api_uid_t - /// `key.swift_version` - package let swiftVersion: sourcekitd_api_uid_t - /// `key.value` - package let value: sourcekitd_api_uid_t - /// `key.enablediagnostics` - package let enableDiagnostics: sourcekitd_api_uid_t - /// `key.groupname` - package let groupName: sourcekitd_api_uid_t - /// `key.actionname` - package let actionName: sourcekitd_api_uid_t - /// `key.synthesizedextensions` - package let synthesizedExtension: sourcekitd_api_uid_t - /// `key.usingswiftargs` - package let usingSwiftArgs: sourcekitd_api_uid_t - /// `key.names` - package let names: sourcekitd_api_uid_t - /// `key.uids` - package let uiDs: sourcekitd_api_uid_t - /// `key.syntactic_only` - package let syntacticOnly: sourcekitd_api_uid_t - /// `key.parent_loc` - package let parentLoc: sourcekitd_api_uid_t - /// `key.is_test_candidate` - package let isTestCandidate: sourcekitd_api_uid_t - /// `key.overrides` - package let overrides: sourcekitd_api_uid_t - /// `key.associated_usrs` - package let associatedUSRs: sourcekitd_api_uid_t - /// `key.modulename` - package let moduleName: sourcekitd_api_uid_t - /// `key.related_decls` - package let relatedDecls: sourcekitd_api_uid_t - /// `key.simplified` - package let simplified: sourcekitd_api_uid_t - /// `key.rangecontent` - package let rangeContent: sourcekitd_api_uid_t - /// `key.cancel_on_subsequent_request` - package let cancelOnSubsequentRequest: sourcekitd_api_uid_t - /// `key.include_non_editable_base_names` - package let includeNonEditableBaseNames: sourcekitd_api_uid_t - /// `key.renamelocations` - package let renameLocations: sourcekitd_api_uid_t - /// `key.locations` - package let locations: sourcekitd_api_uid_t - /// `key.nametype` - package let nameType: sourcekitd_api_uid_t - /// `key.newname` - package let newName: sourcekitd_api_uid_t - /// `key.categorizededits` - package let categorizedEdits: sourcekitd_api_uid_t - /// `key.categorizedranges` - package let categorizedRanges: sourcekitd_api_uid_t - /// `key.rangesworthnote` - package let rangesWorthNote: sourcekitd_api_uid_t - /// `key.edits` - package let edits: sourcekitd_api_uid_t - /// `key.endline` - package let endLine: sourcekitd_api_uid_t - /// `key.endcolumn` - package let endColumn: sourcekitd_api_uid_t - /// `key.argindex` - package let argIndex: sourcekitd_api_uid_t - /// `key.text` - package let text: sourcekitd_api_uid_t - /// `key.category` - package let category: sourcekitd_api_uid_t - /// `key.categories` - package let categories: sourcekitd_api_uid_t - /// `key.is_function_like` - package let isFunctionLike: sourcekitd_api_uid_t - /// `key.is_non_protocol_type` - package let isNonProtocolType: sourcekitd_api_uid_t - /// `key.refactor_actions` - package let refactorActions: sourcekitd_api_uid_t - /// `key.retrieve_refactor_actions` - package let retrieveRefactorActions: sourcekitd_api_uid_t - /// `key.symbol_graph` - package let symbolGraph: sourcekitd_api_uid_t - /// `key.retrieve_symbol_graph` - package let retrieveSymbolGraph: sourcekitd_api_uid_t - /// `key.parent_contexts` - package let parentContexts: sourcekitd_api_uid_t - /// `key.referenced_symbols` - package let referencedSymbols: sourcekitd_api_uid_t - /// `key.is_spi` - package let isSPI: sourcekitd_api_uid_t - /// `key.actionuid` - package let actionUID: sourcekitd_api_uid_t - /// `key.actionunavailablereason` - package let actionUnavailableReason: sourcekitd_api_uid_t - /// `key.compileid` - package let compileID: sourcekitd_api_uid_t - /// `key.compilerargs-string` - package let compilerArgsString: sourcekitd_api_uid_t - /// `key.implicitmembers` - package let implicitMembers: sourcekitd_api_uid_t - /// `key.expectedtypes` - package let expectedTypes: sourcekitd_api_uid_t - /// `key.members` - package let members: sourcekitd_api_uid_t - /// `key.printedtypebuffer` - package let typeBuffer: sourcekitd_api_uid_t - /// `key.expression_type_list` - package let expressionTypeList: sourcekitd_api_uid_t - /// `key.expression_offset` - package let expressionOffset: sourcekitd_api_uid_t - /// `key.expression_length` - package let expressionLength: sourcekitd_api_uid_t - /// `key.expression_type` - package let expressionType: sourcekitd_api_uid_t - /// `key.variable_type_list` - package let variableTypeList: sourcekitd_api_uid_t - /// `key.variable_offset` - package let variableOffset: sourcekitd_api_uid_t - /// `key.variable_length` - package let variableLength: sourcekitd_api_uid_t - /// `key.variable_type` - package let variableType: sourcekitd_api_uid_t - /// `key.variable_type_explicit` - package let variableTypeExplicit: sourcekitd_api_uid_t - /// `key.fully_qualified` - package let fullyQualified: sourcekitd_api_uid_t - /// `key.canonicalize_type` - package let canonicalizeType: sourcekitd_api_uid_t - /// `key.internal_diagnostic` - package let internalDiagnostic: sourcekitd_api_uid_t - /// `key.vfs.name` - package let vfsName: sourcekitd_api_uid_t - /// `key.vfs.options` - package let vfsOptions: sourcekitd_api_uid_t - /// `key.files` - package let files: sourcekitd_api_uid_t - /// `key.optimize_for_ide` - package let optimizeForIDE: sourcekitd_api_uid_t - /// `key.required_bystanders` - package let requiredBystanders: sourcekitd_api_uid_t - /// `key.reusingastcontext` - package let reusingASTContext: sourcekitd_api_uid_t - /// `key.completion_max_astcontext_reuse_count` - package let completionMaxASTContextReuseCount: sourcekitd_api_uid_t - /// `key.completion_check_dependency_interval` - package let completionCheckDependencyInterval: sourcekitd_api_uid_t - /// `key.annotated.typename` - package let annotatedTypename: sourcekitd_api_uid_t - /// `key.compile_operation` - package let compileOperation: sourcekitd_api_uid_t - /// `key.effective_access` - package let effectiveAccess: sourcekitd_api_uid_t - /// `key.decl_lang` - package let declarationLang: sourcekitd_api_uid_t - /// `key.secondary_symbols` - package let secondarySymbols: sourcekitd_api_uid_t - /// `key.simulate_long_request` - package let simulateLongRequest: sourcekitd_api_uid_t - /// `key.is_synthesized` - package let isSynthesized: sourcekitd_api_uid_t - /// `key.buffer_name` - package let bufferName: sourcekitd_api_uid_t - /// `key.barriers_enabled` - package let barriersEnabled: sourcekitd_api_uid_t - /// `key.expansions` - package let expansions: sourcekitd_api_uid_t - /// `key.macro_roles` - package let macroRoles: sourcekitd_api_uid_t - /// `key.expanded_macro_replacements` - package let expandedMacroReplacements: sourcekitd_api_uid_t - /// `key.index_store_path` - package let indexStorePath: sourcekitd_api_uid_t - /// `key.index_unit_output_path` - package let indexUnitOutputPath: sourcekitd_api_uid_t - /// `key.include_locals` - package let includeLocals: sourcekitd_api_uid_t - /// `key.compress` - package let compress: sourcekitd_api_uid_t - /// `key.ignore_clang_modules` - package let ignoreClangModules: sourcekitd_api_uid_t - /// `key.include_system_modules` - package let includeSystemModules: sourcekitd_api_uid_t - /// `key.ignore_stdlib` - package let ignoreStdlib: sourcekitd_api_uid_t - /// `key.disable_implicit_modules` - package let disableImplicitModules: sourcekitd_api_uid_t - /// `key.compilerargs` - package let compilerArgs: sourcekitd_api_uid_t - /// `key.sourcetext` - package let sourceText: sourcekitd_api_uid_t - /// `key.codecomplete.sort.byname` - package let sortByName: sourcekitd_api_uid_t - /// `key.codecomplete.sort.useimportdepth` - package let useImportDepth: sourcekitd_api_uid_t - /// `key.codecomplete.group.overloads` - package let groupOverloads: sourcekitd_api_uid_t - /// `key.codecomplete.group.stems` - package let groupStems: sourcekitd_api_uid_t - /// `key.codecomplete.filtertext` - package let filterText: sourcekitd_api_uid_t - /// `key.codecomplete.requestlimit` - package let requestLimit: sourcekitd_api_uid_t - /// `key.codecomplete.requeststart` - package let requestStart: sourcekitd_api_uid_t - /// `key.codecomplete.hideunderscores` - package let hideUnderscores: sourcekitd_api_uid_t - /// `key.codecomplete.hidelowpriority` - package let hideLowPriority: sourcekitd_api_uid_t - /// `key.codecomplete.hidebyname` - package let hideByName: sourcekitd_api_uid_t - /// `key.codecomplete.includeexactmatch` - package let includeExactMatch: sourcekitd_api_uid_t - /// `key.codecomplete.addinnerresults` - package let addInnerResults: sourcekitd_api_uid_t - /// `key.codecomplete.addinneroperators` - package let addInnerOperators: sourcekitd_api_uid_t - /// `key.codecomplete.addinitstotoplevel` - package let addInitsToTopLevel: sourcekitd_api_uid_t - /// `key.codecomplete.fuzzymatching` - package let fuzzyMatching: sourcekitd_api_uid_t - /// `key.codecomplete.showtopnonliteralresults` - package let topNonLiteral: sourcekitd_api_uid_t - /// `key.codecomplete.sort.contextweight` - package let contextWeight: sourcekitd_api_uid_t - /// `key.codecomplete.sort.fuzzyweight` - package let fuzzyWeight: sourcekitd_api_uid_t - /// `key.codecomplete.sort.popularitybonus` - package let popularityBonus: sourcekitd_api_uid_t - /// `key.codecomplete.annotateddescription` - package let annotatedDescription: sourcekitd_api_uid_t - /// `key.codecomplete.includeobjectliterals` - package let includeObjectLiterals: sourcekitd_api_uid_t - /// `key.codecomplete.use_new_api` - package let useNewAPI: sourcekitd_api_uid_t - /// `key.codecomplete.addcallwithnodefaultargs` - package let addCallWithNoDefaultArgs: sourcekitd_api_uid_t - /// `key.codecomplete.include_semantic_components` - package let includeSemanticComponents: sourcekitd_api_uid_t - /// `key.codecomplete.use_xpc_serialization` - package let useXPCSerialization: sourcekitd_api_uid_t - /// `key.codecomplete.maxresults` - package let maxResults: sourcekitd_api_uid_t - /// `key.annotated.typename` - package let annotatedTypeName: sourcekitd_api_uid_t - /// `key.priority_bucket` - package let priorityBucket: sourcekitd_api_uid_t - /// `key.identifier` - package let identifier: sourcekitd_api_uid_t - /// `key.text_match_score` - package let textMatchScore: sourcekitd_api_uid_t - /// `key.semantic_score` - package let semanticScore: sourcekitd_api_uid_t - /// `key.semantic_score_components` - package let semanticScoreComponents: sourcekitd_api_uid_t - /// `key.symbol_popularity` - package let symbolPopularity: sourcekitd_api_uid_t - /// `key.module_popularity` - package let modulePopularity: sourcekitd_api_uid_t - /// `key.popularity.key` - package let popularityKey: sourcekitd_api_uid_t - /// `key.popularity.value.int.billion` - package let popularityValueIntBillion: sourcekitd_api_uid_t - /// `key.recent_completions` - package let recentCompletions: sourcekitd_api_uid_t - /// `key.unfiltered_result_count` - package let unfilteredResultCount: sourcekitd_api_uid_t - /// `key.member_access_types` - package let memberAccessTypes: sourcekitd_api_uid_t - /// `key.has_diagnostic` - package let hasDiagnostic: sourcekitd_api_uid_t - /// `key.group_id` - package let groupId: sourcekitd_api_uid_t - /// `key.scoped_popularity_table_path` - package let scopedPopularityTablePath: sourcekitd_api_uid_t - /// `key.popular_modules` - package let popularModules: sourcekitd_api_uid_t - /// `key.notorious_modules` - package let notoriousModules: sourcekitd_api_uid_t - /// `key.codecomplete.setpopularapi_used_score_components` - package let usedScoreComponents: sourcekitd_api_uid_t - /// `key.editor.format.usetabs` - package let useTabs: sourcekitd_api_uid_t - /// `key.editor.format.indentwidth` - package let indentWidth: sourcekitd_api_uid_t - /// `key.editor.format.tabwidth` - package let tabWidth: sourcekitd_api_uid_t - /// `key.editor.format.indent_switch_case` - package let indentSwitchCase: sourcekitd_api_uid_t - - package init(api: sourcekitd_api_functions_t) { - versionMajor = api.uid_get_from_cstr("key.version_major")! - versionMinor = api.uid_get_from_cstr("key.version_minor")! - versionPatch = api.uid_get_from_cstr("key.version_patch")! - results = api.uid_get_from_cstr("key.results")! - request = api.uid_get_from_cstr("key.request")! - notification = api.uid_get_from_cstr("key.notification")! - kind = api.uid_get_from_cstr("key.kind")! - accessLevel = api.uid_get_from_cstr("key.accessibility")! - setterAccessLevel = api.uid_get_from_cstr("key.setter_accessibility")! - keyword = api.uid_get_from_cstr("key.keyword")! - name = api.uid_get_from_cstr("key.name")! - usr = api.uid_get_from_cstr("key.usr")! - originalUSR = api.uid_get_from_cstr("key.original_usr")! - defaultImplementationOf = api.uid_get_from_cstr("key.default_implementation_of")! - interestedUSR = api.uid_get_from_cstr("key.interested_usr")! - genericParams = api.uid_get_from_cstr("key.generic_params")! - genericRequirements = api.uid_get_from_cstr("key.generic_requirements")! - docFullAsXML = api.uid_get_from_cstr("key.doc.full_as_xml")! - docComment = api.uid_get_from_cstr("key.doc_comment")! - line = api.uid_get_from_cstr("key.line")! - column = api.uid_get_from_cstr("key.column")! - receiverUSR = api.uid_get_from_cstr("key.receiver_usr")! - receivers = api.uid_get_from_cstr("key.receivers")! - isDynamic = api.uid_get_from_cstr("key.is_dynamic")! - isImplicit = api.uid_get_from_cstr("key.is_implicit")! - filePath = api.uid_get_from_cstr("key.filepath")! - moduleInterfaceName = api.uid_get_from_cstr("key.module_interface_name")! - hash = api.uid_get_from_cstr("key.hash")! - severity = api.uid_get_from_cstr("key.severity")! - offset = api.uid_get_from_cstr("key.offset")! - length = api.uid_get_from_cstr("key.length")! - sourceFile = api.uid_get_from_cstr("key.sourcefile")! - primaryFile = api.uid_get_from_cstr("key.primary_file")! - enableSyntaxMap = api.uid_get_from_cstr("key.enablesyntaxmap")! - enableStructure = api.uid_get_from_cstr("key.enablesubstructure")! - id = api.uid_get_from_cstr("key.id")! - description = api.uid_get_from_cstr("key.description")! - typeName = api.uid_get_from_cstr("key.typename")! - runtimeName = api.uid_get_from_cstr("key.runtime_name")! - selectorName = api.uid_get_from_cstr("key.selector_name")! - annotatedDecl = api.uid_get_from_cstr("key.annotated_decl")! - fullyAnnotatedDecl = api.uid_get_from_cstr("key.fully_annotated_decl")! - fullyAnnotatedGenericSignature = api.uid_get_from_cstr("key.fully_annotated_generic_signature")! - signatures = api.uid_get_from_cstr("key.signatures")! - activeSignature = api.uid_get_from_cstr("key.active_signature")! - parameters = api.uid_get_from_cstr("key.parameters")! - activeParameter = api.uid_get_from_cstr("key.active_parameter")! - docBrief = api.uid_get_from_cstr("key.doc.brief")! - context = api.uid_get_from_cstr("key.context")! - typeRelation = api.uid_get_from_cstr("key.typerelation")! - moduleImportDepth = api.uid_get_from_cstr("key.moduleimportdepth")! - numBytesToErase = api.uid_get_from_cstr("key.num_bytes_to_erase")! - notRecommended = api.uid_get_from_cstr("key.not_recommended")! - declarations = api.uid_get_from_cstr("key.declarations")! - enableDeclarations = api.uid_get_from_cstr("key.enabledeclarations")! - annotations = api.uid_get_from_cstr("key.annotations")! - semanticTokens = api.uid_get_from_cstr("key.semantic_tokens")! - diagnosticStage = api.uid_get_from_cstr("key.diagnostic_stage")! - syntaxMap = api.uid_get_from_cstr("key.syntaxmap")! - isSystem = api.uid_get_from_cstr("key.is_system")! - related = api.uid_get_from_cstr("key.related")! - inherits = api.uid_get_from_cstr("key.inherits")! - conforms = api.uid_get_from_cstr("key.conforms")! - extends = api.uid_get_from_cstr("key.extends")! - dependencies = api.uid_get_from_cstr("key.dependencies")! - entities = api.uid_get_from_cstr("key.entities")! - nameOffset = api.uid_get_from_cstr("key.nameoffset")! - nameLength = api.uid_get_from_cstr("key.namelength")! - bodyOffset = api.uid_get_from_cstr("key.bodyoffset")! - bodyLength = api.uid_get_from_cstr("key.bodylength")! - docOffset = api.uid_get_from_cstr("key.docoffset")! - docLength = api.uid_get_from_cstr("key.doclength")! - isActive = api.uid_get_from_cstr("key.is_active")! - isLocal = api.uid_get_from_cstr("key.is_local")! - inheritedTypes = api.uid_get_from_cstr("key.inheritedtypes")! - attributes = api.uid_get_from_cstr("key.attributes")! - attribute = api.uid_get_from_cstr("key.attribute")! - elements = api.uid_get_from_cstr("key.elements")! - subStructure = api.uid_get_from_cstr("key.substructure")! - ranges = api.uid_get_from_cstr("key.ranges")! - fixits = api.uid_get_from_cstr("key.fixits")! - generatedBuffers = api.uid_get_from_cstr("key.generated_buffers")! - bufferText = api.uid_get_from_cstr("key.buffer_text")! - originalLocation = api.uid_get_from_cstr("key.original_location")! - diagnostics = api.uid_get_from_cstr("key.diagnostics")! - educationalNotePaths = api.uid_get_from_cstr("key.educational_note_paths")! - formatOptions = api.uid_get_from_cstr("key.editor.format.options")! - codeCompleteOptions = api.uid_get_from_cstr("key.codecomplete.options")! - typeContextInfoOptions = api.uid_get_from_cstr("key.typecontextinfo.options")! - conformingMethodListOptions = api.uid_get_from_cstr("key.conformingmethods.options")! - filterRules = api.uid_get_from_cstr("key.codecomplete.filterrules")! - nextRequestStart = api.uid_get_from_cstr("key.nextrequeststart")! - popular = api.uid_get_from_cstr("key.popular")! - unpopular = api.uid_get_from_cstr("key.unpopular")! - hide = api.uid_get_from_cstr("key.hide")! - platform = api.uid_get_from_cstr("key.platform")! - isDeprecated = api.uid_get_from_cstr("key.is_deprecated")! - isUnavailable = api.uid_get_from_cstr("key.is_unavailable")! - isOptional = api.uid_get_from_cstr("key.is_optional")! - isAsync = api.uid_get_from_cstr("key.is_async")! - message = api.uid_get_from_cstr("key.message")! - introduced = api.uid_get_from_cstr("key.introduced")! - deprecated = api.uid_get_from_cstr("key.deprecated")! - obsoleted = api.uid_get_from_cstr("key.obsoleted")! - cancelBuilds = api.uid_get_from_cstr("key.cancel_builds")! - removeCache = api.uid_get_from_cstr("key.removecache")! - typeUsr = api.uid_get_from_cstr("key.typeusr")! - containerTypeUsr = api.uid_get_from_cstr("key.containertypeusr")! - moduleGroups = api.uid_get_from_cstr("key.modulegroups")! - baseName = api.uid_get_from_cstr("key.basename")! - argNames = api.uid_get_from_cstr("key.argnames")! - selectorPieces = api.uid_get_from_cstr("key.selectorpieces")! - nameKind = api.uid_get_from_cstr("key.namekind")! - localizationKey = api.uid_get_from_cstr("key.localization_key")! - isZeroArgSelector = api.uid_get_from_cstr("key.is_zero_arg_selector")! - swiftVersion = api.uid_get_from_cstr("key.swift_version")! - value = api.uid_get_from_cstr("key.value")! - enableDiagnostics = api.uid_get_from_cstr("key.enablediagnostics")! - groupName = api.uid_get_from_cstr("key.groupname")! - actionName = api.uid_get_from_cstr("key.actionname")! - synthesizedExtension = api.uid_get_from_cstr("key.synthesizedextensions")! - usingSwiftArgs = api.uid_get_from_cstr("key.usingswiftargs")! - names = api.uid_get_from_cstr("key.names")! - uiDs = api.uid_get_from_cstr("key.uids")! - syntacticOnly = api.uid_get_from_cstr("key.syntactic_only")! - parentLoc = api.uid_get_from_cstr("key.parent_loc")! - isTestCandidate = api.uid_get_from_cstr("key.is_test_candidate")! - overrides = api.uid_get_from_cstr("key.overrides")! - associatedUSRs = api.uid_get_from_cstr("key.associated_usrs")! - moduleName = api.uid_get_from_cstr("key.modulename")! - relatedDecls = api.uid_get_from_cstr("key.related_decls")! - simplified = api.uid_get_from_cstr("key.simplified")! - rangeContent = api.uid_get_from_cstr("key.rangecontent")! - cancelOnSubsequentRequest = api.uid_get_from_cstr("key.cancel_on_subsequent_request")! - includeNonEditableBaseNames = api.uid_get_from_cstr("key.include_non_editable_base_names")! - renameLocations = api.uid_get_from_cstr("key.renamelocations")! - locations = api.uid_get_from_cstr("key.locations")! - nameType = api.uid_get_from_cstr("key.nametype")! - newName = api.uid_get_from_cstr("key.newname")! - categorizedEdits = api.uid_get_from_cstr("key.categorizededits")! - categorizedRanges = api.uid_get_from_cstr("key.categorizedranges")! - rangesWorthNote = api.uid_get_from_cstr("key.rangesworthnote")! - edits = api.uid_get_from_cstr("key.edits")! - endLine = api.uid_get_from_cstr("key.endline")! - endColumn = api.uid_get_from_cstr("key.endcolumn")! - argIndex = api.uid_get_from_cstr("key.argindex")! - text = api.uid_get_from_cstr("key.text")! - category = api.uid_get_from_cstr("key.category")! - categories = api.uid_get_from_cstr("key.categories")! - isFunctionLike = api.uid_get_from_cstr("key.is_function_like")! - isNonProtocolType = api.uid_get_from_cstr("key.is_non_protocol_type")! - refactorActions = api.uid_get_from_cstr("key.refactor_actions")! - retrieveRefactorActions = api.uid_get_from_cstr("key.retrieve_refactor_actions")! - symbolGraph = api.uid_get_from_cstr("key.symbol_graph")! - retrieveSymbolGraph = api.uid_get_from_cstr("key.retrieve_symbol_graph")! - parentContexts = api.uid_get_from_cstr("key.parent_contexts")! - referencedSymbols = api.uid_get_from_cstr("key.referenced_symbols")! - isSPI = api.uid_get_from_cstr("key.is_spi")! - actionUID = api.uid_get_from_cstr("key.actionuid")! - actionUnavailableReason = api.uid_get_from_cstr("key.actionunavailablereason")! - compileID = api.uid_get_from_cstr("key.compileid")! - compilerArgsString = api.uid_get_from_cstr("key.compilerargs-string")! - implicitMembers = api.uid_get_from_cstr("key.implicitmembers")! - expectedTypes = api.uid_get_from_cstr("key.expectedtypes")! - members = api.uid_get_from_cstr("key.members")! - typeBuffer = api.uid_get_from_cstr("key.printedtypebuffer")! - expressionTypeList = api.uid_get_from_cstr("key.expression_type_list")! - expressionOffset = api.uid_get_from_cstr("key.expression_offset")! - expressionLength = api.uid_get_from_cstr("key.expression_length")! - expressionType = api.uid_get_from_cstr("key.expression_type")! - variableTypeList = api.uid_get_from_cstr("key.variable_type_list")! - variableOffset = api.uid_get_from_cstr("key.variable_offset")! - variableLength = api.uid_get_from_cstr("key.variable_length")! - variableType = api.uid_get_from_cstr("key.variable_type")! - variableTypeExplicit = api.uid_get_from_cstr("key.variable_type_explicit")! - fullyQualified = api.uid_get_from_cstr("key.fully_qualified")! - canonicalizeType = api.uid_get_from_cstr("key.canonicalize_type")! - internalDiagnostic = api.uid_get_from_cstr("key.internal_diagnostic")! - vfsName = api.uid_get_from_cstr("key.vfs.name")! - vfsOptions = api.uid_get_from_cstr("key.vfs.options")! - files = api.uid_get_from_cstr("key.files")! - optimizeForIDE = api.uid_get_from_cstr("key.optimize_for_ide")! - requiredBystanders = api.uid_get_from_cstr("key.required_bystanders")! - reusingASTContext = api.uid_get_from_cstr("key.reusingastcontext")! - completionMaxASTContextReuseCount = api.uid_get_from_cstr("key.completion_max_astcontext_reuse_count")! - completionCheckDependencyInterval = api.uid_get_from_cstr("key.completion_check_dependency_interval")! - annotatedTypename = api.uid_get_from_cstr("key.annotated.typename")! - compileOperation = api.uid_get_from_cstr("key.compile_operation")! - effectiveAccess = api.uid_get_from_cstr("key.effective_access")! - declarationLang = api.uid_get_from_cstr("key.decl_lang")! - secondarySymbols = api.uid_get_from_cstr("key.secondary_symbols")! - simulateLongRequest = api.uid_get_from_cstr("key.simulate_long_request")! - isSynthesized = api.uid_get_from_cstr("key.is_synthesized")! - bufferName = api.uid_get_from_cstr("key.buffer_name")! - barriersEnabled = api.uid_get_from_cstr("key.barriers_enabled")! - expansions = api.uid_get_from_cstr("key.expansions")! - macroRoles = api.uid_get_from_cstr("key.macro_roles")! - expandedMacroReplacements = api.uid_get_from_cstr("key.expanded_macro_replacements")! - indexStorePath = api.uid_get_from_cstr("key.index_store_path")! - indexUnitOutputPath = api.uid_get_from_cstr("key.index_unit_output_path")! - includeLocals = api.uid_get_from_cstr("key.include_locals")! - compress = api.uid_get_from_cstr("key.compress")! - ignoreClangModules = api.uid_get_from_cstr("key.ignore_clang_modules")! - includeSystemModules = api.uid_get_from_cstr("key.include_system_modules")! - ignoreStdlib = api.uid_get_from_cstr("key.ignore_stdlib")! - disableImplicitModules = api.uid_get_from_cstr("key.disable_implicit_modules")! - compilerArgs = api.uid_get_from_cstr("key.compilerargs")! - sourceText = api.uid_get_from_cstr("key.sourcetext")! - sortByName = api.uid_get_from_cstr("key.codecomplete.sort.byname")! - useImportDepth = api.uid_get_from_cstr("key.codecomplete.sort.useimportdepth")! - groupOverloads = api.uid_get_from_cstr("key.codecomplete.group.overloads")! - groupStems = api.uid_get_from_cstr("key.codecomplete.group.stems")! - filterText = api.uid_get_from_cstr("key.codecomplete.filtertext")! - requestLimit = api.uid_get_from_cstr("key.codecomplete.requestlimit")! - requestStart = api.uid_get_from_cstr("key.codecomplete.requeststart")! - hideUnderscores = api.uid_get_from_cstr("key.codecomplete.hideunderscores")! - hideLowPriority = api.uid_get_from_cstr("key.codecomplete.hidelowpriority")! - hideByName = api.uid_get_from_cstr("key.codecomplete.hidebyname")! - includeExactMatch = api.uid_get_from_cstr("key.codecomplete.includeexactmatch")! - addInnerResults = api.uid_get_from_cstr("key.codecomplete.addinnerresults")! - addInnerOperators = api.uid_get_from_cstr("key.codecomplete.addinneroperators")! - addInitsToTopLevel = api.uid_get_from_cstr("key.codecomplete.addinitstotoplevel")! - fuzzyMatching = api.uid_get_from_cstr("key.codecomplete.fuzzymatching")! - topNonLiteral = api.uid_get_from_cstr("key.codecomplete.showtopnonliteralresults")! - contextWeight = api.uid_get_from_cstr("key.codecomplete.sort.contextweight")! - fuzzyWeight = api.uid_get_from_cstr("key.codecomplete.sort.fuzzyweight")! - popularityBonus = api.uid_get_from_cstr("key.codecomplete.sort.popularitybonus")! - annotatedDescription = api.uid_get_from_cstr("key.codecomplete.annotateddescription")! - includeObjectLiterals = api.uid_get_from_cstr("key.codecomplete.includeobjectliterals")! - useNewAPI = api.uid_get_from_cstr("key.codecomplete.use_new_api")! - addCallWithNoDefaultArgs = api.uid_get_from_cstr("key.codecomplete.addcallwithnodefaultargs")! - includeSemanticComponents = api.uid_get_from_cstr("key.codecomplete.include_semantic_components")! - useXPCSerialization = api.uid_get_from_cstr("key.codecomplete.use_xpc_serialization")! - maxResults = api.uid_get_from_cstr("key.codecomplete.maxresults")! - annotatedTypeName = api.uid_get_from_cstr("key.annotated.typename")! - priorityBucket = api.uid_get_from_cstr("key.priority_bucket")! - identifier = api.uid_get_from_cstr("key.identifier")! - textMatchScore = api.uid_get_from_cstr("key.text_match_score")! - semanticScore = api.uid_get_from_cstr("key.semantic_score")! - semanticScoreComponents = api.uid_get_from_cstr("key.semantic_score_components")! - symbolPopularity = api.uid_get_from_cstr("key.symbol_popularity")! - modulePopularity = api.uid_get_from_cstr("key.module_popularity")! - popularityKey = api.uid_get_from_cstr("key.popularity.key")! - popularityValueIntBillion = api.uid_get_from_cstr("key.popularity.value.int.billion")! - recentCompletions = api.uid_get_from_cstr("key.recent_completions")! - unfilteredResultCount = api.uid_get_from_cstr("key.unfiltered_result_count")! - memberAccessTypes = api.uid_get_from_cstr("key.member_access_types")! - hasDiagnostic = api.uid_get_from_cstr("key.has_diagnostic")! - groupId = api.uid_get_from_cstr("key.group_id")! - scopedPopularityTablePath = api.uid_get_from_cstr("key.scoped_popularity_table_path")! - popularModules = api.uid_get_from_cstr("key.popular_modules")! - notoriousModules = api.uid_get_from_cstr("key.notorious_modules")! - usedScoreComponents = api.uid_get_from_cstr("key.codecomplete.setpopularapi_used_score_components")! - useTabs = api.uid_get_from_cstr("key.editor.format.usetabs")! - indentWidth = api.uid_get_from_cstr("key.editor.format.indentwidth")! - tabWidth = api.uid_get_from_cstr("key.editor.format.tabwidth")! - indentSwitchCase = api.uid_get_from_cstr("key.editor.format.indent_switch_case")! - } -} - -// swift-format-ignore: TypeNamesShouldBeCapitalized -// Matching C style types -package struct sourcekitd_api_requests { - /// `source.request.protocol_version` - package let protocolVersion: sourcekitd_api_uid_t - /// `source.request.compiler_version` - package let compilerVersion: sourcekitd_api_uid_t - /// `source.request.crash_exit` - package let crashWithExit: sourcekitd_api_uid_t - /// `source.request.demangle` - package let demangle: sourcekitd_api_uid_t - /// `source.request.mangle_simple_class` - package let mangleSimpleClass: sourcekitd_api_uid_t - /// `source.request.indexsource` - package let index: sourcekitd_api_uid_t - /// `source.request.docinfo` - package let docInfo: sourcekitd_api_uid_t - /// `source.request.codecomplete` - package let codeComplete: sourcekitd_api_uid_t - /// `source.request.codecomplete.open` - package let codeCompleteOpen: sourcekitd_api_uid_t - /// `source.request.codecomplete.close` - package let codeCompleteClose: sourcekitd_api_uid_t - /// `source.request.codecomplete.update` - package let codeCompleteUpdate: sourcekitd_api_uid_t - /// `source.request.codecomplete.cache.ondisk` - package let codeCompleteCacheOnDisk: sourcekitd_api_uid_t - /// `source.request.codecomplete.setpopularapi` - package let codeCompleteSetPopularAPI: sourcekitd_api_uid_t - /// `source.request.codecomplete.setcustom` - package let codeCompleteSetCustom: sourcekitd_api_uid_t - /// `source.request.signaturehelp` - package let signatureHelp: sourcekitd_api_uid_t - /// `source.request.typecontextinfo` - package let typeContextInfo: sourcekitd_api_uid_t - /// `source.request.conformingmethods` - package let conformingMethodList: sourcekitd_api_uid_t - /// `source.request.activeregions` - package let activeRegions: sourcekitd_api_uid_t - /// `source.request.cursorinfo` - package let cursorInfo: sourcekitd_api_uid_t - /// `source.request.rangeinfo` - package let rangeInfo: sourcekitd_api_uid_t - /// `source.request.relatedidents` - package let relatedIdents: sourcekitd_api_uid_t - /// `source.request.editor.open` - package let editorOpen: sourcekitd_api_uid_t - /// `source.request.editor.open.interface` - package let editorOpenInterface: sourcekitd_api_uid_t - /// `source.request.editor.open.interface.header` - package let editorOpenHeaderInterface: sourcekitd_api_uid_t - /// `source.request.editor.open.interface.swiftsource` - package let editorOpenSwiftSourceInterface: sourcekitd_api_uid_t - /// `source.request.editor.open.interface.swifttype` - package let editorOpenSwiftTypeInterface: sourcekitd_api_uid_t - /// `source.request.editor.extract.comment` - package let editorExtractTextFromComment: sourcekitd_api_uid_t - /// `source.request.editor.close` - package let editorClose: sourcekitd_api_uid_t - /// `source.request.editor.replacetext` - package let editorReplaceText: sourcekitd_api_uid_t - /// `source.request.editor.formattext` - package let editorFormatText: sourcekitd_api_uid_t - /// `source.request.editor.expand_placeholder` - package let editorExpandPlaceholder: sourcekitd_api_uid_t - /// `source.request.editor.find_usr` - package let editorFindUSR: sourcekitd_api_uid_t - /// `source.request.editor.find_interface_doc` - package let editorFindInterfaceDoc: sourcekitd_api_uid_t - /// `source.request.buildsettings.register` - package let buildSettingsRegister: sourcekitd_api_uid_t - /// `source.request.module.groups` - package let moduleGroups: sourcekitd_api_uid_t - /// `source.request.name.translation` - package let nameTranslation: sourcekitd_api_uid_t - /// `source.request.convert.markup.xml` - package let markupToXML: sourcekitd_api_uid_t - /// `source.request.statistics` - package let statistics: sourcekitd_api_uid_t - /// `source.request.find-syntactic-rename-ranges` - package let findRenameRanges: sourcekitd_api_uid_t - /// `source.request.find-local-rename-ranges` - package let findLocalRenameRanges: sourcekitd_api_uid_t - /// `source.request.semantic.refactoring` - package let semanticRefactoring: sourcekitd_api_uid_t - /// `source.request.enable-compile-notifications` - package let enableCompileNotifications: sourcekitd_api_uid_t - /// `source.request.test_notification` - package let testNotification: sourcekitd_api_uid_t - /// `source.request.expression.type` - package let collectExpressionType: sourcekitd_api_uid_t - /// `source.request.variable.type` - package let collectVariableType: sourcekitd_api_uid_t - /// `source.request.configuration.global` - package let globalConfiguration: sourcekitd_api_uid_t - /// `source.request.dependency_updated` - package let dependencyUpdated: sourcekitd_api_uid_t - /// `source.request.diagnostics` - package let diagnostics: sourcekitd_api_uid_t - /// `source.request.semantic_tokens` - package let semanticTokens: sourcekitd_api_uid_t - /// `source.request.compile` - package let compile: sourcekitd_api_uid_t - /// `source.request.compile.close` - package let compileClose: sourcekitd_api_uid_t - /// `source.request.enable_request_barriers` - package let enableRequestBarriers: sourcekitd_api_uid_t - /// `source.request.syntactic_macro_expansion` - package let syntacticMacroExpansion: sourcekitd_api_uid_t - /// `source.request.index_to_store` - package let indexToStore: sourcekitd_api_uid_t - /// `source.request.codecomplete.documentation` - package let codeCompleteDocumentation: sourcekitd_api_uid_t - /// `source.request.codecomplete.diagnostic` - package let codeCompleteDiagnostic: sourcekitd_api_uid_t - - package init(api: sourcekitd_api_functions_t) { - protocolVersion = api.uid_get_from_cstr("source.request.protocol_version")! - compilerVersion = api.uid_get_from_cstr("source.request.compiler_version")! - crashWithExit = api.uid_get_from_cstr("source.request.crash_exit")! - demangle = api.uid_get_from_cstr("source.request.demangle")! - mangleSimpleClass = api.uid_get_from_cstr("source.request.mangle_simple_class")! - index = api.uid_get_from_cstr("source.request.indexsource")! - docInfo = api.uid_get_from_cstr("source.request.docinfo")! - codeComplete = api.uid_get_from_cstr("source.request.codecomplete")! - codeCompleteOpen = api.uid_get_from_cstr("source.request.codecomplete.open")! - codeCompleteClose = api.uid_get_from_cstr("source.request.codecomplete.close")! - codeCompleteUpdate = api.uid_get_from_cstr("source.request.codecomplete.update")! - codeCompleteCacheOnDisk = api.uid_get_from_cstr("source.request.codecomplete.cache.ondisk")! - codeCompleteSetPopularAPI = api.uid_get_from_cstr("source.request.codecomplete.setpopularapi")! - codeCompleteSetCustom = api.uid_get_from_cstr("source.request.codecomplete.setcustom")! - signatureHelp = api.uid_get_from_cstr("source.request.signaturehelp")! - typeContextInfo = api.uid_get_from_cstr("source.request.typecontextinfo")! - conformingMethodList = api.uid_get_from_cstr("source.request.conformingmethods")! - activeRegions = api.uid_get_from_cstr("source.request.activeregions")! - cursorInfo = api.uid_get_from_cstr("source.request.cursorinfo")! - rangeInfo = api.uid_get_from_cstr("source.request.rangeinfo")! - relatedIdents = api.uid_get_from_cstr("source.request.relatedidents")! - editorOpen = api.uid_get_from_cstr("source.request.editor.open")! - editorOpenInterface = api.uid_get_from_cstr("source.request.editor.open.interface")! - editorOpenHeaderInterface = api.uid_get_from_cstr("source.request.editor.open.interface.header")! - editorOpenSwiftSourceInterface = api.uid_get_from_cstr("source.request.editor.open.interface.swiftsource")! - editorOpenSwiftTypeInterface = api.uid_get_from_cstr("source.request.editor.open.interface.swifttype")! - editorExtractTextFromComment = api.uid_get_from_cstr("source.request.editor.extract.comment")! - editorClose = api.uid_get_from_cstr("source.request.editor.close")! - editorReplaceText = api.uid_get_from_cstr("source.request.editor.replacetext")! - editorFormatText = api.uid_get_from_cstr("source.request.editor.formattext")! - editorExpandPlaceholder = api.uid_get_from_cstr("source.request.editor.expand_placeholder")! - editorFindUSR = api.uid_get_from_cstr("source.request.editor.find_usr")! - editorFindInterfaceDoc = api.uid_get_from_cstr("source.request.editor.find_interface_doc")! - buildSettingsRegister = api.uid_get_from_cstr("source.request.buildsettings.register")! - moduleGroups = api.uid_get_from_cstr("source.request.module.groups")! - nameTranslation = api.uid_get_from_cstr("source.request.name.translation")! - markupToXML = api.uid_get_from_cstr("source.request.convert.markup.xml")! - statistics = api.uid_get_from_cstr("source.request.statistics")! - findRenameRanges = api.uid_get_from_cstr("source.request.find-syntactic-rename-ranges")! - findLocalRenameRanges = api.uid_get_from_cstr("source.request.find-local-rename-ranges")! - semanticRefactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")! - enableCompileNotifications = api.uid_get_from_cstr("source.request.enable-compile-notifications")! - testNotification = api.uid_get_from_cstr("source.request.test_notification")! - collectExpressionType = api.uid_get_from_cstr("source.request.expression.type")! - collectVariableType = api.uid_get_from_cstr("source.request.variable.type")! - globalConfiguration = api.uid_get_from_cstr("source.request.configuration.global")! - dependencyUpdated = api.uid_get_from_cstr("source.request.dependency_updated")! - diagnostics = api.uid_get_from_cstr("source.request.diagnostics")! - semanticTokens = api.uid_get_from_cstr("source.request.semantic_tokens")! - compile = api.uid_get_from_cstr("source.request.compile")! - compileClose = api.uid_get_from_cstr("source.request.compile.close")! - enableRequestBarriers = api.uid_get_from_cstr("source.request.enable_request_barriers")! - syntacticMacroExpansion = api.uid_get_from_cstr("source.request.syntactic_macro_expansion")! - indexToStore = api.uid_get_from_cstr("source.request.index_to_store")! - codeCompleteDocumentation = api.uid_get_from_cstr("source.request.codecomplete.documentation")! - codeCompleteDiagnostic = api.uid_get_from_cstr("source.request.codecomplete.diagnostic")! - } -} - -// swift-format-ignore: TypeNamesShouldBeCapitalized -// Matching C style types -package struct sourcekitd_api_values { - /// `source.lang.swift.decl.function.free` - package let declFunctionFree: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.free` - package let refFunctionFree: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.method.instance` - package let declMethodInstance: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.method.instance` - package let refMethodInstance: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.method.static` - package let declMethodStatic: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.method.static` - package let refMethodStatic: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.method.class` - package let declMethodClass: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.method.class` - package let refMethodClass: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.getter` - package let declAccessorGetter: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.getter` - package let refAccessorGetter: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.setter` - package let declAccessorSetter: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.setter` - package let refAccessorSetter: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.willset` - package let declAccessorWillSet: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.willset` - package let refAccessorWillSet: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.didset` - package let declAccessorDidSet: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.didset` - package let refAccessorDidSet: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.address` - package let declAccessorAddress: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.address` - package let refAccessorAddress: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.mutableaddress` - package let declAccessorMutableAddress: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.mutableaddress` - package let refAccessorMutableAddress: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.read` - package let declAccessorRead: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.read` - package let refAccessorRead: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.modify` - package let declAccessorModify: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.modify` - package let refAccessorModify: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.accessor.init` - package let declAccessorInit: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.accessor.init` - package let refAccessorInit: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.constructor` - package let declConstructor: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.constructor` - package let refConstructor: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.destructor` - package let declDestructor: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.destructor` - package let refDestructor: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.operator.prefix` - package let declFunctionPrefixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.operator.postfix` - package let declFunctionPostfixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.operator.infix` - package let declFunctionInfixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.operator.prefix` - package let refFunctionPrefixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.operator.postfix` - package let refFunctionPostfixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.operator.infix` - package let refFunctionInfixOperator: sourcekitd_api_uid_t - /// `source.lang.swift.decl.precedencegroup` - package let declPrecedenceGroup: sourcekitd_api_uid_t - /// `source.lang.swift.ref.precedencegroup` - package let refPrecedenceGroup: sourcekitd_api_uid_t - /// `source.lang.swift.decl.function.subscript` - package let declSubscript: sourcekitd_api_uid_t - /// `source.lang.swift.ref.function.subscript` - package let refSubscript: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.global` - package let declVarGlobal: sourcekitd_api_uid_t - /// `source.lang.swift.ref.var.global` - package let refVarGlobal: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.instance` - package let declVarInstance: sourcekitd_api_uid_t - /// `source.lang.swift.ref.var.instance` - package let refVarInstance: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.static` - package let declVarStatic: sourcekitd_api_uid_t - /// `source.lang.swift.ref.var.static` - package let refVarStatic: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.class` - package let declVarClass: sourcekitd_api_uid_t - /// `source.lang.swift.ref.var.class` - package let refVarClass: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.local` - package let declVarLocal: sourcekitd_api_uid_t - /// `source.lang.swift.ref.var.local` - package let refVarLocal: sourcekitd_api_uid_t - /// `source.lang.swift.decl.var.parameter` - package let declVarParam: sourcekitd_api_uid_t - /// `source.lang.swift.decl.module` - package let declModule: sourcekitd_api_uid_t - /// `source.lang.swift.decl.class` - package let declClass: sourcekitd_api_uid_t - /// `source.lang.swift.ref.class` - package let refClass: sourcekitd_api_uid_t - /// `source.lang.swift.decl.actor` - package let declActor: sourcekitd_api_uid_t - /// `source.lang.swift.ref.actor` - package let refActor: sourcekitd_api_uid_t - /// `source.lang.swift.decl.struct` - package let declStruct: sourcekitd_api_uid_t - /// `source.lang.swift.ref.struct` - package let refStruct: sourcekitd_api_uid_t - /// `source.lang.swift.decl.enum` - package let declEnum: sourcekitd_api_uid_t - /// `source.lang.swift.ref.enum` - package let refEnum: sourcekitd_api_uid_t - /// `source.lang.swift.decl.enumcase` - package let declEnumCase: sourcekitd_api_uid_t - /// `source.lang.swift.decl.enumelement` - package let declEnumElement: sourcekitd_api_uid_t - /// `source.lang.swift.ref.enumelement` - package let refEnumElement: sourcekitd_api_uid_t - /// `source.lang.swift.decl.protocol` - package let declProtocol: sourcekitd_api_uid_t - /// `source.lang.swift.ref.protocol` - package let refProtocol: sourcekitd_api_uid_t - /// `source.lang.swift.decl.extension` - package let declExtension: sourcekitd_api_uid_t - /// `source.lang.swift.decl.extension.struct` - package let declExtensionStruct: sourcekitd_api_uid_t - /// `source.lang.swift.decl.extension.class` - package let declExtensionClass: sourcekitd_api_uid_t - /// `source.lang.swift.decl.extension.enum` - package let declExtensionEnum: sourcekitd_api_uid_t - /// `source.lang.swift.decl.extension.protocol` - package let declExtensionProtocol: sourcekitd_api_uid_t - /// `source.lang.swift.decl.associatedtype` - package let declAssociatedType: sourcekitd_api_uid_t - /// `source.lang.swift.ref.associatedtype` - package let refAssociatedType: sourcekitd_api_uid_t - /// `source.lang.swift.decl.opaquetype` - package let declOpaqueType: sourcekitd_api_uid_t - /// `source.lang.swift.ref.opaquetype` - package let refOpaqueType: sourcekitd_api_uid_t - /// `source.lang.swift.decl.typealias` - package let declTypeAlias: sourcekitd_api_uid_t - /// `source.lang.swift.ref.typealias` - package let refTypeAlias: sourcekitd_api_uid_t - /// `source.lang.swift.decl.generic_type_param` - package let declGenericTypeParam: sourcekitd_api_uid_t - /// `source.lang.swift.ref.generic_type_param` - package let refGenericTypeParam: sourcekitd_api_uid_t - /// `source.lang.swift.decl.macro` - package let declMacro: sourcekitd_api_uid_t - /// `source.lang.swift.ref.macro` - package let refMacro: sourcekitd_api_uid_t - /// `source.lang.swift.ref.module` - package let refModule: sourcekitd_api_uid_t - /// `source.lang.swift.commenttag` - package let commentTag: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.foreach` - package let stmtForEach: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.for` - package let stmtFor: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.while` - package let stmtWhile: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.repeatwhile` - package let stmtRepeatWhile: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.if` - package let stmtIf: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.guard` - package let stmtGuard: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.switch` - package let stmtSwitch: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.case` - package let stmtCase: sourcekitd_api_uid_t - /// `source.lang.swift.stmt.brace` - package let stmtBrace: sourcekitd_api_uid_t - /// `source.lang.swift.expr.call` - package let exprCall: sourcekitd_api_uid_t - /// `source.lang.swift.expr.argument` - package let exprArg: sourcekitd_api_uid_t - /// `source.lang.swift.expr.array` - package let exprArray: sourcekitd_api_uid_t - /// `source.lang.swift.expr.dictionary` - package let exprDictionary: sourcekitd_api_uid_t - /// `source.lang.swift.expr.object_literal` - package let exprObjectLiteral: sourcekitd_api_uid_t - /// `source.lang.swift.expr.tuple` - package let exprTuple: sourcekitd_api_uid_t - /// `source.lang.swift.expr.closure` - package let exprClosure: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.id` - package let structureElemId: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.expr` - package let structureElemExpr: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.init_expr` - package let structureElemInitExpr: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.condition_expr` - package let structureElemCondExpr: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.pattern` - package let structureElemPattern: sourcekitd_api_uid_t - /// `source.lang.swift.structure.elem.typeref` - package let structureElemTypeRef: sourcekitd_api_uid_t - /// `source.lang.swift.range.singlestatement` - package let rangeSingleStatement: sourcekitd_api_uid_t - /// `source.lang.swift.range.singleexpression` - package let rangeSingleExpression: sourcekitd_api_uid_t - /// `source.lang.swift.range.singledeclaration` - package let rangeSingleDeclaration: sourcekitd_api_uid_t - /// `source.lang.swift.range.multistatement` - package let rangeMultiStatement: sourcekitd_api_uid_t - /// `source.lang.swift.range.multitypememberdeclaration` - package let rangeMultiTypeMemberDeclaration: sourcekitd_api_uid_t - /// `source.lang.swift.range.invalid` - package let rangeInvalid: sourcekitd_api_uid_t - /// `source.lang.name.kind.objc` - package let nameObjc: sourcekitd_api_uid_t - /// `source.lang.name.kind.swift` - package let nameSwift: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.keyword` - package let keyword: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.identifier` - package let identifier: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.operator` - package let `operator`: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.typeidentifier` - package let typeIdentifier: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.buildconfig.keyword` - package let buildConfigKeyword: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.buildconfig.id` - package let buildConfigId: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.pounddirective.keyword` - package let poundDirectiveKeyword: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.attribute.id` - package let attributeId: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.attribute.builtin` - package let attributeBuiltin: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.number` - package let number: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.string` - package let string: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.string_interpolation_anchor` - package let stringInterpolation: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.comment` - package let comment: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.doccomment` - package let docComment: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.doccomment.field` - package let docCommentField: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.comment.mark` - package let commentMarker: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.comment.url` - package let commentURL: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.placeholder` - package let placeholder: sourcekitd_api_uid_t - /// `source.lang.swift.syntaxtype.objectliteral` - package let objectLiteral: sourcekitd_api_uid_t - /// `source.lang.swift.expr` - package let expr: sourcekitd_api_uid_t - /// `source.lang.swift.stmt` - package let stmt: sourcekitd_api_uid_t - /// `source.lang.swift.type` - package let type: sourcekitd_api_uid_t - /// `source.lang.swift.foreach.sequence` - package let forEachSequence: sourcekitd_api_uid_t - /// `source.diagnostic.severity.note` - package let diagNote: sourcekitd_api_uid_t - /// `source.diagnostic.severity.warning` - package let diagWarning: sourcekitd_api_uid_t - /// `source.diagnostic.severity.error` - package let diagError: sourcekitd_api_uid_t - /// `source.diagnostic.category.deprecation` - package let diagDeprecation: sourcekitd_api_uid_t - /// `source.diagnostic.category.no_usage` - package let diagNoUsage: sourcekitd_api_uid_t - /// `source.codecompletion.everything` - package let codeCompletionEverything: sourcekitd_api_uid_t - /// `source.codecompletion.module` - package let codeCompletionModule: sourcekitd_api_uid_t - /// `source.codecompletion.keyword` - package let codeCompletionKeyword: sourcekitd_api_uid_t - /// `source.codecompletion.literal` - package let codeCompletionLiteral: sourcekitd_api_uid_t - /// `source.codecompletion.custom` - package let codeCompletionCustom: sourcekitd_api_uid_t - /// `source.codecompletion.identifier` - package let codeCompletionIdentifier: sourcekitd_api_uid_t - /// `source.codecompletion.description` - package let codeCompletionDescription: sourcekitd_api_uid_t - /// `source.edit.kind.active` - package let editActive: sourcekitd_api_uid_t - /// `source.edit.kind.inactive` - package let editInactive: sourcekitd_api_uid_t - /// `source.edit.kind.selector` - package let editSelector: sourcekitd_api_uid_t - /// `source.edit.kind.string` - package let editString: sourcekitd_api_uid_t - /// `source.edit.kind.comment` - package let editComment: sourcekitd_api_uid_t - /// `source.edit.kind.mismatch` - package let editMismatch: sourcekitd_api_uid_t - /// `source.edit.kind.unknown` - package let editUnknown: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.basename` - package let renameRangeBase: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.keyword-basename` - package let renameRangeKeywordBase: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.parameter-and-whitespace` - package let renameRangeParam: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.noncollapsible-parameter` - package let renameRangeNoncollapsibleParam: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.decl-argument-label` - package let renameRangeDeclArgLabel: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.call-argument-label` - package let renameRangeCallArgLabel: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.call-argument-colon` - package let renameRangeCallArgColon: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.call-argument-combined` - package let renameRangeCallArgCombined: sourcekitd_api_uid_t - /// `source.refactoring.range.kind.selector-argument-label` - package let renameRangeSelectorArgLabel: sourcekitd_api_uid_t - /// `source.syntacticrename.definition` - package let definition: sourcekitd_api_uid_t - /// `source.syntacticrename.reference` - package let reference: sourcekitd_api_uid_t - /// `source.syntacticrename.call` - package let call: sourcekitd_api_uid_t - /// `source.syntacticrename.unknown` - package let unknown: sourcekitd_api_uid_t - /// `source.statistic.num-requests` - package let statNumRequests: sourcekitd_api_uid_t - /// `source.statistic.num-semantic-requests` - package let statNumSemaRequests: sourcekitd_api_uid_t - /// `source.statistic.instruction-count` - package let statInstructionCount: sourcekitd_api_uid_t - /// `source.lang.swift` - package let swift: sourcekitd_api_uid_t - /// `source.lang.objc` - package let objC: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.expression` - package let macroRoleExpression: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.declaration` - package let macroRoleDeclaration: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.codeitem` - package let macroRoleCodeItem: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.accessor` - package let macroRoleAccessor: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.member_attribute` - package let macroRoleMemberAttribute: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.member` - package let macroRoleMember: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.peer` - package let macroRolePeer: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.conformance` - package let macroRoleConformance: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.extension` - package let macroRoleExtension: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.preamble` - package let macroRolePreamble: sourcekitd_api_uid_t - /// `source.lang.swift.macro_role.body` - package let macroRoleBody: sourcekitd_api_uid_t - /// `source.lang.swift.keyword` - package let completionKindKeyword: sourcekitd_api_uid_t - /// `source.lang.swift.pattern` - package let completionKindPattern: sourcekitd_api_uid_t - /// `source.diagnostic.stage.swift.sema` - package let semaDiagStage: sourcekitd_api_uid_t - /// `source.diagnostic.stage.swift.parse` - package let parseDiagStage: sourcekitd_api_uid_t - /// `source.notification.sema_disabled` - package let semaDisabledNotification: sourcekitd_api_uid_t - /// `source.notification.sema_enabled` - package let semaEnabledNotification: sourcekitd_api_uid_t - /// `source.notification.editor.documentupdate` - package let documentUpdateNotification: sourcekitd_api_uid_t - /// `source.diagnostic.severity.remark` - package let diagRemark: sourcekitd_api_uid_t - - package init(api: sourcekitd_api_functions_t) { - declFunctionFree = api.uid_get_from_cstr("source.lang.swift.decl.function.free")! - refFunctionFree = api.uid_get_from_cstr("source.lang.swift.ref.function.free")! - declMethodInstance = api.uid_get_from_cstr("source.lang.swift.decl.function.method.instance")! - refMethodInstance = api.uid_get_from_cstr("source.lang.swift.ref.function.method.instance")! - declMethodStatic = api.uid_get_from_cstr("source.lang.swift.decl.function.method.static")! - refMethodStatic = api.uid_get_from_cstr("source.lang.swift.ref.function.method.static")! - declMethodClass = api.uid_get_from_cstr("source.lang.swift.decl.function.method.class")! - refMethodClass = api.uid_get_from_cstr("source.lang.swift.ref.function.method.class")! - declAccessorGetter = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.getter")! - refAccessorGetter = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.getter")! - declAccessorSetter = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.setter")! - refAccessorSetter = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.setter")! - declAccessorWillSet = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.willset")! - refAccessorWillSet = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.willset")! - declAccessorDidSet = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.didset")! - refAccessorDidSet = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.didset")! - declAccessorAddress = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.address")! - refAccessorAddress = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.address")! - declAccessorMutableAddress = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.mutableaddress")! - refAccessorMutableAddress = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.mutableaddress")! - declAccessorRead = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.read")! - refAccessorRead = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.read")! - declAccessorModify = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.modify")! - refAccessorModify = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.modify")! - declAccessorInit = api.uid_get_from_cstr("source.lang.swift.decl.function.accessor.init")! - refAccessorInit = api.uid_get_from_cstr("source.lang.swift.ref.function.accessor.init")! - declConstructor = api.uid_get_from_cstr("source.lang.swift.decl.function.constructor")! - refConstructor = api.uid_get_from_cstr("source.lang.swift.ref.function.constructor")! - declDestructor = api.uid_get_from_cstr("source.lang.swift.decl.function.destructor")! - refDestructor = api.uid_get_from_cstr("source.lang.swift.ref.function.destructor")! - declFunctionPrefixOperator = api.uid_get_from_cstr("source.lang.swift.decl.function.operator.prefix")! - declFunctionPostfixOperator = api.uid_get_from_cstr("source.lang.swift.decl.function.operator.postfix")! - declFunctionInfixOperator = api.uid_get_from_cstr("source.lang.swift.decl.function.operator.infix")! - refFunctionPrefixOperator = api.uid_get_from_cstr("source.lang.swift.ref.function.operator.prefix")! - refFunctionPostfixOperator = api.uid_get_from_cstr("source.lang.swift.ref.function.operator.postfix")! - refFunctionInfixOperator = api.uid_get_from_cstr("source.lang.swift.ref.function.operator.infix")! - declPrecedenceGroup = api.uid_get_from_cstr("source.lang.swift.decl.precedencegroup")! - refPrecedenceGroup = api.uid_get_from_cstr("source.lang.swift.ref.precedencegroup")! - declSubscript = api.uid_get_from_cstr("source.lang.swift.decl.function.subscript")! - refSubscript = api.uid_get_from_cstr("source.lang.swift.ref.function.subscript")! - declVarGlobal = api.uid_get_from_cstr("source.lang.swift.decl.var.global")! - refVarGlobal = api.uid_get_from_cstr("source.lang.swift.ref.var.global")! - declVarInstance = api.uid_get_from_cstr("source.lang.swift.decl.var.instance")! - refVarInstance = api.uid_get_from_cstr("source.lang.swift.ref.var.instance")! - declVarStatic = api.uid_get_from_cstr("source.lang.swift.decl.var.static")! - refVarStatic = api.uid_get_from_cstr("source.lang.swift.ref.var.static")! - declVarClass = api.uid_get_from_cstr("source.lang.swift.decl.var.class")! - refVarClass = api.uid_get_from_cstr("source.lang.swift.ref.var.class")! - declVarLocal = api.uid_get_from_cstr("source.lang.swift.decl.var.local")! - refVarLocal = api.uid_get_from_cstr("source.lang.swift.ref.var.local")! - declVarParam = api.uid_get_from_cstr("source.lang.swift.decl.var.parameter")! - declModule = api.uid_get_from_cstr("source.lang.swift.decl.module")! - declClass = api.uid_get_from_cstr("source.lang.swift.decl.class")! - refClass = api.uid_get_from_cstr("source.lang.swift.ref.class")! - declActor = api.uid_get_from_cstr("source.lang.swift.decl.actor")! - refActor = api.uid_get_from_cstr("source.lang.swift.ref.actor")! - declStruct = api.uid_get_from_cstr("source.lang.swift.decl.struct")! - refStruct = api.uid_get_from_cstr("source.lang.swift.ref.struct")! - declEnum = api.uid_get_from_cstr("source.lang.swift.decl.enum")! - refEnum = api.uid_get_from_cstr("source.lang.swift.ref.enum")! - declEnumCase = api.uid_get_from_cstr("source.lang.swift.decl.enumcase")! - declEnumElement = api.uid_get_from_cstr("source.lang.swift.decl.enumelement")! - refEnumElement = api.uid_get_from_cstr("source.lang.swift.ref.enumelement")! - declProtocol = api.uid_get_from_cstr("source.lang.swift.decl.protocol")! - refProtocol = api.uid_get_from_cstr("source.lang.swift.ref.protocol")! - declExtension = api.uid_get_from_cstr("source.lang.swift.decl.extension")! - declExtensionStruct = api.uid_get_from_cstr("source.lang.swift.decl.extension.struct")! - declExtensionClass = api.uid_get_from_cstr("source.lang.swift.decl.extension.class")! - declExtensionEnum = api.uid_get_from_cstr("source.lang.swift.decl.extension.enum")! - declExtensionProtocol = api.uid_get_from_cstr("source.lang.swift.decl.extension.protocol")! - declAssociatedType = api.uid_get_from_cstr("source.lang.swift.decl.associatedtype")! - refAssociatedType = api.uid_get_from_cstr("source.lang.swift.ref.associatedtype")! - declOpaqueType = api.uid_get_from_cstr("source.lang.swift.decl.opaquetype")! - refOpaqueType = api.uid_get_from_cstr("source.lang.swift.ref.opaquetype")! - declTypeAlias = api.uid_get_from_cstr("source.lang.swift.decl.typealias")! - refTypeAlias = api.uid_get_from_cstr("source.lang.swift.ref.typealias")! - declGenericTypeParam = api.uid_get_from_cstr("source.lang.swift.decl.generic_type_param")! - refGenericTypeParam = api.uid_get_from_cstr("source.lang.swift.ref.generic_type_param")! - declMacro = api.uid_get_from_cstr("source.lang.swift.decl.macro")! - refMacro = api.uid_get_from_cstr("source.lang.swift.ref.macro")! - refModule = api.uid_get_from_cstr("source.lang.swift.ref.module")! - commentTag = api.uid_get_from_cstr("source.lang.swift.commenttag")! - stmtForEach = api.uid_get_from_cstr("source.lang.swift.stmt.foreach")! - stmtFor = api.uid_get_from_cstr("source.lang.swift.stmt.for")! - stmtWhile = api.uid_get_from_cstr("source.lang.swift.stmt.while")! - stmtRepeatWhile = api.uid_get_from_cstr("source.lang.swift.stmt.repeatwhile")! - stmtIf = api.uid_get_from_cstr("source.lang.swift.stmt.if")! - stmtGuard = api.uid_get_from_cstr("source.lang.swift.stmt.guard")! - stmtSwitch = api.uid_get_from_cstr("source.lang.swift.stmt.switch")! - stmtCase = api.uid_get_from_cstr("source.lang.swift.stmt.case")! - stmtBrace = api.uid_get_from_cstr("source.lang.swift.stmt.brace")! - exprCall = api.uid_get_from_cstr("source.lang.swift.expr.call")! - exprArg = api.uid_get_from_cstr("source.lang.swift.expr.argument")! - exprArray = api.uid_get_from_cstr("source.lang.swift.expr.array")! - exprDictionary = api.uid_get_from_cstr("source.lang.swift.expr.dictionary")! - exprObjectLiteral = api.uid_get_from_cstr("source.lang.swift.expr.object_literal")! - exprTuple = api.uid_get_from_cstr("source.lang.swift.expr.tuple")! - exprClosure = api.uid_get_from_cstr("source.lang.swift.expr.closure")! - structureElemId = api.uid_get_from_cstr("source.lang.swift.structure.elem.id")! - structureElemExpr = api.uid_get_from_cstr("source.lang.swift.structure.elem.expr")! - structureElemInitExpr = api.uid_get_from_cstr("source.lang.swift.structure.elem.init_expr")! - structureElemCondExpr = api.uid_get_from_cstr("source.lang.swift.structure.elem.condition_expr")! - structureElemPattern = api.uid_get_from_cstr("source.lang.swift.structure.elem.pattern")! - structureElemTypeRef = api.uid_get_from_cstr("source.lang.swift.structure.elem.typeref")! - rangeSingleStatement = api.uid_get_from_cstr("source.lang.swift.range.singlestatement")! - rangeSingleExpression = api.uid_get_from_cstr("source.lang.swift.range.singleexpression")! - rangeSingleDeclaration = api.uid_get_from_cstr("source.lang.swift.range.singledeclaration")! - rangeMultiStatement = api.uid_get_from_cstr("source.lang.swift.range.multistatement")! - rangeMultiTypeMemberDeclaration = api.uid_get_from_cstr("source.lang.swift.range.multitypememberdeclaration")! - rangeInvalid = api.uid_get_from_cstr("source.lang.swift.range.invalid")! - nameObjc = api.uid_get_from_cstr("source.lang.name.kind.objc")! - nameSwift = api.uid_get_from_cstr("source.lang.name.kind.swift")! - keyword = api.uid_get_from_cstr("source.lang.swift.syntaxtype.keyword")! - identifier = api.uid_get_from_cstr("source.lang.swift.syntaxtype.identifier")! - `operator` = api.uid_get_from_cstr("source.lang.swift.syntaxtype.operator")! - typeIdentifier = api.uid_get_from_cstr("source.lang.swift.syntaxtype.typeidentifier")! - buildConfigKeyword = api.uid_get_from_cstr("source.lang.swift.syntaxtype.buildconfig.keyword")! - buildConfigId = api.uid_get_from_cstr("source.lang.swift.syntaxtype.buildconfig.id")! - poundDirectiveKeyword = api.uid_get_from_cstr("source.lang.swift.syntaxtype.pounddirective.keyword")! - attributeId = api.uid_get_from_cstr("source.lang.swift.syntaxtype.attribute.id")! - attributeBuiltin = api.uid_get_from_cstr("source.lang.swift.syntaxtype.attribute.builtin")! - number = api.uid_get_from_cstr("source.lang.swift.syntaxtype.number")! - string = api.uid_get_from_cstr("source.lang.swift.syntaxtype.string")! - stringInterpolation = api.uid_get_from_cstr("source.lang.swift.syntaxtype.string_interpolation_anchor")! - comment = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment")! - docComment = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment")! - docCommentField = api.uid_get_from_cstr("source.lang.swift.syntaxtype.doccomment.field")! - commentMarker = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment.mark")! - commentURL = api.uid_get_from_cstr("source.lang.swift.syntaxtype.comment.url")! - placeholder = api.uid_get_from_cstr("source.lang.swift.syntaxtype.placeholder")! - objectLiteral = api.uid_get_from_cstr("source.lang.swift.syntaxtype.objectliteral")! - expr = api.uid_get_from_cstr("source.lang.swift.expr")! - stmt = api.uid_get_from_cstr("source.lang.swift.stmt")! - type = api.uid_get_from_cstr("source.lang.swift.type")! - forEachSequence = api.uid_get_from_cstr("source.lang.swift.foreach.sequence")! - diagNote = api.uid_get_from_cstr("source.diagnostic.severity.note")! - diagWarning = api.uid_get_from_cstr("source.diagnostic.severity.warning")! - diagError = api.uid_get_from_cstr("source.diagnostic.severity.error")! - diagDeprecation = api.uid_get_from_cstr("source.diagnostic.category.deprecation")! - diagNoUsage = api.uid_get_from_cstr("source.diagnostic.category.no_usage")! - codeCompletionEverything = api.uid_get_from_cstr("source.codecompletion.everything")! - codeCompletionModule = api.uid_get_from_cstr("source.codecompletion.module")! - codeCompletionKeyword = api.uid_get_from_cstr("source.codecompletion.keyword")! - codeCompletionLiteral = api.uid_get_from_cstr("source.codecompletion.literal")! - codeCompletionCustom = api.uid_get_from_cstr("source.codecompletion.custom")! - codeCompletionIdentifier = api.uid_get_from_cstr("source.codecompletion.identifier")! - codeCompletionDescription = api.uid_get_from_cstr("source.codecompletion.description")! - editActive = api.uid_get_from_cstr("source.edit.kind.active")! - editInactive = api.uid_get_from_cstr("source.edit.kind.inactive")! - editSelector = api.uid_get_from_cstr("source.edit.kind.selector")! - editString = api.uid_get_from_cstr("source.edit.kind.string")! - editComment = api.uid_get_from_cstr("source.edit.kind.comment")! - editMismatch = api.uid_get_from_cstr("source.edit.kind.mismatch")! - editUnknown = api.uid_get_from_cstr("source.edit.kind.unknown")! - renameRangeBase = api.uid_get_from_cstr("source.refactoring.range.kind.basename")! - renameRangeKeywordBase = api.uid_get_from_cstr("source.refactoring.range.kind.keyword-basename")! - renameRangeParam = api.uid_get_from_cstr("source.refactoring.range.kind.parameter-and-whitespace")! - renameRangeNoncollapsibleParam = api.uid_get_from_cstr("source.refactoring.range.kind.noncollapsible-parameter")! - renameRangeDeclArgLabel = api.uid_get_from_cstr("source.refactoring.range.kind.decl-argument-label")! - renameRangeCallArgLabel = api.uid_get_from_cstr("source.refactoring.range.kind.call-argument-label")! - renameRangeCallArgColon = api.uid_get_from_cstr("source.refactoring.range.kind.call-argument-colon")! - renameRangeCallArgCombined = api.uid_get_from_cstr("source.refactoring.range.kind.call-argument-combined")! - renameRangeSelectorArgLabel = api.uid_get_from_cstr("source.refactoring.range.kind.selector-argument-label")! - definition = api.uid_get_from_cstr("source.syntacticrename.definition")! - reference = api.uid_get_from_cstr("source.syntacticrename.reference")! - call = api.uid_get_from_cstr("source.syntacticrename.call")! - unknown = api.uid_get_from_cstr("source.syntacticrename.unknown")! - statNumRequests = api.uid_get_from_cstr("source.statistic.num-requests")! - statNumSemaRequests = api.uid_get_from_cstr("source.statistic.num-semantic-requests")! - statInstructionCount = api.uid_get_from_cstr("source.statistic.instruction-count")! - swift = api.uid_get_from_cstr("source.lang.swift")! - objC = api.uid_get_from_cstr("source.lang.objc")! - macroRoleExpression = api.uid_get_from_cstr("source.lang.swift.macro_role.expression")! - macroRoleDeclaration = api.uid_get_from_cstr("source.lang.swift.macro_role.declaration")! - macroRoleCodeItem = api.uid_get_from_cstr("source.lang.swift.macro_role.codeitem")! - macroRoleAccessor = api.uid_get_from_cstr("source.lang.swift.macro_role.accessor")! - macroRoleMemberAttribute = api.uid_get_from_cstr("source.lang.swift.macro_role.member_attribute")! - macroRoleMember = api.uid_get_from_cstr("source.lang.swift.macro_role.member")! - macroRolePeer = api.uid_get_from_cstr("source.lang.swift.macro_role.peer")! - macroRoleConformance = api.uid_get_from_cstr("source.lang.swift.macro_role.conformance")! - macroRoleExtension = api.uid_get_from_cstr("source.lang.swift.macro_role.extension")! - macroRolePreamble = api.uid_get_from_cstr("source.lang.swift.macro_role.preamble")! - macroRoleBody = api.uid_get_from_cstr("source.lang.swift.macro_role.body")! - completionKindKeyword = api.uid_get_from_cstr("source.lang.swift.keyword")! - completionKindPattern = api.uid_get_from_cstr("source.lang.swift.pattern")! - semaDiagStage = api.uid_get_from_cstr("source.diagnostic.stage.swift.sema")! - parseDiagStage = api.uid_get_from_cstr("source.diagnostic.stage.swift.parse")! - semaDisabledNotification = api.uid_get_from_cstr("source.notification.sema_disabled")! - semaEnabledNotification = api.uid_get_from_cstr("source.notification.sema_enabled")! - documentUpdateNotification = api.uid_get_from_cstr("source.notification.editor.documentupdate")! - diagRemark = api.uid_get_from_cstr("source.diagnostic.severity.remark")! - } -} diff --git a/Sources/SourceKitD/sourcekitd_uids.swift.gyb b/Sources/SourceKitD/sourcekitd_uids.swift.gyb deleted file mode 100644 index da1f41864..000000000 --- a/Sources/SourceKitD/sourcekitd_uids.swift.gyb +++ /dev/null @@ -1,141 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -// Automatically Generated From UIDs.swift.gyb. -// Do Not Edit Directly! To regenerate run Utilities/generate-uids.py - -%{ - from gyb_sourcekit_support.UIDs import UID_KEYS, UID_REQUESTS, UID_KINDS, KEY, REQUEST, KIND - # Ignore the following admonition it applies to the resulting .swift file only -}% -%{ - def variable_name(name): - if name == 'Operator': - return '`operator`' - word_index = 0 - threshold_index = 1 - for c in name: - if c.islower(): - if word_index > threshold_index: - word_index -= 1 - break - word_index += 1 - if word_index == 0: - return name - return name[:word_index].lower() + name[word_index:] - - ADDITIONAL_KEYS = [ - # Maintained from translateCodeCompletionOptions in SwiftCompletion.cpp# - KEY('SortByName', 'key.codecomplete.sort.byname'), - KEY('UseImportDepth', 'key.codecomplete.sort.useimportdepth'), - KEY('GroupOverloads', 'key.codecomplete.group.overloads'), - KEY('GroupStems', 'key.codecomplete.group.stems'), - KEY('FilterText', 'key.codecomplete.filtertext'), - KEY('RequestLimit', 'key.codecomplete.requestlimit'), - KEY('RequestStart', 'key.codecomplete.requeststart'), - KEY('HideUnderscores', 'key.codecomplete.hideunderscores'), - KEY('HideLowPriority', 'key.codecomplete.hidelowpriority'), - KEY('HideByName', 'key.codecomplete.hidebyname'), - KEY('IncludeExactMatch', 'key.codecomplete.includeexactmatch'), - KEY('AddInnerResults', 'key.codecomplete.addinnerresults'), - KEY('AddInnerOperators', 'key.codecomplete.addinneroperators'), - KEY('AddInitsToTopLevel', 'key.codecomplete.addinitstotoplevel'), - KEY('FuzzyMatching', 'key.codecomplete.fuzzymatching'), - KEY('TopNonLiteral', 'key.codecomplete.showtopnonliteralresults'), - KEY('ContextWeight', 'key.codecomplete.sort.contextweight'), - KEY('FuzzyWeight', 'key.codecomplete.sort.fuzzyweight'), - KEY('PopularityBonus', 'key.codecomplete.sort.popularitybonus'), - KEY('AnnotatedDescription', 'key.codecomplete.annotateddescription'), - KEY('IncludeObjectLiterals', 'key.codecomplete.includeobjectliterals'), - - # Used exclusively within the SourceKit Plugin - KEY('UseNewAPI', 'key.codecomplete.use_new_api'), - KEY('AddCallWithNoDefaultArgs', 'key.codecomplete.addcallwithnodefaultargs'), - KEY('IncludeSemanticComponents', 'key.codecomplete.include_semantic_components'), - KEY('UseXPCSerialization', 'key.codecomplete.use_xpc_serialization'), - KEY('MaxResults', 'key.codecomplete.maxresults'), - KEY('AnnotatedTypeName', 'key.annotated.typename'), - KEY('PriorityBucket', 'key.priority_bucket'), - KEY('Identifier', 'key.identifier'), - KEY('TextMatchScore', 'key.text_match_score'), - KEY('SemanticScore', 'key.semantic_score'), - KEY('SemanticScoreComponents', 'key.semantic_score_components'), - KEY('SymbolPopularity', 'key.symbol_popularity'), - KEY('ModulePopularity', 'key.module_popularity'), - KEY('PopularityKey', 'key.popularity.key'), - KEY('PopularityValueIntBillion', 'key.popularity.value.int.billion'), - KEY('RecentCompletions', 'key.recent_completions'), - KEY('UnfilteredResultCount', 'key.unfiltered_result_count'), - KEY('MemberAccessTypes', 'key.member_access_types'), - KEY('HasDiagnostic', 'key.has_diagnostic'), - KEY('GroupId', 'key.group_id'), - KEY('ScopedPopularityTablePath', 'key.scoped_popularity_table_path'), - KEY('PopularModules', 'key.popular_modules'), - KEY('NotoriousModules', 'key.notorious_modules'), - KEY('UsedScoreComponents', 'key.codecomplete.setpopularapi_used_score_components'), - - - # Maintained from applyFormatOptions in SwiftEditor.cpp - KEY('UseTabs', 'key.editor.format.usetabs'), - KEY('IndentWidth', 'key.editor.format.indentwidth'), - KEY('TabWidth', 'key.editor.format.tabwidth'), - KEY('IndentSwitchCase', 'key.editor.format.indent_switch_case'), - ] - - ADDITIONAL_REQUESTS = [ - REQUEST('CodeCompleteDocumentation', 'source.request.codecomplete.documentation'), - REQUEST('CodeCompleteDiagnostic', 'source.request.codecomplete.diagnostic'), - ] - - # We should automatically generate these. - ADDITIONAL_VALUES = [ - # Maintained from SwiftToSourceKitCompletionAdapter::handleResult in SwiftCompletion.cpp - KIND('CompletionKindKeyword', 'source.lang.swift.keyword'), - KIND('CompletionKindPattern', 'source.lang.swift.pattern'), - - # Maintained from SwiftEditor.cpp - KIND('SemaDiagStage', 'source.diagnostic.stage.swift.sema'), - KIND('ParseDiagStage', 'source.diagnostic.stage.swift.parse'), - - # Maintained from sourcekitd.cpp - KIND('SemaDisabledNotification', 'source.notification.sema_disabled'), - - # Maintained from initializeService in Requests.cpp - KIND('SemaEnabledNotification', 'source.notification.sema_enabled'), - KIND('DocumentUpdateNotification', 'source.notification.editor.documentupdate'), - - # Used exclusively within the SourceKit Plugin - KIND('DiagRemark', 'source.diagnostic.severity.remark'), - ] - - TYPES_AND_KEYS = [ - ('sourcekitd_api_keys', UID_KEYS + ADDITIONAL_KEYS), - ('sourcekitd_api_requests', UID_REQUESTS + ADDITIONAL_REQUESTS), - ('sourcekitd_api_values', UID_KINDS + ADDITIONAL_VALUES), - ] -}% - -package import Csourcekitd -% for (struct_type, uids) in TYPES_AND_KEYS: - -package struct ${struct_type} { -% for key in uids: - /// `${key.externalName}` - package let ${variable_name(key.internalName)}: sourcekitd_api_uid_t -% end - - package init(api: sourcekitd_api_functions_t) { -% for key in uids: - ${variable_name(key.internalName)} = api.uid_get_from_cstr("${key.externalName}")! -% end - } -} -% end diff --git a/Sources/SourceKitDForPlugin b/Sources/SourceKitDForPlugin deleted file mode 120000 index 9c224c633..000000000 --- a/Sources/SourceKitDForPlugin +++ /dev/null @@ -1 +0,0 @@ -SourceKitD \ No newline at end of file diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt deleted file mode 100644 index 7b9677fff..000000000 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ /dev/null @@ -1,53 +0,0 @@ - -add_library(SourceKitLSP STATIC - CapabilityRegistry.swift - DocumentManager.swift - DocumentSnapshot+FromFileContents.swift - DocumentSnapshot+PositionConversions.swift - GeneratedInterfaceDocumentURLData.swift - Hooks.swift - IndexProgressManager.swift - IndexStoreDB+MainFilesProvider.swift - LanguageService.swift - LanguageServiceRegistry.swift - LogMessageNotification+representingStructureUsingEmojiPrefixIfNecessary.swift - MacroExpansionReferenceDocumentURLData.swift - MessageHandlingDependencyTracker.swift - OnDiskDocumentManager.swift - ReferenceDocumentURL.swift - Rename.swift - SemanticTokensLegend+SourceKitLSPLegend.swift - SharedWorkDoneProgressManager.swift - SourceKitIndexDelegate.swift - SourceKitLSPCommandMetadata.swift - SourceKitLSPServer.swift - SymbolLocation+DocumentURI.swift - SyntacticTestIndex.swift - TestDiscovery.swift - TextEdit+IsNoop.swift - Workspace.swift -) -set_target_properties(SourceKitLSP PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY} -) -target_link_libraries(SourceKitLSP PUBLIC - BuildServerProtocol - BuildServerIntegration - LanguageServerProtocol - LanguageServerProtocolExtensions - SemanticIndex - SKOptions - SKUtilities - SwiftExtensions - ToolchainRegistry - IndexStoreDB - SwiftSyntax::SwiftSyntax -) -target_link_libraries(SourceKitLSP PRIVATE - LanguageServerProtocolJSONRPC - SKLogging - SourceKitD - TSCExtensions - $<$>:FoundationXML> -) - diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift deleted file mode 100644 index 319500ab2..000000000 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ /dev/null @@ -1,410 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions - -/// A class which tracks the client's capabilities as well as our dynamic -/// capability registrations in order to avoid registering conflicting -/// capabilities. -package final actor CapabilityRegistry { - /// The client's capabilities as they were reported when sourcekit-lsp was launched. - package nonisolated let clientCapabilities: ClientCapabilities - - // MARK: Tracking capabilities dynamically registered in the client - - /// Dynamically registered completion options. - private var completion: [CapabilityRegistration: CompletionRegistrationOptions] = [:] - - /// Dynamically registered signature help options. - private var signatureHelp: [CapabilityRegistration: SignatureHelpRegistrationOptions] = [:] - - /// Dynamically registered folding range options. - private var foldingRange: [CapabilityRegistration: FoldingRangeRegistrationOptions] = [:] - - /// Dynamically registered semantic tokens options. - private var semanticTokens: [CapabilityRegistration: SemanticTokensRegistrationOptions] = [:] - - /// Dynamically registered inlay hint options. - private var inlayHint: [CapabilityRegistration: InlayHintRegistrationOptions] = [:] - - /// Dynamically registered pull diagnostics options. - private var pullDiagnostics: [CapabilityRegistration: DiagnosticRegistrationOptions] = [:] - - /// Dynamically registered file watchers. - private var didChangeWatchedFiles: (id: String, options: DidChangeWatchedFilesRegistrationOptions)? - - /// Dynamically registered command IDs. - private var commandIds: Set = [] - - // MARK: Query if client has dynamic registration - - package var clientHasDynamicCompletionRegistration: Bool { - clientCapabilities.textDocument?.completion?.dynamicRegistration == true - } - - package var clientHasDynamicSignatureHelpRegistration: Bool { - clientCapabilities.textDocument?.signatureHelp?.dynamicRegistration == true - } - - package var clientHasDynamicFoldingRangeRegistration: Bool { - clientCapabilities.textDocument?.foldingRange?.dynamicRegistration == true - } - - package var clientHasDynamicSemanticTokensRegistration: Bool { - clientCapabilities.textDocument?.semanticTokens?.dynamicRegistration == true - } - - package var clientHasDynamicInlayHintRegistration: Bool { - clientCapabilities.textDocument?.inlayHint?.dynamicRegistration == true - } - - package var clientHasDynamicDocumentDiagnosticsRegistration: Bool { - clientCapabilities.textDocument?.diagnostic?.dynamicRegistration == true - } - - package var clientHasDynamicExecuteCommandRegistration: Bool { - clientCapabilities.workspace?.executeCommand?.dynamicRegistration == true - } - - package var clientHasDynamicDidChangeWatchedFilesRegistration: Bool { - clientCapabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration == true - } - - // MARK: Other capability queries - - package var clientHasDiagnosticsCodeDescriptionSupport: Bool { - clientCapabilities.textDocument?.publishDiagnostics?.codeDescriptionSupport == true - } - - public var supportedCodeLensCommands: [SupportedCodeLensCommand: String] { - clientCapabilities.textDocument?.codeLens?.supportedCommands ?? [:] - } - - /// Since LSP 3.17.0, diagnostics can be reported through pull-based requests in addition to the existing push-based - /// publish notifications. - /// - /// The `DiagnosticOptions` were added at the same time as the pull diagnostics request and allow specification of - /// options for the pull diagnostics request. If the client doesn't reject this dynamic capability registration, - /// it supports the pull diagnostics request. - package func clientSupportsPullDiagnostics(for language: Language) -> Bool { - registration(for: [language], in: pullDiagnostics) != nil - } - - package nonisolated var clientSupportsActiveDocumentNotification: Bool { - return clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method) - } - - package nonisolated func clientHasExperimentalCapability(_ name: String) -> Bool { - guard case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental else { - return false - } - // Before Swift 6.3 we expected experimental client capabilities to be passed as `"capabilityName": true`. - // This proved to be insufficient for experimental capabilities that evolved over time. Since 6.3 we encourage - // clients to pass experimental capabilities as `"capabilityName": { "supported": true }`, which allows the addition - // of more configuration parameters to the capability. - switch experimentalCapabilities[name] { - case .bool(true): - return true - case .dictionary(let dict): - return dict["supported"] == .bool(true) - default: - return false - } - } - - // MARK: Initializer - - package init(clientCapabilities: ClientCapabilities) { - self.clientCapabilities = clientCapabilities - } - - // MARK: Query registered capabilities - - /// Return a registration in `registrations` for one or more of the given - /// `languages`. - private func registration( - for languages: [Language], - in registrations: [CapabilityRegistration: T] - ) -> T? { - var languageIds: Set = [] - for language in languages { - languageIds.insert(language.rawValue) - } - - for registration in registrations { - let options = registration.value.textDocumentRegistrationOptions - guard let filters = options.documentSelector else { continue } - for filter in filters { - guard let filterLanguage = filter.language else { continue } - if languageIds.contains(filterLanguage) { - return registration.value - } - } - } - return nil - } - - // MARK: Dynamic registration of server capabilities - - /// Register a dynamic server capability with the client. - /// - /// If the registration of `options` for the given `method` and `languages` was successful, the capability will be - /// added to `registrationDict` by calling `setRegistrationDict`. - /// If registration failed, the capability won't be added to `registrationDict`. - private func registerLanguageSpecificCapability< - Options: RegistrationOptions & TextDocumentRegistrationOptionsProtocol & Equatable - >( - options: Options, - forMethod method: String, - languages: [Language], - in server: SourceKitLSPServer, - registrationDict: [CapabilityRegistration: Options], - setRegistrationDict: (CapabilityRegistration, Options?) -> Void - ) async { - if let registration = registration(for: languages, in: registrationDict) { - if options != registration { - logger.fault( - """ - Failed to dynamically register for \(method, privacy: .public) for \(languages, privacy: .public) \ - due to pre-existing options: - Existing options: \(String(reflecting: registration), privacy: .public) - New options: \(String(reflecting: options), privacy: .public) - """ - ) - } - return - } - - let registration = CapabilityRegistration( - method: method, - registerOptions: options.encodeToLSPAny() - ) - - // Add the capability to the registration dictionary. - // This ensures that concurrent calls for the same capability don't register it as well. - // If the capability is rejected by the client, we remove it again. - setRegistrationDict(registration, options) - - do { - _ = try await server.client.send(RegisterCapabilityRequest(registrations: [registration])) - } catch { - setRegistrationDict(registration, nil) - } - } - - /// Dynamically register completion capabilities if the client supports it and - /// we haven't yet registered any completion capabilities for the given - /// languages. - package func registerCompletionIfNeeded( - options: CompletionOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicCompletionRegistration else { return } - - await registerLanguageSpecificCapability( - options: CompletionRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - completionOptions: options - ), - forMethod: CompletionRequest.method, - languages: languages, - in: server, - registrationDict: completion, - setRegistrationDict: { completion[$0] = $1 } - ) - } - - package func registerSignatureHelpIfNeeded( - options: SignatureHelpOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicCompletionRegistration else { return } - - await registerLanguageSpecificCapability( - options: SignatureHelpRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - signatureHelpOptions: options - ), - forMethod: SignatureHelpRequest.method, - languages: languages, - in: server, - registrationDict: signatureHelp, - setRegistrationDict: { signatureHelp[$0] = $1 } - ) - } - - package func registerDidChangeWatchedFiles( - watchers: [FileSystemWatcher], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicDidChangeWatchedFilesRegistration else { return } - if let registration = didChangeWatchedFiles { - do { - _ = try await server.client.send( - UnregisterCapabilityRequest(unregistrations: [ - Unregistration(id: registration.id, method: DidChangeWatchedFilesNotification.method) - ]) - ) - } catch { - logger.error("Failed to unregister capability \(DidChangeWatchedFilesNotification.method).") - return - } - } - let registrationOptions = DidChangeWatchedFilesRegistrationOptions( - watchers: watchers - ) - let registration = CapabilityRegistration( - method: DidChangeWatchedFilesNotification.method, - registerOptions: registrationOptions.encodeToLSPAny() - ) - - self.didChangeWatchedFiles = (registration.id, registrationOptions) - - do { - _ = try await server.client.send(RegisterCapabilityRequest(registrations: [registration])) - } catch { - logger.error("Failed to dynamically register for watched files: \(error.forLogging)") - self.didChangeWatchedFiles = nil - } - } - - /// Dynamically register folding range capabilities if the client supports it and - /// we haven't yet registered any folding range capabilities for the given - /// languages. - package func registerFoldingRangeIfNeeded( - options: FoldingRangeOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicFoldingRangeRegistration else { return } - - await registerLanguageSpecificCapability( - options: FoldingRangeRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - foldingRangeOptions: options - ), - forMethod: FoldingRangeRequest.method, - languages: languages, - in: server, - registrationDict: foldingRange, - setRegistrationDict: { foldingRange[$0] = $1 } - ) - } - - /// Dynamically register semantic tokens capabilities if the client supports - /// it and we haven't yet registered any semantic tokens capabilities for the - /// given languages. - package func registerSemanticTokensIfNeeded( - options: SemanticTokensOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicSemanticTokensRegistration else { return } - - await registerLanguageSpecificCapability( - options: SemanticTokensRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - semanticTokenOptions: options - ), - forMethod: SemanticTokensRegistrationOptions.method, - languages: languages, - in: server, - registrationDict: semanticTokens, - setRegistrationDict: { semanticTokens[$0] = $1 } - ) - } - - /// Dynamically register inlay hint capabilities if the client supports - /// it and we haven't yet registered any inlay hint capabilities for the - /// given languages. - package func registerInlayHintIfNeeded( - options: InlayHintOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicInlayHintRegistration else { return } - - await registerLanguageSpecificCapability( - options: InlayHintRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - inlayHintOptions: options - ), - forMethod: InlayHintRequest.method, - languages: languages, - in: server, - registrationDict: inlayHint, - setRegistrationDict: { inlayHint[$0] = $1 } - ) - } - - /// Dynamically register (pull model) diagnostic capabilities, - /// if the client supports it. - package func registerDiagnosticIfNeeded( - options: DiagnosticOptions, - for languages: [Language], - server: SourceKitLSPServer - ) async { - guard clientHasDynamicDocumentDiagnosticsRegistration else { return } - - await registerLanguageSpecificCapability( - options: DiagnosticRegistrationOptions( - documentSelector: DocumentSelector(for: languages), - diagnosticOptions: options - ), - forMethod: DocumentDiagnosticsRequest.method, - languages: languages, - in: server, - registrationDict: pullDiagnostics, - setRegistrationDict: { pullDiagnostics[$0] = $1 } - ) - } - - /// Dynamically register executeCommand with the given IDs if the client supports - /// it and we haven't yet registered the given command IDs yet. - package func registerExecuteCommandIfNeeded( - commands: [String], - server: SourceKitLSPServer - ) { - guard clientHasDynamicExecuteCommandRegistration else { return } - - var newCommands = Set(commands) - newCommands.subtract(self.commandIds) - - // We only want to send the registration with unregistered command IDs since - // clients such as VS Code only allow a command to be registered once. We could - // unregister all our commandIds first but this is simpler. - guard !newCommands.isEmpty else { return } - self.commandIds.formUnion(newCommands) - - let registration = CapabilityRegistration( - method: ExecuteCommandRequest.method, - registerOptions: ExecuteCommandRegistrationOptions(commands: Array(newCommands)).encodeToLSPAny() - ) - - let _ = server.client.send(RegisterCapabilityRequest(registrations: [registration])) { result in - if let error = result.failure { - logger.error("Failed to dynamically register commands: \(error.forLogging)") - } - } - } -} - -fileprivate extension DocumentSelector { - init(for languages: [Language], scheme: String? = nil) { - self.init(languages.map { DocumentFilter(language: $0.rawValue, scheme: scheme) }) - } -} diff --git a/Sources/SourceKitLSP/DocumentManager.swift b/Sources/SourceKitLSP/DocumentManager.swift deleted file mode 100644 index f06522749..000000000 --- a/Sources/SourceKitLSP/DocumentManager.swift +++ /dev/null @@ -1,243 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Dispatch -import Foundation -package import LanguageServerProtocol -import SKLogging -package import SKUtilities -import SemanticIndex -package import SwiftSyntax - -/// An immutable snapshot of a document at a given time. -/// -/// ``DocumentSnapshot`` is always derived from a ``Document``. That is, the -/// data structure that is stored internally by the ``DocumentManager`` is a -/// ``Document``. The purpose of a ``DocumentSnapshot`` is to be able to work -/// with one version of a document without having to think about it changing. -package struct DocumentSnapshot: Identifiable, Sendable { - /// An ID that uniquely identifies the version of the document stored in this - /// snapshot. - package struct ID: Hashable, Comparable, Sendable { - package let uri: DocumentURI - package let version: Int - - /// Returns `true` if the snapshots reference the same document but rhs has a - /// later version than `lhs`. - /// - /// Snapshot IDs of different documents are not comparable to each other and - /// will always return `false`. - package static func < (lhs: DocumentSnapshot.ID, rhs: DocumentSnapshot.ID) -> Bool { - return lhs.uri == rhs.uri && lhs.version < rhs.version - } - } - - package let id: ID - package let language: Language - package let lineTable: LineTable - - package var uri: DocumentURI { id.uri } - package var version: Int { id.version } - package var text: String { lineTable.content } - - package init( - uri: DocumentURI, - language: Language, - version: Int, - lineTable: LineTable - ) { - self.id = ID(uri: uri, version: version) - self.language = language - self.lineTable = lineTable - } -} - -package final class Document { - package let uri: DocumentURI - package let language: Language - var latestVersion: Int - var latestLineTable: LineTable - - init(uri: DocumentURI, language: Language, version: Int, text: String) { - self.uri = uri - self.language = language - self.latestVersion = version - self.latestLineTable = LineTable(text) - } - - /// **Not thread safe!** Use `DocumentManager.latestSnapshot` instead. - fileprivate var latestSnapshot: DocumentSnapshot { - DocumentSnapshot( - uri: self.uri, - language: self.language, - version: latestVersion, - lineTable: latestLineTable - ) - } -} - -package final class DocumentManager: InMemoryDocumentManager, Sendable { - - package enum Error: Swift.Error { - case alreadyOpen(DocumentURI) - case missingDocument(DocumentURI) - } - - // TODO: Migrate this to be an AsyncQueue (https://github.com/swiftlang/sourcekit-lsp/issues/1597) - private let queue: DispatchQueue = DispatchQueue(label: "document-manager-queue") - - // `nonisolated(unsafe)` is fine because `documents` is guarded by queue. - private nonisolated(unsafe) var documents: [DocumentURI: Document] = [:] - - package init() {} - - /// All currently opened documents. - package var openDocuments: Set { - return queue.sync { - return Set(documents.keys) - } - } - - /// Opens a new document with the given content and metadata. - /// - /// - returns: The initial contents of the file. - /// - throws: Error.alreadyOpen if the document is already open. - @discardableResult - package func open(_ uri: DocumentURI, language: Language, version: Int, text: String) throws -> DocumentSnapshot { - return try queue.sync { - let document = Document(uri: uri, language: language, version: version, text: text) - if nil != documents.updateValue(document, forKey: uri) { - throw Error.alreadyOpen(uri) - } - return document.latestSnapshot - } - } - - /// Closes the given document. - /// - /// - returns: The initial contents of the file. - /// - throws: Error.missingDocument if the document is not open. - package func close(_ uri: DocumentURI) throws { - try queue.sync { - if nil == documents.removeValue(forKey: uri) { - throw Error.missingDocument(uri) - } - } - } - - /// Applies the given edits to the document. - /// - /// - Parameters: - /// - uri: The URI of the document to update - /// - newVersion: The new version of the document. Must be greater than the - /// latest version of the document. - /// - edits: The edits to apply to the document - /// - Returns: The snapshot of the document before the edit, the snapshot - /// of the document after the edit, and the edits. The edits are sequential, ie. - /// the edits are expected to be applied in order and later values in this array - /// assume that previous edits are already applied. - @discardableResult - package func edit( - _ uri: DocumentURI, - newVersion: Int, - edits: [TextDocumentContentChangeEvent] - ) throws -> (preEditSnapshot: DocumentSnapshot, postEditSnapshot: DocumentSnapshot, edits: [SourceEdit]) { - return try queue.sync { - guard let document = documents[uri] else { - throw Error.missingDocument(uri) - } - let preEditSnapshot = document.latestSnapshot - - var sourceEdits: [SourceEdit] = [] - for edit in edits { - sourceEdits.append(SourceEdit(edit: edit, lineTableBeforeEdit: document.latestLineTable)) - - if let range = edit.range { - document.latestLineTable.replace( - fromLine: range.lowerBound.line, - utf16Offset: range.lowerBound.utf16index, - toLine: range.upperBound.line, - utf16Offset: range.upperBound.utf16index, - with: edit.text - ) - } else { - // Full text replacement. - document.latestLineTable = LineTable(edit.text) - } - } - - if newVersion <= document.latestVersion { - logger.error("Document version did not increase on edit from \(document.latestVersion) to \(newVersion)") - } - document.latestVersion = newVersion - return (preEditSnapshot, document.latestSnapshot, sourceEdits) - } - } - - package func latestSnapshot(_ uri: DocumentURI) throws -> DocumentSnapshot { - return try queue.sync { - guard let document = documents[uri] else { - throw ResponseError.unknown("Failed to find snapshot for '\(uri)'") - } - return document.latestSnapshot - } - } - - /// Returns the latest open snapshot of `uri` or, if no document with that URI is open, reads the file contents of - /// that file from disk. - package func latestSnapshotOrDisk(_ uri: DocumentURI, language: Language) -> DocumentSnapshot? { - if let snapshot = try? self.latestSnapshot(uri) { - return snapshot - } - return try? DocumentSnapshot(withContentsFromDisk: uri, language: language) - } - - package func fileHasInMemoryModifications(_ uri: DocumentURI) -> Bool { - guard let document = try? latestSnapshot(uri), let fileURL = uri.fileURL else { - return false - } - - guard let onDiskFileContents = try? String(contentsOf: fileURL, encoding: .utf8) else { - // If we can't read the file on disk, it can't match any on-disk state, so it's in-memory state - return true - } - return onDiskFileContents != document.lineTable.content - } -} - -fileprivate extension SourceEdit { - /// Constructs a `SourceEdit` from the given `TextDocumentContentChangeEvent`. - /// - /// Returns `nil` if the `TextDocumentContentChangeEvent` refers to line:column positions that don't exist in - /// `LineTable`. - init(edit: TextDocumentContentChangeEvent, lineTableBeforeEdit: LineTable) { - if let range = edit.range { - let offset = lineTableBeforeEdit.utf8OffsetOf( - line: range.lowerBound.line, - utf16Column: range.lowerBound.utf16index - ) - let end = lineTableBeforeEdit.utf8OffsetOf( - line: range.upperBound.line, - utf16Column: range.upperBound.utf16index - ) - self.init( - range: AbsolutePosition(utf8Offset: offset).. Raw UTF-8 - - /// Converts the given UTF-8 offset to `String.Index`. - /// - /// If the offset is out-of-bounds of the snapshot, returns the closest valid index and logs a fault containing the - /// file and line of the caller (from `callerFile` and `callerLine`). - package func indexOf(utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> String.Index { - guard utf8Offset >= 0 else { - logger.fault( - """ - UTF-8 offset \(utf8Offset) is negative while converting it to String.Index \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return text.startIndex - } - guard let index = text.utf8.index(text.startIndex, offsetBy: utf8Offset, limitedBy: text.endIndex) else { - logger.fault( - """ - UTF-8 offset \(utf8Offset) is past end of file while converting it to String.Index \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return text.endIndex - } - return index - } - - // MARK: Position <-> Raw UTF-8 offset - - /// Converts the given UTF-16-based line:column position to the UTF-8 offset of that position within the source file. - /// - /// If `position` does not refer to a valid position with in the snapshot, returns the offset of the closest valid - /// position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - package func utf8Offset(of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Int { - return lineTable.utf8OffsetOf( - line: position.line, - utf16Column: position.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - } - - /// Converts the given UTF-8 offset to a UTF-16-based line:column position. - /// - /// If the offset is after the end of the snapshot, returns `nil` and logs a fault containing the file and line of - /// the caller (from `callerFile` and `callerLine`). - package func positionOf(utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Position { - let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf( - utf8Offset: utf8Offset, - callerFile: callerFile, - callerLine: callerLine - ) - return Position(line: line, utf16index: utf16Column) - } - - /// Converts the given UTF-16 based line:column range to a UTF-8 based offset range. - /// - /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to - /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and - /// `callerLine`). - package func utf8OffsetRange( - of range: Range, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Range { - let startOffset = utf8Offset(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine) - let endOffset = utf8Offset(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) - return startOffset.. String.Index - - /// Converts the given UTF-16-based `line:column` position to a `String.Index`. - /// - /// If `position` does not refer to a valid position with in the snapshot, returns the index of the closest valid - /// position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - package func index( - of position: Position, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> String.Index { - return lineTable.stringIndexOf( - line: position.line, - utf16Column: position.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - } - - /// Converts the given UTF-16-based `line:column` range to a `String.Index` range. - /// - /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to - /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and - /// `callerLine`). - package func indexRange( - of range: Range, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Range { - return self.index(of: range.lowerBound).. Position { - let utf16Column = lineTable.utf16ColumnAt( - line: zeroBasedLine, - utf8Column: utf8Column, - callerFile: callerFile, - callerLine: callerLine - ) - return Position(line: zeroBasedLine, utf16index: utf16Column) - } - - /// Converts the given `String.Index` to a UTF-16-based line:column position. - package func position(of index: String.Index, fromLine: Int = 0) -> Position { - let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf(index, fromLine: fromLine) - return Position(line: line, utf16index: utf16Column) - } - - // MARK: Position <-> AbsolutePosition - - /// Converts the given UTF-8-offset-based `AbsolutePosition` to a UTF-16-based line:column. - /// - /// If the `AbsolutePosition` out of bounds of the source file, returns the closest valid position and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). - package func position( - of position: AbsolutePosition, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Position { - return positionOf(utf8Offset: position.utf8Offset, callerFile: callerFile, callerLine: callerLine) - } - - /// Converts the given UTF-16-based line:column `Position` to a UTF-8-offset-based `AbsolutePosition`. - /// - /// If the UTF-16 based line:column pair does not refer to a valid position within the snapshot, returns the closest - /// valid position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - package func absolutePosition( - of position: Position, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> AbsolutePosition { - let offset = utf8Offset(of: position, callerFile: callerFile, callerLine: callerLine) - return AbsolutePosition(utf8Offset: offset) - } - - /// Converts the lower and upper bound of the given UTF-8-offset-based `AbsolutePosition` range to a UTF-16-based - /// line:column range for use in LSP. - /// - /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to - /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and - /// `callerLine`). - package func absolutePositionRange( - of range: Range, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Range { - let lowerBound = self.position(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine) - let upperBound = self.position(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) - return lowerBound.. Range { - let lowerBound = self.position(of: node.position, callerFile: callerFile, callerLine: callerLine) - let upperBound = self.position(of: node.endPosition, callerFile: callerFile, callerLine: callerLine) - return lowerBound.., - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Range { - let utf8OffsetRange = utf8OffsetRange(of: range, callerFile: callerFile, callerLine: callerLine) - return Range( - position: AbsolutePosition(utf8Offset: utf8OffsetRange.startIndex), - length: SourceLength(utf8Length: utf8OffsetRange.count) - ) - } - - // MARK: Position <-> RenameLocation - - /// Converts the given UTF-8-based line:column `RenamedLocation` to a UTF-16-based line:column `Position`. - /// - /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns the closest - /// valid position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). - package func position( - of renameLocation: RenameLocation, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Position { - return positionOf( - zeroBasedLine: renameLocation.line - 1, - utf8Column: renameLocation.utf8Column - 1, - callerFile: callerFile, - callerLine: callerLine - ) - } - - // MAR: Position <-> SymbolLocation - - /// Converts the given UTF-8-offset-based `SymbolLocation` to a UTF-16-based line:column `Position`. - /// - /// If the UTF-8 offset is out-of-bounds of the snapshot, returns the closest valid position and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). - package func position( - of symbolLocation: SymbolLocation, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Position { - return positionOf( - zeroBasedLine: symbolLocation.line - 1, - utf8Column: symbolLocation.utf8Column - 1, - callerFile: callerFile, - callerLine: callerLine - ) - } - - // MARK: AbsolutePosition <-> RenameLocation - - /// Converts the given UTF-8-based line:column `RenamedLocation` to a UTF-8-offset-based `AbsolutePosition`. - /// - /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns the offset of - /// the closest valid position and logs a fault containing the file and line of the caller (from `callerFile` and - /// `callerLine`). - package func absolutePosition( - of renameLocation: RenameLocation, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> AbsolutePosition { - let utf8Offset = lineTable.utf8OffsetOf( - line: renameLocation.line - 1, - utf8Column: renameLocation.utf8Column - 1, - callerFile: callerFile, - callerLine: callerLine - ) - return AbsolutePosition(utf8Offset: utf8Offset) - } -} diff --git a/Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift b/Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift deleted file mode 100644 index 275a12b1d..000000000 --- a/Sources/SourceKitLSP/GeneratedInterfaceDocumentURLData.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol - -/// Represents url of generated interface reference document. -package struct GeneratedInterfaceDocumentURLData: Hashable, ReferenceURLData { - package static let documentType = "generated-swift-interface" - - private struct Parameters { - static let moduleName = "moduleName" - static let groupName = "groupName" - static let sourcekitdDocumentName = "sourcekitdDocument" - static let buildSettingsFrom = "buildSettingsFrom" - } - - /// The module that should be shown in this generated interface. - package let moduleName: String - - /// The group that should be shown in this generated interface, if applicable. - package let groupName: String? - - /// The name by which this document is referred to in sourcekitd. - package let sourcekitdDocumentName: String - - /// The document from which the build settings for the generated interface should be inferred. - package let buildSettingsFrom: DocumentURI - - package var displayName: String { - if let groupName { - return "\(moduleName).\(groupName.replacing("/", with: ".")).swiftinterface" - } - return "\(moduleName).swiftinterface" - } - - var queryItems: [URLQueryItem] { - var result = [ - URLQueryItem(name: Parameters.moduleName, value: moduleName) - ] - if let groupName { - result.append(URLQueryItem(name: Parameters.groupName, value: groupName)) - } - result += [ - URLQueryItem(name: Parameters.sourcekitdDocumentName, value: sourcekitdDocumentName), - URLQueryItem(name: Parameters.buildSettingsFrom, value: buildSettingsFrom.stringValue), - ] - return result - } - - package var uri: DocumentURI { - get throws { - try ReferenceDocumentURL.generatedInterface(self).uri - } - } - - package init(moduleName: String, groupName: String?, sourcekitdDocumentName: String, primaryFile: DocumentURI) { - self.moduleName = moduleName - self.groupName = groupName - self.sourcekitdDocumentName = sourcekitdDocumentName - self.buildSettingsFrom = primaryFile.buildSettingsFile - } - - init(queryItems: [URLQueryItem]) throws { - guard let moduleName = queryItems.last(where: { $0.name == Parameters.moduleName })?.value, - let sourcekitdDocumentName = queryItems.last(where: { $0.name == Parameters.sourcekitdDocumentName })?.value, - let primaryFile = queryItems.last(where: { $0.name == Parameters.buildSettingsFrom })?.value - else { - throw ReferenceDocumentURLError(description: "Invalid queryItems for generated interface reference document url") - } - - self.moduleName = moduleName - self.groupName = queryItems.last(where: { $0.name == Parameters.groupName })?.value - self.sourcekitdDocumentName = sourcekitdDocumentName - self.buildSettingsFrom = try DocumentURI(string: primaryFile) - } -} diff --git a/Sources/SourceKitLSP/Hooks.swift b/Sources/SourceKitLSP/Hooks.swift deleted file mode 100644 index e614fd974..000000000 --- a/Sources/SourceKitLSP/Hooks.swift +++ /dev/null @@ -1,53 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -package import SemanticIndex - -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath - -/// Closures can be used to inspect or modify internal behavior in SourceKit-LSP. -public struct Hooks: Sendable { - package var indexHooks: IndexHooks - - package var buildServerHooks: BuildServerHooks - - /// A hook that will be executed before a request is handled. - /// - /// This allows requests to be artificially delayed. - package var preHandleRequest: (@Sendable (any RequestType) async -> Void)? - - /// Closure that is executed before a request is forwarded to clangd. - /// - /// This allows tests to simulate a `clangd` process that's unresponsive. - package var preForwardRequestToClangd: (@Sendable (any RequestType) async -> Void)? - - public init() { - self.init(indexHooks: IndexHooks(), buildServerHooks: BuildServerHooks()) - } - - package init( - indexHooks: IndexHooks = IndexHooks(), - buildServerHooks: BuildServerHooks = BuildServerHooks(), - preHandleRequest: (@Sendable (any RequestType) async -> Void)? = nil, - preForwardRequestToClangd: (@Sendable (any RequestType) async -> Void)? = nil - ) { - self.indexHooks = indexHooks - self.buildServerHooks = buildServerHooks - self.preHandleRequest = preHandleRequest - self.preForwardRequestToClangd = preForwardRequestToClangd - } -} diff --git a/Sources/SourceKitLSP/IndexProgressManager.swift b/Sources/SourceKitLSP/IndexProgressManager.swift deleted file mode 100644 index 2d9e89666..000000000 --- a/Sources/SourceKitLSP/IndexProgressManager.swift +++ /dev/null @@ -1,140 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SKOptions -import SemanticIndex -import SwiftExtensions - -/// Listens for index status updates from `SemanticIndexManagers`. From that information, it manages a -/// `WorkDoneProgress` that communicates the index progress to the editor. -actor IndexProgressManager { - /// A queue on which `indexTaskWasQueued` and `indexProgressStatusDidChange` are handled. - /// - /// This allows the two functions two be `nonisolated` (and eg. the caller of `indexProgressStatusDidChange` doesn't have to - /// wait for the work done progress to be updated) while still guaranteeing that there is only one - /// `indexProgressStatusDidChangeImpl` running at a time, preventing race conditions that would cause two - /// `WorkDoneProgressManager`s to be created. - private let queue = AsyncQueue() - - /// The `SourceKitLSPServer` for which this manages the index progress. It gathers all `SemanticIndexManagers` from - /// the workspaces in the `SourceKitLSPServer`. - private weak var sourceKitLSPServer: SourceKitLSPServer? - - /// This is the target number of index tasks (eg. the `3` in `1/3 done`). - /// - /// Every time a new index task is scheduled, this number gets incremented, so that it only ever increases. - /// When indexing of one session is done (ie. when there are no more `scheduled` or `executing` tasks in any - /// `SemanticIndexManager`), `queuedIndexTasks` gets reset to 0 and the work done progress gets ended. - /// This way, when the next work done progress is started, it starts at zero again. - /// - /// The number of outstanding tasks is determined from the `scheduled` and `executing` tasks in all the - /// `SemanticIndexManager`s. - /// - /// Note that the `queuedIndexTasks` might exceed the number of files in the project, eg. in the following scenario: - /// - Schedule indexing of A.swift and B.swift -> 0 / 2 - /// - Indexing of A.swift finishes -> 1 / 2 - /// - A.swift is modified and should be indexed again -> 1 / 3 - /// - B.swift finishes indexing -> 2 / 3 - /// - A.swift finishes indexing for the second time -> 3 / 3 -> Status disappears - private var queuedIndexTasks = 0 - - /// While there are ongoing index tasks, a `WorkDoneProgressManager` that displays the work done progress. - private var workDoneProgress: WorkDoneProgressManager? - - init(sourceKitLSPServer: SourceKitLSPServer) { - self.sourceKitLSPServer = sourceKitLSPServer - } - - /// Called when a new file is scheduled to be indexed. Increments the target index count, eg. the 3 in `1/3`. - nonisolated func indexTasksWereScheduled(count: Int) { - queue.async { - await self.indexTasksWereScheduledImpl(count: count) - } - } - - private func indexTasksWereScheduledImpl(count: Int) async { - queuedIndexTasks += count - await indexProgressStatusDidChangeImpl() - } - - /// Called when a `SemanticIndexManager` finishes indexing a file. Adjusts the done index count, eg. the 1 in `1/3`. - nonisolated func indexProgressStatusDidChange() { - queue.async { - await self.indexProgressStatusDidChangeImpl() - } - } - - private func indexProgressStatusDidChangeImpl() async { - guard let sourceKitLSPServer else { - await workDoneProgress?.end() - workDoneProgress = nil - return - } - var status = IndexProgressStatus.upToDate - for indexManager in await sourceKitLSPServer.workspaces.asyncCompactMap({ await $0.semanticIndexManager }) { - status = status.merging(with: await indexManager.progressStatus) - } - - var message: String - let percentage: Int - switch status { - case .preparingFileForEditorFunctionality: - message = "Preparing current file" - percentage = 0 - case .generatingBuildGraph: - message = "Determining files" - percentage = 0 - case .indexing(let preparationTasks, let indexTasks): - // We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all - // `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks. - // Clip the finished tasks to 0 because showing a negative number there looks stupid. - let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0) - if indexTasks.isEmpty { - message = "Preparing targets" - if preparationTasks.isEmpty { - logger.fault("Indexer status is 'indexing' but there is no update indexstore or preparation task") - } - } else { - message = "\(finishedTasks) / \(queuedIndexTasks)" - } - logger.debug("In-progress index tasks: \(indexTasks.map(\.key.file.sourceFile))") - if queuedIndexTasks != 0 { - percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100) - } else { - percentage = 0 - } - case .upToDate: - // Nothing left to index. Reset the target count and dismiss the work done progress. - queuedIndexTasks = 0 - await workDoneProgress?.end() - workDoneProgress = nil - return - } - - if let workDoneProgress { - await workDoneProgress.update(message: message, percentage: percentage) - } else { - workDoneProgress = WorkDoneProgressManager( - server: sourceKitLSPServer, - capabilityRegistry: await sourceKitLSPServer.capabilityRegistry, - tokenPrefix: "indexing", - initialDebounce: sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault, - title: "Indexing", - message: message, - percentage: percentage - ) - } - } -} diff --git a/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift b/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift deleted file mode 100644 index a879c0f23..000000000 --- a/Sources/SourceKitLSP/IndexStoreDB+MainFilesProvider.swift +++ /dev/null @@ -1,42 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import Foundation -import IndexStoreDB -package import LanguageServerProtocol -import SKLogging -import SemanticIndex -import SwiftExtensions - -extension UncheckedIndex: BuildServerIntegration.MainFilesProvider { - /// - Important: This may return realpaths when the build server might not be using realpaths. Use - /// `BuildServerManager.mainFiles(containing:)` to work around that problem. - package func mainFiles(containing uri: DocumentURI, crossLanguage: Bool) -> Set { - let mainFiles: Set - if let filePath = orLog("File path to get main files", { try uri.fileURL?.filePath }) { - let mainFilePaths = self.underlyingIndexStoreDB.mainFilesContainingFile( - path: filePath, - crossLanguage: crossLanguage - ) - mainFiles = Set( - mainFilePaths - .filter { FileManager.default.fileExists(atPath: $0) } - .map({ DocumentURI(filePath: $0, isDirectory: false) }) - ) - } else { - mainFiles = [] - } - logger.info("Main files for \(uri.forLogging): \(mainFiles)") - return mainFiles - } -} diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift deleted file mode 100644 index 453503d8f..000000000 --- a/Sources/SourceKitLSP/LanguageService.swift +++ /dev/null @@ -1,538 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -import Foundation -package import IndexStoreDB -package import LanguageServerProtocol -import SKLogging -package import SKOptions -package import SwiftSyntax -package import ToolchainRegistry - -/// The state of a `ToolchainLanguageServer` -package enum LanguageServerState { - /// The language server is running with semantic functionality enabled - case connected - /// The language server server has crashed and we are waiting for it to relaunch - case connectionInterrupted - /// The language server has relaunched but semantic functionality is currently disabled - case semanticFunctionalityDisabled -} - -package struct AnnotatedTestItem: Sendable { - /// The test item to be annotated - package var testItem: TestItem - - /// Whether the `TestItem` is an extension. - package var isExtension: Bool - - package init( - testItem: TestItem, - isExtension: Bool - ) { - self.testItem = testItem - self.isExtension = isExtension - } -} - -package struct RenameLocation: Sendable { - /// How the identifier at a given location is being used. - /// - /// This is primarily used to influence how argument labels should be renamed in Swift and if a location should be - /// rejected if argument labels don't match. - package enum Usage { - /// The definition of a function/subscript/variable/... - case definition - - /// The symbol is being referenced. - /// - /// This includes - /// - References to variables - /// - Unapplied references to functions (`myStruct.memberFunc`) - /// - Calls to subscripts (`myArray[1]`, location is `[` here, length 1) - case reference - - /// A function that is being called. - case call - - /// Unknown name usage occurs if we don't have an entry in the index that - /// tells us whether the location is a call, reference or a definition. The - /// most common reasons why this happens is if the editor is adding syntactic - /// results (eg. from comments or string literals). - case unknown - } - - /// The line of the identifier to be renamed (1-based). - package let line: Int - - /// The column of the identifier to be renamed in UTF-8 bytes (1-based). - package let utf8Column: Int - - package let usage: Usage - - package init(line: Int, utf8Column: Int, usage: RenameLocation.Usage) { - self.line = line - self.utf8Column = utf8Column - self.usage = usage - } -} - -/// The textual output of a module interface. -package struct GeneratedInterfaceDetails: ResponseType, Hashable { - package var uri: DocumentURI - package var position: Position? - - package init(uri: DocumentURI, position: Position?) { - self.uri = uri - self.position = position - } -} - -/// Provides language specific functionality to sourcekit-lsp from a specific toolchain. -/// -/// For example, we may have a language service that provides semantic functionality for c-family using a clangd server, -/// launched from a specific toolchain or from sourcekitd. -package protocol LanguageService: AnyObject, Sendable { - - // MARK: - Creation - - init( - sourceKitLSPServer: SourceKitLSPServer, - toolchain: Toolchain, - options: SourceKitLSPOptions, - hooks: Hooks, - workspace: Workspace - ) async throws - - /// Returns `true` if this instance of the language server can handle documents in `workspace` using the given - /// toolchain. - /// - /// If this returns `false`, a new language server will be started for `workspace`. - func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool - - /// Identifiers of the commands that this language service can handle. - static var builtInCommands: [String] { get } - - /// Experimental capabilities that should be reported to the client if this language service is enabled. - static var experimentalCapabilities: [String: LSPAny] { get } - - // MARK: - Lifetime - - func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult - func clientInitialized(_ initialized: InitializedNotification) async - - /// Shut the server down and return once the server has finished shutting down - func shutdown() async - - /// Add a handler that is called whenever the state of the language server changes. - func addStateChangeHandler( - handler: @Sendable @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void - ) async - - // MARK: - Text synchronization - - /// Sent to open up a document on the Language Server. - /// - /// This may be called before or after a corresponding `documentUpdatedBuildSettings` call for the same document. - func openDocument(_ notification: DidOpenTextDocumentNotification, snapshot: DocumentSnapshot) async - - /// Sent to close a document on the Language Server. - func closeDocument(_ notification: DidCloseTextDocumentNotification) async - - /// Sent to open up a document on the Language Server whose contents are on-disk. - /// - /// The snapshot will have a synthesized name and the caller is responsible for synthesizing build settings for it. - /// - /// - Important: This should only be called by `OnDiskDocumentManager`. - func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws - - /// Sent to close a document that was opened by `openOnDiskDocument`. - /// - /// - Important: This should only be called by `OnDiskDocumentManager`. - func closeOnDiskDocument(uri: DocumentURI) async throws - - /// Re-open the given document, discarding any in-memory state and forcing an AST to be re-built after build settings - /// have been changed. This needs to be handled via a notification to ensure that no other request for this document - /// is executing at the same time. - /// - /// Only intended for `SwiftLanguageService`. - func reopenDocument(_ notification: ReopenTextDocumentNotification) async - - func changeDocument( - _ notification: DidChangeTextDocumentNotification, - preEditSnapshot: DocumentSnapshot, - postEditSnapshot: DocumentSnapshot, - edits: [SourceEdit] - ) async - func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async - func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async - - /// Called when files are changed on disk and the editor sends a `workspace/didChangeWatchedFiles` notification to - /// SourceKit-LSP. - func filesDidChange(_ events: [FileEvent]) async - - // MARK: - Build Server Integration - - /// Sent when the build server has resolved build settings, such as for the initial build settings - /// or when the settings have changed (e.g. modified build server files). This may be sent before - /// the respective `DocumentURI` has been opened. - func documentUpdatedBuildSettings(_ uri: DocumentURI) async - - /// Sent when the build server has detected that dependencies of the given files have changed - /// (e.g. header files, swiftmodule files, other compiler input files). - func documentDependenciesUpdated(_ uris: Set) async - - // MARK: - Text Document - - func completion(_ req: CompletionRequest) async throws -> CompletionList - func completionItemResolve(_ req: CompletionItemResolveRequest) async throws -> CompletionItem - func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? - func hover(_ req: HoverRequest) async throws -> HoverResponse? - func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse - func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] - - /// Retrieve the symbol graph for the given position in the given snapshot, including the USR of the symbol at the - /// given position and the doc comments of the symbol at that position. - func symbolGraph( - for snapshot: DocumentSnapshot, - at position: Position - ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) - - /// Return the symbol graph at the given location for the contents of the document as they are on-disk (opposed to the - /// in-memory modified version of the document). - func symbolGraph( - forOnDiskContentsAt location: SymbolLocation, - in workspace: Workspace, - manager: OnDiskDocumentManager - ) async throws -> String - - /// Request a generated interface of a module to display in the IDE. - /// - /// - Parameters: - /// - document: The document whose compiler arguments should be used to generate the interface. - /// - moduleName: The module to generate an index for. - /// - groupName: The module group name. - /// - symbol: The symbol USR to search for in the generated module interface. - func openGeneratedInterface( - document: DocumentURI, - moduleName: String, - groupName: String?, - symbolUSR symbol: String? - ) async throws -> GeneratedInterfaceDetails? - - /// - Note: Only called as a fallback if the definition could not be found in the index. - func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? - - func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? - func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? - func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? - func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? - func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] - func documentSemanticTokens(_ req: DocumentSemanticTokensRequest) async throws -> DocumentSemanticTokensResponse? - func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest - ) async throws -> DocumentSemanticTokensDeltaResponse? - func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest - ) async throws -> DocumentSemanticTokensResponse? - func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] - func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? - func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] - func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] - func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport - func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? - func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? - func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? - - // MARK: - Rename - - /// Entry point to perform rename. - /// - /// Rename is implemented as a two-step process: This function returns all the edits it knows need to be performed. - /// For Swift these edits are those within the current file. In addition, it can return a USR + the old name of the - /// symbol to be renamed so that `SourceKitLSPServer` can perform an index lookup to discover more locations to rename - /// within the entire workspace. `SourceKitLSPServer` will transform those into edits by calling - /// `editsToRename(locations:in:oldName:newName:)` on the toolchain server to perform the actual rename. - func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) - - /// Given a list of `locations``, return the list of edits that need to be performed to rename these occurrences from - /// `oldName` to `newName`. - func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName: CrossLanguageName, - newName: CrossLanguageName - ) async throws -> [TextEdit] - - /// Return compound decl name that will be used as a placeholder for a rename request at a specific position. - func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? - - func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? - - /// If there is a function-like definition at the given `renamePosition`, rename all the references to parameters - /// inside the function's body. - /// - /// For example, this produces the edit to rename the occurrence of `x` inside `print` in the following - /// - /// ```swift - /// func foo(x: Int) { print(x) } - /// ``` - /// - /// - Parameters: - /// - snapshot: A `DocumentSnapshot` containing the file contents - /// - renameLocation: The position of the function's base name (in front of `foo` in the above example) - /// - newName: The new name of the function (eg. `bar(y:)` in the above example) - func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] - - // MARK: - Other - - func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? - - func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse - - /// Perform a syntactic scan of the file at the given URI for test cases and test classes. - /// - /// This is used as a fallback to show the test cases in a file if the index for a given file is not up-to-date. - /// - /// A return value of `nil` indicates that this language service does not support syntactic test discovery. - func syntacticDocumentTests(for uri: DocumentURI, in workspace: Workspace) async throws -> [AnnotatedTestItem]? - - /// Syntactically scans the file at the given URL for tests declared within it. - /// - /// Does not write the results to the index. - /// - /// The order of the returned tests is not defined. The results should be sorted before being returned to the editor. - static func syntacticTestItems(in uri: DocumentURI) async -> [AnnotatedTestItem] - - /// A position that is canonical for all positions within a declaration. For example, if we have the following - /// declaration, then all `|` markers should return the same canonical position. - /// ``` - /// func |fo|o(|ba|r: Int) - /// ``` - /// The actual position returned by the method does not matter. All that's relevant is the canonicalization. - /// - /// Returns `nil` if no canonical position could be determined. - func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? - - /// Crash the language server. Should be used for crash recovery testing only. - func crash() async -} - -/// Default implementations for methods that satisfy the following criteria: -/// - `SourceKitLSPServer` does not expect side effects to happen when they are called -/// - The method can throw or there is a reasonable default value -/// - It is reasonable to expect that not all language services need to implement it -package extension LanguageService { - static var builtInCommands: [String] { [] } - - static var experimentalCapabilities: [String: LSPAny] { [:] } - - func clientInitialized(_ initialized: InitializedNotification) async {} - - func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws { - throw ResponseError.unknown("\(#function) not implemented in \(Self.self) for \(snapshot.uri)") - } - - func closeOnDiskDocument(uri: DocumentURI) async throws { - throw ResponseError.unknown("\(#function) not implemented in \(Self.self) for \(uri)") - } - - func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async {} - - func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async {} - - func filesDidChange(_ events: [FileEvent]) async {} - - func documentUpdatedBuildSettings(_ uri: DocumentURI) async {} - - func documentDependenciesUpdated(_ uris: Set) async {} - - func completion(_ req: CompletionRequest) async throws -> CompletionList { - throw ResponseError.requestNotImplemented(CompletionRequest.self) - } - - func completionItemResolve(_ req: CompletionItemResolveRequest) async throws -> CompletionItem { - throw ResponseError.requestNotImplemented(CompletionItemResolveRequest.self) - } - - func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? { - throw ResponseError.requestNotImplemented(SignatureHelpRequest.self) - } - - func hover(_ req: HoverRequest) async throws -> HoverResponse? { - throw ResponseError.requestNotImplemented(HoverRequest.self) - } - - func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse { - throw ResponseError.requestNotImplemented(DoccDocumentationRequest.self) - } - - func symbolInfo(_ request: SymbolInfoRequest) async throws -> [SymbolDetails] { - throw ResponseError.requestNotImplemented(SymbolInfoRequest.self) - } - - func symbolGraph( - for snapshot: DocumentSnapshot, - at position: Position - ) async throws -> (symbolGraph: String, usr: String, overrideDocComments: [String]) { - throw ResponseError.internalError("\(#function) not implemented in \(Self.self) for \(snapshot.uri)") - } - - func symbolGraph( - forOnDiskContentsAt location: SymbolLocation, - in workspace: Workspace, - manager: OnDiskDocumentManager - ) async throws -> String { - throw ResponseError.internalError("\(#function) not implemented in \(Self.self) for \(location.path)") - } - - func openGeneratedInterface( - document: DocumentURI, - moduleName: String, - groupName: String?, - symbolUSR symbol: String? - ) async throws -> GeneratedInterfaceDetails? { - throw ResponseError.internalError("Generated interface not implemented in \(Self.self) for \(document)") - } - - func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { - throw ResponseError.requestNotImplemented(DefinitionRequest.self) - } - - func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { - throw ResponseError.requestNotImplemented(DeclarationRequest.self) - } - - func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { - throw ResponseError.requestNotImplemented(DocumentHighlightRequest.self) - } - - func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? { - throw ResponseError.requestNotImplemented(FoldingRangeRequest.self) - } - - func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? { - throw ResponseError.requestNotImplemented(DocumentSymbolRequest.self) - } - - func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] { - throw ResponseError.requestNotImplemented(DocumentColorRequest.self) - } - - func documentSemanticTokens(_ req: DocumentSemanticTokensRequest) async throws -> DocumentSemanticTokensResponse? { - throw ResponseError.requestNotImplemented(DocumentSemanticTokensRequest.self) - } - - func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest - ) async throws -> DocumentSemanticTokensDeltaResponse? { - throw ResponseError.requestNotImplemented(DocumentSemanticTokensDeltaRequest.self) - } - - func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest - ) async throws -> DocumentSemanticTokensResponse? { - throw ResponseError.requestNotImplemented(DocumentSemanticTokensRangeRequest.self) - } - - func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] { - throw ResponseError.requestNotImplemented(ColorPresentationRequest.self) - } - - func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { - throw ResponseError.requestNotImplemented(CodeActionRequest.self) - } - - func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { - throw ResponseError.requestNotImplemented(InlayHintRequest.self) - } - - func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] { - throw ResponseError.requestNotImplemented(CodeLensRequest.self) - } - - func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport { - throw ResponseError.requestNotImplemented(DocumentDiagnosticsRequest.self) - } - - func documentFormatting(_ req: DocumentFormattingRequest) async throws -> [TextEdit]? { - throw ResponseError.requestNotImplemented(DocumentFormattingRequest.self) - } - - func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? { - throw ResponseError.requestNotImplemented(DocumentRangeFormattingRequest.self) - } - - func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { - throw ResponseError.requestNotImplemented(DocumentOnTypeFormattingRequest.self) - } - - func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { - throw ResponseError.requestNotImplemented(RenameRequest.self) - } - - func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName: CrossLanguageName, - newName: CrossLanguageName - ) async throws -> [TextEdit] { - throw ResponseError.internalError("\(#function) not implemented in \(Self.self) for \(snapshot.uri)") - } - - func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { - throw ResponseError.requestNotImplemented(PrepareRenameRequest.self) - } - - func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { - throw ResponseError.requestNotImplemented(IndexedRenameRequest.self) - } - - func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] { - logger.error("\(#function) not implemented in \(Self.self) for \(snapshot.uri)") - return [] - } - - func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { - throw ResponseError.requestNotImplemented(ExecuteCommandRequest.self) - } - - func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { - throw ResponseError.requestNotImplemented(GetReferenceDocumentRequest.self) - } - - func syntacticDocumentTests(for uri: DocumentURI, in workspace: Workspace) async throws -> [AnnotatedTestItem]? { - throw ResponseError.internalError("syntacticDocumentTests not implemented in \(Self.self) for \(uri)") - } - - func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? { - logger.error("\(#function) not implemented in \(Self.self) for \(uri)") - return nil - } - - func crash() async { - logger.error("\(Self.self) cannot be crashed") - } -} diff --git a/Sources/SourceKitLSP/LanguageServiceRegistry.swift b/Sources/SourceKitLSP/LanguageServiceRegistry.swift deleted file mode 100644 index b591d3ae9..000000000 --- a/Sources/SourceKitLSP/LanguageServiceRegistry.swift +++ /dev/null @@ -1,68 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import IndexStoreDB -import LanguageServerProtocol -import SKLogging - -/// Wrapper around `LanguageService.Type`, making it conform to `Hashable`. -struct LanguageServiceType: Hashable { - let type: LanguageService.Type - - init(_ type: any LanguageService.Type) { - self.type = type - } - - static func == (lhs: LanguageServiceType, rhs: LanguageServiceType) -> Bool { - return ObjectIdentifier(lhs.type) == ObjectIdentifier(rhs.type) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(type)) - } -} - -/// Registry in which conformers to `LanguageService` can be registered to server semantic functionality for a set of -/// languages. -package struct LanguageServiceRegistry { - private var byLanguage: [Language: [LanguageServiceType]] = [:] - - package init() {} - - package mutating func register(_ languageService: LanguageService.Type, for languages: [Language]) { - for language in languages { - let services = byLanguage[language] ?? [] - if services.contains(LanguageServiceType(languageService)) { - logger.fault("\(languageService) already registered for \(language, privacy: .public)") - continue - } - byLanguage[language, default: []].append(LanguageServiceType(languageService)) - } - } - - /// The language services that can handle a document of the given language. - /// - /// Multiple language services may be able to handle a document. Depending on the use case, callers need to combine - /// the results of the language services. - /// If it is possible to merge the results of the language service (eg. combining code actions from multiple language - /// services), that's the preferred choice. - /// Otherwise the language services occurring early in the array should be given precedence and the results of the - /// first language service that produces some should be returned. - func languageServices(for language: Language) -> [LanguageService.Type] { - return byLanguage[language]?.map(\.type) ?? [] - } - - /// All language services that are registered in the registry. - var languageServices: Set { - return Set(byLanguage.values.flatMap { $0 }) - } -} diff --git a/Sources/SourceKitLSP/LogMessageNotification+representingStructureUsingEmojiPrefixIfNecessary.swift b/Sources/SourceKitLSP/LogMessageNotification+representingStructureUsingEmojiPrefixIfNecessary.swift deleted file mode 100644 index 0fa20b780..000000000 --- a/Sources/SourceKitLSP/LogMessageNotification+representingStructureUsingEmojiPrefixIfNecessary.swift +++ /dev/null @@ -1,73 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKOptions - -import struct TSCBasic.ProcessResult - -private func numberToEmojis(_ number: Int, numEmojis: Int) -> String { - let emojis = ["🟥", "🟩", "🟦", "⬜️", "🟪", "⬛️", "🟨", "🟫"] - var number = abs(number) - var result = "" - for _ in 0.. String { - var message: Substring = message[...] - while message.last?.isNewline ?? false { - message = message.dropLast(1) - } - let messageWithEmojiLinePrefixes = message.split(separator: "\n", omittingEmptySubsequences: false).map { - "\(taskID.emojiRepresentation) \($0)" - }.joined(separator: "\n") - return messageWithEmojiLinePrefixes -} - -extension LogMessageNotification { - /// If the client does not support the experimental structured logs capability and the `LogMessageNotification` - /// contains structure, clear it and instead prefix the message with a an emoji sequence that indicates the task it - /// belongs to. - func representingTaskIDUsingEmojiPrefixIfNecessary( - options: SourceKitLSPOptions - ) -> LogMessageNotification { - if options.hasExperimentalFeature(.structuredLogs) { - // Client supports structured logs, nothing to do. - return self - } - var transformed = self - if let structure = transformed.structure { - if case .begin(let begin) = structure { - transformed.message = "\(begin.title)\n\(message)" - } - transformed.message = prefixMessageWithTaskEmoji(taskID: structure.taskID, message: transformed.message) - transformed.structure = nil - } - return transformed - } -} diff --git a/Sources/SourceKitLSP/MacroExpansionReferenceDocumentURLData.swift b/Sources/SourceKitLSP/MacroExpansionReferenceDocumentURLData.swift deleted file mode 100644 index eb114bf36..000000000 --- a/Sources/SourceKitLSP/MacroExpansionReferenceDocumentURLData.swift +++ /dev/null @@ -1,209 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import Foundation -package import LanguageServerProtocol -import RegexBuilder - -/// Represents url of macro expansion reference document as follows: -/// `sourcekit-lsp://swift-macro-expansion/LaCb-LcCd.swift?fromLine=&fromColumn=&toLine=&toColumn=&bufferName=&parent=` -/// -/// Here, -/// - `LaCb-LcCd.swift`, the `displayName`, represents where the macro will expand to or -/// replace in the source file (i.e. `macroExpansionEditRange`) -/// - `fromLine`, `fromColumn`, `toLine`, `toColumn` represents the cursor's selection range in -/// its `parent` (i.e. `parentSelectionRange`) -/// - `bufferName` denotes the buffer name of the specific macro expansion edit -/// - `parent` denoting the URI of the document from which the macro was expanded. For a first-level macro expansion, -/// this is a file URI. For nested macro expansions, this is a `sourcekit-lsp://swift-macro-expansion` URL. -package struct MacroExpansionReferenceDocumentURLData: ReferenceURLData { - package static let documentType = "swift-macro-expansion" - - /// The document from which this macro was expanded. For first-level macro expansions, this is a file URL. For - /// second-level macro expansions, this is a `sourcekit-lsp://swift-macro-expansion/` URL, third-level macro - /// expansions are a `sourcekit-lsp:` URL that themselves have a `sourcekit-lsp:` URL as their parent. - package var parent: DocumentURI - - /// The range that was selected in `parent` when the macro was expanded. - package var parentSelectionRange: Range - - /// ## Example - /// - /// User's source File: - /// URL: `file:///path/to/swift_file.swift` - /// ```swift - /// let a = 10 - /// let b = 5 - /// print(#stringify(a + b)) - /// ``` - /// - /// Generated content of reference document url: - /// URL: - /// `sourcekit-lsp://swift-macro-expansion/L3C7-L3C23.swift?fromLine=3&fromColumn=8&toLine=3&toColumn=8&bufferName=@__swift_macro_..._Stringify_.swift&parent=/path/to/swift_file.swift` - /// ```swift - /// (a + b, "a + b") - /// ``` - /// - /// Here the `bufferName` of the reference document url is `@__swift_macro_..._Stringify_.swift` - package var bufferName: String - - /// The range at which the expanded macro should be inserted. For freestanding macros, this will be the full range of - /// the macro expansion expr/decl. - /// For attached macros, this is the position at which the buffer should be inserted, which could be at a different - /// location than the macro attribute (eg. attached member macros). - package var macroExpansionEditRange: Range - - package init( - macroExpansionEditRange: Range, - parent: DocumentURI, - parentSelectionRange: Range, - bufferName: String - ) { - self.macroExpansionEditRange = macroExpansionEditRange - self.parent = parent - self.parentSelectionRange = parentSelectionRange - self.bufferName = bufferName - } - - package var displayName: String { - "L\(macroExpansionEditRange.lowerBound.line + 1)C\(macroExpansionEditRange.lowerBound.utf16index + 1)-L\(macroExpansionEditRange.upperBound.line + 1)C\(macroExpansionEditRange.upperBound.utf16index + 1).swift" - } - - package var queryItems: [URLQueryItem] { - [ - URLQueryItem(name: Parameters.fromLine, value: String(parentSelectionRange.lowerBound.line)), - URLQueryItem(name: Parameters.fromColumn, value: String(parentSelectionRange.lowerBound.utf16index)), - URLQueryItem(name: Parameters.toLine, value: String(parentSelectionRange.upperBound.line)), - URLQueryItem(name: Parameters.toColumn, value: String(parentSelectionRange.upperBound.utf16index)), - URLQueryItem(name: Parameters.bufferName, value: bufferName), - - // *Note*: Having `parent` as the last parameter will ensure that the url's parameters aren't mistaken to be its - // `parent`'s parameters in certain environments where percent encoding gets removed or added - // unnecessarily (for example: VS Code) - URLQueryItem(name: Parameters.parent, value: parent.stringValue), - ] - } - - package init(displayName: String, queryItems: [URLQueryItem]) throws { - guard let parent = queryItems.last(where: { $0.name == Parameters.parent })?.value, - let fromLine = Int(queryItems.last(where: { $0.name == Parameters.fromLine })?.value ?? ""), - let fromColumn = Int(queryItems.last(where: { $0.name == Parameters.fromColumn })?.value ?? ""), - let toLine = Int(queryItems.last(where: { $0.name == Parameters.toLine })?.value ?? ""), - let toColumn = Int(queryItems.last(where: { $0.name == Parameters.toColumn })?.value ?? ""), - let bufferName = queryItems.last(where: { $0.name == Parameters.bufferName })?.value - else { - throw ReferenceDocumentURLError(description: "Invalid queryItems for macro expansion reference document url") - } - - self.parent = try DocumentURI(string: parent) - self.parentSelectionRange = - Position(line: fromLine, utf16index: fromColumn).. { - switch try? ReferenceDocumentURL(from: parent) { - case .macroExpansion(let data): - data.primaryFileSelectionRange - case .generatedInterface, nil: - self.parentSelectionRange - } - } - - private struct Parameters { - static let parent = "parent" - static let fromLine = "fromLine" - static let fromColumn = "fromColumn" - static let toLine = "toLine" - static let toColumn = "toColumn" - static let bufferName = "bufferName" - } - - private static func parse(displayName: String) throws -> Range { - let regex = Regex { - "L" - TryCapture { - OneOrMore(.digit) - } transform: { - Int($0) - } - "C" - TryCapture { - OneOrMore(.digit) - } transform: { - Int($0) - } - "-L" - TryCapture { - OneOrMore(.digit) - } transform: { - Int($0) - } - "C" - TryCapture { - OneOrMore(.digit) - } transform: { - Int($0) - } - ".swift" - } - - guard let match = try? regex.wholeMatch(in: displayName) else { - throw ReferenceDocumentURLError( - description: "Wrong format of display name of macro expansion reference document: '\(displayName)'" - ) - } - - return Position( - line: match.1 - 1, - utf16index: match.2 - 1 - ).. Bool { - switch (self, other) { - // globalConfigurationChange - case (.globalConfigurationChange, _): return true - case (_, .globalConfigurationChange): return true - - // workspaceRequest - case (.workspaceRequest, .workspaceRequest): return false - case (.documentUpdate, .workspaceRequest): return true - case (.workspaceRequest, .documentUpdate): return true - case (.workspaceRequest, .documentRequest): return false - case (.documentRequest, .workspaceRequest): return false - - // documentUpdate - case (.documentUpdate(let selfUri), .documentUpdate(let otherUri)): - return selfUri == otherUri - case (.documentUpdate(let selfUri), .documentRequest(let otherUri)): - return selfUri.buildSettingsFile == otherUri.buildSettingsFile - case (.documentRequest(let selfUri), .documentUpdate(let otherUri)): - return selfUri.buildSettingsFile == otherUri.buildSettingsFile - - // documentRequest - case (.documentRequest, .documentRequest): - return false - - // freestanding - case (.freestanding, _): - return false - case (_, .freestanding): - return false - } - } - - package init(_ notification: some NotificationType) { - switch notification { - case is CancelRequestNotification: - self = .freestanding - case is CancelWorkDoneProgressNotification: - self = .freestanding - case is DidChangeActiveDocumentNotification: - // The notification doesn't change behavior in an observable way, so we can treat it as freestanding. - self = .freestanding - case is DidChangeConfigurationNotification: - self = .globalConfigurationChange - case let notification as DidChangeNotebookDocumentNotification: - self = .documentUpdate(notification.notebookDocument.uri) - case let notification as DidChangeTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is DidChangeWatchedFilesNotification: - // Technically, the watched files notification can change the response of any other request (eg. because a target - // needs to be re-prepared). But treating it as a `globalConfiguration` inserts a lot of barriers in request - // handling and significantly prevents parallelism. Since many editors batch file change notifications already, - // they might have delayed the file change notification even more, which is equivalent to handling the - // notification a little later inside SourceKit-LSP. Thus, treating it as `freestanding` should be acceptable. - self = .freestanding - case is DidChangeWorkspaceFoldersNotification: - self = .globalConfigurationChange - case let notification as DidCloseNotebookDocumentNotification: - self = .documentUpdate(notification.notebookDocument.uri) - case let notification as DidCloseTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is DidCreateFilesNotification: - self = .freestanding - case is DidDeleteFilesNotification: - self = .freestanding - case let notification as DidOpenNotebookDocumentNotification: - self = .documentUpdate(notification.notebookDocument.uri) - case let notification as DidOpenTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is DidRenameFilesNotification: - self = .freestanding - case let notification as DidSaveNotebookDocumentNotification: - self = .documentUpdate(notification.notebookDocument.uri) - case let notification as DidSaveTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is ExitNotification: - self = .globalConfigurationChange - case is InitializedNotification: - self = .globalConfigurationChange - case is LogMessageNotification: - self = .freestanding - case is LogTraceNotification: - self = .freestanding - case is PublishDiagnosticsNotification: - self = .freestanding - case let notification as ReopenTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is SetTraceNotification: - // `$/setTrace` changes a global configuration setting but it doesn't affect the result of any other request. To - // avoid blocking other requests on a `$/setTrace` notification the client might send during launch, we treat it - // as a freestanding message. - // Also, we don't do anything with this notification at the moment, so it doesn't matter. - self = .freestanding - case is ShowMessageNotification: - self = .freestanding - case let notification as WillSaveTextDocumentNotification: - self = .documentUpdate(notification.textDocument.uri) - case is WorkDoneProgress: - self = .freestanding - default: - logger.error( - """ - Unknown notification \(type(of: notification)). Treating as a freestanding notification. \ - This might lead to out-of-order request handling - """ - ) - self = .freestanding - } - } - - package init(_ request: some RequestType) { - switch request { - case is ApplyEditRequest: - self = .freestanding - case is CallHierarchyIncomingCallsRequest: - self = .freestanding - case is CallHierarchyOutgoingCallsRequest: - self = .freestanding - case is CodeActionResolveRequest: - self = .freestanding - case is CodeLensRefreshRequest: - self = .freestanding - case is CodeLensResolveRequest: - self = .freestanding - case is CompletionItemResolveRequest: - self = .freestanding - case is CreateWorkDoneProgressRequest: - self = .freestanding - case is DiagnosticsRefreshRequest: - self = .freestanding - case is DocumentLinkResolveRequest: - self = .freestanding - case let request as ExecuteCommandRequest: - if let uri = request.textDocument?.uri { - self = .documentRequest(uri) - } else { - self = .freestanding - } - case let request as GetReferenceDocumentRequest: - self = .documentRequest(request.uri) - case is InitializeRequest: - self = .globalConfigurationChange - case is InlayHintRefreshRequest: - self = .freestanding - case is InlayHintResolveRequest: - self = .freestanding - case is InlineValueRefreshRequest: - self = .freestanding - case is IsIndexingRequest: - self = .freestanding - case is OutputPathsRequest: - self = .freestanding - case is RenameRequest: - // Rename might touch multiple files. Make it a global configuration change so that edits to all files that might - // be affected have been processed. - self = .globalConfigurationChange - case is RegisterCapabilityRequest: - self = .globalConfigurationChange - case is SetOptionsRequest: - // The request does not modify any global state in an observable way, so we can treat it as a freestanding - // request. - self = .freestanding - case is ShowMessageRequest: - self = .freestanding - case is ShutdownRequest: - self = .globalConfigurationChange - case is SourceKitOptionsRequest: - self = .freestanding - case is SynchronizeRequest: - self = .globalConfigurationChange - case is TriggerReindexRequest: - self = .globalConfigurationChange - case is TypeHierarchySubtypesRequest: - self = .freestanding - case is TypeHierarchySupertypesRequest: - self = .freestanding - case is UnregisterCapabilityRequest: - self = .globalConfigurationChange - case is WillCreateFilesRequest: - self = .freestanding - case is WillDeleteFilesRequest: - self = .freestanding - case is WillRenameFilesRequest: - self = .freestanding - case is WorkspaceDiagnosticsRequest: - self = .freestanding - case is WorkspaceFoldersRequest: - self = .freestanding - case is WorkspaceSemanticTokensRefreshRequest: - self = .freestanding - case is WorkspaceSymbolResolveRequest: - self = .freestanding - case is WorkspaceSymbolsRequest: - self = .freestanding - case is WorkspaceTestsRequest: - self = .workspaceRequest - case let request as any TextDocumentRequest: - self = .documentRequest(request.textDocument.uri) - default: - logger.error( - """ - Unknown request \(type(of: request)). Treating as a freestanding request. \ - This might lead to out-of-order request handling - """ - ) - self = .freestanding - } - } -} diff --git a/Sources/SourceKitLSP/OnDiskDocumentManager.swift b/Sources/SourceKitLSP/OnDiskDocumentManager.swift deleted file mode 100644 index f66610e05..000000000 --- a/Sources/SourceKitLSP/OnDiskDocumentManager.swift +++ /dev/null @@ -1,95 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -import Foundation -package import LanguageServerProtocol -import SKLogging -import SKUtilities -import SwiftExtensions - -package actor OnDiskDocumentManager { - private let sourceKitLSPServer: SourceKitLSPServer - private var openSnapshots: - [DocumentURI: (snapshot: DocumentSnapshot, buildSettings: FileBuildSettings, workspace: Workspace)] - - fileprivate init(sourceKitLSPServer: SourceKitLSPServer) { - self.sourceKitLSPServer = sourceKitLSPServer - openSnapshots = [:] - } - - /// Opens a dummy ``DocumentSnapshot`` with contents from disk for a given ``DocumentURI`` and ``Language``. - /// - /// The snapshot will remain cached until ``closeAllDocuments()`` is called. - package func open( - uri: DocumentURI, - language: Language, - in workspace: Workspace - ) async throws -> (snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) { - guard let fileURL = uri.fileURL else { - throw ResponseError.unknown("Cannot create snapshot with on-disk contents for non-file URI \(uri.forLogging)") - } - - if let cachedSnapshot = openSnapshots[uri] { - return (cachedSnapshot.snapshot, cachedSnapshot.buildSettings) - } - - let snapshot = DocumentSnapshot( - uri: try DocumentURI(filePath: "\(UUID().uuidString)/\(fileURL.filePath)", isDirectory: false), - language: language, - version: 0, - lineTable: LineTable(try String(contentsOf: fileURL, encoding: .utf8)) - ) - let languageService = try await sourceKitLSPServer.primaryLanguageService(for: uri, language, in: workspace) - - let originalBuildSettings = await workspace.buildServerManager.buildSettingsInferredFromMainFile( - for: uri, - language: language, - fallbackAfterTimeout: false - ) - guard let originalBuildSettings else { - throw ResponseError.unknown("Failed to infer build settings for \(uri)") - } - let patchedBuildSettings = originalBuildSettings.patching(newFile: snapshot.uri, originalFile: uri) - try await languageService.openOnDiskDocument(snapshot: snapshot, buildSettings: patchedBuildSettings) - openSnapshots[uri] = (snapshot, patchedBuildSettings, workspace) - return (snapshot, patchedBuildSettings) - } - - /// Close all of the ``DocumentSnapshot``s that were opened by this ``OnDiskDocumentManager``. - fileprivate func closeAllDocuments() async { - for (snapshot, _, workspace) in openSnapshots.values { - await orLog("Closing snapshot from on-disk contents: \(snapshot.uri.forLogging)") { - let languageService = - try await sourceKitLSPServer.primaryLanguageService(for: snapshot.uri, snapshot.language, in: workspace) - try await languageService.closeOnDiskDocument(uri: snapshot.uri) - } - } - openSnapshots = [:] - } -} - -package extension SourceKitLSPServer { - nonisolated func withOnDiskDocumentManager( - _ body: (OnDiskDocumentManager) async throws -> T - ) async rethrows -> T { - let manager = OnDiskDocumentManager(sourceKitLSPServer: self) - do { - let result = try await body(manager) - await manager.closeAllDocuments() - return result - } catch { - await manager.closeAllDocuments() - throw error - } - } -} diff --git a/Sources/SourceKitLSP/ReferenceDocumentURL.swift b/Sources/SourceKitLSP/ReferenceDocumentURL.swift deleted file mode 100644 index 11ed7adff..000000000 --- a/Sources/SourceKitLSP/ReferenceDocumentURL.swift +++ /dev/null @@ -1,173 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol - -protocol ReferenceURLData { - static var documentType: String { get } - var displayName: String { get } - var queryItems: [URLQueryItem] { get } -} - -/// A Reference Document is a document whose url scheme is `sourcekit-lsp:` and whose content can only be retrieved -/// using `GetReferenceDocumentRequest`. The enum represents a specific type of reference document and its -/// associated value represents the data necessary to generate the document's contents and its url -/// -/// The `url` will be of the form: `sourcekit-lsp:///?` -/// Here, -/// - The `` denotes the kind of the content present in the reference document -/// - The `` denotes the parameter-value pairs such as "p1=v1&p2=v2&..." needed to generate -/// the content of the reference document. -/// - The `` is the displayed file name of the reference document. It doesn't involve in generating -/// the content of the reference document. -package enum ReferenceDocumentURL { - package static let scheme = "sourcekit-lsp" - - case macroExpansion(MacroExpansionReferenceDocumentURLData) - case generatedInterface(GeneratedInterfaceDocumentURLData) - - var url: URL { - get throws { - let data: ReferenceURLData = - switch self { - case .macroExpansion(let data): data - case .generatedInterface(let data): data - } - - var components = URLComponents() - components.scheme = Self.scheme - components.host = type(of: data).documentType - components.path = "/\(data.displayName)" - components.queryItems = data.queryItems - - guard let url = components.url else { - throw ReferenceDocumentURLError(description: "Unable to create URL for reference document") - } - - return url - } - } - - package var uri: DocumentURI { - get throws { - DocumentURI(try url) - } - } - - package init(from uri: DocumentURI) throws { - try self.init(from: uri.arbitrarySchemeURL) - } - - init(from url: URL) throws { - guard url.scheme == Self.scheme else { - throw ReferenceDocumentURLError(description: "Invalid Scheme for reference document") - } - - let documentType = url.host - - switch documentType { - case MacroExpansionReferenceDocumentURLData.documentType: - guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems else { - throw ReferenceDocumentURLError( - description: "No queryItems passed for macro expansion reference document: \(url)" - ) - } - - let macroExpansionURLData = try MacroExpansionReferenceDocumentURLData( - displayName: url.lastPathComponent, - queryItems: queryItems - ) - self = .macroExpansion(macroExpansionURLData) - case GeneratedInterfaceDocumentURLData.documentType: - guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems else { - throw ReferenceDocumentURLError( - description: "No queryItems passed for generated interface reference document: \(url)" - ) - } - - let macroExpansionURLData = try GeneratedInterfaceDocumentURLData(queryItems: queryItems) - self = .generatedInterface(macroExpansionURLData) - case nil: - throw ReferenceDocumentURLError( - description: "Bad URL for reference document: \(url)" - ) - case let documentType?: - throw ReferenceDocumentURLError( - description: "Invalid document type in URL for reference document: \(documentType)" - ) - } - } - - /// The path that should be passed as `keys.sourcefile` to sourcekitd in conjunction with a `keys.primaryFile`. - /// - /// For macro expansions, this is the buffer name that the URI references. - var sourcekitdSourceFile: String { - switch self { - case .macroExpansion(let data): return data.bufferName - case .generatedInterface(let data): return data.sourcekitdDocumentName - } - } - - /// The file that should be used to retrieve build settings for this reference document. - var buildSettingsFile: DocumentURI { - switch self { - case .macroExpansion(let data): return data.primaryFile - case .generatedInterface(let data): return data.buildSettingsFrom - } - } - - var primaryFile: DocumentURI? { - switch self { - case .macroExpansion(let data): return data.primaryFile - case .generatedInterface(let data): return data.buildSettingsFrom.primaryFile - } - } -} - -extension DocumentURI { - /// The path that should be passed as `keys.sourcefile` to sourcekitd in conjunction with a `keys.primaryFile`. - /// - /// For normal document URIs, this is the pseudo path of this URI. For macro expansions, this is the buffer name - /// that the URI references. - package var sourcekitdSourceFile: String { - if let referenceDocument = try? ReferenceDocumentURL(from: self) { - referenceDocument.sourcekitdSourceFile - } else { - self.pseudoPath - } - } - - /// If this is a URI to a reference document, the URI of the source file from which this reference document was - /// derived. - /// - /// The primary file is used to determine the workspace and language service that is used to generate the reference - /// document as well as getting the reference document's build settings. - package var primaryFile: DocumentURI? { - if let referenceDocument = try? ReferenceDocumentURL(from: self) { - return referenceDocument.primaryFile - } - return nil - } - - /// The file that should be used to retrieve build settings for this reference document. - package var buildSettingsFile: DocumentURI { - if let referenceDocument = try? ReferenceDocumentURL(from: self) { - return referenceDocument.buildSettingsFile - } - return self - } -} - -package struct ReferenceDocumentURLError: Error, CustomStringConvertible { - package var description: String -} diff --git a/Sources/SourceKitLSP/Rename.swift b/Sources/SourceKitLSP/Rename.swift deleted file mode 100644 index b61506d77..000000000 --- a/Sources/SourceKitLSP/Rename.swift +++ /dev/null @@ -1,470 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import IndexStoreDB -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SemanticIndex -import SwiftExtensions - -// MARK: - Helper types - -private extension RenameLocation.Usage { - init(roles: SymbolRole) { - if roles.contains(.definition) || roles.contains(.declaration) { - self = .definition - } else if roles.contains(.call) { - self = .call - } else { - self = .reference - } - } -} - -private extension IndexSymbolKind { - var isMethod: Bool { - switch self { - case .instanceMethod, .classMethod, .staticMethod: - return true - default: return false - } - } -} - -// MARK: - Name translation - -/// A name that has a representation both in Swift and clang-based languages. -/// -/// These names might differ. For example, an Objective-C method gets translated by the clang importer to form the Swift -/// name or it could have a `SWIFT_NAME` attribute that defines the method's name in Swift. Similarly, a Swift symbol -/// might specify the name by which it gets exposed to Objective-C using the `@objc` attribute. -package struct CrossLanguageName: Sendable { - package init(clangName: String? = nil, swiftName: String? = nil, definitionLanguage: Language) { - self.clangName = clangName - self.swiftName = swiftName - self.definitionLanguage = definitionLanguage - } - - /// The name of the symbol in clang languages or `nil` if the symbol is defined in Swift, doesn't have any references - /// from clang languages and thus hasn't been translated. - package let clangName: String? - - /// The name of the symbol in Swift or `nil` if the symbol is defined in clang, doesn't have any references from - /// Swift and thus hasn't been translated. - package let swiftName: String? - - /// the language that the symbol is defined in. - package let definitionLanguage: Language - - /// The name of the symbol in the language that it is defined in. - package var definitionName: String? { - switch definitionLanguage { - case .c, .cpp, .objective_c, .objective_cpp: - return clangName - case .swift: - return swiftName - default: - return nil - } - } -} - -package protocol NameTranslatorService: Sendable { - func translateClangNameToSwift( - at symbolLocation: SymbolLocation, - in snapshot: DocumentSnapshot, - isObjectiveCSelector: Bool, - name: String - ) async throws -> String - - func translateSwiftNameToClang( - at symbolLocation: SymbolLocation, - in uri: DocumentURI, - name: String - ) async throws -> String -} - -// MARK: - SourceKitLSPServer - -/// The kinds of symbol occurrence roles that should be renamed. -private let renameRoles: SymbolRole = [.declaration, .definition, .reference] - -extension SourceKitLSPServer { - /// Returns a `DocumentSnapshot`, a position and the corresponding language service that references - /// `usr` from a Swift file. If `usr` is not referenced from Swift, returns `nil`. - private func getReferenceFromSwift( - usr: String, - index: CheckedIndex, - workspace: Workspace - ) async -> (swiftLanguageService: NameTranslatorService, snapshot: DocumentSnapshot, location: SymbolLocation)? { - var reference: SymbolOccurrence? = nil - index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) { - if $0.symbolProvider == .swift { - reference = $0 - // We have found a reference from Swift. Stop iteration. - return false - } - return true - } - - guard let reference else { - return nil - } - let uri = reference.location.documentUri - guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: .swift) else { - return nil - } - let swiftLanguageService = await orLog("Getting NameTranslatorService") { - try await self.primaryLanguageService(for: uri, .swift, in: workspace) as? NameTranslatorService - } - guard let swiftLanguageService else { - return nil - } - return (swiftLanguageService, snapshot, reference.location) - } - - /// Returns a `CrossLanguageName` for the symbol with the given USR. - /// - /// If the symbol is used across clang/Swift languages, the cross-language name will have both a `swiftName` and a - /// `clangName` set. Otherwise it only has the name of the language it's defined in set. - /// - /// If `overrideName` is passed, the name of the symbol will be assumed to be `overrideName` in its native language. - /// This is used to create a `CrossLanguageName` for the new name of a renamed symbol. - private func getCrossLanguageName( - forUsr usr: String, - overrideName: String? = nil, - workspace: Workspace, - index: CheckedIndex - ) async throws -> CrossLanguageName? { - let definitions = index.occurrences(ofUSR: usr, roles: [.definition]) - if definitions.isEmpty { - logger.error("No definitions for \(usr) found") - return nil - } - if definitions.count > 1 { - logger.log("Multiple definitions for \(usr) found") - } - // There might be multiple definitions of the same symbol eg. in different `#if` branches. In this case pick any of - // them because with very high likelihood they all translate to the same clang and Swift name. Sort the entries to - // ensure that we deterministically pick the same entry every time. - for definitionOccurrence in definitions.sorted() { - do { - return try await getCrossLanguageName( - forDefinitionOccurrence: definitionOccurrence, - overrideName: overrideName, - workspace: workspace, - index: index - ) - } catch { - // If getting the cross-language name fails for this occurrence, try the next definition, if there are multiple. - logger.log( - "Getting cross-language name for occurrence at \(definitionOccurrence.location) failed. \(error.forLogging)" - ) - } - } - return nil - } - - private func getCrossLanguageName( - forDefinitionOccurrence definitionOccurrence: SymbolOccurrence, - overrideName: String? = nil, - workspace: Workspace, - index: CheckedIndex - ) async throws -> CrossLanguageName { - let definitionSymbol = definitionOccurrence.symbol - let usr = definitionSymbol.usr - let definitionLanguage: Language = - switch definitionSymbol.language { - case .c: .c - case .cxx: .cpp - case .objc: .objective_c - case .swift: .swift - } - let definitionDocumentUri = definitionOccurrence.location.documentUri - - let definitionName = overrideName ?? definitionSymbol.name - - switch definitionLanguage.semanticKind { - case .clang: - let swiftName: String? - if let swiftReference = await getReferenceFromSwift(usr: usr, index: index, workspace: workspace) { - let isObjectiveCSelector = definitionLanguage == .objective_c && definitionSymbol.kind.isMethod - swiftName = try await swiftReference.swiftLanguageService.translateClangNameToSwift( - at: swiftReference.location, - in: swiftReference.snapshot, - isObjectiveCSelector: isObjectiveCSelector, - name: definitionName - ) - } else { - logger.debug("Not translating \(definitionSymbol) to Swift because it is not referenced from Swift") - swiftName = nil - } - return CrossLanguageName(clangName: definitionName, swiftName: swiftName, definitionLanguage: definitionLanguage) - case .swift: - guard - let swiftLanguageService = try await self.primaryLanguageService( - for: definitionDocumentUri, - definitionLanguage, - in: workspace - ) as? NameTranslatorService - else { - throw ResponseError.unknown("Failed to get language service for the document defining \(usr)") - } - // Continue iteration if the symbol provider is not clang. - // If we terminate early by returning `false` from the closure, `forEachSymbolOccurrence` returns `true`, - // indicating that we have found a reference from clang. - let hasReferenceFromClang = !index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) { - return $0.symbolProvider != .clang - } - let clangName: String? - if hasReferenceFromClang { - clangName = try await swiftLanguageService.translateSwiftNameToClang( - at: definitionOccurrence.location, - in: definitionDocumentUri, - name: definitionName - ) - } else { - clangName = nil - } - return CrossLanguageName(clangName: clangName, swiftName: definitionName, definitionLanguage: definitionLanguage) - default: - throw ResponseError.unknown("Cannot rename symbol because it is defined in an unknown language") - } - } - - /// Starting from the given USR, compute the transitive closure of all declarations that are overridden or override - /// the symbol, including the USR itself. - /// - /// This includes symbols that need to traverse the inheritance hierarchy up and down. For example, it includes all - /// occurrences of `foo` in the following when started from `Inherited.foo`. - /// - /// ```swift - /// class Base { func foo() {} } - /// class Inherited: Base { override func foo() {} } - /// class OtherInherited: Base { override func foo() {} } - /// ``` - private func overridingAndOverriddenUsrs(of usr: String, index: CheckedIndex) -> [String] { - var workList = [usr] - var usrs: [String] = [] - while let usr = workList.popLast() { - usrs.append(usr) - var relatedUsrs = index.occurrences(relatedToUSR: usr, roles: .overrideOf).map(\.symbol.usr) - relatedUsrs += index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap { occurrence in - occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr) - } - for overriddenUsr in relatedUsrs { - if usrs.contains(overriddenUsr) || workList.contains(overriddenUsr) { - // Already handling this USR. Nothing to do. - continue - } - workList.append(overriddenUsr) - } - } - return usrs - } - - func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? { - let uri = request.textDocument.uri - let snapshot = try documentManager.latestSnapshot(uri) - - guard let workspace = await workspaceForDocument(uri: uri) else { - throw ResponseError.workspaceNotOpen(uri) - } - let primaryFileLanguageService = try await primaryLanguageService(for: uri, snapshot.language, in: workspace) - - // Determine the local edits and the USR to rename - let renameResult = try await primaryFileLanguageService.rename(request) - - // We only check if the files exist. If a source file has been modified on disk, we will still try to perform a - // rename. Rename will check if the expected old name exists at the location in the index and, if not, ignore that - // location. This way we are still able to rename occurrences in files where eg. only one line has been modified but - // all the line:column locations of occurrences are still up-to-date. - // This should match the check level in prepareRename. - guard let usr = renameResult.usr, let index = await workspace.index(checkedFor: .deletedFiles) else { - // We don't have enough information to perform a cross-file rename. - return renameResult.edits - } - - let oldName = try await getCrossLanguageName(forUsr: usr, workspace: workspace, index: index) - let newName = try await getCrossLanguageName( - forUsr: usr, - overrideName: request.newName, - workspace: workspace, - index: index - ) - - guard let oldName, let newName else { - // We failed to get the translated name, so we can't to global rename. - // Do local rename within the current file instead as fallback. - return renameResult.edits - } - - var changes: [DocumentURI: [TextEdit]] = [:] - if oldName.definitionLanguage == snapshot.language { - // If this is not a cross-language rename, we can use the local edits returned by - // the language service's rename function. - // If this is cross-language rename, that's not possible because the user would eg. - // enter a new clang name, which needs to be translated to the Swift name before - // changing the current file. - changes = renameResult.edits.changes ?? [:] - } - - // If we have a USR + old name, perform an index lookup to find workspace-wide symbols to rename. - // First, group all occurrences of that USR by the files they occur in. - var locationsByFile: [DocumentURI: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)] = [:] - - let usrsToRename = overridingAndOverriddenUsrs(of: usr, index: index) - let occurrencesToRename = usrsToRename.flatMap { index.occurrences(ofUSR: $0, roles: renameRoles) } - for occurrence in occurrencesToRename { - let uri = occurrence.location.documentUri - - // Determine whether we should add the location produced by the index to those that will be renamed, or if it has - // already been handled by the set provided by the AST. - if changes[uri] != nil { - if occurrence.symbol.usr == usr { - // If the language server's rename function already produced AST-based locations for this symbol, no need to - // perform an indexed rename for it. - continue - } - switch occurrence.symbolProvider { - case .swift: - // sourcekitd only produces AST-based results for the direct calls to this USR. This is because the Swift - // AST only has upwards references to superclasses and overridden methods, not the other way round. It is - // thus not possible to (easily) compute an up-down closure like described in `overridingAndOverriddenUsrs`. - // We thus need to perform an indexed rename for other, related USRs. - break - case .clang: - // clangd produces AST-based results for the entire class hierarchy, so nothing to do. - continue - } - } - - let renameLocation = RenameLocation( - line: occurrence.location.line, - utf8Column: occurrence.location.utf8Column, - usage: RenameLocation.Usage(roles: occurrence.roles) - ) - if let existingLocations = locationsByFile[uri] { - if existingLocations.symbolProvider != occurrence.symbolProvider { - logger.fault( - """ - Found mismatching symbol providers for \(uri.forLogging): \ - \(String(describing: existingLocations.symbolProvider), privacy: .public) vs \ - \(String(describing: occurrence.symbolProvider), privacy: .public) - """ - ) - } - locationsByFile[uri] = (existingLocations.renameLocations + [renameLocation], occurrence.symbolProvider) - } else { - locationsByFile[uri] = ([renameLocation], occurrence.symbolProvider) - } - } - - // Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into - // edits. - let urisAndEdits = - await locationsByFile - .concurrentMap { - ( - uri: DocumentURI, - value: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind) - ) -> (DocumentURI, [TextEdit])? in - let language: Language - switch value.symbolProvider { - case .clang: - // Technically, we still don't know the language of the source file but defaulting to C is sufficient to - // ensure we get the clang toolchain language server, which is all we care about. - language = .c - case .swift: - language = .swift - } - // Create a document snapshot to operate on. If the document is open, load it from the document manager, - // otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column - // conversions. - guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: language) else { - logger.error("Failed to get document snapshot for \(uri.forLogging)") - return nil - } - let languageService = await orLog("Getting language service to compute edits in file") { - try await self.primaryLanguageService(for: uri, language, in: workspace) - } - guard let languageService else { - return nil - } - - var edits: [TextEdit] = - await orLog("Getting edits for rename location") { - return try await languageService.editsToRename( - locations: value.renameLocations, - in: snapshot, - oldName: oldName, - newName: newName - ) - } ?? [] - for location in value.renameLocations where location.usage == .definition { - edits += await languageService.editsToRenameParametersInFunctionBody( - snapshot: snapshot, - renameLocation: location, - newName: newName - ) - } - edits = edits.filter { !$0.isNoOp(in: snapshot) } - return (uri, edits) - }.compactMap { $0 } - for (uri, editsForUri) in urisAndEdits { - if !editsForUri.isEmpty { - changes[uri, default: []] += editsForUri - } - } - var edits = renameResult.edits - edits.changes = changes - return edits - } - - func prepareRename( - _ request: PrepareRenameRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> PrepareRenameResponse? { - guard let languageServicePrepareRename = try await languageService.prepareRename(request) else { - return nil - } - var prepareRenameResult = languageServicePrepareRename.prepareRename - - guard - let index = await workspace.index(checkedFor: .deletedFiles), - let usr = languageServicePrepareRename.usr, - let oldName = try await self.getCrossLanguageName(forUsr: usr, workspace: workspace, index: index), - var definitionName = oldName.definitionName - else { - return prepareRenameResult - } - if oldName.definitionLanguage == .swift, definitionName.hasSuffix("()") { - definitionName = String(definitionName.dropLast(2)) - } - - // Get the name of the symbol's definition, if possible. - // This is necessary for cross-language rename. Eg. when renaming an Objective-C method from Swift, - // the user still needs to enter the new Objective-C name. - prepareRenameResult.placeholder = definitionName - return prepareRenameResult - } - - func indexedRename( - _ request: IndexedRenameRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> WorkspaceEdit? { - return try await languageService.indexedRename(request) - } -} diff --git a/Sources/SourceKitLSP/SemanticTokensLegend+SourceKitLSPLegend.swift b/Sources/SourceKitLSP/SemanticTokensLegend+SourceKitLSPLegend.swift deleted file mode 100644 index 21dc91c0c..000000000 --- a/Sources/SourceKitLSP/SemanticTokensLegend+SourceKitLSPLegend.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol - -extension SemanticTokenTypes { - // LSP doesn’t know about actors. Display actors as classes. - package static var actor: Self { Self.class } - - /// Token types are looked up by index - package var tokenType: UInt32 { - UInt32(Self.all.firstIndex(of: self)!) - } -} - -extension SemanticTokensLegend { - /// The semantic tokens legend that is used between SourceKit-LSP and the editor. - package static let sourceKitLSPLegend = SemanticTokensLegend( - tokenTypes: SemanticTokenTypes.all.map(\.name), - tokenModifiers: SemanticTokenModifiers.all.compactMap(\.name) - ) -} diff --git a/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift b/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift deleted file mode 100644 index 7982badae..000000000 --- a/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift +++ /dev/null @@ -1,108 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SKOptions -import SwiftExtensions - -extension WorkDoneProgressManager { - init?( - server: SourceKitLSPServer, - capabilityRegistry: CapabilityRegistry?, - tokenPrefix: String, - initialDebounce: Duration? = nil, - title: String, - message: String? = nil, - percentage: Int? = nil - ) { - guard let capabilityRegistry, capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else { - return nil - } - self.init( - connectionToClient: server.client, - waitUntilClientInitialized: { [weak server] in await server?.waitUntilInitialized() }, - tokenPrefix: tokenPrefix, - initialDebounce: initialDebounce, - title: title, - message: message, - percentage: percentage - ) - } -} - -/// A `WorkDoneProgressManager` that essentially has two states. If any operation tracked by this type is currently -/// running, it displays a work done progress in the client. If multiple operations are running at the same time, it -/// doesn't show multiple work done progress in the client. For example, we only want to show one progress indicator -/// when sourcekitd has crashed, not one per `SwiftLanguageService`. -package actor SharedWorkDoneProgressManager { - private weak var sourceKitLSPServer: SourceKitLSPServer? - - /// The number of in-progress operations. When greater than 0 `workDoneProgress` is non-nil and a work done progress - /// is displayed to the user. - private var inProgressOperations = 0 - private var workDoneProgress: WorkDoneProgressManager? - - private let tokenPrefix: String - private let title: String - private let message: String? - - init( - sourceKitLSPServer: SourceKitLSPServer, - tokenPrefix: String, - title: String, - message: String? = nil - ) { - self.sourceKitLSPServer = sourceKitLSPServer - self.tokenPrefix = tokenPrefix - self.title = title - self.message = message - } - - package func start() async { - guard let sourceKitLSPServer else { - return - } - // Do all asynchronous operations up-front so that incrementing `inProgressOperations` and setting `workDoneProgress` - // cannot be interrupted by an `await` call - let initialDebounceDuration = sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault - let capabilityRegistry = await sourceKitLSPServer.capabilityRegistry - - inProgressOperations += 1 - if let capabilityRegistry, workDoneProgress == nil { - workDoneProgress = WorkDoneProgressManager( - server: sourceKitLSPServer, - capabilityRegistry: capabilityRegistry, - tokenPrefix: tokenPrefix, - initialDebounce: initialDebounceDuration, - title: title, - message: message - ) - } - } - - package func end() async { - if inProgressOperations > 0 { - inProgressOperations -= 1 - } else { - logger.fault( - "Unbalanced calls to SharedWorkDoneProgressManager.start and end for \(self.tokenPrefix, privacy: .public)" - ) - } - if inProgressOperations == 0, let workDoneProgress { - self.workDoneProgress = nil - await workDoneProgress.end() - } - } -} diff --git a/Sources/SourceKitLSP/SourceKitIndexDelegate.swift b/Sources/SourceKitLSP/SourceKitIndexDelegate.swift deleted file mode 100644 index 227d1500a..000000000 --- a/Sources/SourceKitLSP/SourceKitIndexDelegate.swift +++ /dev/null @@ -1,60 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Dispatch -import IndexStoreDB -import LanguageServerProtocolExtensions -import SKLogging -import SwiftExtensions - -/// `IndexDelegate` for the SourceKit workspace. -actor SourceKitIndexDelegate: IndexDelegate { - /// Registered `MainFilesDelegate`s to notify when main files change. - var mainFilesChangedCallback: @Sendable () async -> Void - - /// The count of pending unit events. Whenever this transitions to 0, it represents a time where - /// the index finished processing known events. Of course, that may have already changed by the - /// time we are notified. - let pendingUnitCount = AtomicInt32(initialValue: 0) - - package init(mainFilesChangedCallback: @escaping @Sendable () async -> Void) { - self.mainFilesChangedCallback = mainFilesChangedCallback - } - - nonisolated package func processingAddedPending(_ count: Int) { - pendingUnitCount.value += Int32(count) - } - - nonisolated package func processingCompleted(_ count: Int) { - pendingUnitCount.value -= Int32(count) - if pendingUnitCount.value == 0 { - Task { - await indexChanged() - } - } - - if pendingUnitCount.value < 0 { - // Technically this is not data race safe because `pendingUnitCount` might change between the check and us setting - // it to 0. But then, this should never happen anyway, so it's fine. - logger.fault("pendingUnitCount dropped below zero: \(self.pendingUnitCount.value)") - pendingUnitCount.value = 0 - Task { - await indexChanged() - } - } - } - - private func indexChanged() async { - logger.debug("IndexStoreDB changed") - await mainFilesChangedCallback() - } -} diff --git a/Sources/SourceKitLSP/SourceKitLSPCommandMetadata.swift b/Sources/SourceKitLSP/SourceKitLSPCommandMetadata.swift deleted file mode 100644 index 26fdcc835..000000000 --- a/Sources/SourceKitLSP/SourceKitLSPCommandMetadata.swift +++ /dev/null @@ -1,92 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging - -/// Represents metadata that SourceKit-LSP injects at every command returned by code actions. -/// The ExecuteCommand is not a TextDocumentRequest, so metadata is injected to allow SourceKit-LSP -/// to determine where a command should be executed. -package struct SourceKitLSPCommandMetadata: Codable, Hashable { - - package var sourcekitlsp_textDocument: TextDocumentIdentifier - - package init?(fromLSPDictionary dictionary: [String: LSPAny]) { - let textDocumentKey = CodingKeys.sourcekitlsp_textDocument.stringValue - guard case .dictionary(let textDocumentDict)? = dictionary[textDocumentKey], - let textDocument = TextDocumentIdentifier(fromLSPDictionary: textDocumentDict) - else { - return nil - } - self.init(textDocument: textDocument) - } - - package init(textDocument: TextDocumentIdentifier) { - self.sourcekitlsp_textDocument = textDocument - } - - package func encodeToLSPAny() -> LSPAny { - return .dictionary([ - CodingKeys.sourcekitlsp_textDocument.stringValue: sourcekitlsp_textDocument.encodeToLSPAny() - ]) - } -} - -extension CodeActionRequest { - package func injectMetadata(toResponse response: CodeActionRequestResponse?) -> CodeActionRequestResponse? { - let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) - let metadataArgument = metadata.encodeToLSPAny() - switch response { - case .codeActions(var codeActions)?: - for i in 0..() - - /// The queue on which we keep track of `inProgressTextDocumentRequests` to ensure updates to - /// `inProgressTextDocumentRequests` are handled in order. - package let textDocumentTrackingQueue = AsyncQueue() - - /// The queue on which all modifications of `workspaceForUri` happen. This means that the value of - /// `workspacesAndIsImplicit` and `workspaceForUri` can't change while executing a closure on `workspaceQueue`. - private let workspaceQueue = AsyncQueue() - - /// The connection to the editor. - package nonisolated let client: Connection - - /// Set to `true` after the `SourceKitLSPServer` has send the reply to the `InitializeRequest`. - /// - /// Initialization can be awaited using `waitUntilInitialized`. - private var initialized: Bool = false - - private let _options: ThreadSafeBox - nonisolated package var options: SourceKitLSPOptions { - _options.value - } - - package let hooks: Hooks - - let toolchainRegistry: ToolchainRegistry - - package var capabilityRegistry: CapabilityRegistry? - - let languageServiceRegistry: LanguageServiceRegistry - - var languageServices: [LanguageServiceType: [LanguageService]] = [:] - - package nonisolated let documentManager = DocumentManager() - - /// The `TaskScheduler` that schedules all background indexing tasks. - /// - /// Shared process-wide to ensure the scheduled index operations across multiple workspaces don't exceed the maximum - /// number of processor cores that the user allocated to background indexing. - private let indexTaskScheduler: TaskScheduler - - /// Implicitly unwrapped optional so we can create an `IndexProgressManager` that has a weak reference to - /// `SourceKitLSPServer`. - /// `nonisolated(unsafe)` because `indexProgressManager` will not be modified after it is assigned from the - /// initializer. - private(set) nonisolated(unsafe) var indexProgressManager: IndexProgressManager! - - /// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to - /// `SourceKitLSPServer`. - /// `nonisolated(unsafe)` because `sourcekitdCrashedWorkDoneProgress` will not be modified after it is assigned from - /// the initializer. - nonisolated(unsafe) package private(set) var sourcekitdCrashedWorkDoneProgress: SharedWorkDoneProgressManager! - - /// Stores which workspace the given URI has been opened in. - /// - /// - Important: Must only be modified from `workspaceQueue`. This means that the value of `workspaceForUri` - /// can't change while executing an operation on `workspaceQueue`. - private var workspaceForUri: [DocumentURI: WeakWorkspace] = [:] - - /// The open workspaces. - /// - /// Implicit workspaces are workspaces that weren't actually specified by the client during initialization or by a - /// `didChangeWorkspaceFolders` request. Instead, they were opened by sourcekit-lsp because a file could not be - /// handled by any of the open workspaces but one of the file's parent directories had handling capabilities for it. - /// - /// - Important: Must only be modified from `workspaceQueue`. This means that the value of `workspacesAndIsImplicit` - /// can't change while executing an operation on `workspaceQueue`. - private var workspacesAndIsImplicit: [(workspace: Workspace, isImplicit: Bool)] = [] { - didSet { - self.scheduleUpdateOfUriToWorkspace() - } - } - - var workspaces: [Workspace] { - return workspacesAndIsImplicit.map(\.workspace) - } - - package func setWorkspaces(_ newValue: [(workspace: Workspace, isImplicit: Bool)]) { - workspaceQueue.async { - self.workspacesAndIsImplicit = newValue - } - } - - /// For all currently handled text document requests a mapping from the document to the corresponding request ID and - /// the method of the request (ie. the value of `TextDocumentRequest.method`). - private var inProgressTextDocumentRequests: [DocumentURI: [(id: RequestID, requestMethod: String)]] = [:] - - var onExit: () -> Void - - /// The files that we asked the client to watch. - private var watchers: Set = [] - - private static func maxConcurrentIndexingTasksByPriority( - isIndexingPaused: Bool, - options: SourceKitLSPOptions - ) -> [(priority: TaskPriority, maxConcurrentTasks: Int)] { - // Use `processorCount` instead of `activeProcessorCount` here because `activeProcessorCount` may be decreased due - // to thermal throttling. We don't want to consistently limit the concurrent indexing tasks if SourceKit-LSP was - // launched during a period of thermal throttling. - let processorCount = ProcessInfo.processInfo.processorCount - let lowPriorityCores = - if isIndexingPaused { - 0 - } else { - max( - Int(options.indexOrDefault.maxCoresPercentageToUseForBackgroundIndexingOrDefault * Double(processorCount)), - 1 - ) - } - return [ - (TaskPriority.medium, processorCount), - (TaskPriority.low, lowPriorityCores), - ] - } - - /// Creates a language server for the given client. - package init( - client: Connection, - toolchainRegistry: ToolchainRegistry, - languageServerRegistry: LanguageServiceRegistry, - options: SourceKitLSPOptions, - hooks: Hooks, - onExit: @escaping () -> Void = {} - ) { - self.toolchainRegistry = toolchainRegistry - self.languageServiceRegistry = languageServerRegistry - self._options = ThreadSafeBox(initialValue: options) - self.hooks = hooks - self.onExit = onExit - - self.client = client - self.indexTaskScheduler = TaskScheduler( - maxConcurrentTasksByPriority: Self.maxConcurrentIndexingTasksByPriority(isIndexingPaused: false, options: options) - ) - self.indexProgressManager = nil - self.indexProgressManager = IndexProgressManager(sourceKitLSPServer: self) - self.sourcekitdCrashedWorkDoneProgress = SharedWorkDoneProgressManager( - sourceKitLSPServer: self, - tokenPrefix: "sourcekitd-crashed", - title: "SourceKit-LSP: Restoring functionality", - message: "Please run 'sourcekit-lsp diagnose' to file an issue" - ) - } - - /// Await until the server has send the reply to the initialize request. - package func waitUntilInitialized() async { - // The polling of `initialized` is not perfect but it should be OK, because - // - In almost all cases the server should already be initialized. - // - If it's not initialized, we expect initialization to finish fairly quickly. Even if initialization takes 5s - // this only results in 50 polls, which is acceptable. - // Alternative solutions that signal via an async sequence seem overkill here. - while !initialized { - do { - try await Task.sleep(for: .seconds(0.1)) - } catch { - break - } - } - } - - /// Search through all the parent directories of `uri` and check if any of these directories contain a workspace that - /// can be handled by with a build server. - /// - /// The search will not consider any directory that is not a child of any of the directories in `rootUris`. This - /// prevents us from picking up a workspace that is outside of the folders that the user opened. - private func findImplicitWorkspace(for uri: DocumentURI) async -> Workspace? { - guard var url = uri.fileURL?.deletingLastPathComponent() else { - return nil - } - - // Roots of opened workspaces - only consider explicit here (all implicit must necessarily be subdirectories of - // the explicit workspace roots) - let workspaceRoots = workspacesAndIsImplicit.filter { !$0.isImplicit }.compactMap { $0.workspace.rootUri?.fileURL } - - // We want to skip creating another workspace if any existing already has the same config path. This could happen if - // an existing workspace hasn't reloaded after a new file was added to it (and thus that build server needs to be - // reloaded). - let configPaths = await workspacesAndIsImplicit.asyncCompactMap { - await $0.workspace.buildServerManager.configPath - } - - while url.pathComponents.count > 1 && workspaceRoots.contains(where: { $0.isPrefix(of: url) }) { - defer { - url.deleteLastPathComponent() - } - - let uri = DocumentURI(url) - let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: uri) - - // Some build servers consider paths outside of the folder (eg. BSP has settings in the home directory). If we - // allowed those paths, then the very first folder that the file is in would always be its own build server - so - // skip them in that case. - guard - let buildServerSpec = determineBuildServer( - forWorkspaceFolder: uri, - onlyConsiderRoot: true, - options: options, - hooks: hooks.buildServerHooks - ) - else { - continue - } - if configPaths.contains(buildServerSpec.configPath) { - continue - } - - // No existing workspace matches this root - create one. - guard - let workspace = await orLog( - "Creating implicit workspace", - { try await createWorkspace(workspaceFolder: uri, options: options, buildServerSpec: buildServerSpec) } - ) - else { - continue - } - return workspace - } - return nil - } - - package func workspaceForDocument(uri: DocumentURI) async -> Workspace? { - let uri = uri.buildSettingsFile - if let cachedWorkspace = self.workspaceForUri[uri]?.value { - return cachedWorkspace - } - - return await self.workspaceQueue.async { - await self.computeWorkspaceForDocument(uri: uri) - }.valuePropagatingCancellation - } - - /// This method must be executed on `workspaceQueue` to ensure that the file handling capabilities of the - /// workspaces don't change during the computation. Otherwise, we could run into a race condition like the following: - /// 1. We don't have an entry for file `a.swift` in `workspaceForUri` and start the computation - /// 2. We find that the first workspace in `self.workspaces` can handle this file. - /// 3. During the `await ... .fileHandlingCapability` for a second workspace the file handling capabilities for the - /// first workspace change, meaning it can no longer handle the document. This resets `workspaceForUri` - /// assuming that the URI to workspace relation will get re-computed. - /// 4. But we then set `workspaceForUri[uri]` to the workspace found in step (2), caching an out-of-date result. - /// - /// Furthermore, the computation of the workspace for a URI can create a new implicit workspace, which modifies - /// `workspacesAndIsImplicit` and which must only be modified on `workspaceQueue`. - /// - /// - Important: Must only be invoked from `workspaceQueue`. - private func computeWorkspaceForDocument(uri: DocumentURI) async -> Workspace? { - // Pick the workspace with the best FileHandlingCapability for this file. - // If there is a tie, use the workspace that occurred first in the list. - var bestWorkspace = await self.workspaces.asyncFirst { - await !$0.buildServerManager.targets(for: uri).isEmpty - } - if bestWorkspace == nil { - // We weren't able to handle the document with any of the known workspaces. See if any of the document's parent - // directories contain a workspace that might be able to handle the document - if let workspace = await self.findImplicitWorkspace(for: uri) { - logger.log("Opening implicit workspace at \(workspace.rootUri.forLogging) to handle \(uri.forLogging)") - self.workspacesAndIsImplicit.append((workspace: workspace, isImplicit: true)) - bestWorkspace = workspace - } - } - let workspace = bestWorkspace ?? self.workspaces.first - self.workspaceForUri[uri] = WeakWorkspace(workspace) - return workspace - } - - /// Check that the entries in `workspaceForUri` are still up-to-date after workspaces might have changed. - /// - /// For any entries that are not up-to-date, close the document in the old workspace and open it in the new document. - /// - /// This method returns immediately and schedules the check in the background as a global configuration change. - /// Requests may still be served by their old workspace until this configuration change is executed by - /// `SourceKitLSPServer`. - private func scheduleUpdateOfUriToWorkspace() { - messageHandlingQueue.async(priority: .low, metadata: .globalConfigurationChange) { - logger.info("Updating URI to workspace") - // For each document that has moved to a different workspace, close it in - // the old workspace and open it in the new workspace. - for docUri in self.documentManager.openDocuments { - await self.workspaceQueue.async { - let oldWorkspace = self.workspaceForUri[docUri]?.value - let newWorkspace = await self.computeWorkspaceForDocument(uri: docUri) - guard newWorkspace !== oldWorkspace else { - return // Nothing to do, workspace didn't change for this document - } - guard let snapshot = try? self.documentManager.latestSnapshot(docUri) else { - return - } - if let oldWorkspace = oldWorkspace { - await self.closeDocument( - DidCloseTextDocumentNotification( - textDocument: TextDocumentIdentifier(docUri) - ), - workspace: oldWorkspace - ) - } - logger.info( - "Changing workspace of \(docUri.forLogging) from \(oldWorkspace?.rootUri?.forLogging) to \(newWorkspace?.rootUri?.forLogging)" - ) - self.workspaceForUri[docUri] = WeakWorkspace(newWorkspace) - if let newWorkspace = newWorkspace { - await self.openDocument( - DidOpenTextDocumentNotification( - textDocument: TextDocumentItem( - uri: docUri, - language: snapshot.language, - version: snapshot.version, - text: snapshot.text - ) - ), - workspace: newWorkspace - ) - } - }.valuePropagatingCancellation - } - // `indexProgressManager` iterates over all workspaces in the SourceKitLSPServer. Modifying workspaces might thus - // update the index progress status. - self.indexProgressManager.indexProgressStatusDidChange() - } - } - - /// Execute `notificationHandler` with the request as well as the workspace - /// and language that handle this document. - private func withLanguageServiceAndWorkspace( - for notification: NotificationType, - notificationHandler: @escaping (NotificationType, LanguageService) async -> Void - ) async { - let doc = notification.textDocument.uri - guard let workspace = await self.workspaceForDocument(uri: doc) else { - return - } - - // This should be created as soon as we receive an open call, even if the document - // isn't yet ready. - for languageService in workspace.languageServices(for: doc) { - await notificationHandler(notification, languageService) - } - } - - private func handleRequest( - for request: RequestAndReply, - requestHandler: - @Sendable @escaping ( - RequestType, Workspace, LanguageService - ) async throws -> - RequestType.Response - ) async { - await request.reply { - let request = request.params - let doc = request.textDocument.uri - guard let workspace = await self.workspaceForDocument(uri: request.textDocument.uri) else { - throw ResponseError.workspaceNotOpen(request.textDocument.uri) - } - let languageServices = workspace.languageServices(for: doc) - if languageServices.isEmpty { - throw ResponseError.unknown("No language service for '\(request.textDocument.uri)' found") - } - // Return the results from the first language service that doesn't throw a `requestNotImplemented` error. - for languageService in languageServices { - do { - return try await requestHandler(request, workspace, languageService) - } catch let error as ResponseError where error.code == .requestNotImplemented { - continue - } - } - throw ResponseError.unknown("No language service implements \(type(of: request).method)") - } - } - - /// Send the given notification to the editor. - package nonisolated func sendNotificationToClient(_ notification: some NotificationType) { - client.send(notification) - } - - /// Send the given request to the editor. - package func sendRequestToClient(_ request: R) async throws -> R.Response { - return try await client.send(request) - } - - /// After the language service has crashed, send `DidOpenTextDocumentNotification`s to a newly instantiated language service for previously open documents. - package func reopenDocuments(for languageService: LanguageService) async { - for documentUri in self.documentManager.openDocuments { - guard let workspace = await self.workspaceForDocument(uri: documentUri) else { - continue - } - guard workspace.languageServices(for: documentUri).contains(where: { $0 === languageService }) else { - continue - } - guard let snapshot = try? self.documentManager.latestSnapshot(documentUri) else { - // The document has been closed since we retrieved its URI. We don't care about it anymore. - continue - } - - // Close the document properly in the document manager and build server manager to start with a clean sheet when - // re-opening it. - // This closes and re-opens the document in all of its language services, not just the crashed language service - // but since crashing language services should be rare, this is acceptable. - let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri)) - await self.closeDocument(closeNotification, workspace: workspace) - - let textDocument = TextDocumentItem( - uri: documentUri, - language: snapshot.language, - version: snapshot.version, - text: snapshot.text - ) - await self.openDocument(DidOpenTextDocumentNotification(textDocument: textDocument), workspace: workspace) - } - } - - /// If a language service of type `serverType` that can handle `workspace` using the given toolchain has already been - /// started, return it, otherwise return `nil`. - private func existingLanguageService( - _ serverType: LanguageService.Type, - toolchain: Toolchain, - workspace: Workspace - ) -> LanguageService? { - for languageService in languageServices[LanguageServiceType(serverType), default: []] { - if languageService.canHandle(workspace: workspace, toolchain: toolchain) { - return languageService - } - } - return nil - } - - /// Get the language services that can handle the given languages in the given workspace using the given toolchain. - /// - /// If we have language services that can handle this combination but that haven't been started yet, start them. - func languageServices( - for toolchain: Toolchain, - _ language: Language, - in workspace: Workspace - ) async -> [LanguageService] { - var result: [any LanguageService] = [] - for serverType in languageServiceRegistry.languageServices(for: language) { - if let languageService = existingLanguageService(serverType, toolchain: toolchain, workspace: workspace) { - result.append(languageService) - continue - } - - // Start a new service. - let languageService: (any LanguageService)? = await orLog("failed to start language service") { - [options = workspace.options, hooks] in - let service = try await serverType.init( - sourceKitLSPServer: self, - toolchain: toolchain, - options: options, - hooks: hooks, - workspace: workspace - ) - - let pid = Int(ProcessInfo.processInfo.processIdentifier) - let resp = try await service.initialize( - InitializeRequest( - processId: pid, - rootPath: nil, - rootURI: workspace.rootUri, - initializationOptions: nil, - capabilities: workspace.capabilityRegistry.clientCapabilities, - trace: .off, - workspaceFolders: nil - ) - ) - let languages = languageClass(for: language) - await self.registerCapabilities( - for: resp.capabilities, - languages: languages, - registry: workspace.capabilityRegistry - ) - - var syncKind: TextDocumentSyncKind - switch resp.capabilities.textDocumentSync { - case .options(let options): - syncKind = options.change ?? .incremental - case .kind(let kind): - syncKind = kind - default: - syncKind = .incremental - } - guard syncKind == .incremental else { - throw ResponseError.internalError("non-incremental update not implemented") - } - - await service.clientInitialized(InitializedNotification()) - - if let concurrentlyInitializedService = existingLanguageService( - serverType, - toolchain: toolchain, - workspace: workspace - ) { - // Since we 'await' above, another call to languageService might have - // happened concurrently, passed the `existingLanguageService` check at - // the top and started initializing another language service. - // If this race happened, just shut down our server and return the - // other one. - await service.shutdown() - return concurrentlyInitializedService - } - - languageServices[LanguageServiceType(serverType), default: []].append(service) - return service - } - guard let languageService else { - // If a language service fails to start, don't try starting language services with lower precedence. Otherwise - // we get into a situation where eg. `SwiftLanguageService`` fails to start (eg. because the toolchain doesn't - // contain sourcekitd) and the `DocumentationLanguageService` now becomes the primary language service for the - // document, trying to serve documentation, completion etc. which is not intended. - break - } - result.append(languageService) - } - if result.isEmpty { - logger.error("Unable to infer language server type for language '\(language)'") - } - return result - } - - /// Get the language services that can handle the given document. - /// - /// If we have language services that can handle this document but that haven't been started yet, start them. - package func languageServices( - for uri: DocumentURI, - _ language: Language, - in workspace: Workspace - ) async -> [LanguageService] { - let existingLanguageServices = workspace.languageServices(for: uri) - if !existingLanguageServices.isEmpty { - return existingLanguageServices - } - - let toolchain = await workspace.buildServerManager.toolchain( - for: await workspace.buildServerManager.canonicalTarget(for: uri), - language: language - ) - guard let toolchain else { - logger.error("Failed to determine toolchain for \(uri)") - return [] - } - let languageServices = await self.languageServices(for: toolchain, language, in: workspace) - - if languageServices.isEmpty { - logger.error("No language service found to handle \(uri.forLogging)") - } - - logger.log( - """ - Using toolchain at \(toolchain.path.description) (\(toolchain.identifier, privacy: .public)) \ - for \(uri.forLogging) - """ - ) - - return workspace.setLanguageServices(for: uri, languageServices) - } - - /// The language service with the highest precedence that can handle the given document. - /// - /// If we have language services that can handle this document but that haven't been started yet, start them. - /// - /// If no language service exists for this document, throw an error. - package func primaryLanguageService( - for uri: DocumentURI, - _ language: Language, - in workspace: Workspace - ) async throws -> LanguageService { - guard let languageService = await languageServices(for: uri, language, in: workspace).first else { - throw ResponseError.unknown("No language service found for \(uri)") - } - return languageService - } -} - -// MARK: - MessageHandler - -extension SourceKitLSPServer: QueueBasedMessageHandler { - private enum ImplicitTextDocumentRequestCancellationReason { - case documentChanged - case documentClosed - } - - package nonisolated func didReceive(notification: some NotificationType) { - let textDocumentUri: DocumentURI - let cancellationReason: ImplicitTextDocumentRequestCancellationReason - switch notification { - case let params as DidChangeTextDocumentNotification: - textDocumentUri = params.textDocument.uri - cancellationReason = .documentChanged - case let params as DidCloseTextDocumentNotification: - textDocumentUri = params.textDocument.uri - cancellationReason = .documentClosed - default: - return - } - textDocumentTrackingQueue.async(priority: .high) { - await self.cancelTextDocumentRequests(for: textDocumentUri, reason: cancellationReason) - } - } - - /// Cancel all in-progress text document requests for the given document. - /// - /// As a user makes an edit to a file, these requests are most likely no longer relevant. It also makes sure that a - /// long-running sourcekitd request can't block the entire language server if the client does not cancel all requests. - /// For example, consider the following sequence of requests: - /// - `textDocument/semanticTokens/full` for document A - /// - `textDocument/didChange` for document A - /// - `textDocument/formatting` for document A - /// - /// If the editor is not cancelling the semantic tokens request on edit (like VS Code does), then the `didChange` - /// notification is blocked on the semantic tokens request finishing. Hence, we also can't run the - /// `textDocument/formatting` request. Cancelling the semantic tokens on the edit fixes the issue. - /// - /// This method is a no-op if `cancelTextDocumentRequestsOnEditAndClose` is disabled. - /// - /// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are - /// registered before a notification that triggers cancellation might come in. - private func cancelTextDocumentRequests(for uri: DocumentURI, reason: ImplicitTextDocumentRequestCancellationReason) { - guard self.options.cancelTextDocumentRequestsOnEditAndCloseOrDefault else { - return - } - for (requestID, requestMethod) in self.inProgressTextDocumentRequests[uri, default: []] { - if reason == .documentChanged && requestMethod == CompletionRequest.method { - // As the user types, we filter the code completion results. Cancelling the completion request on every - // keystroke means that we will never build the initial list of completion results for this code - // completion session if building that list takes longer than the user's typing cadence (eg. for global - // completions) and we will thus not show any completions. - continue - } - logger.info("Implicitly cancelling request \(requestID)") - self.messageHandlingHelper.cancelRequest(id: requestID) - } - } - - package func handle(notification: some NotificationType) async { - logger.log("Received notification: \(notification.forLogging)") - - switch notification { - case let notification as DidChangeActiveDocumentNotification: - await self.didChangeActiveDocument(notification) - case let notification as DidChangeTextDocumentNotification: - await self.changeDocument(notification) - case let notification as DidChangeWorkspaceFoldersNotification: - await self.didChangeWorkspaceFolders(notification) - case let notification as DidCloseTextDocumentNotification: - await self.closeDocument(notification) - case let notification as DidChangeWatchedFilesNotification: - await self.didChangeWatchedFiles(notification) - case let notification as DidOpenTextDocumentNotification: - await self.openDocument(notification) - case let notification as DidSaveTextDocumentNotification: - await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.didSaveDocument) - case let notification as InitializedNotification: - self.clientInitialized(notification) - case let notification as ExitNotification: - await self.exit(notification) - case let notification as ReopenTextDocumentNotification: - await self.reopenDocument(notification) - case let notification as WillSaveTextDocumentNotification: - await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.willSaveDocument) - // IMPORTANT: When adding a new entry to this switch, also add it to the `MessageHandlingDependencyTracker` initializer. - default: - break - } - } - - package nonisolated func didReceive(request: some RequestType, id: RequestID) { - guard let request = request as? any TextDocumentRequest else { - return - } - textDocumentTrackingQueue.async(priority: .background) { - await self.registerInProgressTextDocumentRequest(request, id: id) - } - } - - /// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are - /// registered before a notification that triggers cancellation might come in. - private func registerInProgressTextDocumentRequest(_ request: T, id: RequestID) { - self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].append((id: id, requestMethod: T.method)) - } - - package func handle( - request params: Request, - id: RequestID, - reply: @Sendable @escaping (LSPResult) -> Void - ) async { - defer { - if let request = params as? any TextDocumentRequest { - textDocumentTrackingQueue.async(priority: .background) { - self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].removeAll { $0.id == id } - } - } - } - - await self.hooks.preHandleRequest?(params) - - let startDate = Date() - - let request = RequestAndReply(params) { result in - reply(result) - let endDate = Date() - Task { - switch result { - case .success(let response): - logger.log( - """ - Succeeded (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms) - \(Request.method, privacy: .public) - \(response.forLogging) - """ - ) - case .failure(let error): - logger.log( - """ - Failed (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms) - \(Request.method, privacy: .public)(\(id, privacy: .public)) - \(error.forLogging, privacy: .private) - """ - ) - } - } - } - - logger.log("Received request \(id, privacy: .public): \(params.forLogging)") - - if let textDocumentRequest = params as? any TextDocumentRequest { - await self.clientInteractedWithDocument(textDocumentRequest.textDocument.uri) - } - - switch request { - case let request as RequestAndReply: - await request.reply { try await incomingCalls(request.params) } - case let request as RequestAndReply: - await request.reply { try await outgoingCalls(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.prepareCallHierarchy) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.codeAction) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.codeLens) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.colorPresentation) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.completion) - case let request as RequestAndReply: - await request.reply { try await completionItemResolve(request: request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.signatureHelp) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.declaration) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.definition) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.doccDocumentation) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentColor) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentDiagnostic) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentFormatting) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentRangeFormatting) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentOnTypeFormatting) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentSymbolHighlight) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentSemanticTokensDelta) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentSemanticTokensRange) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentSemanticTokens) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentSymbol) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.documentTests) - case let request as RequestAndReply: - await request.reply { try await executeCommand(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.foldingRange) - case let request as RequestAndReply: - await request.reply { try await getReferenceDocument(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.hover) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.implementation) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.indexedRename) - case let request as RequestAndReply: - await request.reply { try await initialize(request.params) } - // Only set `initialized` to `true` after we have sent the response to the initialize request to the client. - initialized = true - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.inlayHint) - case let request as RequestAndReply: - await request.reply { try await self.isIndexing(request.params) } - case let request as RequestAndReply: - await request.reply { try await outputPaths(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.prepareRename) - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.references) - case let request as RequestAndReply: - await request.reply { try await rename(request.params) } - case let request as RequestAndReply: - await request.reply { try await self.setBackgroundIndexingPaused(request.params) } - case let request as RequestAndReply: - await request.reply { try await sourceKitOptions(request.params) } - case let request as RequestAndReply: - await request.reply { try await shutdown(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.symbolInfo) - case let request as RequestAndReply: - await request.reply { try await synchronize(request.params) } - case let request as RequestAndReply: - await request.reply { try await triggerReindex(request.params) } - case let request as RequestAndReply: - await self.handleRequest(for: request, requestHandler: self.prepareTypeHierarchy) - case let request as RequestAndReply: - await request.reply { try await subtypes(request.params) } - case let request as RequestAndReply: - await request.reply { try await supertypes(request.params) } - case let request as RequestAndReply: - await request.reply { try await workspaceSymbols(request.params) } - case let request as RequestAndReply: - await request.reply { try await workspaceTests(request.params) } - // IMPORTANT: When adding a new entry to this switch, also add it to the `MessageHandlingDependencyTracker` initializer. - default: - await request.reply { throw ResponseError.methodNotFound(Request.method) } - } - } -} - -extension SourceKitLSPServer { - nonisolated package func logMessageToIndexLog( - message: String, - type: WindowMessageType, - structure: LanguageServerProtocol.StructuredLogKind? - ) { - self.sendNotificationToClient( - LogMessageNotification( - type: type, - message: message, - logName: "SourceKit-LSP: Indexing", - structure: structure - ).representingTaskIDUsingEmojiPrefixIfNecessary(options: options) - ) - } - - func fileHandlingCapabilityChanged() { - logger.log("Scheduling update of URI to workspace because file handling capability of a workspace changed") - self.scheduleUpdateOfUriToWorkspace() - } -} - -// MARK: - Request and notification handling - -extension SourceKitLSPServer { - - // MARK: - General - - /// Creates a workspace at the given `uri`. - /// - /// A workspace does not necessarily have any build server attached to it, in which case `buildServerSpec` may be - /// `nil` - consider eg. a top level workspace folder with multiple SwiftPM projects inside it. - private func createWorkspace( - workspaceFolder: DocumentURI, - options: SourceKitLSPOptions, - buildServerSpec: BuildServerSpec? - ) async throws -> Workspace { - guard let capabilityRegistry = capabilityRegistry else { - struct NoCapabilityRegistryError: Error {} - logger.log("Cannot open workspace before server is initialized") - throw NoCapabilityRegistryError() - } - - logger.log("Creating workspace at \(workspaceFolder.forLogging)") - logger.logFullObjectInMultipleLogMessages(header: "Workspace options", options.loggingProxy) - - let workspace = await Workspace( - sourceKitLSPServer: self, - documentManager: self.documentManager, - rootUri: workspaceFolder, - capabilityRegistry: capabilityRegistry, - buildServerSpec: buildServerSpec, - toolchainRegistry: self.toolchainRegistry, - options: options, - hooks: hooks, - indexTaskScheduler: indexTaskScheduler - ) - return workspace - } - - /// Determines the build server for the given workspace folder and creates a `Workspace` that uses this inferred build - /// system. - private func createWorkspaceWithInferredBuildServer(workspaceFolder: DocumentURI) async throws -> Workspace { - let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: workspaceFolder) - let buildServerSpec = determineBuildServer( - forWorkspaceFolder: workspaceFolder, - onlyConsiderRoot: false, - options: options, - hooks: hooks.buildServerHooks - ) - return try await self.createWorkspace( - workspaceFolder: workspaceFolder, - options: options, - buildServerSpec: buildServerSpec - ) - } - - func initialize(_ req: InitializeRequest) async throws -> InitializeResult { - logger.logFullObjectInMultipleLogMessages(header: "Initialize request", AnyRequestType(request: req)) - // If the client can handle `PeekDocumentsRequest`, they can enable the - // experimental client capability `"workspace/peekDocuments"` through the `req.capabilities.experimental`. - // - // The below is a workaround for the vscode-swift extension since it cannot set client capabilities. - // It passes "workspace/peekDocuments" through the `initializationOptions`. - var clientCapabilities = req.capabilities - if case .dictionary(let initializationOptions) = req.initializationOptions { - let experimentalClientCapabilities = [ - PeekDocumentsRequest.method, - GetReferenceDocumentRequest.method, - DidChangeActiveDocumentNotification.method, - ] - for capabilityName in experimentalClientCapabilities { - guard let experimentalCapability = initializationOptions[capabilityName] else { - continue - } - var experimentalCapabilities: [String: LSPAny] = - if case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental { - experimentalCapabilities - } else { - [:] - } - experimentalCapabilities[capabilityName] = experimentalCapability - clientCapabilities.experimental = .dictionary(experimentalCapabilities) - } - - // The client announces what CodeLenses it supports, and the LSP will only return - // ones found in the supportedCommands dictionary. - if let codeLens = initializationOptions["textDocument/codeLens"], - case let .dictionary(codeLensConfig) = codeLens, - case let .dictionary(supportedCommands) = codeLensConfig["supportedCommands"] - { - let commandMap = supportedCommands.compactMap { (key, value) in - if case let .string(clientCommand) = value { - return (SupportedCodeLensCommand(rawValue: key), clientCommand) - } - return nil - } - - clientCapabilities.textDocument?.codeLens?.supportedCommands = Dictionary(uniqueKeysWithValues: commandMap) - } - } - - capabilityRegistry = CapabilityRegistry(clientCapabilities: clientCapabilities) - - let initializeOptions = orLog("Parsing options") { try SourceKitLSPOptions(fromLSPAny: req.initializationOptions) } - _options.withLock { options in - options = SourceKitLSPOptions.merging(base: options, override: initializeOptions) - } - - logger.log("Initialized SourceKit-LSP") - logger.logFullObjectInMultipleLogMessages(header: "Global options", options.loggingProxy) - - await workspaceQueue.async { [hooks] in - if let workspaceFolders = req.workspaceFolders { - self.workspacesAndIsImplicit += await workspaceFolders.asyncCompactMap { workspaceFolder in - await orLog("Creating workspace from workspaceFolders") { - return ( - workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: workspaceFolder.uri), - isImplicit: false - ) - } - } - } else if let uri = req.rootURI { - await orLog("Creating workspace from rootURI") { - self.workspacesAndIsImplicit.append( - (workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: uri), isImplicit: false) - ) - } - } else if let path = req.rootPath { - let uri = DocumentURI(URL(fileURLWithPath: path)) - await orLog("Creating workspace from rootPath") { - self.workspacesAndIsImplicit.append( - (workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: uri), isImplicit: false) - ) - } - } - - if self.workspaces.isEmpty { - logger.error("No workspace found") - - let options = self.options - let workspace = await Workspace( - sourceKitLSPServer: self, - documentManager: self.documentManager, - rootUri: req.rootURI, - capabilityRegistry: self.capabilityRegistry!, - buildServerSpec: nil, - toolchainRegistry: self.toolchainRegistry, - options: options, - hooks: hooks, - indexTaskScheduler: self.indexTaskScheduler - ) - - self.workspacesAndIsImplicit.append((workspace: workspace, isImplicit: false)) - } - }.value - - assert(!self.workspaces.isEmpty) - - let result = InitializeResult( - capabilities: await self.serverCapabilities( - for: req.capabilities, - registry: self.capabilityRegistry!, - options: options - ) - ) - logger.logFullObjectInMultipleLogMessages(header: "Initialize response", AnyRequestType(request: req)) - return result - } - - func serverCapabilities( - for client: ClientCapabilities, - registry: CapabilityRegistry, - options: SourceKitLSPOptions - ) async -> ServerCapabilities { - let completionOptions = - await registry.clientHasDynamicCompletionRegistration - ? nil - : LanguageServerProtocol.CompletionOptions( - resolveProvider: true, - triggerCharacters: [".", "("] - ) - - let signatureHelpOptions = - await registry.clientHasDynamicSignatureHelpRegistration - ? nil - : LanguageServerProtocol.SignatureHelpOptions( - triggerCharacters: ["(", "["], - // We retrigger on `:` as it's potentially after an argument label which can change the active parameter or signature. - retriggerCharacters: [",", ":"] - ) - - let onTypeFormattingOptions = - options.hasExperimentalFeature(.onTypeFormatting) - ? DocumentOnTypeFormattingOptions(triggerCharacters: ["\n", "\r\n", "\r", "{", "}", ";", ".", ":", "#"]) - : nil - - let foldingRangeOptions = - await registry.clientHasDynamicFoldingRangeRegistration - ? nil - : ValueOrBool.bool(true) - - let inlayHintOptions = - await registry.clientHasDynamicInlayHintRegistration - ? nil - : ValueOrBool.value(InlayHintOptions(resolveProvider: false)) - - let semanticTokensOptions = - await registry.clientHasDynamicSemanticTokensRegistration - ? nil - : SemanticTokensOptions( - legend: SemanticTokensLegend.sourceKitLSPLegend, - range: .bool(true), - full: .bool(true) - ) - - let executeCommandOptions = - await registry.clientHasDynamicExecuteCommandRegistration - ? nil - : ExecuteCommandOptions(commands: languageServiceRegistry.languageServices.flatMap { $0.type.builtInCommands }) - - var experimentalCapabilities: [String: LSPAny] = [ - WorkspaceTestsRequest.method: .dictionary(["version": .int(2)]), - DocumentTestsRequest.method: .dictionary(["version": .int(2)]), - TriggerReindexRequest.method: .dictionary(["version": .int(1)]), - GetReferenceDocumentRequest.method: .dictionary(["version": .int(1)]), - DidChangeActiveDocumentNotification.method: .dictionary(["version": .int(1)]), - ] - for (key, value) in languageServiceRegistry.languageServices.flatMap({ $0.type.experimentalCapabilities }) { - if let existingValue = experimentalCapabilities[key] { - logger.error( - "Conflicting experimental capabilities for \(key): \(existingValue.forLogging) vs \(value.forLogging)" - ) - } - experimentalCapabilities[key] = value - } - - return ServerCapabilities( - textDocumentSync: .options( - TextDocumentSyncOptions( - openClose: true, - change: .incremental - ) - ), - hoverProvider: .bool(true), - completionProvider: completionOptions, - signatureHelpProvider: signatureHelpOptions, - definitionProvider: .bool(true), - implementationProvider: .bool(true), - referencesProvider: .bool(true), - documentHighlightProvider: .bool(true), - documentSymbolProvider: .bool(true), - workspaceSymbolProvider: .bool(true), - codeActionProvider: .value( - CodeActionServerCapabilities( - clientCapabilities: client.textDocument?.codeAction, - codeActionOptions: CodeActionOptions(codeActionKinds: nil), - supportsCodeActions: true - ) - ), - codeLensProvider: CodeLensOptions(), - documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)), - documentRangeFormattingProvider: .value(DocumentRangeFormattingOptions(workDoneProgress: false)), - documentOnTypeFormattingProvider: onTypeFormattingOptions, - renameProvider: .value(RenameOptions(prepareProvider: true)), - colorProvider: .bool(true), - foldingRangeProvider: foldingRangeOptions, - declarationProvider: .bool(true), - executeCommandProvider: executeCommandOptions, - workspace: WorkspaceServerCapabilities( - workspaceFolders: .init( - supported: true, - changeNotifications: .bool(true) - ) - ), - callHierarchyProvider: .bool(true), - typeHierarchyProvider: .bool(true), - semanticTokensProvider: semanticTokensOptions, - inlayHintProvider: inlayHintOptions, - experimental: .dictionary(experimentalCapabilities) - ) - } - - func registerCapabilities( - for server: ServerCapabilities, - languages: [Language], - registry: CapabilityRegistry - ) async { - // IMPORTANT: When adding new capabilities here, also add the value of that capability in `SwiftLanguageService` - // to SourceKitLSPServer.serverCapabilities. That way the capabilities get registered for all languages in case the - // client does not support dynamic capability registration. - - if let completionOptions = server.completionProvider { - await registry.registerCompletionIfNeeded(options: completionOptions, for: languages, server: self) - } - if let signatureHelpOptions = server.signatureHelpProvider { - await registry.registerSignatureHelpIfNeeded(options: signatureHelpOptions, for: languages, server: self) - } - if server.foldingRangeProvider?.isSupported == true { - await registry.registerFoldingRangeIfNeeded(options: FoldingRangeOptions(), for: languages, server: self) - } - if let semanticTokensOptions = server.semanticTokensProvider { - await registry.registerSemanticTokensIfNeeded(options: semanticTokensOptions, for: languages, server: self) - } - if let inlayHintProvider = server.inlayHintProvider, inlayHintProvider.isSupported { - let options: InlayHintOptions - switch inlayHintProvider { - case .bool(true): - options = InlayHintOptions() - case .bool(false): - return - case .value(let opts): - options = opts - } - await registry.registerInlayHintIfNeeded(options: options, for: languages, server: self) - } - // We use the registration for the diagnostics provider to decide whether to enable pull-diagnostics (see comment - // on `CapabilityRegistry.clientSupportPullDiagnostics`. - // Thus, we can't statically register this capability in the server options. We need the client's reply to decide - // whether it supports pull diagnostics. - if let diagnosticOptions = server.diagnosticProvider { - await registry.registerDiagnosticIfNeeded(options: diagnosticOptions, for: languages, server: self) - } - if let commandOptions = server.executeCommandProvider { - await registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands, server: self) - } - } - - func clientInitialized(_: InitializedNotification) { - // Nothing to do. - } - - /// The server is about to exit, and the server should flush any buffered state. - /// - /// The server shall not be used to handle more requests (other than possibly - /// `shutdown` and `exit`) and should attempt to flush any buffered state - /// immediately, such as sending index changes to disk. - /// - /// - Note: this method should be safe to call multiple times, since we want to be resilient against multiple - // possible shutdown sequences, including pipe failure. - package func prepareForExit() async { - // We are shutting down / closing all workspaces and language services, so clear the arrays caching them. - let languageServices = self.languageServices - self.languageServices = [:] - - let workspaces = await self.workspaceQueue.async { - let workspaces = self.workspaces - self.workspacesAndIsImplicit = [] - return workspaces - }.valuePropagatingCancellation - - // Concurrently shut all things down. - await withTaskGroup(of: Void.self) { taskGroup in - taskGroup.addTask { - await orLog("Shutting down index scheduler") { - await self.indexTaskScheduler.shutDown() - } - } - for service in languageServices.values.flatMap({ $0 }) { - taskGroup.addTask { - await service.shutdown() - } - } - for workspace in workspaces { - taskGroup.addTask { - await orLog("Shutting down build server") { - await workspace.buildServerManager.shutdown() - } - } - } - } - - // Make sure we emit all pending log messages. When we're not using `NonDarwinLogger` this is a no-op. - await NonDarwinLogger.flush() - } - - func shutdown(_ request: ShutdownRequest) async throws -> ShutdownRequest.Response { - await prepareForExit() - - // Wait for all services to shut down before sending the shutdown response. - // Otherwise we might terminate sourcekit-lsp while it still has open - // connections to the toolchain servers, which could send messages to - // sourcekit-lsp while it is being deallocated, causing crashes. - return ShutdownRequest.Response() - } - - func exit(_ notification: ExitNotification) async { - // Should have been called in shutdown, but allow misbehaving clients. - await prepareForExit() - - // Call onExit only once, and hop off queue to allow the handler to call us back. - self.onExit() - } - - /// Start watching for changes with the given patterns. - func watchFiles(_ fileWatchers: [FileSystemWatcher]) async { - await self.waitUntilInitialized() - if fileWatchers.allSatisfy({ self.watchers.contains($0) }) { - // All watchers already registered. Nothing to do. - return - } - self.watchers.formUnion(fileWatchers) - await self.capabilityRegistry?.registerDidChangeWatchedFiles( - watchers: self.watchers.sorted { $0.globPattern < $1.globPattern }, - server: self - ) - } - - func isIndexing(_ request: IsIndexingRequest) async throws -> IsIndexingResponse { - guard self.options.hasExperimentalFeature(.isIndexingRequest) else { - throw ResponseError.unknown("\(IsIndexingRequest.method) indexing is an experimental request") - } - let isIndexing = - await workspaces - .asyncCompactMap { await $0.semanticIndexManager } - .asyncContains { await $0.progressStatus != .upToDate } - return IsIndexingResponse(indexing: isIndexing) - } - - // MARK: - Text synchronization - - func openDocument(_ notification: DidOpenTextDocumentNotification) async { - let uri = notification.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { - logger.error( - "Received open notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..." - ) - return - } - await openDocument(notification, workspace: workspace) - await self.clientInteractedWithDocument(uri) - } - - private func openDocument(_ notification: DidOpenTextDocumentNotification, workspace: Workspace) async { - // Immediately open the document even if the build server isn't ready. This is important since - // we check that the document is open when we receive messages from the build server. - let snapshot = orLog("Opening document") { - try documentManager.open( - notification.textDocument.uri, - language: notification.textDocument.language, - version: notification.textDocument.version, - text: notification.textDocument.text - ) - } - guard let snapshot else { - // Already logged failure - return - } - - let textDocument = notification.textDocument - let uri = textDocument.uri - let language = textDocument.language - - let languageServices = await languageServices(for: uri, language, in: workspace) - - if languageServices.isEmpty { - // If we can't create a service, this document is unsupported and we can bail here. - return - } - - await workspace.buildServerManager.registerForChangeNotifications(for: uri, language: language) - - // If the document is ready, we can immediately send the notification. - for languageService in languageServices { - await languageService.openDocument(notification, snapshot: snapshot) - } - } - - func closeDocument(_ notification: DidCloseTextDocumentNotification) async { - let uri = notification.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { - logger.error( - "Received close notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..." - ) - return - } - await self.closeDocument(notification, workspace: workspace) - } - - func reopenDocument(_ notification: ReopenTextDocumentNotification) async { - let uri = notification.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { - logger.error( - "Received reopen notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..." - ) - return - } - - for languageService in workspace.languageServices(for: uri) { - await languageService.reopenDocument(notification) - } - } - - func closeDocument(_ notification: DidCloseTextDocumentNotification, workspace: Workspace) async { - // Immediately close the document. We need to be sure to clear our pending work queue in case - // the build server still isn't ready. - orLog("failed to close document", level: .error) { - try documentManager.close(notification.textDocument.uri) - } - - let uri = notification.textDocument.uri - - await workspace.buildServerManager.unregisterForChangeNotifications(for: uri) - - for languageService in workspace.languageServices(for: uri) { - await languageService.closeDocument(notification) - } - - workspaceQueue.async { - self.workspaceForUri[notification.textDocument.uri] = nil - } - } - - func changeDocument(_ notification: DidChangeTextDocumentNotification) async { - let uri = notification.textDocument.uri - - guard let workspace = await workspaceForDocument(uri: uri) else { - logger.error( - "Received change notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..." - ) - return - } - await self.clientInteractedWithDocument(uri) - - // If the document is ready, we can handle the change right now. - let editResult = orLog("Editing document") { - try documentManager.edit( - notification.textDocument.uri, - newVersion: notification.textDocument.version, - edits: notification.contentChanges - ) - } - guard let (preEditSnapshot, postEditSnapshot, edits) = editResult else { - // Already logged failure - return - } - for languageService in workspace.languageServices(for: uri) { - await languageService.changeDocument( - notification, - preEditSnapshot: preEditSnapshot, - postEditSnapshot: postEditSnapshot, - edits: edits - ) - } - } - - /// If the client doesn't support the `window/didChangeActiveDocument` notification, when the client interacts with a - /// document, infer that this is the currently active document and poke preparation of its target. We don't want to - /// wait for the preparation to finish because that would cause too big a delay. - /// - /// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis, - /// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't - /// get requests, ensuring that we don't unnecessarily prepare them. - func clientInteractedWithDocument(_ uri: DocumentURI) async { - if self.capabilityRegistry?.clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method) ?? false { - // The client actively notifies us about the currently active document, so we shouldn't infer the active document - // based on other requests that the client sends us. - return - } - await self.didChangeActiveDocument( - DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(uri)) - ) - } - - func didChangeActiveDocument(_ notification: DidChangeActiveDocumentNotification) async { - guard let activeDocument = notification.textDocument?.uri else { - // The client no longer has a SourceKit-LSP document open. Mark in-progress preparations as irrelevant to cancel - // them if they haven't started yet. - for workspace in workspaces { - await workspace.semanticIndexManager?.markPreparationForEditorFunctionalityTaskAsIrrelevant() - } - return - } - let documentWorkspace = await self.workspaceForDocument(uri: activeDocument) - for workspace in workspaces { - if workspace === documentWorkspace { - await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: activeDocument) - } else { - await workspace.semanticIndexManager?.markPreparationForEditorFunctionalityTaskAsIrrelevant() - } - } - } - - func willSaveDocument( - _ notification: WillSaveTextDocumentNotification, - languageService: LanguageService - ) async { - await languageService.willSaveDocument(notification) - } - - func didSaveDocument( - _ notification: DidSaveTextDocumentNotification, - languageService: LanguageService - ) async { - await languageService.didSaveDocument(notification) - } - - func didChangeWorkspaceFolders(_ notification: DidChangeWorkspaceFoldersNotification) async { - // There is a theoretical race condition here: While we await in this function, - // the open documents or workspaces could have changed. Because of this, - // we might close a document in a workspace that is no longer responsible - // for it. - // In practice, it is fine: sourcekit-lsp will not handle any new messages - // while we are executing this function and thus there's no risk of - // documents or workspaces changing. To hit the race condition, you need - // to invoke the API of `SourceKitLSPServer` directly and open documents - // while this function is executing. Even in such an API use case, hitting - // that race condition seems very unlikely. - var preChangeWorkspaces: [DocumentURI: Workspace] = [:] - for docUri in self.documentManager.openDocuments { - preChangeWorkspaces[docUri] = await self.workspaceForDocument(uri: docUri) - } - await workspaceQueue.async { - if let removed = notification.event.removed { - self.workspacesAndIsImplicit.removeAll { workspace in - // Close all implicit workspaces as well because we could have opened a new explicit workspace that now contains - // files from a previous implicit workspace. - return workspace.isImplicit - || removed.contains(where: { workspaceFolder in workspace.workspace.rootUri == workspaceFolder.uri }) - } - } - if let added = notification.event.added { - let newWorkspaces = await added.asyncCompactMap { workspaceFolder in - await orLog("Creating workspace after workspace folder change") { - try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: workspaceFolder.uri) - } - } - self.workspacesAndIsImplicit += newWorkspaces.map { (workspace: $0, isImplicit: false) } - } - }.value - } - - func didChangeWatchedFiles(_ notification: DidChangeWatchedFilesNotification) async { - // We can't make any assumptions about which file changes a particular build - // system is interested in. Just because it doesn't have build settings for - // a file doesn't mean a file can't affect the build server's build settings - // (e.g. Package.swift doesn't have build settings but affects build - // settings). Inform the build server about all file changes. - await workspaces.concurrentForEach { await $0.filesDidChange(notification.changes) } - - for languageService in languageServices.values.flatMap(\.self) { - await languageService.filesDidChange(notification.changes) - } - } - - func setBackgroundIndexingPaused(_ request: SetOptionsRequest) async throws -> VoidResponse { - guard self.options.hasExperimentalFeature(.setOptionsRequest) else { - throw ResponseError.unknown("\(SetOptionsRequest.method) indexing is an experimental request") - } - if let backgroundIndexingPaused = request.backgroundIndexingPaused { - await self.indexTaskScheduler.setMaxConcurrentTasksByPriority( - Self.maxConcurrentIndexingTasksByPriority(isIndexingPaused: backgroundIndexingPaused, options: self.options) - ) - } - - return VoidResponse() - } - - func sourceKitOptions(_ request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse { - guard options.hasExperimentalFeature(.sourceKitOptionsRequest) else { - throw ResponseError.unknown("\(SourceKitOptionsRequest.method) is an experimental request") - } - let uri = request.textDocument.uri - guard let workspace = await self.workspaceForDocument(uri: uri) else { - throw ResponseError.workspaceNotOpen(uri) - } - - let target: BuildTargetIdentifier? - if let requestedTarget = request.target { - target = BuildTargetIdentifier(uri: requestedTarget) - } else if let canonicalTarget = await workspace.buildServerManager.canonicalTarget(for: uri) { - target = canonicalTarget - } else { - target = nil - } - let didPrepareTarget: Bool? - if request.prepareTarget, let target, let semanticIndexManager = await workspace.semanticIndexManager { - didPrepareTarget = await semanticIndexManager.prepareTargetsForSourceKitOptions(target: target) - } else { - didPrepareTarget = nil - } - let buildSettings = await workspace.buildServerManager.buildSettingsInferredFromMainFile( - for: request.textDocument.uri, - target: target, - language: nil, - fallbackAfterTimeout: request.allowFallbackSettings - ) - guard let buildSettings else { - throw ResponseError.unknown("Unable to determine build settings") - } - return SourceKitOptionsResponse( - compilerArguments: buildSettings.compilerArguments, - workingDirectory: buildSettings.workingDirectory, - kind: buildSettings.isFallback ? .fallback : .normal, - didPrepareTarget: didPrepareTarget, - data: buildSettings.data - ) - } - - func outputPaths(_ request: OutputPathsRequest) async throws -> OutputPathsResponse { - guard options.hasExperimentalFeature(.outputPathsRequest) else { - throw ResponseError.unknown("\(OutputPathsRequest.method) is an experimental request") - } - guard let workspace = self.workspaces.first(where: { $0.rootUri == request.workspace }) else { - throw ResponseError.unknown("No workspace with URI \(request.workspace.forLogging) found") - } - guard await workspace.buildServerManager.initializationData?.outputPathsProvider ?? false else { - throw ResponseError.unknown("Build server for \(request.workspace.forLogging) does not support output paths") - } - let outputPaths = try await workspace.buildServerManager.outputPaths(in: [ - BuildTargetIdentifier(uri: request.target) - ]) - return OutputPathsResponse(outputPaths: outputPaths) - } - - // MARK: - Language features - - func completion( - _ req: CompletionRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> CompletionList { - return try await languageService.completion(req) - } - - func completionItemResolve( - request: CompletionItemResolveRequest - ) async throws -> CompletionItem { - // Swift completion items specify the URI of the item they originate from in the `data` - guard case .dictionary(let dict) = request.item.data, case .string(let uriString) = dict["uri"], - let uri = try? DocumentURI(string: uriString) - else { - return request.item - } - guard let workspace = await self.workspaceForDocument(uri: uri) else { - throw ResponseError.workspaceNotOpen(uri) - } - let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language - return try await primaryLanguageService(for: uri, language, in: workspace).completionItemResolve(request) - } - - func doccDocumentation( - _ req: DoccDocumentationRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DoccDocumentationResponse { - return try await languageService.doccDocumentation(req) - } - - func hover( - _ req: HoverRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> HoverResponse? { - return try await languageService.hover(req) - } - - func signatureHelp( - _ req: SignatureHelpRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> SignatureHelp? { - return try await languageService.signatureHelp(req) - } - - /// Handle a workspace/symbol request, returning the SymbolInformation. - /// - returns: An array with SymbolInformation for each matching symbol in the workspace. - func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? { - // Ignore short queries since they are: - // - noisy and slow, since they can match many symbols - // - normally unintentional, triggered when the user types slowly or if the editor doesn't - // debounce events while the user is typing - guard req.query.count >= minWorkspaceSymbolPatternLength else { - return [] - } - var symbolsAndIndex: [(symbol: SymbolOccurrence, index: CheckedIndex)] = [] - for workspace in workspaces { - guard let index = await workspace.index(checkedFor: .deletedFiles) else { - continue - } - var symbolOccurrences: [SymbolOccurrence] = [] - index.forEachCanonicalSymbolOccurrence( - containing: req.query, - anchorStart: false, - anchorEnd: false, - subsequence: true, - ignoreCase: true - ) { symbol in - if Task.isCancelled { - return false - } - guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else { - return true - } - symbolOccurrences.append(symbol) - return true - } - try Task.checkCancellation() - symbolsAndIndex += symbolOccurrences.map { - return ($0, index) - } - } - return symbolsAndIndex.sorted(by: { $0.symbol < $1.symbol }).map { symbolOccurrence, index in - let symbolPosition = Position( - line: symbolOccurrence.location.line - 1, // 1-based -> 0-based - // Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the - // file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read. - utf16index: symbolOccurrence.location.utf8Column - 1 - ) - - let symbolLocation = Location( - uri: symbolOccurrence.location.documentUri, - range: Range(symbolPosition) - ) - - let containerNames = index.containerNames(of: symbolOccurrence) - let containerName: String? - if containerNames.isEmpty { - containerName = nil - } else { - switch symbolOccurrence.symbol.language { - case .cxx, .c, .objc: containerName = containerNames.joined(separator: "::") - case .swift: containerName = containerNames.joined(separator: ".") - } - } - - return WorkspaceSymbolItem.symbolInformation( - SymbolInformation( - name: symbolOccurrence.symbol.name, - kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), - deprecated: nil, - location: symbolLocation, - containerName: containerName - ) - ) - } - } - - /// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document. - func symbolInfo( - _ req: SymbolInfoRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [SymbolDetails] { - return try await languageService.symbolInfo(req) - } - - func documentSymbolHighlight( - _ req: DocumentHighlightRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [DocumentHighlight]? { - return try await languageService.documentSymbolHighlight(req) - } - - func foldingRange( - _ req: FoldingRangeRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [FoldingRange]? { - return try await languageService.foldingRange(req) - } - - func documentSymbol( - _ req: DocumentSymbolRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DocumentSymbolResponse? { - return try await languageService.documentSymbol(req) - } - - func documentColor( - _ req: DocumentColorRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [ColorInformation] { - return try await languageService.documentColor(req) - } - - func documentSemanticTokens( - _ req: DocumentSemanticTokensRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DocumentSemanticTokensResponse? { - return try await languageService.documentSemanticTokens(req) - } - - func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DocumentSemanticTokensDeltaResponse? { - return try await languageService.documentSemanticTokensDelta(req) - } - - func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DocumentSemanticTokensResponse? { - return try await languageService.documentSemanticTokensRange(req) - } - - func documentFormatting( - _ req: DocumentFormattingRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [TextEdit]? { - return try await languageService.documentFormatting(req) - } - - func documentRangeFormatting( - _ req: DocumentRangeFormattingRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [TextEdit]? { - return try await languageService.documentRangeFormatting(req) - } - - func documentOnTypeFormatting( - _ req: DocumentOnTypeFormattingRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [TextEdit]? { - return try await languageService.documentOnTypeFormatting(req) - } - - func colorPresentation( - _ req: ColorPresentationRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [ColorPresentation] { - return try await languageService.colorPresentation(req) - } - - func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { - guard let uri = req.textDocument?.uri else { - logger.error("Attempted to perform executeCommand request without an URL") - return nil - } - - let executeCommand = ExecuteCommandRequest( - command: req.command, - arguments: req.argumentsWithoutSourceKitMetadata - ) - guard let workspace = await self.workspaceForDocument(uri: uri) else { - throw ResponseError.workspaceNotOpen(uri) - } - let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language - // First, check if we have a language service that explicitly declares support for this command. - if let languageService = await languageServices(for: uri, language, in: workspace) - .first(where: { type(of: $0).builtInCommands.contains(req.command) }) - { - return try await languageService.executeCommand(executeCommand) - } - // Otherwise handle it in the primary language service. This is important to handle eg. commands in clangd, which - // are not declared as built-in commands. - return try await primaryLanguageService(for: uri, language, in: workspace).executeCommand(executeCommand) - } - - func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { - let buildSettingsUri = try ReferenceDocumentURL(from: req.uri).buildSettingsFile - - guard let workspace = await self.workspaceForDocument(uri: buildSettingsUri) else { - throw ResponseError.workspaceNotOpen(buildSettingsUri) - } - let language: Language - - // The document that provided the build settings might no longer be open, so we need to be able to infer the - // language by other means as well. - if let snapshot = try? documentManager.latestSnapshot(buildSettingsUri) { - language = snapshot.language - } else if let target = await workspace.buildServerManager.canonicalTarget(for: buildSettingsUri), - let lang = await workspace.buildServerManager.defaultLanguage(for: buildSettingsUri, in: target) - { - language = lang - } else if let lang = Language(inferredFromFileExtension: buildSettingsUri) { - language = lang - } else { - throw ResponseError.unknown("Unable to infer language for \(buildSettingsUri)") - } - - return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace).getReferenceDocument(req) - } - - func codeAction( - _ req: CodeActionRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> CodeActionRequestResponse? { - let response = try await languageService.codeAction(req) - return req.injectMetadata(toResponse: response) - } - - func codeLens( - _ req: CodeLensRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [CodeLens] { - return try await languageService.codeLens(req) - } - - func inlayHint( - _ req: InlayHintRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [InlayHint] { - return try await languageService.inlayHint(req) - } - - func documentDiagnostic( - _ req: DocumentDiagnosticsRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> DocumentDiagnosticReport { - return try await languageService.documentDiagnostic(req) - } - - /// Converts a location from the symbol index to an LSP location. - /// - /// - Parameter location: The symbol index location - /// - Returns: The LSP location - private nonisolated func indexToLSPLocation(_ location: SymbolLocation) -> Location? { - guard !location.path.isEmpty else { return nil } - return Location( - uri: location.documentUri, - range: Range( - Position( - // 1-based -> 0-based - // Note that we still use max(0, ...) as a fallback if the location is zero. - line: max(0, location.line - 1), - // Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the - // file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read. - utf16index: max(0, location.utf8Column - 1) - ) - ) - ) - } - - func declaration( - _ req: DeclarationRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> LocationsOrLocationLinksResponse? { - return try await languageService.declaration(req) - } - - /// Return the locations for jump to definition from the given `SymbolDetails`. - private func definitionLocations( - for symbol: SymbolDetails, - in uri: DocumentURI, - languageService: LanguageService - ) async throws -> [Location] { - // If this symbol is a module then generate a textual interface - if symbol.kind == .module { - // For module symbols, prefer using systemModule information if available - let moduleName: String - let groupName: String? - - if let systemModule = symbol.systemModule { - moduleName = systemModule.moduleName - groupName = systemModule.groupName - } else if let name = symbol.name { - moduleName = name - groupName = nil - } else { - return [] - } - - let interfaceLocation = try await self.definitionInInterface( - moduleName: moduleName, - groupName: groupName, - symbolUSR: nil, - originatorUri: uri, - languageService: languageService - ) - return [interfaceLocation] - } - - if symbol.isSystem ?? false, let systemModule = symbol.systemModule { - let location = try await self.definitionInInterface( - moduleName: systemModule.moduleName, - groupName: systemModule.groupName, - symbolUSR: symbol.usr, - originatorUri: uri, - languageService: languageService - ) - return [location] - } - - guard let index = await self.workspaceForDocument(uri: uri)?.index(checkedFor: .deletedFiles) else { - if let bestLocalDeclaration = symbol.bestLocalDeclaration { - return [bestLocalDeclaration] - } else { - return [] - } - } - guard let usr = symbol.usr else { return [] } - logger.info("Performing indexed jump-to-definition with USR \(usr)") - var occurrences = index.definitionOrDeclarationOccurrences(ofUSR: usr) - if symbol.isDynamic ?? true { - lazy var transitiveReceiverUsrs: [String]? = { - if let receiverUsrs = symbol.receiverUsrs { - return transitiveSubtypeClosure( - ofUsrs: receiverUsrs, - index: index - ) - } else { - return nil - } - }() - occurrences += occurrences.flatMap { - let overriddenUsrs = index.occurrences(relatedToUSR: $0.symbol.usr, roles: .overrideOf).map(\.symbol.usr) - let overriddenSymbolDefinitions = overriddenUsrs.compactMap { - index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) - } - // Only contain overrides that are children of one of the receiver types or their subtypes or extensions. - return overriddenSymbolDefinitions.filter { override in - override.relations.contains(where: { - guard $0.roles.contains(.childOf) else { - return false - } - if let transitiveReceiverUsrs, !transitiveReceiverUsrs.contains($0.symbol.usr) { - return false - } - return true - }) - } - } - } - - if occurrences.isEmpty, let bestLocalDeclaration = symbol.bestLocalDeclaration { - return [bestLocalDeclaration] - } - - return occurrences.compactMap { indexToLSPLocation($0.location) }.sorted() - } - - /// Returns the result of a `DefinitionRequest` by running a `SymbolInfoRequest`, inspecting - /// its result and doing index lookups, if necessary. - /// - /// In contrast to `definition`, this does not fall back to sending a `DefinitionRequest` to the - /// toolchain language server. - private func indexBasedDefinition( - _ req: DefinitionRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [Location] { - let symbols = try await languageService.symbolInfo( - SymbolInfoRequest( - textDocument: req.textDocument, - position: req.position - ) - ) - - let canonicalOriginatorLocation = await languageService.canonicalDeclarationPosition( - of: req.position, - in: req.textDocument.uri - ) - - // Returns `true` if `location` points to the same declaration that the definition request was initiated from. - @Sendable func isAtCanonicalOriginatorLocation(_ location: Location) async -> Bool { - guard location.uri == req.textDocument.uri, let canonicalOriginatorLocation else { - return false - } - return await languageService.canonicalDeclarationPosition(of: location.range.lowerBound, in: location.uri) - == canonicalOriginatorLocation - } - - var locations = try await symbols.asyncFlatMap { (symbol) -> [Location] in - var locations: [Location] - if let bestLocalDeclaration = symbol.bestLocalDeclaration, - !(symbol.isDynamic ?? true), - symbol.usr?.hasPrefix("s:") ?? false /* Swift symbols have USRs starting with s: */ - { - // If we have a known non-dynamic symbol within Swift, we don't need to do an index lookup. - // For non-Swift symbols, we need to perform an index lookup because the best local declaration will point to - // a header file but jump-to-definition should prefer the implementation (there's the declaration request to - // jump to the function's declaration). - locations = [bestLocalDeclaration] - } else { - locations = try await self.definitionLocations( - for: symbol, - in: req.textDocument.uri, - languageService: languageService - ) - } - - // If the symbol's location is is where we initiated rename from, also show the declarations that the symbol - // overrides. - if let location = locations.only, - let usr = symbol.usr, - let index = await workspace.index(checkedFor: .deletedFiles), - await isAtCanonicalOriginatorLocation(location) - { - let baseUSRs = index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap { - $0.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr) - } - locations += baseUSRs.compactMap { - guard let baseDeclOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) else { - return nil - } - return indexToLSPLocation(baseDeclOccurrence.location) - } - } - - return locations - } - - // Remove any duplicate locations. We might end up with duplicate locations when performing a definition request - // on eg. `MyStruct()` when no explicit initializer is declared. In this case we get two symbol infos, one for the - // declaration of the `MyStruct` type and one for the initializer, which is implicit and thus has the location of - // the `MyStruct` declaration itself. - locations = locations.unique - - // Try removing any results that would point back to the location we are currently at. This ensures that eg. in the - // following case we only show line 2 when performing jump-to-definition on `TestImpl.doThing`. - // - // ``` - // protocol TestProtocol { - // func doThing() - // } - // struct TestImpl: TestProtocol { - // func doThing() { } - // } - // ``` - // - // If this would result in no locations, don't apply the filter. This way, performing jump-to-definition in the - // middle of a function's base name takes us to the base name start, indicating that jump-to-definition was able to - // resolve the location and didn't fail. - let nonOriginatorLocations = await locations.asyncFilter { await !isAtCanonicalOriginatorLocation($0) } - if !nonOriginatorLocations.isEmpty { - locations = nonOriginatorLocations - } - return locations - } - - func definition( - _ req: DefinitionRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> LocationsOrLocationLinksResponse? { - let indexBasedResponse = try await indexBasedDefinition(req, workspace: workspace, languageService: languageService) - // If we're unable to handle the definition request using our index, see if the - // language service can handle it (e.g. clangd can provide AST based definitions). - // We are on only calling the language service's `definition` function if your index-based lookup failed. - // If this fallback request fails, its error is usually not very enlightening. For example the - // `SwiftLanguageService` will always respond with `unsupported method`. Thus, only log such a failure instead of - // returning it to the client. - if indexBasedResponse.isEmpty { - return await orLog("Fallback definition request", level: .info) { - return try await languageService.definition(req) - } - } - let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(indexBasedResponse) - return .locations(remappedLocations) - } - - /// Generate the generated interface for the given module, write it to disk and return the location to which to jump - /// to get to the definition of `symbolUSR`. - /// - /// `originatorUri` is the URI of the file from which the definition request is performed. It is used to determine the - /// compiler arguments to generate the generated interface. - func definitionInInterface( - moduleName: String, - groupName: String?, - symbolUSR: String?, - originatorUri: DocumentURI, - languageService: LanguageService - ) async throws -> Location { - // Let openGeneratedInterface handle all the logic, including checking if we're already in the right interface - let documentForBuildSettings = originatorUri.buildSettingsFile - - guard - let interfaceDetails = try await languageService.openGeneratedInterface( - document: documentForBuildSettings, - moduleName: moduleName, - groupName: groupName, - symbolUSR: symbolUSR - ) - else { - throw ResponseError.unknown("Could not generate Swift Interface for \(moduleName)") - } - let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0) - let loc = Location(uri: interfaceDetails.uri, range: Range(position)) - return loc - } - - func implementation( - _ req: ImplementationRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> LocationsOrLocationLinksResponse? { - let symbols = try await languageService.symbolInfo( - SymbolInfoRequest( - textDocument: req.textDocument, - position: req.position - ) - ) - guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else { - return nil - } - let locations = symbols.flatMap { (symbol) -> [Location] in - guard let usr = symbol.usr else { return [] } - var occurrences = index.occurrences(ofUSR: usr, roles: .baseOf) - if occurrences.isEmpty { - occurrences = index.occurrences(relatedToUSR: usr, roles: .overrideOf) - } - - return occurrences.compactMap { indexToLSPLocation($0.location) } - } - return .locations(locations.sorted()) - } - - func references( - _ req: ReferencesRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [Location] { - let symbols = try await languageService.symbolInfo( - SymbolInfoRequest( - textDocument: req.textDocument, - position: req.position - ) - ) - guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else { - return [] - } - let locations = symbols.flatMap { (symbol) -> [Location] in - guard let usr = symbol.usr else { return [] } - logger.info("Finding references for USR \(usr)") - var roles: SymbolRole = [.reference] - if req.context.includeDeclaration { - roles.formUnion([.declaration, .definition]) - } - return index.occurrences(ofUSR: usr, roles: roles).compactMap { indexToLSPLocation($0.location) } - } - return locations.unique.sorted() - } - - private func indexToLSPCallHierarchyItem( - definition: SymbolOccurrence, - index: CheckedIndex - ) -> CallHierarchyItem? { - guard let location = indexToLSPLocation(definition.location) else { - return nil - } - let name = index.fullyQualifiedName(of: definition) - let symbol = definition.symbol - return CallHierarchyItem( - name: name, - kind: symbol.kind.asLspSymbolKind(), - tags: nil, - detail: nil, - uri: location.uri, - range: location.range, - selectionRange: location.range, - // We encode usr and uri for incoming/outgoing call lookups in the implementation-specific data field - data: .dictionary([ - "usr": .string(symbol.usr), - "uri": .string(location.uri.stringValue), - ]) - ) - } - - func prepareCallHierarchy( - _ req: CallHierarchyPrepareRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [CallHierarchyItem]? { - let symbols = try await languageService.symbolInfo( - SymbolInfoRequest( - textDocument: req.textDocument, - position: req.position - ) - ) - guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else { - return nil - } - // For call hierarchy preparation we only locate the definition - let usrs = symbols.compactMap(\.usr) - - // Only return a single call hierarchy item. Returning multiple doesn't make sense because they will all have the - // same USR (because we query them by USR) and will thus expand to the exact same call hierarchy. - let callHierarchyItems = usrs.compactMap { (usr) -> CallHierarchyItem? in - guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { - return nil - } - return self.indexToLSPCallHierarchyItem(definition: definition, index: index) - }.sorted(by: { Location(uri: $0.uri, range: $0.range) < Location(uri: $1.uri, range: $1.range) }) - - // Ideally, we should show multiple symbols. But VS Code fails to display call hierarchies with multiple root items, - // failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one. - return Array(callHierarchyItems.prefix(1)) - } - - /// Extracts our implementation-specific data about a call hierarchy - /// item as encoded in `indexToLSPCallHierarchyItem`. - /// - /// - Parameter data: The opaque data structure to extract - /// - Returns: The extracted data if successful or nil otherwise - private nonisolated func extractCallHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { - guard case let .dictionary(data) = rawData, - case let .string(uriString) = data["uri"], - case let .string(usr) = data["usr"], - let uri = orLog("DocumentURI for call hierarchy item", { try DocumentURI(string: uriString) }) - else { - return nil - } - return (uri: uri, usr: usr) - } - - func incomingCalls(_ req: CallHierarchyIncomingCallsRequest) async throws -> [CallHierarchyIncomingCall]? { - guard let data = extractCallHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) - else { - return [] - } - var callableUsrs = [data.usr] - // Also show calls to the functions that this method overrides. This includes overridden class methods and - // satisfied protocol requirements. - callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in - occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr) - } - // callOccurrences are all the places that any of the USRs in callableUsrs is called. - // We also load the `calledBy` roles to get the method that contains the reference to this call. - let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .containedBy) } - .filter(\.shouldShowInCallHierarchy) - - // Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the - // function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call - // `SymbolOccurrence`s. - // This way, we can group multiple calls to `bar` within `foo` to a single item with multiple `fromRanges`. - var callersToCalls: [Symbol: [SymbolOccurrence]] = [:] - - for call in callOccurrences { - // Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a - // call to a USR in callableUSRs. In practice, this should always be a single item. - let callers = call.relations.filter { $0.roles.contains(.containedBy) }.map(\.symbol) - for caller in callers { - callersToCalls[caller, default: []].append(call) - } - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPCallHierarchyItem2( - definition: SymbolOccurrence, - index: CheckedIndex - ) -> CallHierarchyItem? { - return self.indexToLSPCallHierarchyItem(definition: definition, index: index) - } - - let calls = callersToCalls.compactMap { (caller: Symbol, calls: [SymbolOccurrence]) -> CallHierarchyIncomingCall? in - // Resolve the caller's definition to find its location - guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr) else { - return nil - } - - let locations = calls.compactMap { indexToLSPLocation2($0.location) }.sorted() - guard !locations.isEmpty else { - return nil - } - guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { - return nil - } - - return CallHierarchyIncomingCall(from: item, fromRanges: locations.map(\.range)) - } - return calls.sorted(by: { $0.from.name < $1.from.name }) - } - - func outgoingCalls(_ req: CallHierarchyOutgoingCallsRequest) async throws -> [CallHierarchyOutgoingCall]? { - guard let data = extractCallHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) - else { - return [] - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPCallHierarchyItem2( - definition: SymbolOccurrence, - index: CheckedIndex - ) -> CallHierarchyItem? { - return self.indexToLSPCallHierarchyItem(definition: definition, index: index) - } - - let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr) - let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .containedBy) } - .filter(\.shouldShowInCallHierarchy) - let calls = callOccurrences.compactMap { occurrence -> CallHierarchyOutgoingCall? in - guard occurrence.symbol.kind.isCallable else { - return nil - } - guard let location = indexToLSPLocation2(occurrence.location) else { - return nil - } - - // Resolve the callee's definition to find its location - guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { - return nil - } - - guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else { - return nil - } - - return CallHierarchyOutgoingCall(to: item, fromRanges: [location.range]) - } - return calls.sorted(by: { $0.to.name < $1.to.name }) - } - - private func indexToLSPTypeHierarchyItem( - definition: SymbolOccurrence, - moduleName: String?, - index: CheckedIndex - ) -> TypeHierarchyItem? { - let name: String - let detail: String? - - guard let location = indexToLSPLocation(definition.location) else { - return nil - } - - let symbol = definition.symbol - switch symbol.kind { - case .extension: - // Query the conformance added by this extension - let conformances = index.occurrences(relatedToUSR: symbol.usr, roles: .baseOf) - if conformances.isEmpty { - name = symbol.name - } else { - name = "\(symbol.name): \(conformances.map(\.symbol.name).sorted().joined(separator: ", "))" - } - // Add the file name and line to the detail string - if let url = location.uri.fileURL, - let basename = (try? AbsolutePath(validating: url.filePath))?.basename - { - detail = "Extension at \(basename):\(location.range.lowerBound.line + 1)" - } else if let moduleName = moduleName { - detail = "Extension in \(moduleName)" - } else { - detail = "Extension" - } - default: - name = index.fullyQualifiedName(of: definition) - detail = moduleName - } - - return TypeHierarchyItem( - name: name, - kind: symbol.kind.asLspSymbolKind(), - tags: nil, - detail: detail, - uri: location.uri, - range: location.range, - selectionRange: location.range, - // We encode usr and uri for incoming/outgoing type lookups in the implementation-specific data field - data: .dictionary([ - "usr": .string(symbol.usr), - "uri": .string(location.uri.stringValue), - ]) - ) - } - - func prepareTypeHierarchy( - _ req: TypeHierarchyPrepareRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [TypeHierarchyItem]? { - let symbols = try await languageService.symbolInfo( - SymbolInfoRequest( - textDocument: req.textDocument, - position: req.position - ) - ) - guard !symbols.isEmpty else { - return nil - } - guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else { - return nil - } - let usrs = symbols.filter { - // Only include references to type. For example, we don't want to find the type hierarchy of a constructor when - // starting the type hierarchy on `Foo()`. - // Consider a symbol a class if its kind is `nil`, eg. for a symbol returned by clang's SymbolInfo, which - // doesn't support the `kind` field. - switch $0.kind { - case .class, .enum, .interface, .struct, nil: return true - default: return false - } - }.compactMap(\.usr) - - let typeHierarchyItems = usrs.compactMap { (usr) -> TypeHierarchyItem? in - guard let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else { - return nil - } - // Filter symbols based on their kind in the index since the filter on the symbol info response might have - // returned `nil` for the kind, preventing us from doing any filtering there. - switch info.symbol.kind { - case .unknown, .macro, .function, .variable, .field, .enumConstant, .instanceMethod, .classMethod, .staticMethod, - .instanceProperty, .classProperty, .staticProperty, .constructor, .destructor, .conversionFunction, .parameter, - .concept, .commentTag: - return nil - case .module, .namespace, .namespaceAlias, .enum, .struct, .class, .protocol, .extension, .union, .typealias, - .using: - break - } - return self.indexToLSPTypeHierarchyItem( - definition: info, - moduleName: info.location.moduleName, - index: index - ) - } - .sorted(by: { $0.name < $1.name }) - - if typeHierarchyItems.isEmpty { - // When returning an empty array, VS Code fails with the following two errors. Returning `nil` works around those - // VS Code-internal errors showing up - // - MISSING provider - // - Cannot read properties of null (reading 'kind') - return nil - } - // Ideally, we should show multiple symbols. But VS Code fails to display type hierarchies with multiple root items, - // failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one. - return Array(typeHierarchyItems.prefix(1)) - } - - /// Extracts our implementation-specific data about a type hierarchy - /// item as encoded in `indexToLSPTypeHierarchyItem`. - /// - /// - Parameter data: The opaque data structure to extract - /// - Returns: The extracted data if successful or nil otherwise - private nonisolated func extractTypeHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { - guard case let .dictionary(data) = rawData, - case let .string(uriString) = data["uri"], - case let .string(usr) = data["usr"], - let uri = orLog("DocumentURI for type hierarchy item", { try DocumentURI(string: uriString) }) - else { - return nil - } - return (uri: uri, usr: usr) - } - - func supertypes(_ req: TypeHierarchySupertypesRequest) async throws -> [TypeHierarchyItem]? { - guard let data = extractTypeHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) - else { - return [] - } - - // Resolve base types - let baseOccurs = index.occurrences(relatedToUSR: data.usr, roles: .baseOf) - - // Resolve retroactive conformances via the extensions - let extensions = index.occurrences(ofUSR: data.usr, roles: .extendedBy) - let retroactiveConformanceOccurs = extensions.flatMap { occurrence -> [SymbolOccurrence] in - if occurrence.relations.count > 1 { - // When the occurrence has an `extendedBy` relation, it's an extension declaration. An extension can only extend - // a single type, so there can only be a single relation here. - logger.fault("Expected at most extendedBy relation but got \(occurrence.relations.count)") - } - guard let related = occurrence.relations.sorted().first else { - return [] - } - return index.occurrences(relatedToUSR: related.symbol.usr, roles: .baseOf) - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPTypeHierarchyItem2( - definition: SymbolOccurrence, - moduleName: String?, - index: CheckedIndex - ) -> TypeHierarchyItem? { - return self.indexToLSPTypeHierarchyItem( - definition: definition, - moduleName: moduleName, - index: index - ) - } - - // Convert occurrences to type hierarchy items - let occurs = baseOccurs + retroactiveConformanceOccurs - let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in - // Resolve the supertype's definition to find its location - guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else { - return nil - } - - return indexToLSPTypeHierarchyItem2( - definition: definition, - moduleName: definition.location.moduleName, - index: index - ) - } - return types.sorted(by: { $0.name < $1.name }) - } - - func subtypes(_ req: TypeHierarchySubtypesRequest) async throws -> [TypeHierarchyItem]? { - guard let data = extractTypeHierarchyItemData(req.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index(checkedFor: .deletedFiles) - else { - return [] - } - - // Resolve child types and extensions - let occurs = index.occurrences(ofUSR: data.usr, roles: [.baseOf, .extendedBy]) - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPLocation2(_ location: SymbolLocation) -> Location? { - return self.indexToLSPLocation(location) - } - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func indexToLSPTypeHierarchyItem2( - definition: SymbolOccurrence, - moduleName: String?, - index: CheckedIndex - ) -> TypeHierarchyItem? { - return self.indexToLSPTypeHierarchyItem( - definition: definition, - moduleName: moduleName, - index: index - ) - } - - // Convert occurrences to type hierarchy items - let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in - if occurrence.relations.count > 1 { - // An occurrence with a `baseOf` or `extendedBy` relation is an occurrence inside an inheritance clause. - // Such an occurrence can only be the source of a single type, namely the one that the inheritance clause belongs - // to. - logger.fault("Expected at most extendedBy or baseOf relation but got \(occurrence.relations.count)") - } - guard let related = occurrence.relations.sorted().first else { - return nil - } - - // Resolve the subtype's definition to find its location - guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr) else { - return nil - } - - return indexToLSPTypeHierarchyItem2( - definition: definition, - moduleName: definition.location.moduleName, - index: index - ) - } - return types.sorted { $0.name < $1.name } - } - - func synchronize(_ req: SynchronizeRequest) async throws -> VoidResponse { - if req.buildServerUpdates != nil, !self.options.hasExperimentalFeature(.synchronizeForBuildSystemUpdates) { - throw ResponseError.unknown("\(SynchronizeRequest.method).buildServerUpdates is an experimental request option") - } - if req.copyFileMap != nil, !self.options.hasExperimentalFeature(.synchronizeCopyFileMap) { - throw ResponseError.unknown("\(SynchronizeRequest.method).copyFileMap is an experimental request option") - } - for workspace in workspaces { - await workspace.synchronize(req) - } - return VoidResponse() - } - - func triggerReindex(_ req: TriggerReindexRequest) async throws -> VoidResponse { - for workspace in workspaces { - await workspace.semanticIndexManager?.scheduleReindex() - } - return VoidResponse() - } -} - -private func languageClass(for language: Language) -> [Language] { - switch language { - case .c, .cpp, .objective_c, .objective_cpp: - return [.c, .cpp, .objective_c, .objective_cpp] - case .swift: - return [.swift] - default: - return [language] - } -} - -/// Minimum supported pattern length for a `workspace/symbol` request, smaller pattern -/// strings are not queried and instead we return no results. -private let minWorkspaceSymbolPatternLength = 3 - -/// The maximum number of results to return from a `workspace/symbol` request. -private let maxWorkspaceSymbolResults = 4096 - -package typealias Diagnostic = LanguageServerProtocol.Diagnostic - -fileprivate extension CheckedIndex { - /// Take the name of containers into account to form a fully-qualified name for the given symbol. - /// This means that we will form names of nested types and type-qualify methods. - func fullyQualifiedName(of symbolOccurrence: SymbolOccurrence) -> String { - let symbol = symbolOccurrence.symbol - let containerNames = self.containerNames(of: symbolOccurrence) - guard let containerName = containerNames.last else { - // No containers, so nothing to do. - return symbol.name - } - switch symbol.language { - case .objc where symbol.kind == .instanceMethod || symbol.kind == .instanceProperty: - return "-[\(containerName) \(symbol.name)]" - case .objc where symbol.kind == .classMethod || symbol.kind == .classProperty: - return "+[\(containerName) \(symbol.name)]" - case .cxx, .c, .objc: - // C shouldn't have container names for call hierarchy and Objective-C should be covered above. - // Fall back to using the C++ notation using `::`. - return (containerNames + [symbol.name]).joined(separator: "::") - case .swift: - return (containerNames + [symbol.name]).joined(separator: ".") - } - } -} - -extension IndexSymbolKind { - func asLspSymbolKind() -> SymbolKind { - switch self { - case .class: - return .class - case .classMethod, .instanceMethod, .staticMethod: - return .method - case .instanceProperty, .staticProperty, .classProperty: - return .property - case .enum: - return .enum - case .enumConstant: - return .enumMember - case .protocol: - return .interface - case .function, .conversionFunction: - return .function - case .variable: - return .variable - case .struct: - return .struct - case .parameter: - return .typeParameter - case .module, .namespace: - return .namespace - case .field: - return .property - case .constructor: - return .constructor - case .destructor: - return .null - case .commentTag, .concept, .extension, .macro, .namespaceAlias, .typealias, .union, .unknown, .using: - return .null - } - } - - var isCallable: Bool { - switch self { - case .function, .instanceMethod, .classMethod, .staticMethod, .constructor, .destructor, .conversionFunction: - return true - case .unknown, .module, .namespace, .namespaceAlias, .macro, .enum, .struct, .protocol, .extension, .union, - .typealias, .field, .enumConstant, .parameter, .using, .concept, .commentTag, .variable, .instanceProperty, - .class, .staticProperty, .classProperty: - return false - } - } -} - -fileprivate extension SymbolOccurrence { - /// Whether this is a call-like occurrence that should be shown in the call hierarchy. - var shouldShowInCallHierarchy: Bool { - !roles.intersection([.addressOf, .call, .read, .reference, .write]).isEmpty - } -} - -/// Simple struct for pending notifications/requests, including a cancellation handler. -/// For convenience the notifications/request handlers are type erased via wrapping. -private struct NotificationRequestOperation { - let operation: () async -> Void - let cancellationHandler: (() -> Void)? -} - -/// Used to queue up notifications and requests for documents which are blocked -/// on build server operations such as fetching build settings. -/// -/// Note: This is not thread safe. Must be called from the `SourceKitLSPServer.queue`. -private struct DocumentNotificationRequestQueue { - fileprivate var queue = [NotificationRequestOperation]() - - /// Add an operation to the end of the queue. - mutating func add(operation: @escaping () async -> Void, cancellationHandler: (() -> Void)? = nil) { - queue.append(NotificationRequestOperation(operation: operation, cancellationHandler: cancellationHandler)) - } - - /// Cancel all operations in the queue. No-op for operations without a cancellation - /// handler. - mutating func cancelAll() { - for task in queue { - if let cancellationHandler = task.cancellationHandler { - cancellationHandler() - } - } - queue = [] - } -} - -/// Returns the USRs of the subtypes of `usrs` as well as their subtypes and extensions, transitively. -private func transitiveSubtypeClosure(ofUsrs usrs: [String], index: CheckedIndex) -> [String] { - var result: [String] = [] - for usr in usrs { - result.append(usr) - let directSubtypes = index.occurrences(ofUSR: usr, roles: [.baseOf, .extendedBy]).flatMap { occurrence in - occurrence.relations.filter { $0.roles.contains(.baseOf) || $0.roles.contains(.extendedBy) }.map(\.symbol.usr) - } - let transitiveSubtypes = transitiveSubtypeClosure(ofUsrs: directSubtypes, index: index) - result += transitiveSubtypes - } - return result -} - -fileprivate extension Sequence where Element: Hashable { - /// Removes all duplicate elements from the sequence, maintaining order. - var unique: [Element] { - var set = Set() - return self.filter { set.insert($0).inserted } - } -} - -fileprivate extension URL { - func isPrefix(of other: URL) -> Bool { - guard self.pathComponents.count < other.pathComponents.count else { - return false - } - return other.pathComponents[0..) - - /// Retrieve information about syntactically discovered tests from the index. - case read - - /// Reads can be concurrent and files can be indexed concurrently. But we need to wait for all files to finish - /// indexing before reading the index. - func isDependency(of other: TaskMetadata) -> Bool { - switch (self, other) { - case (.initialPopulation, _): - // The initial population need to finish before we can do anything with the task. - return true - case (_, .initialPopulation): - // Should never happen because the initial population should only be scheduled once before any other operations - // on the test index. But be conservative in case we do get an `initialPopulation` somewhere in between and use it - // as a full blocker on the queue. - return true - case (.read, .read): - // We allow concurrent reads - return false - case (.read, .index(_)): - // We allow index tasks scheduled after a read task to be be executed before the read. - // This effectively means that a `read` requires the index to be updated *at least* up to the state at which the - // read was scheduled. If more changes come in in the meantime, it is OK for the read to pick them up. This also - // ensures that reads aren't parallelization barriers. - return false - case (.index(_), .read): - // We require all index tasks scheduled before the read to be finished. - // This ensures that the index has been updated at least to the state of file at which the read was scheduled. - return true - case (.index(let lhsUris), .index(let rhsUris)): - // Technically, we should be able to allow simultaneous indexing of the same file. But conceptually the code - // becomes simpler if we don't need to think racing indexing tasks for the same file and it shouldn't make a - // performance impact in practice because if a first task indexes a file, a subsequent index task for the same - // file will realize that the index is already up-to-date based on the file's mtime and early exit. - return !lhsUris.intersection(rhsUris).isEmpty - } - } -} - -/// Data from a syntactic scan of a source file for tests. -private struct IndexedTests { - /// The tests within the source file. - let tests: [AnnotatedTestItem] - - /// The modification date of the source file when it was scanned. A file won't get re-scanned if its modification date - /// is older or the same as this date. - let sourceFileModificationDate: Date -} - -/// An in-memory syntactic index of test items within a workspace. -/// -/// The index does not get persisted to disk but instead gets rebuilt every time a workspace is opened (ie. usually when -/// sourcekit-lsp is launched). Building it takes only a few seconds, even for large projects. -actor SyntacticTestIndex { - private let languageServiceRegistry: LanguageServiceRegistry - - /// The tests discovered by the index. - private var indexedTests: [DocumentURI: IndexedTests] = [:] - - /// Files that have been removed using `removeFileForIndex`. - /// - /// We need to keep track of these files because when the files get removed, there might be an in-progress indexing - /// operation running for that file. We need to ensure that this indexing operation doesn't add the removed file - /// back to `indexTests`. - private var removedFiles: Set = [] - - /// The queue on which the index is being updated and queried. - /// - /// Tracking dependencies between tasks within this queue allows us to start indexing tasks in parallel with low - /// priority and elevate their priority once a read task comes in, which has higher priority and depends on the - /// indexing tasks to finish. - private let indexingQueue = AsyncQueue() - - init( - languageServiceRegistry: LanguageServiceRegistry, - determineTestFiles: @Sendable @escaping () async -> [DocumentURI] - ) { - self.languageServiceRegistry = languageServiceRegistry - indexingQueue.async(priority: .low, metadata: .initialPopulation) { - let testFiles = await determineTestFiles() - - // Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly - // because it keeps the number of pending items in `indexingQueue` low and adding a new task to `indexingQueue` is - // in O(number of pending tasks), since we need to scan for dependency edges to add, which would make scanning files - // be O(number of files). - // Over-subscribe the processor count in case one batch finishes more quickly than another. - let batches = testFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.activeProcessorCount * 4) - await batches.concurrentForEach { filesInBatch in - for uri in filesInBatch { - await self.rescanFileAssumingOnQueue(uri) - } - } - } - } - - private func removeFilesFromIndex(_ removedFiles: Set) { - self.removedFiles.formUnion(removedFiles) - for removedFile in removedFiles { - self.indexedTests[removedFile] = nil - } - } - - /// Called when the list of files that may contain tests is updated. - /// - /// All files that are not in the new list of test files will be removed from the index. - func listOfTestFilesDidChange(_ testFiles: [DocumentURI]) { - let removedFiles = Set(self.indexedTests.keys).subtracting(testFiles) - removeFilesFromIndex(removedFiles) - - rescanFiles(testFiles) - } - - func filesDidChange(_ events: [FileEvent]) { - var removedFiles: Set = [] - var filesToRescan: [DocumentURI] = [] - for fileEvent in events { - switch fileEvent.type { - case .created: - // We don't know if this is a potential test file. It would need to be added to the index via - // `listOfTestFilesDidChange` - break - case .changed: - filesToRescan.append(fileEvent.uri) - case .deleted: - removedFiles.insert(fileEvent.uri) - default: - logger.error("Ignoring unknown FileEvent type \(fileEvent.type.rawValue) in SyntacticTestIndex") - } - } - removeFilesFromIndex(removedFiles) - rescanFiles(filesToRescan) - } - - /// Called when a list of files was updated. Re-scans those files - private func rescanFiles(_ uris: [DocumentURI]) { - // If we scan a file again, it might have been added after being removed before. Remove it from the list of removed - // files. - removedFiles.subtract(uris) - - // If we already know that the file has an up-to-date index, avoid re-scheduling it to be indexed. This ensures - // that we don't bloat `indexingQueue` if the build server is sending us repeated `buildTarget/didChange` - // notifications. - // This check does not need to be perfect and there might be an in-progress index operation that is about to index - // the file. In that case we still schedule anothe rescan of that file and notice in `rescanFilesAssumingOnQueue` - // that the index is already up-to-date, which makes the rescan a no-op. - let uris = uris.filter { uri in - if let url = uri.fileURL, - let indexModificationDate = self.indexedTests[uri]?.sourceFileModificationDate, - let fileModificationDate = try? FileManager.default.attributesOfItem(atPath: url.filePath)[.modificationDate] - as? Date, - indexModificationDate >= fileModificationDate - { - return false - } - return true - } - - guard !uris.isEmpty else { - return - } - - logger.info( - "Syntactically scanning \(uris.count) files for tests: \(uris.map(\.arbitrarySchemeURL.lastPathComponent).joined(separator: ", "))" - ) - - // Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly - // because it keeps the number of pending items in `indexingQueue` low and adding a new task to `indexingQueue` is - // in O(number of pending tasks), since we need to scan for dependency edges to add, which would make scanning files - // be O(number of files). - // Over-subscribe the processor count in case one batch finishes more quickly than another. - let batches = uris.partition(intoNumberOfBatches: ProcessInfo.processInfo.activeProcessorCount * 4) - for batch in batches { - self.indexingQueue.async(priority: .low, metadata: .index(Set(batch))) { - for uri in batch { - await self.rescanFileAssumingOnQueue(uri) - } - } - } - } - - /// Re-scans a single file. - /// - /// - Important: This method must be called in a task that is executing on `indexingQueue`. - private func rescanFileAssumingOnQueue(_ uri: DocumentURI) async { - guard let url = uri.fileURL else { - logger.log("Not indexing \(uri.forLogging) for tests because it is not a file URL") - return - } - if Task.isCancelled { - return - } - guard !removedFiles.contains(uri) else { - return - } - guard FileManager.default.fileExists(at: url) else { - // File no longer exists. Probably deleted since we scheduled it for indexing. Nothing to worry about. - logger.info("Not indexing \(uri.forLogging) for tests because it does not exist") - return - } - guard - let fileModificationDate = try? FileManager.default.attributesOfItem(atPath: url.filePath)[.modificationDate] - as? Date - else { - logger.fault("Not indexing \(uri.forLogging) for tests because the modification date could not be determined") - return - } - if let indexModificationDate = self.indexedTests[uri]?.sourceFileModificationDate, - indexModificationDate >= fileModificationDate - { - // Index already up to date. - return - } - if Task.isCancelled { - return - } - guard let language = Language(inferredFromFileExtension: uri) else { - logger.log("Not indexing \(uri.forLogging) because the language service could not be inferred") - return - } - let testItems = await languageServiceRegistry.languageServices(for: language).asyncFlatMap { - await $0.syntacticTestItems(in: uri) - } - - guard !removedFiles.contains(uri) else { - // Check whether the file got removed while we were scanning it for tests. If so, don't add it back to - // `indexedTests`. - return - } - self.indexedTests[uri] = IndexedTests(tests: testItems, sourceFileModificationDate: fileModificationDate) - } - - /// Gets all the tests in the syntactic index. - /// - /// This waits for any pending document updates to be indexed before returning a result. - nonisolated func tests() async -> [AnnotatedTestItem] { - let readTask = indexingQueue.async(metadata: .read) { - return await self.indexedTests.values.flatMap { $0.tests } - } - return await readTask.value - } -} diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift deleted file mode 100644 index 5aaf939d5..000000000 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ /dev/null @@ -1,599 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import BuildServerIntegration -import Foundation -package import IndexStoreDB -import LanguageServerProtocol -import SKLogging -import SemanticIndex -import SwiftExtensions - -package enum TestStyle { - package static let xcTest = "XCTest" - package static let swiftTesting = "swift-testing" -} - -fileprivate extension SymbolOccurrence { - /// Assuming that this is a symbol occurrence returned by the index, return whether it can constitute the definition - /// of a test case. - /// - /// The primary intention for this is to filter out references to test cases and extension declarations of test cases. - /// The latter is important to filter so we don't include extension declarations for the derived `DiscoveredTests` - /// files on non-Darwin platforms. - var canBeTestDefinition: Bool { - guard roles.contains(.definition) else { - return false - } - guard symbol.kind == .class || symbol.kind == .instanceMethod else { - return false - } - return true - } -} - -/// Find the innermost range of a document symbol that contains the given position. -private func findInnermostSymbolRange( - containing position: Position, - documentSymbolsResponse: DocumentSymbolResponse -) -> Range? { - switch documentSymbolsResponse { - case .documentSymbols(let documentSymbols): - return findInnermostSymbolRange(containing: position, documentSymbols: documentSymbols) - case .symbolInformation(let symbolInformation): - return findInnermostSymbolRange(containing: position, symbolInformation: symbolInformation) - } -} - -private func findInnermostSymbolRange( - containing position: Position, - documentSymbols: [DocumentSymbol] -) -> Range? { - for documentSymbol in documentSymbols where documentSymbol.range.contains(position) { - if let children = documentSymbol.children, - let rangeOfChild = findInnermostSymbolRange( - containing: position, - documentSymbolsResponse: .documentSymbols(children) - ) - { - // If a child contains the position, prefer that because it's more specific. - return rangeOfChild - } - return documentSymbol.range - } - return nil -} - -/// Return the smallest range in `symbolInformation` containing `position`. -private func findInnermostSymbolRange( - containing position: Position, - symbolInformation symbolInformationArray: [SymbolInformation] -) -> Range? { - var bestRange: Range? = nil - for symbolInformation in symbolInformationArray where symbolInformation.location.range.contains(position) { - let range = symbolInformation.location.range - if bestRange == nil || (bestRange!.lowerBound < range.lowerBound && range.upperBound < bestRange!.upperBound) { - bestRange = range - } - } - return bestRange -} - -extension SourceKitLSPServer { - /// Converts a flat list of test symbol occurrences to a hierarchical `TestItem` array, inferring the hierarchical - /// structure from `childOf` relations between the symbol occurrences. - /// - /// `resolvePositions` resolves the position of a test to a `Location` that is effectively a range. This allows us to - /// provide ranges for the test cases in source code instead of only the test's location that we get from the index. - private func testItems( - for testSymbolOccurrences: [SymbolOccurrence], - index: CheckedIndex?, - resolveLocation: (DocumentURI, Position) -> Location - ) -> [AnnotatedTestItem] { - // Arrange tests by the USR they are contained in. This allows us to emit test methods as children of test classes. - // `occurrencesByParent[nil]` are the root test symbols that aren't a child of another test symbol. - var occurrencesByParent: [String?: [SymbolOccurrence]] = [:] - - var testSymbolUsrs = Set(testSymbolOccurrences.map(\.symbol.usr)) - - // Gather any extension declarations that contains tests and add them to `occurrencesByParent` so we can properly - // arrange their test items as the extension's children. - for testSymbolOccurrence in testSymbolOccurrences { - for parentSymbol in testSymbolOccurrence.relations.filter({ $0.roles.contains(.childOf) }).map(\.symbol) { - guard parentSymbol.kind == .extension else { - continue - } - guard let definition = index?.primaryDefinitionOrDeclarationOccurrence(ofUSR: parentSymbol.usr) else { - logger.fault("Unable to find primary definition of extension '\(parentSymbol.usr)' containing tests") - continue - } - testSymbolUsrs.insert(parentSymbol.usr) - occurrencesByParent[nil, default: []].append(definition) - } - } - - for testSymbolOccurrence in testSymbolOccurrences { - let childOfUsrs = testSymbolOccurrence.relations - .filter { $0.roles.contains(.childOf) }.map(\.symbol.usr).filter { testSymbolUsrs.contains($0) } - if childOfUsrs.count > 1 { - logger.fault( - "Test symbol \(testSymbolOccurrence.symbol.usr) is child or multiple symbols: \(childOfUsrs.joined(separator: ", "))" - ) - } - occurrencesByParent[childOfUsrs.sorted().first, default: []].append(testSymbolOccurrence) - } - - /// Returns a test item for the given `testSymbolOccurrence`. - /// - /// Also includes test items for all tests that are children of this test. - /// - /// `context` is used to build the test's ID. It is an array containing the names of all parent symbols. These will - /// be joined with the test symbol's name using `/` to form the test ID. The test ID can be used to run an - /// individual test. - func testItem( - for testSymbolOccurrence: SymbolOccurrence, - documentManager: DocumentManager, - context: [String] - ) -> AnnotatedTestItem { - let symbolPosition: Position - if let snapshot = try? documentManager.latestSnapshot( - testSymbolOccurrence.location.documentUri - ) { - symbolPosition = snapshot.position(of: testSymbolOccurrence.location) - } else { - // Technically, we always need to convert UTF-8 columns to UTF-16 columns, which requires reading the file. - // In practice, they are almost always the same. - // We chose to avoid hitting the file system even if it means that we might report an incorrect column. - symbolPosition = Position( - line: testSymbolOccurrence.location.line - 1, // 1-based -> 0-based - utf16index: testSymbolOccurrence.location.utf8Column - 1 - ) - } - let id = (context + [testSymbolOccurrence.symbol.name]).joined(separator: "/") - let location = resolveLocation(testSymbolOccurrence.location.documentUri, symbolPosition) - - let children = - occurrencesByParent[testSymbolOccurrence.symbol.usr, default: []] - .sorted() - .map { - testItem(for: $0, documentManager: documentManager, context: context + [testSymbolOccurrence.symbol.name]) - } - return AnnotatedTestItem( - testItem: TestItem( - id: id, - label: testSymbolOccurrence.symbol.name, - disabled: false, - style: TestStyle.xcTest, - location: location, - children: children.map(\.testItem), - tags: [] - ), - isExtension: testSymbolOccurrence.symbol.kind == .extension - ) - } - - let documentManager = self.documentManager - return occurrencesByParent[nil, default: []] - .sorted() - .map { testItem(for: $0, documentManager: documentManager, context: []) } - } - - /// Return all the tests in the given workspace. - /// - /// This merges tests from the semantic index, the syntactic index and in-memory file states. - /// - /// The returned list of tests is not sorted. It should be sorted before being returned to the editor. - private func tests(in workspace: Workspace) async -> [AnnotatedTestItem] { - // If files have recently been added to the workspace (which is communicated by a `workspace/didChangeWatchedFiles` - // notification, wait these changes to be reflected in the build server so we can include the updated files in the - // tests. - await workspace.buildServerManager.waitForUpToDateBuildGraph() - - // Gather all tests classes and test methods. We include test from different sources: - // - For all files that have been not been modified since they were last indexed in the semantic index, include - // XCTests from the semantic index. - // - For all files that have been modified since the last semantic index but that don't have any in-memory - // modifications (ie. modifications that the user has made in the editor but not saved), include XCTests from - // the syntactic test index - // - For all files that don't have any in-memory modifications, include swift-testing tests from the syntactic test - // index. - // - All files that have in-memory modifications are syntactically scanned for tests here. - let index = await workspace.index(checkedFor: .inMemoryModifiedFiles(documentManager)) - - // TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed - func documentManagerHasInMemoryModifications(_ uri: DocumentURI) -> Bool { - return documentManager.fileHasInMemoryModifications(uri) - } - - let snapshotsWithInMemoryState = documentManager.openDocuments.filter { uri in - // Use the index to check for in-memory modifications so we can re-use its cache. If no index exits, ask the - // document manager directly. - if let index { - return index.fileHasInMemoryModifications(uri) - } else { - return documentManagerHasInMemoryModifications(uri) - } - }.compactMap { uri in - orLog("Getting snapshot of open document") { - try documentManager.latestSnapshot(uri) - } - } - - let testsFromFilesWithInMemoryState = await snapshotsWithInMemoryState.concurrentMap { - (snapshot) -> [AnnotatedTestItem] in - // When secondary language services can provide tests, we need to query them for tests as well. For now there is - // too much overhead associated with calling `documentTestsWithoutMergingExtensions` for language services that - // don't have any test discovery functionality. - return await orLog("Getting document tests for \(snapshot.uri)") { - try await self.documentTestsWithoutMergingExtensions( - DocumentTestsRequest(textDocument: TextDocumentIdentifier(snapshot.uri)), - workspace: workspace, - languageService: self.primaryLanguageService(for: snapshot.uri, snapshot.language, in: workspace) - ) - } ?? [] - }.flatMap { $0 } - - let semanticTestSymbolOccurrences = index?.unitTests().filter { return $0.canBeTestDefinition } ?? [] - - let testsFromSyntacticIndex = await workspace.syntacticTestIndex.tests() - let testsFromSemanticIndex = testItems( - for: semanticTestSymbolOccurrences, - index: index, - resolveLocation: { uri, position in Location(uri: uri, range: Range(position)) } - ) - let filesWithTestsFromSemanticIndex = Set(testsFromSemanticIndex.map(\.testItem.location.uri)) - - let indexOnlyDiscardingDeletedFiles = await workspace.index(checkedFor: .deletedFiles) - - let syntacticTestsToInclude = - testsFromSyntacticIndex - .compactMap { (item) -> AnnotatedTestItem? in - let testItem = item.testItem - if testItem.style == TestStyle.swiftTesting { - // Swift-testing tests aren't part of the semantic index. Always include them. - return item - } - if filesWithTestsFromSemanticIndex.contains(testItem.location.uri) { - // If we have an semantic tests from this file, then the semantic index is up-to-date for this file. We thus - // don't need to include results from the syntactic index. - return nil - } - if snapshotsWithInMemoryState.contains(where: { $0.uri == testItem.location.uri }) { - // If the file has been modified in the editor, the syntactic index (which indexes on-disk files) is no longer - // up-to-date. Include the tests from `testsFromFilesWithInMemoryState`. - return nil - } - if index?.hasAnyUpToDateUnit(for: testItem.location.uri) ?? false { - // We don't have a test for this file in the semantic index but an up-to-date unit file. This means that the - // index is up-to-date and has more knowledge that identifies a `TestItem` as not actually being a test, eg. - // because it starts with `test` but doesn't appear in a class inheriting from `XCTestCase`. - return nil - } - // Filter out any test items that we know aren't actually tests based on the semantic index. - // This might call `symbols(inFilePath:)` multiple times if there are multiple top-level test items (ie. - // XCTestCase subclasses, swift-testing handled above) for the same file. In practice test files usually contain - // a single XCTestCase subclass, so caching doesn't make sense here. - // Also, this is only called for files containing test cases but for which the semantic index is out-of-date. - if let filtered = testItem.filterUsing( - semanticSymbols: indexOnlyDiscardingDeletedFiles?.symbols(inFilePath: testItem.location.uri.pseudoPath) - ) { - return AnnotatedTestItem(testItem: filtered, isExtension: item.isExtension) - } - return nil - } - - // We don't need to sort the tests here because they will get - return testsFromSemanticIndex + syntacticTestsToInclude + testsFromFilesWithInMemoryState - } - - func workspaceTests(_ req: WorkspaceTestsRequest) async throws -> [TestItem] { - return await self.workspaces - .concurrentMap { await self.tests(in: $0).prefixTestsWithModuleName(workspace: $0) } - .flatMap { $0 } - .sorted { $0.testItem.location < $1.testItem.location } - .mergingTestsInExtensions() - .deduplicatingIds() - } - - func documentTests( - _ req: DocumentTestsRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [TestItem] { - return try await documentTestsWithoutMergingExtensions(req, workspace: workspace, languageService: languageService) - .prefixTestsWithModuleName(workspace: workspace) - .mergingTestsInExtensions() - .deduplicatingIds() - } - - private func documentTestsWithoutMergingExtensions( - _ req: DocumentTestsRequest, - workspace: Workspace, - languageService: LanguageService - ) async throws -> [AnnotatedTestItem] { - let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri) - let mainFileUri = await workspace.buildServerManager.mainFile( - for: req.textDocument.uri, - language: snapshot.language - ) - - let syntacticTests = try await languageService.syntacticDocumentTests(for: req.textDocument.uri, in: workspace) - - // We `syntacticDocumentTests` returns `nil`, it indicates that it doesn't support syntactic test discovery. - // In that case, the semantic index is the only source of tests we have and we thus want to show tests from the - // semantic index, even if they are out-of-date. The alternative would be showing now tests after an edit to a file. - let indexCheckLevel: IndexCheckLevel = - syntacticTests == nil ? .deletedFiles : .inMemoryModifiedFiles(documentManager) - - if let index = await workspace.index(checkedFor: indexCheckLevel) { - var syntacticSwiftTestingTests: [AnnotatedTestItem] { - syntacticTests?.filter { $0.testItem.style == TestStyle.swiftTesting } ?? [] - } - - let testSymbols = - index.unitTests(referencedByMainFiles: [mainFileUri.pseudoPath]) - .filter { $0.canBeTestDefinition } - - if !testSymbols.isEmpty { - let documentSymbols = await orLog("Getting document symbols for test ranges") { - try await languageService.documentSymbol(DocumentSymbolRequest(textDocument: req.textDocument)) - } - - // We have test symbols from the semantic index. Return them but also include the syntactically discovered - // swift-testing tests, which aren't part of the semantic index. - return testItems( - for: testSymbols, - index: index, - resolveLocation: { uri, position in - if uri == snapshot.uri, let documentSymbols, - let range = findInnermostSymbolRange(containing: position, documentSymbolsResponse: documentSymbols) - { - return Location(uri: uri, range: range) - } - return Location(uri: uri, range: Range(position)) - } - ) + syntacticSwiftTestingTests - } - if index.hasAnyUpToDateUnit(for: mainFileUri) { - // The semantic index is up-to-date and doesn't contain any tests. We don't need to do a syntactic fallback for - // XCTest. We do still need to return swift-testing tests which don't have a semantic index. - return syntacticSwiftTestingTests - } - } - // We don't have any up-to-date semantic index entries for this file. Syntactically look for tests. - return syntacticTests ?? [] - } -} - -extension TestItem { - /// Use out-of-date semantic information to filter syntactic symbols. - /// - /// If the syntactic index found a test item, check if the semantic index knows about a symbol with that name. If it - /// does and that item is not marked as a test symbol, we can reasonably assume that this item still looks like a test - /// but is semantically known to not be a test. It will thus get filtered out. - /// - /// `semanticSymbols` should be all the symbols in the source file that this `TestItem` occurs in, retrieved using - /// `symbols(inFilePath:)` from the index. - fileprivate func filterUsing(semanticSymbols: [Symbol]?) -> TestItem? { - guard let semanticSymbols else { - return self - } - // We only check if we know of any symbol with the test item's name in this file. We could try to incorporate - // structure here (ie. look for a method within a class) but that makes the index lookup more difficult and in - // practice it is very unlikely that a test file will have two symbols with the same name, one of which is marked - // as a unit test while the other one is not. - let semanticSymbolsWithName = semanticSymbols.filter { $0.name == self.label } - if !semanticSymbolsWithName.isEmpty, - semanticSymbolsWithName.allSatisfy({ !$0.properties.contains(.unitTest) }) - { - return nil - } - var test = self - test.children = test.children.compactMap { $0.filterUsing(semanticSymbols: semanticSymbols) } - return test - } -} - -extension AnnotatedTestItem { - /// Use out-of-date semantic information to filter syntactic symbols. - /// - /// Delegates to the `TestItem`'s `filterUsing(semanticSymbols:)` method to perform the filtering. - package func filterUsing(semanticSymbols: [Symbol]?) -> AnnotatedTestItem? { - guard let testItem = self.testItem.filterUsing(semanticSymbols: semanticSymbols) else { - return nil - } - var test = self - test.testItem = testItem - return test - } -} - -fileprivate extension [AnnotatedTestItem] { - /// When the test scanners discover tests in extensions they are captured in their own parent `TestItem`, not the - /// `TestItem` generated from the class/struct's definition. This is largely because of the syntatic nature of the - /// test scanners as they are today, which only know about tests within the context of the current file. Extensions - /// defined in separate files must be organized in their own `TestItem` since at the time of their creation there - /// isn't enough information to connect them back to the tests defined in the main type definition. - /// - /// This is a more syntatic than semantic view of the `TestItem` hierarchy than the end user likely wants. - /// If we think of the enclosing class or struct as the test suite, then extensions on that class or struct should be - /// additions to that suite, just like extensions on types are, from the user's perspective, transparently added to - /// their type. - /// - /// This method walks the `AnnotatedTestItem` tree produced by the test scanners and merges in the tests defined in - /// extensions into the final `TestItem`s that represent the type definition. - /// - /// This causes extensions to be merged into their type's definition if the type's definition exists in the list of - /// test items. If the type's definition is not a test item in this collection, the first extension of that type will - /// be used as the primary test location. - /// - /// For example if there are two files - /// - /// FileA.swift - /// ```swift - /// @Suite struct MyTests { - /// @Test func oneIsTwo {} - /// } - /// ``` - /// - /// FileB.swift - /// ```swift - /// extension MyTests { - /// @Test func twoIsThree() {} - /// } - /// ``` - /// - /// Then `workspace/tests` will return - /// - `MyTests` (FileA.swift:1) - /// - `oneIsTwo` - /// - `twoIsThree` - /// - /// And `textDocument/tests` for FileB.swift will return - /// - `MyTests` (FileB.swift:1) - /// - `twoIsThree` - /// - /// A node's parent is identified by the node's ID with the last component dropped. - func mergingTestsInExtensions() -> [TestItem] { - var itemDict: [String: AnnotatedTestItem] = [:] - for item in self { - let id = item.testItem.id - if var rootItem = itemDict[id] { - // If we've encountered an extension first, and this is the - // type declaration, then use the type declaration TestItem - // as the root item. - if rootItem.isExtension && !item.isExtension { - var newItem = item - newItem.testItem.children += rootItem.testItem.children - rootItem = newItem - } else { - rootItem.testItem.children += item.testItem.children - } - - // If this item shares an ID with a sibling and both are leaf - // test items, store it by its disambiguated id to ensure we - // don't overwrite the existing element. - if rootItem.testItem.children.isEmpty && item.testItem.children.isEmpty { - itemDict[item.testItem.ambiguousTestDifferentiator] = item - } else { - itemDict[id] = rootItem - } - } else { - itemDict[id] = item - } - } - - if itemDict.isEmpty { - return [] - } - - var mergedIds = Set() - for item in self { - let id = item.testItem.id - let parentID = id.components(separatedBy: "/").dropLast().joined(separator: "/") - // If the parent exists, add the current item to its children and remove it from the root - if var parent = itemDict[parentID] { - parent.testItem.children.append(item.testItem) - mergedIds.insert(parent.testItem.id) - itemDict[parent.testItem.id] = parent - itemDict[id] = nil - } - } - - // Sort the tests by location, prioritizing TestItems not in extensions. - let sortedItems = itemDict.values - .sorted { ($0.isExtension != $1.isExtension) ? !$0.isExtension : ($0.testItem.location < $1.testItem.location) } - - let result = sortedItems.map { - guard !$0.testItem.children.isEmpty, mergedIds.contains($0.testItem.id) else { - return $0.testItem - } - var newItem = $0.testItem - newItem.children = newItem.children - .map { AnnotatedTestItem(testItem: $0, isExtension: false) } - .mergingTestsInExtensions() - return newItem - } - return result - } - - func prefixTestsWithModuleName(workspace: Workspace) async -> Self { - return await self.asyncMap({ - return AnnotatedTestItem( - testItem: await $0.testItem.prefixIDWithModuleName(workspace: workspace), - isExtension: $0.isExtension - ) - }) - } -} - -fileprivate extension [TestItem] { - /// If multiple testItems share the same ID we add more context to make it unique. - /// Two tests can share the same ID when two swift testing tests accept - /// arguments of different types, i.e: - /// ``` - /// @Test(arguments: [1,2,3]) func foo(_ x: Int) {} - /// @Test(arguments: ["a", "b", "c"]) func foo(_ x: String) {} - /// ``` - /// - /// or when tests are in separate files but don't conflict because they are marked - /// private, i.e: - /// ``` - /// File1.swift: @Test private func foo() {} - /// File2.swift: @Test private func foo() {} - /// ``` - /// - /// If we encounter one of these cases, we need to deduplicate the ID - /// by appending `/filename:filename:lineNumber`. - func deduplicatingIds() -> [TestItem] { - var idCounts: [String: Int] = [:] - for element in self where element.children.isEmpty { - idCounts[element.id, default: 0] += 1 - } - - return self.map { - var newItem = $0 - newItem.children = newItem.children.deduplicatingIds() - if idCounts[newItem.id, default: 0] > 1 { - newItem.id = newItem.ambiguousTestDifferentiator - } - return newItem - } - } -} - -extension TestItem { - /// A fully qualified name to disambiguate identical TestItem IDs. - /// This matches the IDs produced by `swift test list` when there are - /// tests that cannot be disambiguated by their simple ID. - fileprivate var ambiguousTestDifferentiator: String { - let filename = self.location.uri.arbitrarySchemeURL.lastPathComponent - let position = location.range.lowerBound - // Lines and columns start at 1. - // swift-testing tests start from _after_ the @ symbol in @Test, so we need to add an extra column. - // see https://github.com/swiftlang/swift-testing/blob/cca6de2be617aded98ecdecb0b3b3a81eec013f3/Sources/TestingMacros/Support/AttributeDiscovery.swift#L153 - let columnOffset = self.style == TestStyle.swiftTesting ? 2 : 1 - return "\(self.id)/\(filename):\(position.line + 1):\(position.utf16index + columnOffset)" - } - - fileprivate func prefixIDWithModuleName(workspace: Workspace) async -> TestItem { - guard let canonicalTarget = await workspace.buildServerManager.canonicalTarget(for: self.location.uri), - let moduleName = await workspace.buildServerManager.moduleName(for: self.location.uri, in: canonicalTarget) - else { - return self - } - - var newTest = self - newTest.id = "\(moduleName).\(newTest.id)" - newTest.children = await newTest.children.asyncMap({ await $0.prefixIDWithModuleName(workspace: workspace) }) - return newTest - } -} diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift deleted file mode 100644 index d1a28cbcd..000000000 --- a/Sources/SourceKitLSP/Workspace.swift +++ /dev/null @@ -1,539 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -package import BuildServerProtocol -import Foundation -import IndexStoreDB -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SKOptions -package import SemanticIndex -import SwiftExtensions -import TSCExtensions -import ToolchainRegistry - -import struct TSCBasic.AbsolutePath -import struct TSCBasic.RelativePath - -/// Actor that caches realpaths for `sourceFilesWithSameRealpath`. -fileprivate actor SourceFilesWithSameRealpathInferrer { - private let buildServerManager: BuildServerManager - private var realpathCache: [DocumentURI: DocumentURI] = [:] - - init(buildServerManager: BuildServerManager) { - self.buildServerManager = buildServerManager - } - - private func realpath(of uri: DocumentURI) -> DocumentURI { - if let cached = realpathCache[uri] { - return cached - } - let value = uri.symlinkTarget ?? uri - realpathCache[uri] = value - return value - } - - /// Returns the URIs of all source files in the project that have the same realpath as a document in `documents` but - /// are not in `documents`. - /// - /// This is useful in the following scenario: A project has target A containing A.swift an target B containing B.swift - /// B.swift is a symlink to A.swift. When A.swift is modified, both the dependencies of A and B need to be marked as - /// having an out-of-date preparation status, not just A. - package func sourceFilesWithSameRealpath(as documents: [DocumentURI]) async -> [DocumentURI] { - let realPaths = Set(documents.map { realpath(of: $0) }) - return await orLog("Determining source files with same realpath") { - var result: [DocumentURI] = [] - let filesAndDirectories = try await buildServerManager.sourceFiles(includeNonBuildableFiles: true) - for file in filesAndDirectories.keys { - if realPaths.contains(realpath(of: file)) && !documents.contains(file) { - result.append(file) - } - } - return result - } ?? [] - } - - func filesDidChange(_ events: [FileEvent]) { - for event in events { - realpathCache[event.uri] = nil - } - } -} - -/// Create an index instance based on the given options and response from the build server. -func createIndex( - initializationData: SourceKitInitializeBuildResponseData?, - mainFilesChangedCallback: @escaping @Sendable () async -> Void, - rootUri: DocumentURI?, - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions, - hooks: Hooks, -) async -> UncheckedIndex? { - let indexOptions = options.indexOrDefault - let indexStorePath: URL? = - if let indexStorePath = initializationData?.indexStorePath { - URL(fileURLWithPath: indexStorePath, relativeTo: rootUri?.fileURL) - } else { - nil - } - let indexDatabasePath: URL? = - if let indexDatabasePath = initializationData?.indexDatabasePath { - URL(fileURLWithPath: indexDatabasePath, relativeTo: rootUri?.fileURL) - } else { - nil - } - let supportsOutputPaths = initializationData?.outputPathsProvider ?? false - if let indexStorePath, let indexDatabasePath, let libPath = await toolchainRegistry.default?.libIndexStore { - do { - let indexDelegate = SourceKitIndexDelegate { - await mainFilesChangedCallback() - } - let prefixMappings = - (indexOptions.indexPrefixMap ?? [:]) - .map { PathMapping(original: $0.key, replacement: $0.value) } - .sorted { - // Fixes an issue where remapPath might match the shortest path first when multiple common prefixes exist - // Sort by path length descending to prioritize more specific paths; - // when lengths are equal, sort lexicographically in ascending order - if $0.original.count != $1.original.count { - return $0.original.count > $1.original.count // Prefer longer paths (more specific) - } else { - return $0.original < $1.original // Alphabetical sort when lengths are equal, ensures stable ordering - } - } - if let indexInjector = hooks.indexHooks.indexInjector { - let indexStoreDB = try await indexInjector.createIndex( - storePath: indexStorePath, - databasePath: indexDatabasePath, - indexStoreLibraryPath: libPath, - delegate: indexDelegate, - prefixMappings: prefixMappings - ) - return UncheckedIndex(indexStoreDB, usesExplicitOutputPaths: await indexInjector.usesExplicitOutputPaths) - } else { - let indexStoreDB = try IndexStoreDB( - storePath: indexStorePath.filePath, - databasePath: indexDatabasePath.filePath, - library: IndexStoreLibrary(dylibPath: libPath.filePath), - delegate: indexDelegate, - useExplicitOutputUnits: supportsOutputPaths, - prefixMappings: prefixMappings - ) - logger.debug( - "Opened IndexStoreDB at \(indexDatabasePath) with store path \(indexStorePath) with explicit output files \(supportsOutputPaths)" - ) - return UncheckedIndex(indexStoreDB, usesExplicitOutputPaths: supportsOutputPaths) - } - } catch { - logger.error("Failed to open IndexStoreDB: \(error.localizedDescription)") - return nil - } - } else { - return nil - } -} - -/// Represents the configuration and state of a project or combination of projects being worked on -/// together. -/// -/// In LSP, this represents the per-workspace state that is typically only available after the -/// "initialize" request has been made. -/// -/// Typically a workspace is contained in a root directory. -package final class Workspace: Sendable, BuildServerManagerDelegate { - /// The ``SourceKitLSPServer`` instance that created this `Workspace`. - private(set) weak nonisolated(unsafe) var sourceKitLSPServer: SourceKitLSPServer? { - didSet { - preconditionFailure("sourceKitLSPServer must not be modified. It is only a var because it is weak") - } - } - - /// The root directory of the workspace. - /// - /// `nil` when SourceKit-LSP is launched without a workspace (ie. no workspace folder or rootURI). - package let rootUri: DocumentURI? - - /// Tracks dynamically registered server capabilities as well as the client's capabilities. - package let capabilityRegistry: CapabilityRegistry - - /// The build server manager to use for documents in this workspace. - package let buildServerManager: BuildServerManager - - private let sourceFilesWithSameRealpathInferrer: SourceFilesWithSameRealpathInferrer - - let options: SourceKitLSPOptions - - /// The source code index, if available. - /// - /// Usually a checked index (retrieved using `index(checkedFor:)`) should be used instead of the unchecked index. - private var uncheckedIndex: UncheckedIndex? { - get async { - return await buildServerManager.mainFilesProvider(as: UncheckedIndex.self) - } - } - - /// The index that syntactically scans the workspace for tests. - let syntacticTestIndex: SyntacticTestIndex - - /// Language service for an open document, if available. - private let languageServices: ThreadSafeBox<[DocumentURI: [LanguageService]]> = ThreadSafeBox(initialValue: [:]) - - /// The task that constructs the `SemanticIndexManager`, which keeps track of whose file's index is up-to-date in the - /// workspace and schedules indexing and preparation tasks for files with out-of-date index. - /// - /// This is a task because we need to wait for build server initialization to construct the `SemanticIndexManager` so - /// that we know the index store and indexstore-db path. Since external build servers may take a while to initialize, - /// we don't want to block the creation of a `Workspace` and thus all syntactic functionality until we have received - /// the build server initialization response. - /// - /// `nil` if background indexing is not enabled. - package let semanticIndexManagerTask: Task - - package var semanticIndexManager: SemanticIndexManager? { - get async { - await semanticIndexManagerTask.value - } - } - - /// If the index uses explicit output paths, the queue on which we update the explicit output paths. - /// - /// The reason we perform these update on a queue is that we can wait for all of them to finish when polling the - /// index. - private let indexUnitOutputPathsUpdateQueue = AsyncQueue() - - private init( - sourceKitLSPServer: SourceKitLSPServer, - rootUri: DocumentURI?, - capabilityRegistry: CapabilityRegistry, - options: SourceKitLSPOptions, - hooks: Hooks, - buildServerManager: BuildServerManager, - indexTaskScheduler: TaskScheduler - ) async { - self.sourceKitLSPServer = sourceKitLSPServer - self.rootUri = rootUri - self.capabilityRegistry = capabilityRegistry - self.options = options - self.buildServerManager = buildServerManager - self.sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer( - buildServerManager: buildServerManager - ) - self.semanticIndexManagerTask = Task { - if options.backgroundIndexingOrDefault, - let uncheckedIndex = await buildServerManager.mainFilesProvider(as: UncheckedIndex.self), - await buildServerManager.initializationData?.prepareProvider ?? false - { - let semanticIndexManager = SemanticIndexManager( - index: uncheckedIndex, - buildServerManager: buildServerManager, - updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, - hooks: hooks.indexHooks, - indexTaskScheduler: indexTaskScheduler, - logMessageToIndexLog: { [weak sourceKitLSPServer] in - sourceKitLSPServer?.logMessageToIndexLog(message: $0, type: $1, structure: $2) - }, - indexTasksWereScheduled: { [weak sourceKitLSPServer] in - sourceKitLSPServer?.indexProgressManager.indexTasksWereScheduled(count: $0) - }, - indexProgressStatusDidChange: { [weak sourceKitLSPServer] in - sourceKitLSPServer?.indexProgressManager.indexProgressStatusDidChange() - } - ) - await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles( - indexFilesWithUpToDateUnit: false - ) - return semanticIndexManager - } else { - return nil - } - } - // Trigger an initial population of `syntacticTestIndex`. - self.syntacticTestIndex = SyntacticTestIndex( - languageServiceRegistry: sourceKitLSPServer.languageServiceRegistry, - determineTestFiles: { - await orLog("Getting list of test files for initial syntactic index population") { - try await buildServerManager.testFiles() - } ?? [] - } - ) - } - - /// Creates a workspace for a given root `DocumentURI`, inferring the `ExternalWorkspace` if possible. - /// - /// - Parameters: - /// - url: The root directory of the workspace, which must be a valid path. - /// - clientCapabilities: The client capabilities provided during server initialization. - /// - toolchainRegistry: The toolchain registry. - convenience init( - sourceKitLSPServer: SourceKitLSPServer, - documentManager: DocumentManager, - rootUri: DocumentURI?, - capabilityRegistry: CapabilityRegistry, - buildServerSpec: BuildServerSpec?, - toolchainRegistry: ToolchainRegistry, - options: SourceKitLSPOptions, - hooks: Hooks, - indexTaskScheduler: TaskScheduler - ) async { - struct ConnectionToClient: BuildServerManagerConnectionToClient { - func waitUntilInitialized() async { - await sourceKitLSPServer?.waitUntilInitialized() - } - - weak var sourceKitLSPServer: SourceKitLSPServer? - func send(_ notification: some NotificationType) { - guard let sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the - // language server. Nothing left to do. - logger.error( - "Ignoring notification \(type(of: notification).method) because connection to editor has been closed" - ) - return - } - sourceKitLSPServer.sendNotificationToClient(notification) - } - - func nextRequestID() -> RequestID { - return .string(UUID().uuidString) - } - - func send( - _ request: Request, - id: RequestID, - reply: @escaping @Sendable (LSPResult) -> Void - ) { - guard let sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the - // language server. Nothing left to do. - reply(.failure(ResponseError.unknown("Connection to the editor closed"))) - return - } - sourceKitLSPServer.client.send(request, id: id, reply: reply) - } - - /// Whether the client can handle `WorkDoneProgress` requests. - var clientSupportsWorkDoneProgress: Bool { - get async { - await sourceKitLSPServer?.capabilityRegistry?.clientCapabilities.window?.workDoneProgress ?? false - } - } - - func watchFiles(_ fileWatchers: [FileSystemWatcher]) async { - await sourceKitLSPServer?.watchFiles(fileWatchers) - } - - func logMessageToIndexLog( - message: String, - type: WindowMessageType, - structure: LanguageServerProtocol.StructuredLogKind? - ) { - guard let sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the - // language server. Nothing left to do. - logger.error("Ignoring index log notification because connection to editor has been closed") - return - } - sourceKitLSPServer.logMessageToIndexLog(message: message, type: type, structure: structure) - } - } - - let buildServerManager = await BuildServerManager( - buildServerSpec: buildServerSpec, - toolchainRegistry: toolchainRegistry, - options: options, - connectionToClient: ConnectionToClient(sourceKitLSPServer: sourceKitLSPServer), - buildServerHooks: hooks.buildServerHooks, - createMainFilesProvider: { (initializationData, mainFilesChangedCallback) -> MainFilesProvider? in - await createIndex( - initializationData: initializationData, - mainFilesChangedCallback: mainFilesChangedCallback, - rootUri: rootUri, - toolchainRegistry: toolchainRegistry, - options: options, - hooks: hooks - ) - } - ) - - logger.log( - "Created workspace at \(rootUri.forLogging) with project root \(buildServerSpec?.projectRoot.description ?? "")" - ) - - await self.init( - sourceKitLSPServer: sourceKitLSPServer, - rootUri: rootUri, - capabilityRegistry: capabilityRegistry, - options: options, - hooks: hooks, - buildServerManager: buildServerManager, - indexTaskScheduler: indexTaskScheduler - ) - await buildServerManager.setDelegate(self) - - // Populate the initial list of unit output paths in the index. - await scheduleUpdateOfUnitOutputPathsInIndexIfNecessary() - } - - /// Returns a `CheckedIndex` that verifies that all the returned entries are up-to-date with the given - /// `IndexCheckLevel`. - package func index(checkedFor checkLevel: IndexCheckLevel) async -> CheckedIndex? { - return await uncheckedIndex?.checked(for: checkLevel) - } - - package func filesDidChange(_ events: [FileEvent]) async { - // First clear any cached realpaths in `sourceFilesWithSameRealpathInferrer`. - await sourceFilesWithSameRealpathInferrer.filesDidChange(events) - - // Now infer any edits for source files that share the same realpath as one of the modified files. - var events = events - events += - await sourceFilesWithSameRealpathInferrer - .sourceFilesWithSameRealpath(as: events.filter { $0.type == .changed }.map(\.uri)) - .map { FileEvent(uri: $0, type: .changed) } - - // Notify all clients about the reported and inferred edits. - await buildServerManager.filesDidChange(events) - - async let updateSyntacticIndex: Void = await syntacticTestIndex.filesDidChange(events) - async let updateSemanticIndex: Void? = await semanticIndexManager?.filesDidChange(events) - _ = await (updateSyntacticIndex, updateSemanticIndex) - } - - /// The language services that can handle the given document. Callers should try to merge the results from the - /// different language service or prefer results from language services that occur earlier in this array, whichever is - /// more suitable. - func languageServices(for uri: DocumentURI) -> [LanguageService] { - return languageServices.value[uri.buildSettingsFile] ?? [] - } - - /// The language service with the highest precedence that can handle the given document. - func primaryLanguageService(for uri: DocumentURI) -> LanguageService? { - return languageServices(for: uri).first - } - - /// Set a language service for a document uri and returns if none exists already. - /// - /// If language services already exist for this document, eg. because two requests start creating a language - /// service for a document and race, `newLanguageServices` is dropped and the existing language services for the - /// document are returned. - func setLanguageServices(for uri: DocumentURI, _ newLanguageService: [any LanguageService]) -> [LanguageService] { - return languageServices.withLock { languageServices in - if let languageService = languageServices[uri] { - return languageService - } - - languageServices[uri] = newLanguageService - return newLanguageService - } - } - - /// Handle a build settings change notification from the build serveer. - /// This has two primary cases: - /// - Initial settings reported for a given file, now we can fully open it - /// - Changed settings for an already open file - package func fileBuildSettingsChanged(_ changedFiles: Set) async { - for uri in changedFiles { - for languageService in languageServices(for: uri) { - await languageService.documentUpdatedBuildSettings(uri) - } - } - } - - /// Handle a dependencies updated notification from the build server. - /// We inform the respective language services as long as the given file is open - /// (not queued for opening). - package func filesDependenciesUpdated(_ changedFiles: Set) async { - var documentsByService: [ObjectIdentifier: (Set, LanguageService)] = [:] - for uri in changedFiles { - logger.log("Dependencies updated for file \(uri.forLogging)") - for languageService in languageServices(for: uri) { - documentsByService[ObjectIdentifier(languageService), default: ([], languageService)].0.insert(uri) - } - } - for (documents, service) in documentsByService.values { - await service.documentDependenciesUpdated(documents) - } - } - - package func buildTargetsChanged(_ changedTargets: Set?) async { - await sourceKitLSPServer?.fileHandlingCapabilityChanged() - await semanticIndexManager?.buildTargetsChanged(changedTargets) - await orLog("Scheduling syntactic test re-indexing") { - let testFiles = try await buildServerManager.testFiles() - await syntacticTestIndex.listOfTestFilesDidChange(testFiles) - } - - await scheduleUpdateOfUnitOutputPathsInIndexIfNecessary() - } - - private func scheduleUpdateOfUnitOutputPathsInIndexIfNecessary() async { - indexUnitOutputPathsUpdateQueue.async { - guard await self.uncheckedIndex?.usesExplicitOutputPaths ?? false else { - return - } - guard await self.buildServerManager.initializationData?.outputPathsProvider ?? false else { - // This can only happen if an index got injected that uses explicit output paths but the build server does not - // support output paths. - logger.error("The index uses explicit output paths but the build server does not support output paths") - return - } - await orLog("Setting new list of unit output paths") { - let outputPaths = try await Set(self.buildServerManager.outputPathsInAllTargets()) - await self.uncheckedIndex?.setUnitOutputPaths(outputPaths) - } - } - } - - package var clientSupportsWorkDoneProgress: Bool { - get async { - await sourceKitLSPServer?.capabilityRegistry?.clientCapabilities.window?.workDoneProgress ?? false - } - } - - package func waitUntilInitialized() async { - await sourceKitLSPServer?.waitUntilInitialized() - } - - package func synchronize(_ request: SynchronizeRequest) async { - if request.buildServerUpdates ?? false || request.index ?? false { - await buildServerManager.waitForUpToDateBuildGraph() - await indexUnitOutputPathsUpdateQueue.async {}.value - } - if request.copyFileMap ?? false { - // Not using `valuePropagatingCancellation` here because that could lead us to the following scenario: - // - An update of the copy file map is scheduled because of a change in the build graph - // - We get a synchronize request - // - Scheduling a new recomputation of the copy file map cancels the previous recomputation - // - We cancel the synchronize request, which would also cancel the copy file map recomputation, leaving us with - // an outdated version - // - // Technically, we might be doing unnecessary work here if the output file map is already up-to-date. But since - // this option is mostly intended for testing purposes, this is acceptable. - await buildServerManager.scheduleRecomputeCopyFileMap().value - } - if request.index ?? false { - await semanticIndexManager?.waitForUpToDateIndex() - await uncheckedIndex?.pollForUnitChangesAndWait() - } - } -} - -/// Wrapper around a workspace that isn't being retained. -package struct WeakWorkspace { - package weak var value: Workspace? - - package init(_ value: Workspace? = nil) { - self.value = value - } -} diff --git a/Sources/SwiftExtensions/Array+Safe.swift b/Sources/SwiftExtensions/Array+Safe.swift index d2a75ebdc..f7ab49e95 100644 --- a/Sources/SwiftExtensions/Array+Safe.swift +++ b/Sources/SwiftExtensions/Array+Safe.swift @@ -13,7 +13,7 @@ extension Array { /// Returns the element at the specified index if it is within the Array's /// bounds, otherwise `nil`. - package subscript(safe index: Index) -> Element? { + @_spi(SourceKitLSP) public subscript(safe index: Index) -> Element? { return index >= 0 && index < count ? self[index] : nil } } diff --git a/Sources/SwiftExtensions/AsyncQueue.swift b/Sources/SwiftExtensions/AsyncQueue.swift index 087013e2d..9ee6da913 100644 --- a/Sources/SwiftExtensions/AsyncQueue.swift +++ b/Sources/SwiftExtensions/AsyncQueue.swift @@ -27,15 +27,15 @@ extension Task: AnyTask { } /// A type that is able to track dependencies between tasks. -package protocol DependencyTracker: Sendable, Hashable { +public protocol DependencyTracker: Sendable, Hashable { /// Whether the task described by `self` needs to finish executing before `other` can start executing. func isDependency(of other: Self) -> Bool } /// A dependency tracker where each task depends on every other, i.e. a serial /// queue. -package struct Serial: DependencyTracker { - package func isDependency(of other: Serial) -> Bool { +public struct Serial: DependencyTracker { + @_spi(SourceKitLSP) public func isDependency(of other: Serial) -> Bool { return true } } @@ -76,10 +76,10 @@ private final class PendingTasks: Sendable { } /// A queue that allows the execution of asynchronous blocks of code. -package final class AsyncQueue: Sendable { +public final class AsyncQueue: Sendable { private let pendingTasks: PendingTasks = PendingTasks() - package init() {} + public init() {} /// Schedule a new closure to be executed on the queue. /// @@ -90,7 +90,7 @@ package final class AsyncQueue: Sendable { /// finish execution before the barrier is executed and all tasks that are /// added later will wait until the barrier finishes execution. @discardableResult - package func async( + @_spi(SourceKitLSP) public func async( priority: TaskPriority? = nil, metadata: TaskMetadata, @_inheritActorContext operation: @escaping @Sendable () async -> Success @@ -111,7 +111,7 @@ package final class AsyncQueue: Sendable { /// /// - Important: The caller is responsible for handling any errors thrown from /// the operation by awaiting the result of the returned task. - package func asyncThrowing( + @_spi(SourceKitLSP) public func asyncThrowing( priority: TaskPriority? = nil, metadata: TaskMetadata, @_inheritActorContext operation: @escaping @Sendable () async throws -> Success @@ -176,7 +176,7 @@ extension AsyncQueue where TaskMetadata == Serial { /// Same as ``async(priority:operation:)`` but specialized for serial queues /// that don't specify any metadata. @discardableResult - package func async( + public func async( priority: TaskPriority? = nil, @_inheritActorContext operation: @escaping @Sendable () async -> Success ) -> Task { @@ -185,7 +185,7 @@ extension AsyncQueue where TaskMetadata == Serial { /// Same as ``asyncThrowing(priority:metadata:operation:)`` but specialized /// for serial queues that don't specify any metadata. - package func asyncThrowing( + public func asyncThrowing( priority: TaskPriority? = nil, @_inheritActorContext operation: @escaping @Sendable () async throws -> Success ) -> Task { diff --git a/Sources/SwiftExtensions/AsyncUtils.swift b/Sources/SwiftExtensions/AsyncUtils.swift index ee4704d9f..598284100 100644 --- a/Sources/SwiftExtensions/AsyncUtils.swift +++ b/Sources/SwiftExtensions/AsyncUtils.swift @@ -10,21 +10,21 @@ // //===----------------------------------------------------------------------===// -import Foundation +public import Foundation /// Wrapper around a task that allows multiple clients to depend on the task's value. /// /// If all of the dependents are cancelled, the underlying task is cancelled as well. -package actor RefCountedCancellableTask { - package let task: Task +@_spi(SourceKitLSP) public actor RefCountedCancellableTask { + @_spi(SourceKitLSP) public let task: Task /// The number of clients that depend on the task's result and that are not cancelled. private var refCount: Int = 0 /// Whether the task has been cancelled. - package private(set) var isCancelled: Bool = false + @_spi(SourceKitLSP) public private(set) var isCancelled: Bool = false - package init(priority: TaskPriority? = nil, operation: @escaping @Sendable @concurrent () async throws -> Success) { + @_spi(SourceKitLSP) public init(priority: TaskPriority? = nil, operation: @escaping @Sendable @concurrent () async throws -> Success) { self.task = Task(priority: priority, operation: operation) } @@ -38,7 +38,7 @@ package actor RefCountedCancellableTask { /// Get the task's value. /// /// If all callers of `value` are cancelled, the underlying task gets cancelled as well. - package var value: Success { + @_spi(SourceKitLSP) public var value: Success { get async throws { if isCancelled { throw CancellationError() @@ -55,13 +55,13 @@ package actor RefCountedCancellableTask { } /// Cancel the task and throw a `CancellationError` to all clients that are awaiting the value. - package func cancel() { + @_spi(SourceKitLSP) public func cancel() { isCancelled = true task.cancel() } } -package extension Task { +public extension Task { /// Awaits the value of the result. /// /// If the current task is cancelled, this will cancel the subtask as well. @@ -76,11 +76,11 @@ package extension Task { } } -package extension Task where Failure == Never { +extension Task where Failure == Never { /// Awaits the value of the result. /// /// If the current task is cancelled, this will cancel the subtask as well. - var valuePropagatingCancellation: Success { + public var valuePropagatingCancellation: Success { get async { await withTaskCancellationHandler { return await self.value @@ -98,7 +98,7 @@ package extension Task where Failure == Never { /// /// If the task executing `withCancellableCheckedThrowingContinuation` gets /// cancelled, `cancel` is invoked with the handle that `operation` provided. -package func withCancellableCheckedThrowingContinuation( +@_spi(SourceKitLSP) public func withCancellableCheckedThrowingContinuation( _ operation: (_ continuation: CheckedContinuation) -> Handle, cancel: @Sendable (Handle) -> Void ) async throws -> Result { @@ -134,7 +134,7 @@ package func withCancellableCheckedThrowingContinuation( + @_spi(SourceKitLSP) public func concurrentMap( maxConcurrentTasks: Int = ProcessInfo.processInfo.activeProcessorCount, _ transform: @escaping @Sendable (Element) async -> TransformedElement ) async -> [TransformedElement] { @@ -167,7 +167,7 @@ extension Collection where Self: Sendable, Element: Sendable { } /// Invoke `body` for every element in the collection and wait for all calls of `body` to finish - package func concurrentForEach(_ body: @escaping @Sendable (Element) async -> Void) async { + @_spi(SourceKitLSP) public func concurrentForEach(_ body: @escaping @Sendable (Element) async -> Void) async { await withTaskGroup(of: Void.self) { taskGroup in for element in self { taskGroup.addTask { @@ -178,20 +178,20 @@ extension Collection where Self: Sendable, Element: Sendable { } } -package struct TimeoutError: Error, CustomStringConvertible { - package var description: String { "Timed out" } +@_spi(SourceKitLSP) public struct TimeoutError: Error, CustomStringConvertible { + @_spi(SourceKitLSP) public var description: String { "Timed out" } - package let handle: TimeoutHandle? + @_spi(SourceKitLSP) public let handle: TimeoutHandle? - package init(handle: TimeoutHandle?) { + @_spi(SourceKitLSP) public init(handle: TimeoutHandle?) { self.handle = handle } } -package final class TimeoutHandle: Equatable, Sendable { - package init() {} +@_spi(SourceKitLSP) public final class TimeoutHandle: Equatable, Sendable { + @_spi(SourceKitLSP) public init() {} - static package func == (_ lhs: TimeoutHandle, _ rhs: TimeoutHandle) -> Bool { + @_spi(SourceKitLSP) public static func == (_ lhs: TimeoutHandle, _ rhs: TimeoutHandle) -> Bool { return lhs === rhs } } @@ -202,7 +202,7 @@ package final class TimeoutHandle: Equatable, Sendable { /// /// If a `handle` is passed in and this `withTimeout` call times out, the thrown `TimeoutError` contains this handle. /// This way a caller can identify whether this call to `withTimeout` timed out or if a nested call timed out. -package func withTimeout( +@_spi(SourceKitLSP) public func withTimeout( _ duration: Duration, handle: TimeoutHandle? = nil, _ body: @escaping @Sendable () async throws -> T @@ -268,7 +268,7 @@ package func withTimeout( /// /// - Important: `body` will not be cancelled when the timeout is received. Use the other overload of `withTimeout` if /// `body` should be cancelled after `timeout`. -package func withTimeout( +@_spi(SourceKitLSP) public func withTimeout( _ timeout: Duration, body: @escaping @Sendable () async throws -> T, resultReceivedAfterTimeout: @escaping @Sendable (_ result: T) async -> Void diff --git a/Sources/SwiftExtensions/Atomics.swift b/Sources/SwiftExtensions/Atomics.swift index f930704fa..4b8f9c275 100644 --- a/Sources/SwiftExtensions/Atomics.swift +++ b/Sources/SwiftExtensions/Atomics.swift @@ -10,13 +10,13 @@ // //===----------------------------------------------------------------------===// -import CAtomics +import ToolsProtocolsCAtomics // TODO: Use atomic types from the standard library (https://github.com/swiftlang/sourcekit-lsp/issues/1949) -package final class AtomicBool: Sendable { +@_spi(SourceKitLSP) public final class AtomicBool: Sendable { private nonisolated(unsafe) let atomic: UnsafeMutablePointer - package init(initialValue: Bool) { + @_spi(SourceKitLSP) public init(initialValue: Bool) { self.atomic = atomic_uint32_create(initialValue ? 1 : 0) } @@ -24,7 +24,7 @@ package final class AtomicBool: Sendable { atomic_uint32_destroy(atomic) } - package var value: Bool { + @_spi(SourceKitLSP) public var value: Bool { get { atomic_uint32_get(atomic) != 0 } @@ -34,10 +34,10 @@ package final class AtomicBool: Sendable { } } -package final class AtomicUInt8: Sendable { +@_spi(SourceKitLSP) public final class AtomicUInt8: Sendable { private nonisolated(unsafe) let atomic: UnsafeMutablePointer - package init(initialValue: UInt8) { + @_spi(SourceKitLSP) public init(initialValue: UInt8) { self.atomic = atomic_uint32_create(UInt32(initialValue)) } @@ -45,7 +45,7 @@ package final class AtomicUInt8: Sendable { atomic_uint32_destroy(atomic) } - package var value: UInt8 { + @_spi(SourceKitLSP) public var value: UInt8 { get { UInt8(atomic_uint32_get(atomic)) } @@ -55,14 +55,14 @@ package final class AtomicUInt8: Sendable { } } -package final class AtomicUInt32: Sendable { +@_spi(SourceKitLSP) public final class AtomicUInt32: Sendable { private nonisolated(unsafe) let atomic: UnsafeMutablePointer - package init(initialValue: UInt32) { + @_spi(SourceKitLSP) public init(initialValue: UInt32) { self.atomic = atomic_uint32_create(initialValue) } - package var value: UInt32 { + @_spi(SourceKitLSP) public var value: UInt32 { get { atomic_uint32_get(atomic) } @@ -75,19 +75,19 @@ package final class AtomicUInt32: Sendable { atomic_uint32_destroy(atomic) } - package func fetchAndIncrement() -> UInt32 { + @_spi(SourceKitLSP) public func fetchAndIncrement() -> UInt32 { return atomic_uint32_fetch_and_increment(atomic) } } -package final class AtomicInt32: Sendable { +@_spi(SourceKitLSP) public final class AtomicInt32: Sendable { private nonisolated(unsafe) let atomic: UnsafeMutablePointer - package init(initialValue: Int32) { + @_spi(SourceKitLSP) public init(initialValue: Int32) { self.atomic = atomic_int32_create(initialValue) } - package var value: Int32 { + @_spi(SourceKitLSP) public var value: Int32 { get { atomic_int32_get(atomic) } @@ -100,7 +100,7 @@ package final class AtomicInt32: Sendable { atomic_int32_destroy(atomic) } - package func fetchAndIncrement() -> Int32 { + @_spi(SourceKitLSP) public func fetchAndIncrement() -> Int32 { return atomic_int32_fetch_and_increment(atomic) } } diff --git a/Sources/SwiftExtensions/CMakeLists.txt b/Sources/SwiftExtensions/CMakeLists.txt index 72c923919..c29efe237 100644 --- a/Sources/SwiftExtensions/CMakeLists.txt +++ b/Sources/SwiftExtensions/CMakeLists.txt @@ -27,7 +27,7 @@ add_library(SwiftExtensions STATIC ${sources}) set_target_properties(SwiftExtensions PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_link_libraries(SwiftExtensions PUBLIC - CAtomics) + ToolsProtocolsCAtomics) target_link_libraries(SwiftExtensions PRIVATE $<$>:Foundation>) @@ -35,6 +35,8 @@ add_library(SwiftExtensionsForPlugin STATIC ${sources}) set_target_properties(SwiftExtensionsForPlugin PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_link_libraries(SwiftExtensionsForPlugin PUBLIC - CAtomics) + ToolsProtocolsCAtomics) target_link_libraries(SwiftExtensionsForPlugin PRIVATE $<$>:Foundation>) + +set_property(GLOBAL APPEND PROPERTY SWIFTTOOLSPROTOCOLS_EXPORTS SwiftExtensions) diff --git a/Sources/SwiftExtensions/Cache.swift b/Sources/SwiftExtensions/Cache.swift index 5b93bf2f0..e01a72e4f 100644 --- a/Sources/SwiftExtensions/Cache.swift +++ b/Sources/SwiftExtensions/Cache.swift @@ -11,12 +11,12 @@ //===----------------------------------------------------------------------===// /// Essentially a dictionary where results are asynchronously computed on access. -package class Cache { +@_spi(SourceKitLSP) public class Cache { private var storage: [Key: Task] = [:] - package init() {} + @_spi(SourceKitLSP) public init() {} - package func get( + @_spi(SourceKitLSP) public func get( _ key: Key, isolation: isolated any Actor, compute: @Sendable @escaping (Key) async throws -> Result @@ -44,7 +44,7 @@ package class Cache { /// Get the value cached for `key`. If no value exists for `key`, try deriving the result from an existing cache entry /// that satisfies `canReuseKey` by applying `transform` to that result. - package func getDerived( + @_spi(SourceKitLSP) public func getDerived( isolation: isolated any Actor, _ key: Key, canReuseKey: @Sendable @escaping (Key) -> Bool, @@ -68,7 +68,7 @@ package class Cache { return nil } - package func clear(isolation: isolated any Actor, where condition: (Key) -> Bool) { + @_spi(SourceKitLSP) public func clear(isolation: isolated any Actor, where condition: (Key) -> Bool) { for key in storage.keys { if condition(key) { storage[key] = nil @@ -76,7 +76,7 @@ package class Cache { } } - package func clearAll(isolation: isolated any Actor) { + @_spi(SourceKitLSP) public func clearAll(isolation: isolated any Actor) { storage.removeAll() } } diff --git a/Sources/SwiftExtensions/Collection+Only.swift b/Sources/SwiftExtensions/Collection+Only.swift index d00b91aba..5c6f1fec8 100644 --- a/Sources/SwiftExtensions/Collection+Only.swift +++ b/Sources/SwiftExtensions/Collection+Only.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -package extension Collection { +@_spi(SourceKitLSP) public extension Collection { /// If the collection contains a single element, return it, otherwise `nil`. var only: Element? { if !isEmpty && index(after: startIndex) == endIndex { diff --git a/Sources/SwiftExtensions/Collection+PartitionIntoBatches.swift b/Sources/SwiftExtensions/Collection+PartitionIntoBatches.swift index 9909aad3c..d11ce097a 100644 --- a/Sources/SwiftExtensions/Collection+PartitionIntoBatches.swift +++ b/Sources/SwiftExtensions/Collection+PartitionIntoBatches.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -package extension Collection where Index == Int { +@_spi(SourceKitLSP) public extension Collection where Index == Int { /// Partition the elements of the collection into `numberOfBatches` roughly equally sized batches. /// /// Elements are assigned to the batches round-robin. This ensures that elements that are close to each other in the diff --git a/Sources/SwiftExtensions/Duration+Seconds.swift b/Sources/SwiftExtensions/Duration+Seconds.swift index 69245dedf..7e469ac95 100644 --- a/Sources/SwiftExtensions/Duration+Seconds.swift +++ b/Sources/SwiftExtensions/Duration+Seconds.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// extension Duration { - package var seconds: Double { + @_spi(SourceKitLSP) public var seconds: Double { return Double(self.components.attoseconds) / 1_000_000_000_000_000_000 + Double(self.components.seconds) } } diff --git a/Sources/SwiftExtensions/FileManagerExtensions.swift b/Sources/SwiftExtensions/FileManagerExtensions.swift index 80dfe587d..45105e6af 100644 --- a/Sources/SwiftExtensions/FileManagerExtensions.swift +++ b/Sources/SwiftExtensions/FileManagerExtensions.swift @@ -10,11 +10,11 @@ // //===----------------------------------------------------------------------===// -package import Foundation +public import Foundation extension FileManager { /// Same as `fileExists(atPath:)` but takes a `URL` instead of a `String`. - package func fileExists(at url: URL) -> Bool { + @_spi(SourceKitLSP) public func fileExists(at url: URL) -> Bool { guard let filePath = try? url.filePath else { return false } @@ -22,14 +22,14 @@ extension FileManager { } /// Returns `true` if an entry exists in the file system at the given URL and that entry is a directory. - package func isDirectory(at url: URL) -> Bool { + @_spi(SourceKitLSP) public func isDirectory(at url: URL) -> Bool { var isDirectory: ObjCBool = false return self.fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue } /// Returns `true` if an entry exists in the file system at the given URL and that entry is a file, ie. not a /// directory. - package func isFile(at url: URL) -> Bool { + @_spi(SourceKitLSP) public func isFile(at url: URL) -> Bool { var isDirectory: ObjCBool = false return self.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue } diff --git a/Sources/SwiftExtensions/LazyValue.swift b/Sources/SwiftExtensions/LazyValue.swift index 59e851b37..5d1069425 100644 --- a/Sources/SwiftExtensions/LazyValue.swift +++ b/Sources/SwiftExtensions/LazyValue.swift @@ -11,12 +11,12 @@ //===----------------------------------------------------------------------===// /// A value that is computed on its first access and saved for later retrievals. -package enum LazyValue { +@_spi(SourceKitLSP) public enum LazyValue { case computed(T) case uninitialized /// If the value has already been computed return it, otherwise compute it using `compute`. - package mutating func cachedValueOrCompute(_ compute: () -> T) -> T { + @_spi(SourceKitLSP) public mutating func cachedValueOrCompute(_ compute: () -> T) -> T { switch self { case .computed(let value): return value @@ -27,7 +27,7 @@ package enum LazyValue { } } - package mutating func reset() { + @_spi(SourceKitLSP) public mutating func reset() { self = .uninitialized } } diff --git a/Sources/SwiftExtensions/PipeAsStringHandler.swift b/Sources/SwiftExtensions/PipeAsStringHandler.swift index 3d12dba7f..a2d38811b 100644 --- a/Sources/SwiftExtensions/PipeAsStringHandler.swift +++ b/Sources/SwiftExtensions/PipeAsStringHandler.swift @@ -10,11 +10,11 @@ // //===----------------------------------------------------------------------===// -package import Foundation +public import Foundation /// Gathers data from a stdout or stderr pipe. When it has accumulated a full line, calls the handler to handle the /// string. -package actor PipeAsStringHandler { +@_spi(SourceKitLSP) public actor PipeAsStringHandler { /// Queue on which all data from the pipe will be handled. This allows us to have a /// nonisolated `handle` function but ensure that data gets processed in order. private let queue = AsyncQueue() @@ -23,7 +23,7 @@ package actor PipeAsStringHandler { /// The closure that actually handles private let handler: @Sendable (String) -> Void - package init(handler: @escaping @Sendable (String) -> Void) { + @_spi(SourceKitLSP) public init(handler: @escaping @Sendable (String) -> Void) { self.handler = handler } @@ -49,7 +49,7 @@ package actor PipeAsStringHandler { } } - package nonisolated func handleDataFromPipe(_ newData: Data) { + @_spi(SourceKitLSP) public nonisolated func handleDataFromPipe(_ newData: Data) { queue.async { await self.handleDataFromPipeImpl(newData) } diff --git a/Sources/SwiftExtensions/Platform.swift b/Sources/SwiftExtensions/Platform.swift index 79b7bfc22..191376eff 100644 --- a/Sources/SwiftExtensions/Platform.swift +++ b/Sources/SwiftExtensions/Platform.swift @@ -10,12 +10,12 @@ // //===----------------------------------------------------------------------===// -package enum Platform: Equatable, Sendable { +@_spi(SourceKitLSP) public enum Platform: Equatable, Sendable { case darwin case linux case windows - package static var current: Platform? { + @_spi(SourceKitLSP) public static var current: Platform? { #if os(Windows) return .windows #elseif canImport(Darwin) @@ -26,7 +26,7 @@ package enum Platform: Equatable, Sendable { } /// The file extension used for a dynamic library on this platform. - package var dynamicLibraryExtension: String { + @_spi(SourceKitLSP) public var dynamicLibraryExtension: String { switch self { case .darwin: return ".dylib" case .linux: return ".so" @@ -34,7 +34,7 @@ package enum Platform: Equatable, Sendable { } } - package var executableExtension: String { + @_spi(SourceKitLSP) public var executableExtension: String { switch self { case .windows: return ".exe" case .linux, .darwin: return "" diff --git a/Sources/SwiftExtensions/Process+terminate.swift b/Sources/SwiftExtensions/Process+terminate.swift index 776f8a0dc..15b94cf27 100644 --- a/Sources/SwiftExtensions/Process+terminate.swift +++ b/Sources/SwiftExtensions/Process+terminate.swift @@ -10,11 +10,11 @@ // //===----------------------------------------------------------------------===// -package import Foundation +public import Foundation extension Foundation.Process { /// If the process has not exited after `duration`, terminate it. - package func terminateIfRunning(after duration: Duration, pollInterval: Duration = .milliseconds(5)) async throws { + @_spi(SourceKitLSP) public func terminateIfRunning(after duration: Duration, pollInterval: Duration = .milliseconds(5)) async throws { for _ in 0..( + @_spi(SourceKitLSP) public func asyncMap( _ transform: (Element) async throws -> T ) async rethrows -> [T] { var result: [T] = [] @@ -26,7 +26,7 @@ extension Sequence { } /// Just like `Sequence.flatMap` but allows an `async` transform function. - package func asyncFlatMap( + @_spi(SourceKitLSP) public func asyncFlatMap( _ transform: (Element) async throws -> SegmentOfResult ) async rethrows -> [SegmentOfResult.Element] { var result: [SegmentOfResult.Element] = [] @@ -40,7 +40,7 @@ extension Sequence { } /// Just like `Sequence.compactMap` but allows an `async` transform function. - package func asyncCompactMap( + @_spi(SourceKitLSP) public func asyncCompactMap( _ transform: (Element) async throws -> T? ) async rethrows -> [T] { var result: [T] = [] @@ -55,7 +55,7 @@ extension Sequence { } /// Just like `Sequence.map` but allows an `async` transform function. - package func asyncFilter( + @_spi(SourceKitLSP) public func asyncFilter( _ predicate: (Element) async throws -> Bool ) async rethrows -> [Element] { var result: [Element] = [] @@ -70,7 +70,7 @@ extension Sequence { } /// Just like `Sequence.first` but allows an `async` predicate function. - package func asyncFirst(where predicate: (Element) async throws -> Bool) async rethrows -> Element? { + @_spi(SourceKitLSP) public func asyncFirst(where predicate: (Element) async throws -> Bool) async rethrows -> Element? { for element in self { if try await predicate(element) { return element @@ -81,7 +81,7 @@ extension Sequence { } /// Just like `Sequence.contains` but allows an `async` predicate function. - package func asyncContains( + @_spi(SourceKitLSP) public func asyncContains( where predicate: (Element) async throws -> Bool ) async rethrows -> Bool { return try await asyncFirst(where: predicate) != nil diff --git a/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift b/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift index df654c99d..931d1fe82 100644 --- a/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift +++ b/Sources/SwiftExtensions/Task+WithPriorityChangedHandler.swift @@ -16,7 +16,7 @@ /// `pollingInterval`. /// The function assumes that the original priority of the task is `initialPriority`. If the task priority changed /// compared to `initialPriority`, the `taskPriorityChanged` will be called. -package func withTaskPriorityChangedHandler( +@_spi(SourceKitLSP) public func withTaskPriorityChangedHandler( initialPriority: TaskPriority = Task.currentPriority, pollingInterval: Duration = .seconds(0.1), @_inheritActorContext operation: @escaping @Sendable () async throws -> T, diff --git a/Sources/SwiftExtensions/ThreadSafeBox.swift b/Sources/SwiftExtensions/ThreadSafeBox.swift index bc0ec9be4..8bf6799c2 100644 --- a/Sources/SwiftExtensions/ThreadSafeBox.swift +++ b/Sources/SwiftExtensions/ThreadSafeBox.swift @@ -15,13 +15,13 @@ import Foundation /// A thread safe container that contains a value of type `T`. /// /// - Note: Unchecked sendable conformance because value is guarded by a lock. -package class ThreadSafeBox: @unchecked Sendable { +@_spi(SourceKitLSP) public class ThreadSafeBox: @unchecked Sendable { /// Lock guarding `_value`. private let lock = NSLock() private var _value: T - package var value: T { + @_spi(SourceKitLSP) public var value: T { get { return lock.withLock { return _value @@ -39,11 +39,11 @@ package class ThreadSafeBox: @unchecked Sendable { } } - package init(initialValue: T) { + @_spi(SourceKitLSP) public init(initialValue: T) { _value = initialValue } - package func withLock(_ body: (inout T) throws -> Result) rethrows -> Result { + @_spi(SourceKitLSP) public func withLock(_ body: (inout T) throws -> Result) rethrows -> Result { return try lock.withLock { return try body(&_value) } @@ -51,7 +51,7 @@ package class ThreadSafeBox: @unchecked Sendable { /// If the value in the box is an optional, return it and reset it to `nil` /// in an atomic operation. - package func takeValue() -> T where U? == T { + @_spi(SourceKitLSP) public func takeValue() -> T where U? == T { lock.withLock { guard let value = self._value else { return nil } self._value = nil diff --git a/Sources/SwiftExtensions/TransitiveClosure.swift b/Sources/SwiftExtensions/TransitiveClosure.swift index 2d610c861..96b748f62 100644 --- a/Sources/SwiftExtensions/TransitiveClosure.swift +++ b/Sources/SwiftExtensions/TransitiveClosure.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -package func transitiveClosure(of values: some Collection, successors: (T) -> Set) -> Set { +@_spi(SourceKitLSP) public func transitiveClosure(of values: some Collection, successors: (T) -> Set) -> Set { var transitiveClosure: Set = [] var workList = Array(values) while let element = workList.popLast() { diff --git a/Sources/SwiftExtensions/URLExtensions.swift b/Sources/SwiftExtensions/URLExtensions.swift index 629313aea..feef46899 100644 --- a/Sources/SwiftExtensions/URLExtensions.swift +++ b/Sources/SwiftExtensions/URLExtensions.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -package import Foundation +public import Foundation #if os(Windows) import WinSDK @@ -37,7 +37,7 @@ extension URL { /// path by stripping away `private` prefixes. Since sourcekitd is not performing this standardization, using /// `resolvingSymlinksInPath` can lead to slightly mismatched URLs between the sourcekit-lsp response and the test /// assertion. - package var realpath: URL { + @_spi(SourceKitLSP) public var realpath: URL { get throws { #if canImport(Darwin) return try self.filePath.withCString { path in @@ -63,7 +63,7 @@ extension URL { /// - It throws an error when called on a non-file URL. /// /// `filePath` should generally be preferred over `path` when dealing with file URLs. - package var filePath: String { + @_spi(SourceKitLSP) public var filePath: String { get throws { guard self.isFileURL else { throw FilePathError.noFileURL(self) @@ -89,7 +89,7 @@ extension URL { /// Assuming this URL is a file URL, checks if it looks like a root path. This is a string check, ie. the return /// value for a path of `"/foo/.."` would be `false`. An error will be thrown is this is a non-file URL. - package var isRoot: Bool { + @_spi(SourceKitLSP) public var isRoot: Bool { get throws { let checkPath = try filePath #if os(Windows) @@ -101,7 +101,7 @@ extension URL { } /// Returns true if the path of `self` starts with the path in `other`. - package func isDescendant(of other: URL) -> Bool { + @_spi(SourceKitLSP) public func isDescendant(of other: URL) -> Bool { return self.pathComponents.dropLast().starts(with: other.pathComponents) } } diff --git a/Sources/SwiftLanguageService/AdjustPositionToStartOfArgument.swift b/Sources/SwiftLanguageService/AdjustPositionToStartOfArgument.swift deleted file mode 100644 index dadc54a2f..000000000 --- a/Sources/SwiftLanguageService/AdjustPositionToStartOfArgument.swift +++ /dev/null @@ -1,82 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging -import SourceKitLSP -import SwiftSyntax - -private class StartOfArgumentFinder: SyntaxAnyVisitor { - let requestedPosition: AbsolutePosition - var resolvedPosition: AbsolutePosition? - - init(position: AbsolutePosition) { - self.requestedPosition = position - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if node.range.contains(requestedPosition) { - return .visitChildren - } - return .skipChildren - } - - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - return visit(arguments: node.arguments) - } - - override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind { - return visit(arguments: node.arguments) - } - - private func visit(arguments: LabeledExprListSyntax) -> SyntaxVisitorContinueKind { - guard (arguments.position...arguments.endPosition).contains(requestedPosition) else { - return .skipChildren - } - - guard !arguments.isEmpty else { - self.resolvedPosition = arguments.position - return .skipChildren - } - - for argument in arguments { - if (argument.position...argument.endPosition).contains(requestedPosition) { - if let trailingComma = argument.trailingComma, - requestedPosition >= trailingComma.endPositionBeforeTrailingTrivia - { - self.resolvedPosition = trailingComma.endPositionBeforeTrailingTrivia - } else { - self.resolvedPosition = argument.expression.positionAfterSkippingLeadingTrivia - } - return .visitChildren - } - } - - return .skipChildren - } -} - -extension SwiftLanguageService { - func adjustPositionToStartOfArgument( - _ position: Position, - in snapshot: DocumentSnapshot - ) async -> Position { - let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - let visitor = StartOfArgumentFinder(position: snapshot.absolutePosition(of: position)) - visitor.walk(tree) - if let resolvedPosition = visitor.resolvedPosition { - return snapshot.position(of: resolvedPosition) - } - return position - } -} diff --git a/Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift b/Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift deleted file mode 100644 index 3ffb08311..000000000 --- a/Sources/SwiftLanguageService/AdjustPositionToStartOfIdentifier.swift +++ /dev/null @@ -1,65 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SourceKitLSP -import SwiftSyntax - -private class StartOfIdentifierFinder: SyntaxAnyVisitor { - let requestedPosition: AbsolutePosition - var resolvedPosition: AbsolutePosition? - - init(position: AbsolutePosition) { - self.requestedPosition = position - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if (node.position...node.endPosition).contains(requestedPosition) { - return .visitChildren - } - return .skipChildren - } - - override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { - if token.tokenKind.isPunctuation || token.tokenKind == .endOfFile { - return .skipChildren - } - if (token.positionAfterSkippingLeadingTrivia...token.endPositionBeforeTrailingTrivia).contains(requestedPosition) { - self.resolvedPosition = token.positionAfterSkippingLeadingTrivia - } - return .skipChildren - } -} - -extension SwiftLanguageService { - /// VS Code considers the position after an identifier as part of an identifier. Ie. if you have `let foo| = 1`, then - /// it considers the cursor to be positioned at the identifier. This scenario is hit, when selecting an identifier by - /// double-clicking it and then eg. performing jump-to-definition. In that case VS Code will send the position after - /// the identifier. - /// `sourcekitd`, on the other hand, does not consider the position after the identifier as part of the identifier. - /// To bridge the gap here, normalize any positions inside, or directly after, an identifier to the identifier's - /// start. - func adjustPositionToStartOfIdentifier( - _ position: Position, - in snapshot: DocumentSnapshot - ) async -> Position { - let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - let visitor = StartOfIdentifierFinder(position: snapshot.absolutePosition(of: position)) - visitor.walk(tree) - if let resolvedPosition = visitor.resolvedPosition { - return snapshot.position(of: resolvedPosition) - } - return position - } - -} diff --git a/Sources/SwiftLanguageService/CMakeLists.txt b/Sources/SwiftLanguageService/CMakeLists.txt deleted file mode 100644 index 5d8f66ba1..000000000 --- a/Sources/SwiftLanguageService/CMakeLists.txt +++ /dev/null @@ -1,84 +0,0 @@ -add_library(SwiftLanguageService STATIC - SemanticRefactoring.swift - SwiftTestingScanner.swift - FoldingRange.swift - CMakeLists.txt - SymbolInfo.swift - RefactoringEdit.swift - CodeActions/ConvertStringConcatenationToStringInterpolation.swift - CodeActions/SyntaxRefactoringCodeActionProvider.swift - CodeActions/ConvertJSONToCodableStruct.swift - CodeActions/SyntaxCodeActionProvider.swift - CodeActions/SyntaxCodeActions.swift - CodeActions/ConvertIntegerLiteral.swift - CodeActions/PackageManifestEdits.swift - CodeActions/AddDocumentation.swift - RefactoringResponse.swift - SyntacticSwiftXCTestScanner.swift - CommentXML.swift - SymbolGraph.swift - ClosureCompletionFormat.swift - SemanticRefactorCommand.swift - DocumentFormatting.swift - DiagnosticReportManager.swift - SemanticTokens.swift - ExpandMacroCommand.swift - Diagnostic.swift - CodeCompletion.swift - SwiftCodeLensScanner.swift - SwiftCommand.swift - RelatedIdentifiers.swift - VariableTypeInfo.swift - CodeCompletionSession.swift - SyntaxTreeManager.swift - Rename.swift - RewriteSourceKitPlaceholders.swift - SwiftLanguageService.swift - TestDiscovery.swift - OpenInterface.swift - SyntaxHighlightingToken.swift - MacroExpansion.swift - AdjustPositionToStartOfIdentifier.swift - SyntaxHighlightingTokenParser.swift - CursorInfo.swift - DocumentSymbols.swift - SyntaxHighlightingTokens.swift - GeneratedInterfaceManager.swift - SignatureHelp.swift - AdjustPositionToStartOfArgument.swift -) -set_target_properties(SwiftLanguageService PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) -target_link_libraries(SwiftLanguageService PUBLIC - BuildServerIntegration - LanguageServerProtocol - SKOptions - SourceKitD - SourceKitLSP - ToolchainRegistry - IndexStoreDB - SwiftSyntax::SwiftBasicFormat - SwiftSyntax::SwiftSyntax -) - -target_link_libraries(SourceKitLSP PRIVATE - BuildServerProtocol - Csourcekitd - LanguageServerProtocolExtensions - LanguageServerProtocolJSONRPC - SemanticIndex - SKLogging - SKUtilities - SwiftExtensions - TSCExtensions - Crypto - TSCBasic - SwiftSyntax::SwiftDiagnostics - SwiftSyntax::SwiftIDEUtils - SwiftSyntax::SwiftParser - SwiftSyntax::SwiftParserDiagnostics - SwiftSyntax::SwiftRefactor - SwiftSyntax::SwiftSyntaxBuilder - $<$>:FoundationXML> -) - diff --git a/Sources/SwiftLanguageService/ClosureCompletionFormat.swift b/Sources/SwiftLanguageService/ClosureCompletionFormat.swift deleted file mode 100644 index b0b5c4f2b..000000000 --- a/Sources/SwiftLanguageService/ClosureCompletionFormat.swift +++ /dev/null @@ -1,68 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public import SwiftBasicFormat -public import SwiftSyntax - -/// A specialization of `BasicFormat` for closure literals in a code completion -/// context. -/// -/// This is more conservative about newline insertion: unless the closure has -/// multiple statements in its body it will not be reformatted to multiple -/// lines. -@_spi(Testing) -public class ClosureCompletionFormat: BasicFormat { - @_spi(Testing) - public override func requiresNewline( - between first: TokenSyntax?, - and second: TokenSyntax? - ) -> Bool { - if let first, isEndOfSmallClosureSignature(first) { - return false - } else if let first, isSmallClosureDelimiter(first, kind: \.leftBrace) { - return false - } else if let second, isSmallClosureDelimiter(second, kind: \.rightBrace) { - return false - } else { - return super.requiresNewline(between: first, and: second) - } - } - - /// Returns `true` if `token` is an opening or closing brace (according to - /// `kind`) of a closure, and that closure has no more than one statement in - /// its body. - private func isSmallClosureDelimiter( - _ token: TokenSyntax, - kind: KeyPath - ) -> Bool { - guard token.keyPathInParent == kind, - let closure = token.parent?.as(ClosureExprSyntax.self) - else { - return false - } - - return closure.statements.count <= 1 - } - - /// Returns `true` if `token` is the last token in the signature of a closure, - /// and that closure has no more than one statement in its body. - private func isEndOfSmallClosureSignature(_ token: TokenSyntax) -> Bool { - guard - token.keyPathInParent == \ClosureSignatureSyntax.inKeyword, - let closure = token.ancestorOrSelf(mapping: { $0.as(ClosureExprSyntax.self) }) - else { - return false - } - - return closure.statements.count <= 1 - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/AddDocumentation.swift b/Sources/SwiftLanguageService/CodeActions/AddDocumentation.swift deleted file mode 100644 index 755b50e14..000000000 --- a/Sources/SwiftLanguageService/CodeActions/AddDocumentation.swift +++ /dev/null @@ -1,151 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftBasicFormat -import SwiftExtensions -import SwiftParser -import SwiftRefactor -package import SwiftSyntax - -/// Insert a documentation template associated with a function or macro. -/// -/// ## Before -/// -/// ```swift -/// static func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? {} -/// ``` -/// -/// ## After -/// -/// ```swift -/// /// -/// /// - Parameters: -/// /// - syntax: -/// /// - context: -/// /// - Returns: -/// static func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? {} -/// ``` -package struct AddDocumentation: EditRefactoringProvider { - package static func textRefactor(syntax: DeclSyntax, in context: Void) -> [SourceEdit] { - let hasDocumentation = syntax.leadingTrivia.contains { trivia in - switch trivia { - case .blockComment, .docBlockComment, .lineComment, .docLineComment: - return true - default: - return false - } - } - - // We consider nodes at the start of the source file at being on a new line - let isOnNewLine = - syntax.leadingTrivia.contains(where: \.isNewline) || syntax.previousToken(viewMode: .sourceAccurate) == nil - - guard !hasDocumentation && isOnNewLine else { - return [] - } - - let newlineAndIndentation = [.newlines(1)] + (syntax.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []) - var content: [TriviaPiece] = [] - content.append(.docLineComment("/// A description")) - - if let parameters = syntax.parameters?.parameters { - if let onlyParam = parameters.only { - let paramToken = onlyParam.secondName?.text ?? onlyParam.firstName.text - content += newlineAndIndentation - content.append(.docLineComment("/// - Parameter \(paramToken):")) - } else { - content += newlineAndIndentation - content.append(.docLineComment("/// - Parameters:")) - content += parameters.flatMap({ param in - newlineAndIndentation + [ - .docLineComment("/// - \(param.secondName?.text ?? param.firstName.text):") - ] - }) - content += newlineAndIndentation - content.append(.docLineComment("///")) - } - } - - if syntax.throwsKeyword != nil { - content += newlineAndIndentation - content.append(.docLineComment("/// - Throws:")) - } - - if syntax.returnType != nil { - content += newlineAndIndentation - content.append(.docLineComment("/// - Returns:")) - } - content += newlineAndIndentation - - let insertPos = syntax.positionAfterSkippingLeadingTrivia - return [ - SourceEdit( - range: insertPos.. Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: DeclSyntax.self, - stoppingIf: { $0.is(CodeBlockItemSyntax.self) || $0.is(MemberBlockItemSyntax.self) || $0.is(ExprSyntax.self) } - ) - } - - static var title: String { "Add documentation" } -} - -extension DeclSyntax { - fileprivate var parameters: FunctionParameterClauseSyntax? { - switch self.as(DeclSyntaxEnum.self) { - case .functionDecl(let functionDecl): - return functionDecl.signature.parameterClause - case .subscriptDecl(let subscriptDecl): - return subscriptDecl.parameterClause - case .initializerDecl(let initializer): - return initializer.signature.parameterClause - case .macroDecl(let macro): - return macro.signature.parameterClause - default: - return nil - } - } - - fileprivate var throwsKeyword: TokenSyntax? { - switch self.as(DeclSyntaxEnum.self) { - case .functionDecl(let functionDecl): - return functionDecl.signature.effectSpecifiers?.throwsClause?.throwsSpecifier - case .initializerDecl(let initializer): - return initializer.signature.effectSpecifiers?.throwsClause?.throwsSpecifier - default: - return nil - } - } - - fileprivate var returnType: TypeSyntax? { - switch self.as(DeclSyntaxEnum.self) { - case .functionDecl(let functionDecl): - return functionDecl.signature.returnClause?.type - case .subscriptDecl(let subscriptDecl): - return subscriptDecl.returnClause.type - case .initializerDecl(let initializer): - return initializer.signature.returnClause?.type - case .macroDecl(let macro): - return macro.signature.returnClause?.type - default: - return nil - } - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift b/Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift deleted file mode 100644 index bb84a2d1f..000000000 --- a/Sources/SwiftLanguageService/CodeActions/ConvertIntegerLiteral.swift +++ /dev/null @@ -1,62 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SourceKitLSP -import SwiftRefactor -import SwiftSyntax -import SwiftSyntaxBuilder - -extension IntegerLiteralExprSyntax.Radix { - static let allCases: [Self] = [.binary, .octal, .decimal, .hex] -} - -/// Syntactic code action provider to convert integer literals between -/// different bases. -struct ConvertIntegerLiteral: SyntaxCodeActionProvider { - static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { - guard - let token = scope.innermostNodeContainingRange, - let integerExpr = token.parent?.as(IntegerLiteralExprSyntax.self), - let integerValue = Int( - integerExpr.split().value.filter { $0 != "_" }, - radix: integerExpr.radix.size - ) - else { - return [] - } - - var actions = [CodeAction]() - let currentRadix = integerExpr.radix - for radix in IntegerLiteralExprSyntax.Radix.allCases { - guard radix != currentRadix else { - continue - } - - let convertedValue: ExprSyntax = - "\(raw: radix.literalPrefix)\(raw: String(integerValue, radix: radix.size))" - let edit = TextEdit( - range: scope.snapshot.range(of: integerExpr), - newText: convertedValue.description - ) - actions.append( - CodeAction( - title: "Convert \(integerExpr) to \(convertedValue)", - kind: .refactorInline, - edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) - ) - ) - } - - return actions - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertJSONToCodableStruct.swift b/Sources/SwiftLanguageService/CodeActions/ConvertJSONToCodableStruct.swift deleted file mode 100644 index fd29e35e7..000000000 --- a/Sources/SwiftLanguageService/CodeActions/ConvertJSONToCodableStruct.swift +++ /dev/null @@ -1,415 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocol -import SwiftBasicFormat -import SwiftParser -import SwiftRefactor -package import SwiftSyntax -import SwiftSyntaxBuilder - -/// Convert JSON literals into corresponding Swift structs that conform to the -/// `Codable` protocol. -/// -/// ## Before -/// -/// ```javascript -/// { -/// "name": "Produce", -/// "shelves": [ -/// { -/// "name": "Discount Produce", -/// "product": { -/// "name": "Banana", -/// "points": 200, -/// "description": "A banana that's perfectly ripe." -/// } -/// } -/// ] -/// } -/// ``` -/// -/// ## After -/// -/// ```swift -/// struct JSONValue: Codable { -/// var name: String -/// var shelves: [Shelves] -/// -/// struct Shelves: Codable { -/// var name: String -/// var product: Product -/// -/// struct Product: Codable { -/// var description: String -/// var name: String -/// var points: Double -/// } -/// } -/// } -/// ``` -package struct ConvertJSONToCodableStruct: EditRefactoringProvider { - package static func textRefactor( - syntax: Syntax, - in context: Void - ) -> [SourceEdit] { - // Dig out a syntax node that looks like it might be JSON or have JSON - // in it. - guard let preflight = preflightRefactoring(syntax) else { - return [] - } - - // Dig out the text that we think might be JSON. - let text: String - switch preflight { - case let .closure(closure): - /// The outer structure of the JSON { ... } looks like a closure in the - /// syntax tree, albeit one with lots of ill-formed syntax in the body. - /// We're only going to look at the text of the closure to see if we - /// have JSON in there. - text = closure.trimmedDescription - case let .endingClosure(closure, unexpected): - text = closure.trimmedDescription + unexpected.description - - case .stringLiteral(_, let literalText): - /// A string literal that could contain JSON within it. - text = literalText - } - - // Try to process this as JSON. - guard - let data = text.data(using: .utf8), - let object = try? JSONSerialization.jsonObject(with: data), - let dictionary = object as? [String: Any] - else { - return [] - } - - // Create the top-level object. - let topLevelObject = JSONObject(dictionary: dictionary) - - // Render the top-level object as a struct. - let indentation = BasicFormat.inferIndentation(of: syntax) - let format = BasicFormat(indentationWidth: indentation) - let decls = topLevelObject.asDeclSyntax(name: "JSONValue") - .formatted(using: format) - - // Render the change into a set of source edits. - switch preflight { - case .closure(let closure): - // Closures are replaced entirely, since they were invalid code to - // start with. - return [ - SourceEdit(range: closure.trimmedRange, replacement: decls.description) - ] - case .endingClosure(let closure, let unexpected): - // Closures are replaced entirely, since they were invalid code to - // start with. - return [ - SourceEdit( - range: closure.positionAfterSkippingLeadingTrivia.. Preflight? { - // Preflight a closure. - // - // A blob of JSON dropped into a Swift source file will look like a - // closure due to the curly braces. The internals might be a syntactic - // disaster, but we don't actually care. - if let closure = syntax.as(ClosureExprSyntax.self) { - if let file = closure.parent?.parent?.parent?.as(SourceFileSyntax.self), - let unexpected = file.unexpectedBetweenStatementsAndEndOfFileToken - { - return .endingClosure(closure, unexpected) - } - return .closure(closure) - } - - // We found a string literal; its contents might be JSON. - if let stringLiteral = syntax.as(StringLiteralExprSyntax.self) { - // Look for an enclosing context and prefer that, because we might have - // a string literal that's inside a closure where the closure itself - // is the JSON. - if let parent = syntax.parent, - let enclosingPreflight = preflightRefactoring(parent) - { - return enclosingPreflight - } - - guard let text = stringLiteral.representedLiteralValue else { - return nil - } - - return .stringLiteral(stringLiteral, text) - } - - // Look further up the syntax tree. - if let parent = syntax.parent { - return preflightRefactoring(parent) - } - - return nil - } -} - -extension ConvertJSONToCodableStruct: SyntaxRefactoringCodeActionProvider { - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Syntax? { - var node: Syntax? = scope.innermostNodeContainingRange - while let unwrappedNode = node, ![.codeBlockItem, .memberBlockItem].contains(unwrappedNode.kind) { - if preflightRefactoring(unwrappedNode) != nil { - return unwrappedNode - } - node = unwrappedNode.parent - } - return nil - } - - static let title = "Create Codable structs from JSON" -} - -/// A JSON object, which is has a set of fields, each of which has the given -/// type. -private struct JSONObject { - /// The fields of the JSON object. - var fields: [String: JSONType] = [:] - - /// Form a JSON object from its fields. - private init(fields: [String: JSONType]) { - self.fields = fields - } - - /// Form a JSON object given a dictionary. - init(dictionary: [String: Any]) { - fields = dictionary.mapValues { JSONType(value: $0) } - } - - /// Merge the fields of this JSON object with another JSON object to produce - /// a JSON object - func merging(with other: JSONObject) -> JSONObject { - // Collect the set of all keys from both JSON objects. - let allKeys = Set(fields.keys).union(other.fields.keys) - - // Form a new JSON object containing the union of the fields - let newFields = allKeys.map { key in - let myValue = fields[key] ?? .null - let otherValue = other.fields[key] ?? .null - return (key, myValue.merging(with: otherValue)) - } - return JSONObject(fields: [String: JSONType](uniqueKeysWithValues: newFields)) - } - - /// Render this JSON object into a struct. - func asDeclSyntax(name: String) -> DeclSyntax { - /// The list of fields in this object, sorted alphabetically. - let sortedFields = fields.sorted(by: { $0.key < $1.key }) - - // Collect the nested types - let nestedTypes: [(name: String, type: JSONObject)] = sortedFields.compactMap { (name, type) in - guard let object = type.innerObject else { - return nil - } - - return (name.capitalized, object) - } - - let members = MemberBlockItemListSyntax { - // Print the fields of this type. - for (fieldName, fieldType) in sortedFields { - MemberBlockItemSyntax( - leadingTrivia: .newline, - decl: "var \(raw: fieldName): \(fieldType.asTypeSyntax(name: fieldName))" as DeclSyntax - ) - } - - // Print any nested types. - for (typeName, object) in nestedTypes { - MemberBlockItemSyntax( - leadingTrivia: (typeName == nestedTypes.first?.name) ? .newlines(2) : .newline, - decl: object.asDeclSyntax(name: typeName) - ) - } - } - - return """ - struct \(raw: name): Codable { - \(members.trimmed) - } - """ - } -} - -/// Describes the type of JSON data. -private enum JSONType { - /// String data - case string - - /// Numeric data - case number - - /// Boolean data - case boolean - - /// A "null", which implies optionality but without any underlying type - /// information. - case null - - /// An array. - indirect case array(JSONType) - - /// An object. - indirect case object(JSONObject) - - /// A value that is optional, for example because it is missing or null in - /// other cases. - indirect case optional(JSONType) - - /// Determine the type of a JSON value. - init(value: Any) { - switch value { - case let string as String: - switch string { - case "true", "false": self = .boolean - default: self = .string - } - case is NSNumber: - self = .number - case let array as [Any]: - // Use null as a fallback for an empty array. - guard let firstValue = array.first else { - self = .array(.null) - return - } - - // Merge the array elements. - let elementType: JSONType = array[1...].reduce( - JSONType(value: firstValue) - ) { (result, value) in - result.merging(with: JSONType(value: value)) - } - self = .array(elementType) - - case is NSNull: - self = .null - case let dictionary as [String: Any]: - self = .object(JSONObject(dictionary: dictionary)) - default: - self = .string - } - } - - /// Merge this JSON type with another JSON type, producing a new JSON type - /// that abstracts over the two. - func merging(with other: JSONType) -> JSONType { - switch (self, other) { - // Exact matches are easy. - case (.string, .string): return .string - case (.number, .number): return .number - case (.boolean, .boolean): return .boolean - case (.null, .null): return .null - - case (.array(let inner), .array(.null)), (.array(.null), .array(let inner)): - // Merging an array with an array of null leaves the array. - return .array(inner) - - case (.array(let inner), .null), (.null, .array(let inner)): - // Merging an array with a null just leaves an array. - return .array(inner) - - case (.array(let left), .array(let right)): - // Merging two arrays merges the element types - return .array(left.merging(with: right)) - - case (.object(let left), .object(let right)): - // Merging two arrays merges the element types - return .object(left.merging(with: right)) - - // Merging a string with a Boolean means we misinterpreted "true" or - // "false" as Boolean when it was meant as a string. - case (.string, .boolean), (.boolean, .string): return .string - - // Merging 'null' with an optional returns the optional. - case (.optional(let inner), .null), (.null, .optional(let inner)): - return .optional(inner) - - // Merging 'null' with anything else makes it an optional. - case (let inner, .null), (.null, let inner): - return .optional(inner) - - // Merging two optionals merges the underlying types and makes the - // result optional. - case (.optional(let left), .optional(let right)): - return .optional(left.merging(with: right)) - - // Merging an optional with anything else merges the underlying bits and - // makes them optional. - case (let outer, .optional(let inner)), (.optional(let inner), let outer): - return .optional(inner.merging(with: outer)) - - // Fall back to the null case when we don't know. - default: - return .null - } - } - - /// Dig out the JSON inner object referenced by this type. - var innerObject: JSONObject? { - switch self { - case .string, .null, .number, .boolean: nil - case .optional(let inner): inner.innerObject - case .array(let inner): inner.innerObject - case .object(let object): object - } - } - - /// Render this JSON type into type syntax. - func asTypeSyntax(name: String) -> TypeSyntax { - switch self { - case .string: "String" - case .number: "Double" - case .boolean: "Bool" - case .null: "Void" - case .optional(let inner): "\(inner.asTypeSyntax(name: name))?" - case .array(let inner): "[\(inner.asTypeSyntax(name: name))]" - case .object(_): "\(raw: name.capitalized)" - } - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertStringConcatenationToStringInterpolation.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStringConcatenationToStringInterpolation.swift deleted file mode 100644 index 02741581a..000000000 --- a/Sources/SwiftLanguageService/CodeActions/ConvertStringConcatenationToStringInterpolation.swift +++ /dev/null @@ -1,226 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SwiftRefactor -import SwiftSyntax - -/// ConvertStringConcatenationToStringInterpolation is a code action that converts a valid string concatenation into a -/// string interpolation. -struct ConvertStringConcatenationToStringInterpolation: SyntaxRefactoringProvider { - static func refactor(syntax: SequenceExprSyntax, in context: Void) throws -> SequenceExprSyntax { - guard let (componentsOnly, commonPounds) = preflight(exprList: syntax.elements) else { - throw RefactoringNotApplicableError("unsupported expression") - } - - var segments: StringLiteralSegmentListSyntax = [] - for component in componentsOnly { - guard let stringLiteral = component.as(StringLiteralExprSyntax.self) else { - segments.append( - .expressionSegment( - ExpressionSegmentSyntax( - pounds: commonPounds, - expressions: [ - LabeledExprSyntax(expression: component.singleLineTrivia) - ] - ) - ) - ) - continue - } - - if let commonPounds, stringLiteral.openingPounds?.tokenKind != commonPounds.tokenKind { - segments += stringLiteral.segments.map { segment in - if case let .expressionSegment(exprSegment) = segment { - .expressionSegment(exprSegment.with(\.pounds, commonPounds)) - } else { - segment - } - } - } else { - segments += stringLiteral.segments - } - } - - return syntax.with( - \.elements, - [ - ExprSyntax( - StringLiteralExprSyntax( - leadingTrivia: syntax.leadingTrivia, - openingPounds: commonPounds, - openingQuote: .stringQuoteToken(), - segments: segments, - closingQuote: .stringQuoteToken(), - closingPounds: commonPounds, - trailingTrivia: componentsOnly.last?.kind == .stringLiteralExpr ? syntax.trailingTrivia : nil - ) - ) - ] - ) - } - - /// If `exprList` is a valid string concatenation, returns 1) all elements in `exprList` with concat operators - /// stripped and 2) the longest pounds amongst all string literals, otherwise returns nil. - /// - /// `exprList` as a valid string concatenation must contain n >= 3 children where n is an odd number with a concat - /// operator `+` separating every other child, which must either be a single-line string literal or a valid - /// expression for string interpolation. `exprList` must also contain at least one string literal child. - /// - /// The following is a valid string concatenation. - /// ``` swift - /// "Hello " + aString + "\(1)World" - /// ``` - /// The following are invalid string concatenations. - /// ``` swift - /// aString + bString // no string literals - /// - /// "Hello " * aString - "World" // non `+` operators - /// - /// """ - /// Hello - /// """ - /// + """ - /// World - /// """ // multi-line string literals - /// ``` - private static func preflight( - exprList: ExprListSyntax - ) -> (componentsOnly: [ExprListSyntax.Element], longestPounds: TokenSyntax?)? { - var iter = exprList.makeIterator() - guard let first = iter.next() else { - return nil - } - - var hasStringComponents = false - var longestPounds: TokenSyntax? - var componentsOnly = [ExprListSyntax.Element]() - componentsOnly.reserveCapacity(exprList.count / 2 + 1) - - if let stringLiteral = first.as(StringLiteralExprSyntax.self) { - guard stringLiteral.isSingleLine else { - return nil - } - hasStringComponents = true - longestPounds = stringLiteral.openingPounds - } - componentsOnly.append(first) - - while let concat = iter.next(), let stringComponent = iter.next() { - guard let concat = concat.as(BinaryOperatorExprSyntax.self), - concat.operator.tokenKind == .binaryOperator("+") && !stringComponent.is(MissingExprSyntax.self) - else { - return nil - } - - if let stringLiteral = stringComponent.as(StringLiteralExprSyntax.self) { - guard stringLiteral.isSingleLine else { - return nil - } - hasStringComponents = true - if let pounds = stringLiteral.openingPounds, - pounds.trimmedLength > (longestPounds?.trimmedLength ?? SourceLength(utf8Length: 0)) - { - longestPounds = pounds - } - } - - componentsOnly[componentsOnly.count - 1].trailingTrivia += concat.leadingTrivia - componentsOnly.append( - stringComponent.with(\.leadingTrivia, stringComponent.leadingTrivia + concat.trailingTrivia) - ) - } - - guard hasStringComponents && componentsOnly.count > 1 else { - return nil - } - - return (componentsOnly, longestPounds) - } -} - -extension ConvertStringConcatenationToStringInterpolation: SyntaxRefactoringCodeActionProvider { - static let title: String = "Convert String Concatenation to String Interpolation" - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> SequenceExprSyntax? { - guard let expr = scope.innermostNodeContainingRange, - let seqExpr = expr.findParentOfSelf( - ofType: SequenceExprSyntax.self, - stoppingIf: { - $0.kind == .codeBlockItem || $0.kind == .memberBlockItem - } - ) - else { - return nil - } - - return seqExpr - } -} - -private extension String { - var uncommented: Substring { - trimmingPrefix { $0 == "/" } - } -} - -private extension StringLiteralExprSyntax { - var isSingleLine: Bool { - openingQuote.tokenKind == .stringQuote - } -} - -private extension SyntaxProtocol { - /// Modifies the trivia to not contain any newlines. This removes whitespace trivia, replaces newlines with - /// whitespaces in block comments and converts line comments to block comments. - var singleLineTrivia: Self { - with(\.leadingTrivia, leadingTrivia.withSingleLineComments.withCommentsOnly(isLeadingTrivia: true)) - .with(\.trailingTrivia, trailingTrivia.withSingleLineComments.withCommentsOnly(isLeadingTrivia: false)) - } -} - -private extension Trivia { - /// Replaces newlines with whitespaces in block comments and converts line comments to block comments. - var withSingleLineComments: Self { - Trivia( - pieces: map { - switch $0 { - case let .lineComment(lineComment): - .blockComment("/*\(lineComment.uncommented)*/") - case let .docLineComment(docLineComment): - .docBlockComment("/**\(docLineComment.uncommented)*/") - case let .blockComment(blockComment), let .docBlockComment(blockComment): - .blockComment(blockComment.replacing("\r\n", with: " ").replacing("\n", with: " ")) - default: - $0 - } - } - ) - } - - /// Removes all non-comment trivia pieces and inserts a whitespace between each comment. - func withCommentsOnly(isLeadingTrivia: Bool) -> Self { - Trivia( - pieces: flatMap { piece -> [TriviaPiece] in - if piece.isComment { - if isLeadingTrivia { - [piece, .spaces(1)] - } else { - [.spaces(1), piece] - } - } else { - [] - } - } - ) - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift b/Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift deleted file mode 100644 index 8ea598fcf..000000000 --- a/Sources/SwiftLanguageService/CodeActions/PackageManifestEdits.swift +++ /dev/null @@ -1,233 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocol -import SourceKitLSP -import SwiftParser -@_spi(PackageRefactor) import SwiftRefactor -import SwiftSyntax - -/// Syntactic code action provider to provide refactoring actions that -/// edit a package manifest. -struct PackageManifestEdits: SyntaxCodeActionProvider { - static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { - guard let call = scope.innermostNodeContainingRange?.findEnclosingCall() else { - return [] - } - - return addTargetActions(call: call, in: scope) + addTestTargetActions(call: call, in: scope) - + addProductActions(call: call, in: scope) - } - - /// Produce code actions to add new targets of various kinds. - static func addTargetActions( - call: FunctionCallExprSyntax, - in scope: SyntaxCodeActionScope - ) -> [CodeAction] { - do { - var actions: [CodeAction] = [] - let variants: [(PackageTarget.TargetKind, String)] = [ - (.library, "library"), - (.executable, "executable"), - (.macro, "macro"), - ] - - for (type, name) in variants { - let target = PackageTarget( - name: "NewTarget", - type: type - ) - - guard - let edit = try AddPackageTarget.textRefactor( - syntax: scope.file, - in: .init(target: target) - ).asWorkspaceEdit(snapshot: scope.snapshot) - else { - continue - } - - actions.append( - CodeAction( - title: "Add \(name) target", - kind: .refactor, - edit: edit - ) - ) - } - - return actions - } catch { - return [] - } - } - - /// Produce code actions to add test target(s) if we are currently on - /// a target for which we know how to create a test. - static func addTestTargetActions( - call: FunctionCallExprSyntax, - in scope: SyntaxCodeActionScope - ) -> [CodeAction] { - guard let calledMember = call.findMemberAccessCallee(), - targetsThatAllowTests.contains(calledMember), - let targetName = call.findStringArgument(label: "name") - else { - return [] - } - - do { - var actions: [CodeAction] = [] - - let variants: [(AddPackageTarget.TestHarness, String)] = [ - (.swiftTesting, "Swift Testing"), - (.xctest, "XCTest"), - ] - for (testingLibrary, libraryName) in variants { - // Describe the target we are going to create. - let target = PackageTarget( - name: "\(targetName)Tests", - type: .test, - dependencies: [.byName(name: targetName)], - ) - - guard - let edit = try AddPackageTarget.textRefactor( - syntax: scope.file, - in: .init(target: target, testHarness: testingLibrary) - ).asWorkspaceEdit(snapshot: scope.snapshot) - else { - continue - } - - actions.append( - CodeAction( - title: "Add test target (\(libraryName))", - kind: .refactor, - edit: edit - ) - ) - } - - return actions - } catch { - return [] - } - } - - /// A list of target kinds that allow the creation of tests. - static let targetsThatAllowTests: [String] = [ - "executableTarget", - "macro", - "target", - ] - - /// Produce code actions to add a product if we are currently on - /// a target for which we can create a product. - static func addProductActions( - call: FunctionCallExprSyntax, - in scope: SyntaxCodeActionScope - ) -> [CodeAction] { - guard let calledMember = call.findMemberAccessCallee(), - targetsThatAllowProducts.contains(calledMember), - let targetName = call.findStringArgument(label: "name") - else { - return [] - } - - do { - let type: ProductDescription.ProductType = - calledMember == "executableTarget" - ? .executable - : .library(.automatic) - - // Describe the target we are going to create. - let product = ProductDescription( - name: targetName, - type: type, - targets: [targetName] - ) - - guard - let edit = try AddProduct.textRefactor( - syntax: scope.file, - in: .init(product: product) - ).asWorkspaceEdit(snapshot: scope.snapshot) - else { - return [] - } - - return [ - CodeAction( - title: "Add product to export this target", - kind: .refactor, - edit: edit - ) - ] - } catch { - return [] - } - } - - /// A list of target kinds that allow the creation of tests. - static let targetsThatAllowProducts: [String] = [ - "executableTarget", - "target", - ] -} - -fileprivate extension SyntaxProtocol { - // Find an enclosing call syntax expression. - func findEnclosingCall() -> FunctionCallExprSyntax? { - var current = Syntax(self) - while true { - if let call = current.as(FunctionCallExprSyntax.self) { - return call - } - - if let parent = current.parent { - current = parent - continue - } - - return nil - } - } -} - -fileprivate extension FunctionCallExprSyntax { - /// Find an argument with the given label that has a string literal as - /// its argument. - func findStringArgument(label: String) -> String? { - for arg in arguments { - if arg.label?.text == label { - return arg.expression.as(StringLiteralExprSyntax.self)? - .representedLiteralValue - } - } - - return nil - } - - /// Find the callee when it is a member access expression referencing - /// a declaration when a specific name. - func findMemberAccessCallee() -> String? { - guard - let memberAccess = self.calledExpression - .as(MemberAccessExprSyntax.self) - else { - return nil - } - - return memberAccess.declName.baseName.text - } -} diff --git a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift deleted file mode 100644 index 35c3c8884..000000000 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift +++ /dev/null @@ -1,90 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging -import SourceKitLSP -import SwiftRefactor -import SwiftSyntax - -/// Describes types that provide one or more code actions based on purely -/// syntactic information. -protocol SyntaxCodeActionProvider: SendableMetatype { - /// Produce code actions within the given scope. Each code action - /// corresponds to one syntactic transformation that can be performed, such - /// as adding or removing separators from an integer literal. - static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] -} - -/// Defines the scope in which a syntactic code action occurs. -struct SyntaxCodeActionScope { - /// The snapshot of the document on which the code actions will be evaluated. - var snapshot: DocumentSnapshot - - /// The actual code action request, which can specify additional parameters - /// to guide the code actions. - var request: CodeActionRequest - - /// The source file in which the syntactic code action will operate. - var file: SourceFileSyntax - - /// The UTF-8 byte range in the source file in which code actions should be - /// considered, i.e., where the cursor or selection is. - var range: Range - - /// The innermost node that contains the entire selected source range - var innermostNodeContainingRange: Syntax? - - init?( - snapshot: DocumentSnapshot, - syntaxTree file: SourceFileSyntax, - request: CodeActionRequest - ) { - self.snapshot = snapshot - self.request = request - self.file = file - - guard let left = tokenForRefactoring(at: request.range.lowerBound, snapshot: snapshot, syntaxTree: file), - let right = tokenForRefactoring(at: request.range.upperBound, snapshot: snapshot, syntaxTree: file) - else { - return nil - } - self.range = left.position.. TokenSyntax? { - let absolutePosition = snapshot.absolutePosition(of: position) - if absolutePosition == syntaxTree.endPosition { - // token(at:) will not find the end of file token if the end of file token has length 0. Special case this and - // return the last proper token in this case. - return syntaxTree.endOfFileToken.previousToken(viewMode: .sourceAccurate) - } - guard let token = syntaxTree.token(at: absolutePosition) else { - return nil - } - // See `adjustPositionToStartOfIdentifier`. We need to be a little more aggressive for the refactorings and also - // adjust to the start of punctuation eg. if the end of the selected range is after a `}`, we want the end token for - // the refactoring to be the `}`, not the token after `}`. - if absolutePosition == token.position, - let previousToken = token.previousToken(viewMode: .sourceAccurate), - previousToken.endPositionBeforeTrailingTrivia == absolutePosition - { - return previousToken - } - return token -} diff --git a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift deleted file mode 100644 index ba1631138..000000000 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift +++ /dev/null @@ -1,33 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftRefactor - -/// List of all of the syntactic code action providers, which can be used -/// to produce code actions using only the swift-syntax tree of a file. -let allSyntaxCodeActions: [SyntaxCodeActionProvider.Type] = { - var result: [SyntaxCodeActionProvider.Type] = [ - AddDocumentation.self, - AddSeparatorsToIntegerLiteral.self, - ConvertIntegerLiteral.self, - ConvertJSONToCodableStruct.self, - ConvertStringConcatenationToStringInterpolation.self, - FormatRawStringLiteral.self, - MigrateToNewIfLetSyntax.self, - OpaqueParameterToGeneric.self, - RemoveSeparatorsFromIntegerLiteral.self, - ] - #if !NO_SWIFTPM_DEPENDENCY - result.append(PackageManifestEdits.self) - #endif - return result -}() diff --git a/Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift deleted file mode 100644 index 08ce89437..000000000 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift +++ /dev/null @@ -1,158 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SourceKitLSP -import SwiftRefactor -import SwiftSyntax - -/// Protocol that adapts a SyntaxRefactoringProvider (that comes from -/// swift-syntax) into a SyntaxCodeActionProvider. -protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefactoringProvider { - static var title: String { get } - - /// Returns the node that the syntax refactoring should be performed on, if code actions are requested for the given - /// scope. - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? -} - -/// SyntaxCodeActionProviders with a \c Void context can automatically be -/// adapted provide a code action based on their refactoring operation. -extension SyntaxRefactoringCodeActionProvider where Self.Context == Void { - static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { - guard let node = nodeToRefactor(in: scope) else { - return [] - } - - guard let sourceEdits = try? Self.textRefactor(syntax: node) else { - return [] - } - - guard let workspaceEdit = sourceEdits.asWorkspaceEdit(snapshot: scope.snapshot) else { - return [] - } - - return [ - CodeAction( - title: Self.title, - kind: .refactorInline, - edit: workspaceEdit - ) - ] - } -} - -// Adapters for specific refactoring provides in swift-syntax. - -extension AddSeparatorsToIntegerLiteral: SyntaxRefactoringCodeActionProvider { - package static var title: String { "Add digit separators" } - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: IntegerLiteralExprSyntax.self, - stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } - ) - } -} - -extension FormatRawStringLiteral: SyntaxRefactoringCodeActionProvider { - package static var title: String { - "Convert string literal to minimal number of '#'s" - } - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: StringLiteralExprSyntax.self, - stoppingIf: { - $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) - || $0.keyPathInParent == \ExpressionSegmentSyntax.expressions - } - ) - } -} - -extension MigrateToNewIfLetSyntax: SyntaxRefactoringCodeActionProvider { - package static var title: String { "Migrate to shorthand 'if let' syntax" } - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: IfExprSyntax.self, - stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } - ) - } -} - -extension OpaqueParameterToGeneric: SyntaxRefactoringCodeActionProvider { - package static var title: String { "Expand 'some' parameters to generic parameters" } - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: DeclSyntax.self, - stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } - ) - } -} - -extension RemoveSeparatorsFromIntegerLiteral: SyntaxRefactoringCodeActionProvider { - package static var title: String { "Remove digit separators" } - - static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input? { - return scope.innermostNodeContainingRange?.findParentOfSelf( - ofType: IntegerLiteralExprSyntax.self, - stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } - ) - } -} - -extension SyntaxProtocol { - /// Finds the innermost parent of the given type while not walking outside of nodes that satisfy `stoppingIf`. - func findParentOfSelf( - ofType: ParentType.Type, - stoppingIf: (Syntax) -> Bool - ) -> ParentType? { - var node: Syntax? = Syntax(self) - while let unwrappedNode = node, !stoppingIf(unwrappedNode) { - if let expectedType = unwrappedNode.as(ParentType.self) { - return expectedType - } - node = unwrappedNode.parent - } - return nil - } -} - -extension [SourceEdit] { - /// Translate source edits into a workspace edit. - /// `snapshot` is the latest snapshot of the document to which these edits belong. - func asWorkspaceEdit(snapshot: DocumentSnapshot) -> WorkspaceEdit? { - let textEdits = compactMap { edit -> TextEdit? in - let edit = TextEdit( - range: snapshot.absolutePositionRange(of: edit.range), - newText: edit.replacement - ) - - if edit.isNoOp(in: snapshot) { - return nil - } - - return edit - } - - if textEdits.isEmpty { - return nil - } - - return WorkspaceEdit( - changes: [snapshot.uri: textEdits] - ) - } -} diff --git a/Sources/SwiftLanguageService/CodeCompletion.swift b/Sources/SwiftLanguageService/CodeCompletion.swift deleted file mode 100644 index 69f1e1e52..000000000 --- a/Sources/SwiftLanguageService/CodeCompletion.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP -import SwiftBasicFormat - -extension SwiftLanguageService { - package func completion(_ req: CompletionRequest) async throws -> CompletionList { - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - - let completionPos = await adjustPositionToStartOfIdentifier(req.position, in: snapshot) - let filterText = String(snapshot.text[snapshot.index(of: completionPos).. CompletionItem { - return try await CodeCompletionSession.completionItemResolve(item: req.item, sourcekitd: sourcekitd) - } -} diff --git a/Sources/SwiftLanguageService/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift deleted file mode 100644 index 430a6ae09..000000000 --- a/Sources/SwiftLanguageService/CodeCompletionSession.swift +++ /dev/null @@ -1,726 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import Dispatch -import Foundation -import LanguageServerProtocol -import SKLogging -import SKOptions -import SKUtilities -import SourceKitD -import SourceKitLSP -import SwiftBasicFormat -import SwiftExtensions -import SwiftParser -@_spi(SourceKitLSP) import SwiftRefactor -import SwiftSyntax - -/// Uniquely identifies a code completion session. We need this so that when resolving a code completion item, we can -/// verify that the item to resolve belongs to the code completion session that is currently open. -struct CompletionSessionID: Equatable { - private static let nextSessionID = AtomicUInt32(initialValue: 0) - - let value: UInt32 - - init(value: UInt32) { - self.value = value - } - - static func next() -> CompletionSessionID { - return CompletionSessionID(value: nextSessionID.fetchAndIncrement()) - } -} - -/// Data that is attached to a `CompletionItem`. -struct CompletionItemData: LSPAnyCodable { - let uri: DocumentURI - let sessionId: CompletionSessionID - let itemId: Int - - init(uri: DocumentURI, sessionId: CompletionSessionID, itemId: Int) { - self.uri = uri - self.sessionId = sessionId - self.itemId = itemId - } - - init?(fromLSPDictionary dictionary: [String: LSPAny]) { - guard case .string(let uriString) = dictionary["uri"], - case .int(let sessionId) = dictionary["sessionId"], - case .int(let itemId) = dictionary["itemId"], - let uri = try? DocumentURI(string: uriString) - else { - return nil - } - self.uri = uri - self.sessionId = CompletionSessionID(value: UInt32(sessionId)) - self.itemId = itemId - } - - func encodeToLSPAny() -> LSPAny { - return .dictionary([ - "uri": .string(uri.stringValue), - "sessionId": .int(Int(sessionId.value)), - "itemId": .int(itemId), - ]) - } -} - -/// Represents a code-completion session for a given source location that can be efficiently -/// re-filtered by calling `update()`. -/// -/// The first call to `update()` opens the session with sourcekitd, which computes the initial -/// completions. Subsequent calls to `update()` will re-filter the original completions. Finally, -/// before creating a new completion session, you must call `close()`. It is an error to create a -/// new completion session with the same source location before closing the original session. -/// -/// At the sourcekitd level, this uses `codecomplete.open`, `codecomplete.update` and -/// `codecomplete.close` requests. -class CodeCompletionSession { - // MARK: - Public static API - - /// The queue on which all code completion requests are executed. - /// - /// This is needed because sourcekitd has a single, global code completion - /// session and we need to make sure that multiple code completion requests - /// don't race each other. - /// - /// Technically, we would only need one queue for each sourcekitd and different - /// sourcekitd could serve code completion requests simultaneously. - /// - /// But it's rare to open multiple sourcekitd instances simultaneously and - /// even rarer to interact with them at the same time, so we have a global - /// queue for now to simplify the implementation. - private static let completionQueue = AsyncQueue() - - /// The code completion session for each sourcekitd instance. - /// - /// `sourcekitd` has a global code completion session, that's why we need to - /// have a global mapping from `sourcekitd` to its currently active code - /// completion session. - /// - /// - Important: Must only be accessed on `completionQueue`. - /// `nonisolated(unsafe)` fine because this is guarded by `completionQueue`. - private static nonisolated(unsafe) var completionSessions: [ObjectIdentifier: CodeCompletionSession] = [:] - - /// Gets the code completion results for the given parameters. - /// - /// If a code completion session that is compatible with the parameters - /// already exists, this just performs an update to the filtering. If it does - /// not, this opens a new code completion session with `sourcekitd` and gets - /// the results. - /// - /// - Parameters: - /// - sourcekitd: The `sourcekitd` instance from which to get code - /// completion results - /// - snapshot: The document in which to perform completion. - /// - completionPosition: The position at which to perform completion. - /// This is the position at which the code completion token logically - /// starts. For example when completing `foo.ba|`, then the completion - /// position should be after the `.`. - /// - completionUtf8Offset: Same as `completionPosition` but as a UTF-8 - /// offset within the buffer. - /// - cursorPosition: The position at which the cursor is positioned. E.g. - /// when completing `foo.ba|`, this is after the `a` (see - /// `completionPosition` for comparison) - /// - compileCommand: The compiler arguments to use. - /// - options: Further options that can be sent from the editor to control - /// completion. - /// - filterText: The text by which to filter code completion results. - /// - mustReuse: If `true` and there is an active session in this - /// `sourcekitd` instance, cancel the request instead of opening a new - /// session. - /// This is set to `true` when triggering a filter from incomplete results - /// so that clients can rely on results being delivered quickly when - /// getting updated results after updating the filter text. - /// - Returns: The code completion results for those parameters. - static func completionList( - sourcekitd: SourceKitD, - snapshot: DocumentSnapshot, - options: SourceKitLSPOptions, - indentationWidth: Trivia?, - completionPosition: Position, - cursorPosition: Position, - compileCommand: SwiftCompileCommand?, - clientCapabilities: ClientCapabilities, - filterText: String - ) async throws -> CompletionList { - let task = completionQueue.asyncThrowing { - if let session = completionSessions[ObjectIdentifier(sourcekitd)], session.state == .open { - let isCompatible = - session.snapshot.uri == snapshot.uri - && session.position == completionPosition - && session.compileCommand == compileCommand - - if isCompatible { - return try await session.update( - filterText: filterText, - position: cursorPosition, - in: snapshot - ) - } - - // The sessions aren't compatible. Close the existing session and open - // a new one below. - await session.close() - } - let session = CodeCompletionSession( - sourcekitd: sourcekitd, - snapshot: snapshot, - options: options, - indentationWidth: indentationWidth, - position: completionPosition, - compileCommand: compileCommand, - clientCapabilities: clientCapabilities - ) - completionSessions[ObjectIdentifier(sourcekitd)] = session - return try await session.open(filterText: filterText, position: cursorPosition, in: snapshot) - } - - return try await task.valuePropagatingCancellation - } - - static func completionItemResolve( - item: CompletionItem, - sourcekitd: SourceKitD - ) async throws -> CompletionItem { - guard let data = CompletionItemData(fromLSPAny: item.data) else { - return item - } - let task = completionQueue.asyncThrowing { - guard let session = completionSessions[ObjectIdentifier(sourcekitd)], data.sessionId == session.id else { - throw ResponseError.unknown("No active completion session for \(data.uri)") - } - return await Self.resolveDocumentation( - in: item, - timeout: session.options.sourcekitdRequestTimeoutOrDefault, - restartTimeout: session.options.semanticServiceRestartTimeoutOrDefault, - sourcekitd: sourcekitd - ) - } - return try await task.valuePropagatingCancellation - } - - /// Close all code completion sessions for the given files. - /// - /// This should only be necessary to do if the dependencies have updated. In all other cases `completionList` will - /// decide whether an existing code completion session can be reused. - static func close(sourcekitd: SourceKitD, uris: Set) { - completionQueue.async { - if let session = completionSessions[ObjectIdentifier(sourcekitd)], uris.contains(session.uri), - session.state == .open - { - await session.close() - } - } - } - - // MARK: - Implementation - - private let id: CompletionSessionID - private let sourcekitd: SourceKitD - private let snapshot: DocumentSnapshot - private let options: SourceKitLSPOptions - /// The inferred indentation width of the source file the completion is being performed in - private let indentationWidth: Trivia? - private let position: Position - private let compileCommand: SwiftCompileCommand? - private let clientSupportsSnippets: Bool - private let clientSupportsDocumentationResolve: Bool - private var state: State = .closed - - private enum State { - case closed - case open - } - - private nonisolated var uri: DocumentURI { snapshot.uri } - private nonisolated var keys: sourcekitd_api_keys { return sourcekitd.keys } - - private init( - sourcekitd: SourceKitD, - snapshot: DocumentSnapshot, - options: SourceKitLSPOptions, - indentationWidth: Trivia?, - position: Position, - compileCommand: SwiftCompileCommand?, - clientCapabilities: ClientCapabilities - ) { - self.id = CompletionSessionID.next() - self.sourcekitd = sourcekitd - self.options = options - self.indentationWidth = indentationWidth - self.snapshot = snapshot - self.position = position - self.compileCommand = compileCommand - self.clientSupportsSnippets = clientCapabilities.textDocument?.completion?.completionItem?.snippetSupport ?? false - self.clientSupportsDocumentationResolve = - clientCapabilities.textDocument?.completion?.completionItem?.resolveSupport?.properties.contains("documentation") - ?? false - } - - private func open( - filterText: String, - position cursorPosition: Position, - in snapshot: DocumentSnapshot - ) async throws -> CompletionList { - logger.info("Opening code completion session: \(self.description) filter=\(filterText)") - guard snapshot.version == self.snapshot.version else { - throw ResponseError(code: .invalidRequest, message: "open must use the original snapshot") - } - - let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) - let req = sourcekitd.dictionary([ - keys.line: sourcekitdPosition.line, - keys.column: sourcekitdPosition.utf8Column, - keys.name: uri.pseudoPath, - keys.sourceFile: uri.pseudoPath, - keys.sourceText: snapshot.text, - keys.codeCompleteOptions: optionsDictionary(filterText: filterText), - ]) - - let dict = try await send(sourceKitDRequest: \.codeCompleteOpen, req, snapshot: snapshot) - self.state = .open - - guard let completions: SKDResponseArray = dict[keys.results] else { - return CompletionList(isIncomplete: false, items: []) - } - - try Task.checkCancellation() - - return await self.completionsFromSKDResponse( - completions, - in: snapshot, - completionPos: self.position, - requestPosition: cursorPosition, - isIncomplete: true - ) - } - - private func update( - filterText: String, - position: Position, - in snapshot: DocumentSnapshot - ) async throws -> CompletionList { - logger.info("Updating code completion session: \(self.description) filter=\(filterText)") - let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) - let req = sourcekitd.dictionary([ - keys.line: sourcekitdPosition.line, - keys.column: sourcekitdPosition.utf8Column, - keys.name: uri.pseudoPath, - keys.sourceFile: uri.pseudoPath, - keys.codeCompleteOptions: optionsDictionary(filterText: filterText), - ]) - - let dict = try await send(sourceKitDRequest: \.codeCompleteUpdate, req, snapshot: snapshot) - guard let completions: SKDResponseArray = dict[keys.results] else { - return CompletionList(isIncomplete: false, items: []) - } - - return await self.completionsFromSKDResponse( - completions, - in: snapshot, - completionPos: self.position, - requestPosition: position, - isIncomplete: true - ) - } - - private func optionsDictionary( - filterText: String - ) -> SKDRequestDictionary { - let dict = sourcekitd.dictionary([ - // Sorting and priority options. - keys.hideUnderscores: 0, - keys.hideLowPriority: 0, - keys.hideByName: 0, - keys.addInnerOperators: 0, - keys.topNonLiteral: 0, - keys.addCallWithNoDefaultArgs: 1, - // Filtering options. - keys.filterText: filterText, - keys.requestLimit: 200, - keys.useNewAPI: 1, - ]) - return dict - } - - private func close() async { - switch self.state { - case .closed: - // Already closed, nothing to do. - break - case .open: - let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) - let req = sourcekitd.dictionary([ - keys.line: sourcekitdPosition.line, - keys.column: sourcekitdPosition.utf8Column, - keys.sourceFile: snapshot.uri.pseudoPath, - keys.name: snapshot.uri.pseudoPath, - keys.codeCompleteOptions: [keys.useNewAPI: 1], - ]) - logger.info("Closing code completion session: \(self.description)") - _ = try? await send(sourceKitDRequest: \.codeCompleteClose, req, snapshot: nil) - self.state = .closed - } - } - - // MARK: - Helpers - - private func send( - sourceKitDRequest requestUid: KeyPath & Sendable, - _ request: SKDRequestDictionary, - snapshot: DocumentSnapshot? - ) async throws -> SKDResponseDictionary { - try await sourcekitd.send( - requestUid, - request, - timeout: options.sourcekitdRequestTimeoutOrDefault, - restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot?.uri.arbitrarySchemeURL, - fileContents: snapshot?.text - ) - } - - private func expandClosurePlaceholders(insertText: String) -> String? { - guard insertText.contains("<#") && insertText.contains("->") else { - // Fast path: There is no closure placeholder to expand - return nil - } - - let (strippedPrefix, exprToExpand) = extractExpressionToExpand(from: insertText) - - // Note we don't need special handling for macro expansions since - // their insertion text doesn't include the '#', so are parsed as - // function calls here. - var parser = Parser(exprToExpand) - let expr = ExprSyntax.parse(from: &parser) - guard let call = OutermostFunctionCallFinder.findOutermostFunctionCall(in: expr), - let expandedCall = try? ExpandEditorPlaceholdersToLiteralClosures.refactor( - syntax: Syntax(call), - in: ExpandEditorPlaceholdersToLiteralClosures.Context( - format: .custom( - ClosureCompletionFormat(indentationWidth: indentationWidth), - allowNestedPlaceholders: true - ) - ) - ) - else { - return nil - } - - let bytesToExpand = Array(exprToExpand.utf8) - - var expandedBytes: [UInt8] = [] - // Add the prefix that we stripped off to allow expression parsing - expandedBytes += strippedPrefix.utf8 - // Add any part of the expression that didn't end up being part of the function call - expandedBytes += bytesToExpand[0.. (strippedPrefix: String, exprToExpand: String) { - if insertText.starts(with: "?.") { - return (strippedPrefix: "?.", exprToExpand: String(insertText.dropFirst(2))) - } else { - return (strippedPrefix: "", exprToExpand: insertText) - } - } - - /// If the code completion text returned by sourcekitd, format it using SwiftBasicFormat. This is needed for - /// completion items returned from sourcekitd that already have the trailing closure expanded. - private func formatMultiLineCompletion(insertText: String) -> String? { - // We only need to format the completion result if it's a multi-line completion that needs adjustment of - // indentation. - guard insertText.contains(where: \.isNewline) else { - return nil - } - - let (strippedPrefix, exprToExpand) = extractExpressionToExpand(from: insertText) - - var parser = Parser(exprToExpand) - let expr = ExprSyntax.parse(from: &parser) - let formatted = expr.formatted(using: ClosureCompletionFormat(indentationWidth: indentationWidth)) - - return strippedPrefix + formatted.description - } - - private func completionsFromSKDResponse( - _ completions: SKDResponseArray, - in snapshot: DocumentSnapshot, - completionPos: Position, - requestPosition: Position, - isIncomplete: Bool - ) async -> CompletionList { - let sourcekitd = self.sourcekitd - let keys = sourcekitd.keys - - var completionItems = completions.compactMap { (value: SKDResponseDictionary) -> CompletionItem? in - guard let name: String = value[keys.description], - var insertText: String = value[keys.sourceText] - else { - return nil - } - - var filterName: String? = value[keys.name] - let typeName: String? = value[sourcekitd.keys.typeName] - let utf8CodeUnitsToErase: Int = value[sourcekitd.keys.numBytesToErase] ?? 0 - - if let closureExpanded = expandClosurePlaceholders(insertText: insertText) { - insertText = closureExpanded - } else if let multilineFormatted = formatMultiLineCompletion(insertText: insertText) { - insertText = multilineFormatted - } - - let text = rewriteSourceKitPlaceholders(in: insertText, clientSupportsSnippets: clientSupportsSnippets) - let isInsertTextSnippet = clientSupportsSnippets && text != insertText - - let textEdit: TextEdit? - let edit = self.computeCompletionTextEdit( - completionPos: completionPos, - requestPosition: requestPosition, - utf8CodeUnitsToErase: utf8CodeUnitsToErase, - newText: text, - snapshot: snapshot - ) - textEdit = edit - - if utf8CodeUnitsToErase != 0, filterName != nil, let textEdit = textEdit { - // To support the case where the client is doing prefix matching on the TextEdit range, - // we need to prepend the deleted text to filterText. - // This also works around a behaviour in VS Code that causes completions to not show up - // if a '.' is being replaced for Optional completion. - let filterPrefix = snapshot.text[snapshot.indexRange(of: textEdit.range.lowerBound..= 10_000 { - logger.fault( - "score out-of-bounds: \(score, privacy: .public), semantic: \(semanticScore, privacy: .public), textual: \(textMatchScore, privacy: .public)" - ) - lexicallySortableScore = 9_999.99999999 - } - sortText = String(format: "%013.8f", lexicallySortableScore) + "-\(name)" - } else { - sortText = nil - } - - let data: CompletionItemData? = - if let identifier: Int = value[keys.identifier] { - CompletionItemData(uri: self.uri, sessionId: self.id, itemId: identifier) - } else { - nil - } - - let kind: sourcekitd_api_uid_t? = value[sourcekitd.keys.kind] - return CompletionItem( - label: name, - kind: kind?.asCompletionItemKind(sourcekitd.values) ?? .value, - detail: typeName, - documentation: nil, - deprecated: notRecommended, - sortText: sortText, - filterText: filterName, - insertText: text, - insertTextFormat: isInsertTextSnippet ? .snippet : .plain, - textEdit: textEdit.map(CompletionItemEdit.textEdit), - data: data.encodeToLSPAny() - ) - } - - if !clientSupportsDocumentationResolve { - let semanticServiceRestartTimeoutOrDefault = self.options.semanticServiceRestartTimeoutOrDefault - completionItems = await completionItems.asyncMap { item in - return await Self.resolveDocumentation( - in: item, - timeout: .seconds(1), - restartTimeout: semanticServiceRestartTimeoutOrDefault, - sourcekitd: sourcekitd - ) - } - } - - return CompletionList(isIncomplete: isIncomplete, items: completionItems) - } - - private static func resolveDocumentation( - in item: CompletionItem, - timeout: Duration, - restartTimeout: Duration, - sourcekitd: SourceKitD - ) async -> CompletionItem { - var item = item - if let itemId = CompletionItemData(fromLSPAny: item.data)?.itemId { - let req = sourcekitd.dictionary([ - sourcekitd.keys.identifier: itemId - ]) - let documentationResponse = await orLog("Retrieving documentation for completion item") { - try await sourcekitd.send( - \.codeCompleteDocumentation, - req, - timeout: timeout, - restartTimeout: restartTimeout, - documentUrl: nil, - fileContents: nil - ) - } - - if let response = documentationResponse, - let docString = documentationString(from: response, sourcekitd: sourcekitd) - { - item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) - } - } - return item - } - - private static func documentationString(from response: SKDResponseDictionary, sourcekitd: SourceKitD) -> String? { - if let docComment: String = response[sourcekitd.keys.docComment] { - return docComment - } - - return response[sourcekitd.keys.docBrief] - } - - private func computeCompletionTextEdit( - completionPos: Position, - requestPosition: Position, - utf8CodeUnitsToErase: Int, - newText: String, - snapshot: DocumentSnapshot - ) -> TextEdit { - let textEditRangeStart = computeCompletionTextEditStart( - completionPos: completionPos, - requestPosition: requestPosition, - utf8CodeUnitsToErase: utf8CodeUnitsToErase, - snapshot: snapshot - ) - return TextEdit(range: textEditRangeStart.. Position { - // Compute the TextEdit - if utf8CodeUnitsToErase == 0 { - // Nothing to delete. Fast path and avoid UTF-8/UTF-16 conversions - return completionPos - } else if utf8CodeUnitsToErase == 1 { - // Fast path: Erasing a single UTF-8 byte code unit means we are also need to erase exactly one UTF-16 code unit, meaning we don't need to process the file contents - if completionPos.utf16index >= 1 { - // We can delete the character. - return Position(line: completionPos.line, utf16index: completionPos.utf16index - 1) - } else { - // Deleting the character would cross line boundaries. This is not supported by LSP. - // Fall back to ignoring utf8CodeUnitsToErase. - // If we discover that multi-lines replacements are often needed, we can add an LSP extension to support multi-line edits. - return completionPos - } - } - - // We need to delete more than one text character. Do the UTF-8/UTF-16 dance. - assert(completionPos.line == requestPosition.line) - // Construct a string index for the edit range start by subtracting the UTF-8 code units to erase from the completion position. - guard let line = snapshot.lineTable.line(at: completionPos.line) else { - logger.fault("Code completion position is in out-of-range line \(completionPos.line)") - return completionPos - } - guard - let deletionStartStringIndex = line.utf8.index( - snapshot.index(of: completionPos), - offsetBy: -utf8CodeUnitsToErase, - limitedBy: line.utf8.startIndex - ) - else { - // Deleting the character would cross line boundaries. This is not supported by LSP. - // Fall back to ignoring utf8CodeUnitsToErase. - // If we discover that multi-lines replacements are often needed, we can add an LSP extension to support multi-line edits. - logger.fault("UTF-8 code units to erase \(utf8CodeUnitsToErase) is before start of line") - return completionPos - } - - // Compute the UTF-16 offset of the deletion start range. - let deletionStartUtf16Offset = line.utf16.distance(from: line.startIndex, to: deletionStartStringIndex) - precondition(deletionStartUtf16Offset >= 0) - - return Position(line: completionPos.line, utf16index: deletionStartUtf16Offset) - } -} - -extension CodeCompletionSession: CustomStringConvertible { - nonisolated var description: String { - "\(uri.pseudoPath):\(position)" - } -} - -private class OutermostFunctionCallFinder: SyntaxAnyVisitor { - /// Once a `FunctionCallExprSyntax` has been visited, that syntax node. - var foundCall: FunctionCallExprSyntax? - - private func shouldVisit(_ node: some SyntaxProtocol) -> Bool { - if foundCall != nil { - return false - } - return true - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - guard shouldVisit(node) else { - return .skipChildren - } - return .visitChildren - } - - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - guard shouldVisit(node) else { - return .skipChildren - } - foundCall = node - return .skipChildren - } - - /// Find the innermost `FunctionCallExprSyntax` that contains `position`. - static func findOutermostFunctionCall( - in tree: some SyntaxProtocol - ) -> FunctionCallExprSyntax? { - let finder = OutermostFunctionCallFinder(viewMode: .sourceAccurate) - finder.walk(tree) - return finder.foundCall - } -} diff --git a/Sources/SwiftLanguageService/CommentXML.swift b/Sources/SwiftLanguageService/CommentXML.swift deleted file mode 100644 index 31b4f44e4..000000000 --- a/Sources/SwiftLanguageService/CommentXML.swift +++ /dev/null @@ -1,177 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import LanguageServerProtocolExtensions - -#if canImport(FoundationXML) -import FoundationXML -#endif - -enum CommentXMLError: Error { - case noRootElement -} - -/// Converts from sourcekit's XML documentation format to Markdown. -/// -/// This code should go away and sourcekitd should return the Markdown directly. -package func xmlDocumentationToMarkdown(_ xmlString: String) throws -> String { - let xml = try XMLDocument(xmlString: xmlString) - guard let root = xml.rootElement() else { - throw CommentXMLError.noRootElement - } - - var convert = XMLToMarkdown() - convert.out.reserveCapacity(xmlString.utf16.count) - convert.toMarkdown(root) - return convert.out -} - -private struct XMLToMarkdown { - var out: String = "" - var indentCount: Int = 0 - let indentWidth: Int = 4 - var lineNumber: Int = 0 - var inParam: Bool = false - - mutating func newlineIfNeeded(count: Int = 1) { - if !out.isEmpty && out.last! != "\n" { - newline(count: count) - } - } - - mutating func newline(count: Int = 1) { - out += String(repeating: "\n", count: count) - out += String(repeating: " ", count: indentWidth * indentCount) - } - - mutating func toMarkdown(_ node: XMLNode) { - switch node.kind { - case .element: - toMarkdown(node as! XMLElement) - default: - out += node.stringValue ?? "" - } - } - - // [XMLNode]? is the type of XMLNode.children. - mutating func toMarkdown(_ nodes: [XMLNode]?, separator: String = "") { - guard let nodes else { - return - } - for node in nodes { - toMarkdown(node) - out += separator - } - } - - mutating func toMarkdown(_ node: XMLElement) { - switch node.name { - case "Declaration": - newlineIfNeeded(count: 2) - out += "```swift\n" - toMarkdown(node.children) - out += "\n```\n" - - case "Name", "USR", "Direction": - break - - case "Abstract", "Para": - if !inParam { - newlineIfNeeded(count: 2) - } - toMarkdown(node.children) - - case "Discussion", "ResultDiscussion", "ThrowsDiscussion": - if !inParam { - newlineIfNeeded(count: 2) - } - out += "### " - switch node.name { - case "Discussion": out += "Discussion" - case "ResultDiscussion": out += "Returns" - case "ThrowsDiscussion": out += "Throws" - default: fatalError("handled in outer switch") - } - newline(count: 2) - toMarkdown(node.children) - - case "Parameters": - newlineIfNeeded(count: 2) - out += "- Parameters:" - indentCount += 1 - toMarkdown(node.children) - indentCount -= 1 - - case "Parameter": - guard let name = node.elements(forName: "Name").first else { break } - newlineIfNeeded() - out += "- " - toMarkdown(name.children) - if let discussion = node.elements(forName: "Discussion").first { - out += ": " - inParam = true - toMarkdown(discussion.children) - inParam = false - } - - case "CodeListing": - lineNumber = 0 - newlineIfNeeded(count: 2) - out += "```\(node.attributes?.first(where: { $0.name == "language" })?.stringValue ?? "")\n" - toMarkdown(node.children, separator: "\n") - out += "```" - newlineIfNeeded(count: 2) - - case "zCodeLineNumbered": - lineNumber += 1 - out += "\(lineNumber).\t" - toMarkdown(node.children) - - case "codeVoice": - out += "`" - toMarkdown(node.children) - out += "`" - - case "emphasis": - out += "*" - toMarkdown(node.children) - out += "*" - - case "bold": - out += "**" - toMarkdown(node.children) - out += "**" - - case "h1", "h2", "h3", "h4", "h5", "h6": - newlineIfNeeded(count: 2) - let n = Int(node.name!.dropFirst()) - out += String(repeating: "#", count: n!) - out += " " - toMarkdown(node.children) - out += "\n\n" - - case "Link": - if let href = node.attributes?.first(where: { $0.name == "href" })?.stringValue { - out += "[" - toMarkdown(node.children) - out += "](\(href))" - } else { - // Not a valid link. - toMarkdown(node.children) - } - - default: - toMarkdown(node.children) - } - } -} diff --git a/Sources/SwiftLanguageService/CursorInfo.swift b/Sources/SwiftLanguageService/CursorInfo.swift deleted file mode 100644 index 3e643d62f..000000000 --- a/Sources/SwiftLanguageService/CursorInfo.swift +++ /dev/null @@ -1,210 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP - -/// Detailed information about a symbol under the cursor. -/// -/// Wraps the information returned by sourcekitd's `cursor_info` request, such as symbol name, USR, -/// and declaration location. This is intended to only do lightweight processing of the data to make -/// it easier to use from Swift. Any expensive processing, such as parsing the XML strings, is -/// handled elsewhere. -struct CursorInfo { - - /// Information common between CursorInfo and SymbolDetails from the `symbolInfo` request, such as - /// name and USR. - var symbolInfo: SymbolDetails - - /// The annotated declaration XML string. - var annotatedDeclaration: String? - - /// The documentation as it is spelled in - var documentation: String? - - /// The refactor actions available at this position. - var refactorActions: [SemanticRefactorCommand]? = nil - - init( - _ symbolInfo: SymbolDetails, - annotatedDeclaration: String?, - documentation: String? - ) { - self.symbolInfo = symbolInfo - self.annotatedDeclaration = annotatedDeclaration - self.documentation = documentation - } - - /// `snapshot` is the snapshot from which the cursor info was invoked. It is necessary to pass that snapshot in here - /// because snapshot might not be open in `documentManager` if cursor info was invoked using file contents from disk - /// using `cursorInfoFromDisk` and thus we couldn't convert positions if the snapshot wasn't passed in explicitly. - /// For all document other than `snapshot`, `documentManager` is used to find the latest snapshot of a document to - /// convert positions. - init?( - _ dict: SKDResponseDictionary, - snapshot: DocumentSnapshot, - documentManager: DocumentManager, - sourcekitd: some SourceKitD - ) { - let keys = sourcekitd.keys - guard let kind: sourcekitd_api_uid_t = dict[keys.kind] else { - // Nothing to report. - return nil - } - - let location: Location? - if let filepath: String = dict[keys.filePath], - let line: Int = dict[keys.line], - let column: Int = dict[keys.column] - { - let uri = DocumentURI(filePath: filepath, isDirectory: false) - let snapshot: DocumentSnapshot? = - if snapshot.uri.sourcekitdSourceFile == filepath { - snapshot - } else { - documentManager.latestSnapshotOrDisk(uri, language: .swift) - } - if let snapshot { - let position = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: column - 1) - location = Location(uri: uri, range: Range(position)) - } else { - logger.error("Failed to get snapshot for \(uri.forLogging) to convert position") - location = nil - } - } else { - location = nil - } - - let module: SymbolDetails.ModuleInfo? - if let moduleName: String = dict[keys.moduleName] { - let groupName: String? = dict[keys.groupName] - module = SymbolDetails.ModuleInfo(moduleName: moduleName, groupName: groupName) - } else { - module = nil - } - - self.init( - SymbolDetails( - name: dict[keys.name], - containerName: nil, - usr: dict[keys.usr], - bestLocalDeclaration: location, - kind: kind.asSymbolKind(sourcekitd.values), - isDynamic: dict[keys.isDynamic] ?? false, - isSystem: dict[keys.isSystem] ?? false, - receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? [], - systemModule: module - ), - annotatedDeclaration: dict[keys.annotatedDecl], - documentation: dict[keys.docComment] - ) - } -} - -/// An error from a cursor info request. -enum CursorInfoError: Error, Equatable { - /// The given range is not valid in the document snapshot. - case invalidRange(Range) - - /// The underlying sourcekitd request failed with the given error. - case responseError(ResponseError) -} - -extension CursorInfoError: CustomStringConvertible { - var description: String { - switch self { - case .invalidRange(let range): - return "invalid range \(range)" - case .responseError(let error): - return "\(error)" - } - } -} - -extension SwiftLanguageService { - /// Provides detailed information about a symbol under the cursor, if any. - /// - /// Wraps the information returned by sourcekitd's `cursor_info` request, such as symbol name, - /// USR, and declaration location. This request does minimal processing of the result. - /// - /// - Parameters: - /// - url: Document URI in which to perform the request. Must be an open document. - /// - range: The position range within the document to lookup the symbol at. - /// - includeSymbolGraph: Whether or not to ask sourcekitd for the complete symbol graph. - /// - fallbackSettingsAfterTimeout: Whether fallback build settings should be used for the cursor info request if no - /// build settings can be retrieved within a timeout. - func cursorInfo( - _ snapshot: DocumentSnapshot, - compileCommand: SwiftCompileCommand?, - _ range: Range, - includeSymbolGraph: Bool = false, - additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil - ) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand], symbolGraph: String?) { - let documentManager = try self.documentManager - - let offsetRange = snapshot.utf8OffsetRange(of: range) - - let keys = self.keys - - let skreq = sourcekitd.dictionary([ - keys.cancelOnSubsequentRequest: 0, - keys.offset: offsetRange.lowerBound, - keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil, - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - keys.retrieveSymbolGraph: includeSymbolGraph ? 1 : 0, - keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?, - ]) - - appendAdditionalParameters?(skreq) - - let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot) - - var cursorInfoResults: [CursorInfo] = [] - if let cursorInfo = CursorInfo(dict, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd) { - cursorInfoResults.append(cursorInfo) - } - cursorInfoResults += - dict[keys.secondarySymbols]? - .compactMap { CursorInfo($0, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd) } ?? [] - let refactorActions = - [SemanticRefactorCommand]( - array: dict[keys.refactorActions], - range: range, - textDocument: TextDocumentIdentifier(snapshot.uri), - keys, - self.sourcekitd.api - ) ?? [] - let symbolGraph: String? = dict[keys.symbolGraph] - - return (cursorInfoResults, refactorActions, symbolGraph) - } - - func cursorInfo( - _ uri: DocumentURI, - _ range: Range, - includeSymbolGraph: Bool = false, - fallbackSettingsAfterTimeout: Bool, - additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil - ) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand], symbolGraph: String?) { - return try await self.cursorInfo( - self.latestSnapshot(for: uri), - compileCommand: await self.compileCommand(for: uri, fallbackAfterTimeout: fallbackSettingsAfterTimeout), - range, - includeSymbolGraph: includeSymbolGraph, - additionalParameters: appendAdditionalParameters - ) - } -} diff --git a/Sources/SwiftLanguageService/Diagnostic.swift b/Sources/SwiftLanguageService/Diagnostic.swift deleted file mode 100644 index 6a844432b..000000000 --- a/Sources/SwiftLanguageService/Diagnostic.swift +++ /dev/null @@ -1,465 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import Foundation -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SourceKitD -import SourceKitLSP -import SwiftDiagnostics -import SwiftExtensions -import SwiftSyntax - -import struct SourceKitLSP.Diagnostic - -extension CodeAction { - /// Creates a CodeAction from a list for sourcekit fixits. - /// - /// If this is from a note, the note's description should be passed as `fromNote`. - init?(fixits: SKDResponseArray, in snapshot: DocumentSnapshot, fromNote: String?) { - var edits: [TextEdit] = [] - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - let editsMapped = fixits.forEach { (_, skfixit) -> Bool in - if let edit = TextEdit(fixit: skfixit, in: snapshot) { - edits.append(edit) - return true - } - return false - } - - if !editsMapped { - logger.fault("Failed to construct TextEdits from response \(fixits)") - return nil - } - - if edits.isEmpty { - return nil - } - - let title: String - if let fromNote = fromNote { - title = fromNote - } else { - guard let generatedTitle = Self.title(for: edits, in: snapshot) else { - return nil - } - title = generatedTitle - } - - self.init( - title: title, - kind: .quickFix, - diagnostics: nil, - edit: WorkspaceEdit(changes: [snapshot.uri: edits]) - ) - } - - init?(_ fixIt: FixIt, in snapshot: DocumentSnapshot) { - var textEdits = [TextEdit]() - for edit in fixIt.edits { - textEdits.append(TextEdit(range: snapshot.absolutePositionRange(of: edit.range), newText: edit.replacement)) - } - - self.init( - title: fixIt.message.message.withFirstLetterUppercased(), - kind: .quickFix, - diagnostics: nil, - edit: WorkspaceEdit(changes: [snapshot.uri: textEdits]) - ) - } - - private static func title(for edits: [TextEdit], in snapshot: DocumentSnapshot) -> String? { - if edits.isEmpty { - return nil - } - let startIndex = snapshot.index(of: edits[0].range.lowerBound) - let endIndex = snapshot.index(of: edits[0].range.upperBound) - guard startIndex <= endIndex, - snapshot.text.indices.contains(startIndex), - endIndex <= snapshot.text.endIndex - else { - return nil - } - let oldText = String(snapshot.text[startIndex.. String { - switch (oldText.isEmpty, newText.isEmpty) { - case (false, false): - return "Replace '\(oldText)' with '\(newText)'" - case (false, true): - return "Remove '\(oldText)'" - case (true, false): - return "Insert '\(newText)'" - case (true, true): - logger.fault("Both oldText and newText of FixIt are empty") - return "Fix" - } - } -} - -extension TextEdit { - - /// Creates a TextEdit from a sourcekitd fixit response dictionary. - init?(fixit: SKDResponseDictionary, in snapshot: DocumentSnapshot) { - let keys = fixit.sourcekitd.keys - if let utf8Offset: Int = fixit[keys.offset], - let length: Int = fixit[keys.length], - let replacement: String = fixit[keys.sourceText], - length > 0 || !replacement.isEmpty - { - // Snippets are only suppored in code completion. - // Remove SourceKit placeholders from Fix-Its because they can't be represented in the editor properly. - let replacementWithoutPlaceholders = rewriteSourceKitPlaceholders(in: replacement, clientSupportsSnippets: false) - - // If both the replacement without placeholders and the fixit are empty, no TextEdit should be created. - if replacementWithoutPlaceholders.isEmpty && length == 0 { - return nil - } - - let position = snapshot.positionOf(utf8Offset: utf8Offset) - let endPosition = snapshot.positionOf(utf8Offset: utf8Offset + length) - self.init(range: position.. String { - if let firstLetter = self.first { - return firstLetter.uppercased() + self.dropFirst() - } else { - return self - } - } -} - -extension Diagnostic { - /// Creates a diagnostic from a sourcekitd response dictionary. - /// - /// `snapshot` is the snapshot of the document for which the diagnostics are generated. - /// `documentManager` is used to resolve positions of notes in secondary files. - init?( - _ diag: SKDResponseDictionary, - in snapshot: DocumentSnapshot, - documentManager: DocumentManager, - useEducationalNoteAsCode: Bool - ) { - let keys = diag.sourcekitd.keys - let values = diag.sourcekitd.values - - guard let filePath: String = diag[keys.filePath] else { - logger.fault("Missing file path in diagnostic") - return nil - } - - func haveSameRealpath(_ lhs: DocumentURI, _ rhs: DocumentURI) -> Bool { - guard let lhsFileURL = lhs.fileURL, let rhsFileURL = rhs.fileURL else { - return false - } - do { - return try lhsFileURL.realpath == rhsFileURL.realpath - } catch { - return false - } - } - - guard - filePath == snapshot.uri.pseudoPath - || haveSameRealpath(DocumentURI(filePath: filePath, isDirectory: false), snapshot.uri) - else { - logger.error("Ignoring diagnostic from a different file: \(filePath)") - return nil - } - - guard let message: String = diag[keys.description]?.withFirstLetterUppercased() else { return nil } - - var range: Range? = nil - if let line: Int = diag[keys.line], - let utf8Column: Int = diag[keys.column], - line > 0, utf8Column > 0 - { - range = Range(snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: utf8Column - 1)) - } else if let utf8Offset: Int = diag[keys.offset] { - range = Range(snapshot.positionOf(utf8Offset: utf8Offset)) - } - - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - // If the diagnostic has a range associated with it that starts at the same location as the diagnostics position, use it to retrieve a proper range for the diagnostic, instead of just reporting a zero-length range. - (diag[keys.ranges] as SKDResponseArray?)?.forEach { index, skRange in - guard let utf8Offset: Int = skRange[keys.offset], - let length: Int = skRange[keys.length] - else { - return true // continue - } - let start = snapshot.positionOf(utf8Offset: utf8Offset) - let end = snapshot.positionOf(utf8Offset: utf8Offset + length) - guard start == range?.lowerBound else { - return true - } - - range = start.. 0, - let primaryPath = educationalNotePaths[0] - { - // Swift >= 6.2 returns a URL rather than a file path - let url: URL? = - if primaryPath.starts(with: "http") { - URL(string: primaryPath) - } else { - URL(fileURLWithPath: primaryPath) - } - - if let url { - let name = url.deletingPathExtension().lastPathComponent - code = .string(name) - codeDescription = .init(href: DocumentURI(url)) - } - } - - var actions: [CodeAction]? = nil - if let skfixits: SKDResponseArray = diag[keys.fixits], - let action = CodeAction(fixits: skfixits, in: snapshot, fromNote: nil) - { - actions = [action] - } - - var notes: [DiagnosticRelatedInformation]? = nil - if let sknotes: SKDResponseArray = diag[keys.diagnostics] { - notes = [] - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - sknotes.forEach { (_, sknote) -> Bool in - guard - let note = DiagnosticRelatedInformation( - sknote, - primaryDocumentSnapshot: snapshot, - documentManager: documentManager - ) - else { return true } - notes?.append(note) - return true - } - } - - var tags: [DiagnosticTag] = [] - if let categories: SKDResponseArray = diag[keys.categories] { - categories.forEachUID { (_, category) in - switch category { - case values.diagDeprecation: - tags.append(.deprecated) - case values.diagNoUsage: - tags.append(.unnecessary) - default: - break - } - return true - } - } - - self.init( - range: range, - severity: severity, - code: code, - codeDescription: codeDescription, - source: "SourceKit", - message: message, - tags: tags, - relatedInformation: notes, - codeActions: actions - ) - } - - init( - _ diag: SwiftDiagnostics.Diagnostic, - in snapshot: DocumentSnapshot - ) { - // Start with a zero-length range based on the position. - // If the diagnostic has highlights associated with it that start at the - // position, use that as the diagnostic's range. - var range = Range(snapshot.position(of: diag.position)) - for highlight in diag.highlights { - let swiftSyntaxRange = highlight.positionAfterSkippingLeadingTrivia.. 0, utf8Column > 0 - { - position = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: utf8Column - 1) - } else if let utf8Offset: Int = diag[keys.offset] { - position = snapshot.positionOf(utf8Offset: utf8Offset) - } - - if position == nil { - return nil - } - - guard let message: String = diag[keys.description]?.withFirstLetterUppercased() else { return nil } - - var actions: [CodeAction]? = nil - if let skfixits: SKDResponseArray = diag[keys.fixits], - let action = CodeAction(fixits: skfixits, in: snapshot, fromNote: message) - { - actions = [action] - } - - self.init( - location: Location(uri: snapshot.uri, range: Range(position!)), - message: message, - codeActions: actions - ) - } - - init(_ note: Note, in snapshot: DocumentSnapshot) { - let nodeRange = note.node.positionAfterSkippingLeadingTrivia..) -> Diagnostic { - var updated = self - updated.range = newRange - return updated - } -} - -/// Whether a diagostic is semantic or syntatic (parse). -enum DiagnosticStage: Hashable { - case parse - case sema -} - -extension DiagnosticStage { - init?(_ uid: sourcekitd_api_uid_t, sourcekitd: SourceKitD) { - switch uid { - case sourcekitd.values.parseDiagStage: - self = .parse - case sourcekitd.values.semaDiagStage: - self = .sema - default: - let desc = sourcekitd.api.uid_get_string_ptr(uid).map { String(cString: $0) } - logger.fault("Unknown diagnostic stage \(desc ?? "nil", privacy: .public)") - return nil - } - } -} - -fileprivate extension SwiftDiagnostics.DiagnosticSeverity { - var lspSeverity: LanguageServerProtocol.DiagnosticSeverity { - switch self { - case .error: return .error - case .warning: return .warning - case .note: return .information - case .remark: return .hint - #if RESILIENT_LIBRARIES - @unknown default: - fatalError("Unknown case") - #endif - } - } -} diff --git a/Sources/SwiftLanguageService/DiagnosticReportManager.swift b/Sources/SwiftLanguageService/DiagnosticReportManager.swift deleted file mode 100644 index 3c44179b1..000000000 --- a/Sources/SwiftLanguageService/DiagnosticReportManager.swift +++ /dev/null @@ -1,203 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -import SKOptions -import SKUtilities -import SourceKitD -import SourceKitLSP -import SwiftDiagnostics -import SwiftExtensions -import SwiftParserDiagnostics - -import struct SourceKitLSP.Diagnostic - -actor DiagnosticReportManager { - /// A task to produce diagnostics, either from a diagnostics request to `sourcekitd` or by using the built-in swift-syntax. - private typealias ReportTask = RefCountedCancellableTask< - (report: RelatedFullDocumentDiagnosticReport, cachable: Bool) - > - - private struct CacheKey: Hashable { - let snapshotID: DocumentSnapshot.ID - let buildSettings: SwiftCompileCommand? - } - - private let sourcekitd: SourceKitD - private let options: SourceKitLSPOptions - private let syntaxTreeManager: SyntaxTreeManager - private let documentManager: DocumentManager - private let clientHasDiagnosticsCodeDescriptionSupport: Bool - - private nonisolated var keys: sourcekitd_api_keys { return sourcekitd.keys } - private nonisolated var requests: sourcekitd_api_requests { return sourcekitd.requests } - - /// The cache that stores reportTasks for snapshot id and buildSettings - /// - /// - Note: The capacity has been chosen without scientific measurements. - private var reportTaskCache = LRUCache(capacity: 5) - - init( - sourcekitd: SourceKitD, - options: SourceKitLSPOptions, - syntaxTreeManager: SyntaxTreeManager, - documentManager: DocumentManager, - clientHasDiagnosticsCodeDescriptionSupport: Bool - ) { - self.sourcekitd = sourcekitd - self.options = options - self.syntaxTreeManager = syntaxTreeManager - self.documentManager = documentManager - self.clientHasDiagnosticsCodeDescriptionSupport = clientHasDiagnosticsCodeDescriptionSupport - } - - func diagnosticReport( - for snapshot: DocumentSnapshot, - buildSettings: SwiftCompileCommand? - ) async throws -> RelatedFullDocumentDiagnosticReport { - if let reportTask = reportTask(for: snapshot.id, buildSettings: buildSettings), await !reportTask.isCancelled { - do { - let cachedValue = try await reportTask.value - if cachedValue.cachable { - return cachedValue.report - } - } catch { - // Do not cache failed requests - } - } - let reportTask: ReportTask - if let buildSettings, !buildSettings.isFallback { - reportTask = ReportTask { - return try await self.requestReport(with: snapshot, compilerArgs: buildSettings.compilerArgs) - } - } else { - logger.log( - "Producing syntactic diagnostics from the built-in swift-syntax because we \(buildSettings != nil ? "have fallback build settings" : "don't have build settings", privacy: .public))" - ) - // If we don't have build settings or we only have fallback build settings, - // sourcekitd won't be able to give us accurate semantic diagnostics. - // Fall back to providing syntactic diagnostics from the built-in - // swift-syntax. That's the best we can do for now. - reportTask = ReportTask { - return try await self.requestFallbackReport(with: snapshot) - } - } - setReportTask(for: snapshot.id, buildSettings: buildSettings, reportTask: reportTask) - return try await reportTask.value.report - } - - func removeItemsFromCache(with uri: DocumentURI) async { - reportTaskCache.removeAll(where: { $0.snapshotID.uri == uri }) - } - - private func requestReport( - with snapshot: DocumentSnapshot, - compilerArgs: [String] - ) async throws -> (report: RelatedFullDocumentDiagnosticReport, cachable: Bool) { - try Task.checkCancellation() - - let keys = self.keys - - let skreq = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - keys.compilerArgs: compilerArgs as [SKDRequestValue], - ]) - - let dict: SKDResponseDictionary - do { - dict = try await self.sourcekitd.send( - \.diagnostics, - skreq, - timeout: options.sourcekitdRequestTimeoutOrDefault, - restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot.uri.arbitrarySchemeURL, - fileContents: snapshot.text - ) - } catch SKDError.requestFailed(let sourcekitdError) { - var errorMessage = sourcekitdError - if errorMessage.contains("semantic editor is disabled") { - throw SKDError.requestFailed(sourcekitdError) - } - if errorMessage.hasPrefix("error response (Request Failed): error: ") { - errorMessage = String(errorMessage.dropFirst(40)) - } - let report = RelatedFullDocumentDiagnosticReport(items: [ - Diagnostic( - range: Position(line: 0, utf16index: 0).. (report: RelatedFullDocumentDiagnosticReport, cachable: Bool) { - // If we don't have build settings or we only have fallback build settings, - // sourcekitd won't be able to give us accurate semantic diagnostics. - // Fall back to providing syntactic diagnostics from the built-in - // swift-syntax. That's the best we can do for now. - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - let swiftSyntaxDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: syntaxTree) - let diagnostics = swiftSyntaxDiagnostics.compactMap { (diag) -> Diagnostic? in - if diag.diagnosticID == StaticTokenError.editorPlaceholder.diagnosticID { - // Ignore errors about editor placeholders in the source file, similar to how sourcekitd ignores them. - return nil - } - return Diagnostic(diag, in: snapshot) - } - let report = RelatedFullDocumentDiagnosticReport(items: diagnostics) - return (report, cachable: true) - } - - /// The reportTask for the given document snapshot and buildSettings. - private func reportTask( - for snapshotID: DocumentSnapshot.ID, - buildSettings: SwiftCompileCommand? - ) -> ReportTask? { - return reportTaskCache[CacheKey(snapshotID: snapshotID, buildSettings: buildSettings)] - } - - /// Set the reportTask for the given document snapshot and buildSettings. - private func setReportTask( - for snapshotID: DocumentSnapshot.ID, - buildSettings: SwiftCompileCommand?, - reportTask: ReportTask - ) { - // Remove any reportTasks for old versions of this document. - reportTaskCache.removeAll(where: { $0.snapshotID <= snapshotID }) - reportTaskCache[CacheKey(snapshotID: snapshotID, buildSettings: buildSettings)] = reportTask - } -} diff --git a/Sources/SwiftLanguageService/DocumentFormatting.swift b/Sources/SwiftLanguageService/DocumentFormatting.swift deleted file mode 100644 index 635dd8935..000000000 --- a/Sources/SwiftLanguageService/DocumentFormatting.swift +++ /dev/null @@ -1,268 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import LanguageServerProtocolJSONRPC -import SKLogging -import SKUtilities -import SourceKitLSP -import SwiftExtensions -import SwiftParser -import SwiftSyntax -import TSCExtensions - -import struct TSCBasic.AbsolutePath -import class TSCBasic.LocalFileOutputByteStream -import class TSCBasic.Process -import protocol TSCBasic.WritableByteStream - -fileprivate extension String { - init?(bytes: [UInt8], encoding: Encoding) { - let data = bytes.withUnsafeBytes { buffer in - guard let baseAddress = buffer.baseAddress else { - return Data() - } - return Data(bytes: baseAddress, count: buffer.count) - } - self.init(data: data, encoding: encoding) - } -} - -/// If a parent directory of `fileURI` contains a `.swift-format` file, return the path to that file. -/// Otherwise, return `nil`. -private func swiftFormatFile(for fileURI: DocumentURI) -> AbsolutePath? { - guard var path = try? AbsolutePath(validating: fileURI.pseudoPath) else { - return nil - } - repeat { - path = path.parentDirectory - let configFile = path.appending(component: ".swift-format") - if FileManager.default.isReadableFile(atPath: configFile.pathString) { - return configFile - } - } while !path.isRoot - return nil -} - -/// If a `.swift-format` file is discovered that applies to `fileURI`, return the path to that file. -/// Otherwise, return a JSON object containing the configuration parameters from `options`. -/// -/// The result of this function can be passed to the `--configuration` parameter of swift-format. -private func swiftFormatConfiguration( - for fileURI: DocumentURI, - options: FormattingOptions -) throws -> String { - if let configFile = swiftFormatFile(for: fileURI) { - // If we find a .swift-format file, we ignore the options passed to us by the editor. - // Most likely, the editor inferred them from the current document and thus the options - // passed by the editor are most likely less correct than those in .swift-format. - return configFile.pathString - } - - // The following options are not supported by swift-format and ignored: - // - trimTrailingWhitespace: swift-format always trims trailing whitespace - // - insertFinalNewline: swift-format always inserts a final newline to the file - // - trimFinalNewlines: swift-format always trims final newlines - - if options.insertSpaces { - return """ - { - "version": 1, - "tabWidth": \(options.tabSize), - "indentation": { "spaces": \(options.tabSize) } - } - """ - } else { - return """ - { - "version": 1, - "tabWidth": \(options.tabSize), - "indentation": { "tabs": 1 } - } - """ - } -} - -extension CollectionDifference.Change { - var offset: Int { - switch self { - case .insert(let offset, element: _, associatedWith: _): - return offset - case .remove(let offset, element: _, associatedWith: _): - return offset - } - } -} - -/// Compute the text edits that need to be made to transform `original` into `edited`. -private func edits(from original: DocumentSnapshot, to edited: String) -> [TextEdit] { - let difference = edited.utf8.difference(from: original.text.utf8) - - let sequentialEdits = difference.map { change in - switch change { - case .insert(let offset, let element, associatedWith: _): - let absolutePosition = AbsolutePosition(utf8Offset: offset) - return SourceEdit(range: absolutePosition.. [TextEdit]? { - return try await format( - snapshot: documentManager.latestSnapshot(req.textDocument.uri), - textDocument: req.textDocument, - options: req.options - ) - } - - package func documentRangeFormatting(_ req: DocumentRangeFormattingRequest) async throws -> [TextEdit]? { - return try await format( - snapshot: documentManager.latestSnapshot(req.textDocument.uri), - textDocument: req.textDocument, - options: req.options, - range: req.range - ) - } - - package func documentOnTypeFormatting(_ req: DocumentOnTypeFormattingRequest) async throws -> [TextEdit]? { - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - guard let line = snapshot.lineTable.line(at: req.position.line) else { - return nil - } - - let lineStartPosition = snapshot.position(of: line.startIndex, fromLine: req.position.line) - let lineEndPosition = snapshot.position(of: line.endIndex, fromLine: req.position.line) - - return try await format( - snapshot: snapshot, - textDocument: req.textDocument, - options: req.options, - range: lineStartPosition..? = nil - ) async throws -> [TextEdit]? { - guard let swiftFormat else { - throw ResponseError.unknown( - "Formatting not supported because the toolchain is missing the swift-format executable" - ) - } - - var args = try [ - swiftFormat.filePath, - "format", - "-", // Read file contents from stdin - "--configuration", - swiftFormatConfiguration(for: textDocument.uri, options: options), - ] - if let range { - let utf8Range = snapshot.utf8OffsetRange(of: range) - // swift-format takes an inclusive range, but Swift's `Range.upperBound` is exclusive. - // Also make sure `upperBound` does not go less than `lowerBound`. - let utf8UpperBound = max(utf8Range.lowerBound, utf8Range.upperBound - 1) - args += [ - "--offsets", - "\(utf8Range.lowerBound):\(utf8UpperBound)", - ] - } - let process = TSCBasic.Process(arguments: args) - let writeStream: any WritableByteStream - do { - writeStream = try process.launch() - } catch { - throw ResponseError.unknown("Launching swift-format failed: \(error)") - } - #if canImport(Darwin) - // On Darwin, we can disable SIGPIPE for a single pipe. This is not available on all platforms, in which case we - // resort to disabling SIGPIPE globally to avoid crashing SourceKit-LSP with SIGPIPE if swift-format crashes before - // we could send all data to its stdin. - if let byteStream = writeStream as? LocalFileOutputByteStream { - orLog("Disable SIGPIPE for swift-format stdin") { - try byteStream.disableSigpipe() - } - } else { - logger.fault("Expected write stream to process to be a LocalFileOutputByteStream") - } - #else - globallyDisableSigpipeIfNeeded() - #endif - - do { - // Send the file to format to swift-format's stdin. That way we don't have to write it to a file. - // - // If we are on Windows, `writeStream` is not a swift-tools-support-core type but a `FileHandle`. In that case, - // call the throwing `write(contentsOf:)` method on it so that we can catch a `ERROR_BROKEN_PIPE` error. The - // `send` method that we use on all other platforms ends up calling the non-throwing `FileHandle.write(_:)`, which - // calls `write(contentsOf:)` using `try!` and thus crashes SourceKit-LSP if the pipe to swift-format is closed, - // eg. because swift-format has crashed. - if let fileHandle = writeStream as? FileHandle { - try fileHandle.write(contentsOf: Data(snapshot.text.utf8)) - } else { - writeStream.send(snapshot.text.utf8) - } - try writeStream.close() - } catch { - throw ResponseError.unknown("Writing to swift-format stdin failed: \(error)") - } - - let result = try await withTimeout(.seconds(60)) { - try await process.waitUntilExitStoppingProcessOnTaskCancellation() - } - guard result.exitStatus == .terminated(code: 0) else { - let swiftFormatErrorMessage: String - switch result.stderrOutput { - case .success(let stderrBytes): - swiftFormatErrorMessage = String(bytes: stderrBytes, encoding: .utf8) ?? "unknown error" - case .failure(let error): - swiftFormatErrorMessage = String(describing: error) - } - throw ResponseError.unknown( - """ - Running swift-format failed - \(swiftFormatErrorMessage) - """ - ) - } - let formattedBytes: [UInt8] - switch result.output { - case .success(let bytes): - formattedBytes = bytes - case .failure(let error): - throw error - } - - guard let formattedString = String(bytes: formattedBytes, encoding: .utf8) else { - throw ResponseError.unknown("Failed to decode response from swift-format as UTF-8") - } - - return edits(from: snapshot, to: formattedString) - } -} diff --git a/Sources/SwiftLanguageService/DocumentSymbols.swift b/Sources/SwiftLanguageService/DocumentSymbols.swift deleted file mode 100644 index 0a732a041..000000000 --- a/Sources/SwiftLanguageService/DocumentSymbols.swift +++ /dev/null @@ -1,340 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -import SourceKitLSP -import SwiftSyntax - -extension SwiftLanguageService { - package func documentSymbol(_ req: DocumentSymbolRequest) async throws -> DocumentSymbolResponse? { - let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri) - - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - - try Task.checkCancellation() - return .documentSymbols( - DocumentSymbolsFinder.find( - in: [Syntax(syntaxTree)], - snapshot: snapshot, - range: syntaxTree.position.. - - /// Accumulating the result in here. - private var result: [DocumentSymbol] = [] - - private init(snapshot: DocumentSnapshot, range: Range) { - self.snapshot = snapshot - self.range = range - super.init(viewMode: .sourceAccurate) - } - - /// Designated entry point for `DocumentSymbolFinder`. - static func find( - in nodes: some Sequence, - snapshot: DocumentSnapshot, - range: Range - ) -> [DocumentSymbol] { - let visitor = DocumentSymbolsFinder(snapshot: snapshot, range: range) - for node in nodes { - visitor.walk(node) - } - return visitor.result - } - - /// Add a symbol with the given parameters to the `result` array. - private func record( - node: some SyntaxProtocol, - name: String, - symbolKind: SymbolKind, - range: Range, - selection: Range - ) -> SyntaxVisitorContinueKind { - if !self.range.overlaps(range) { - return .skipChildren - } - let positionRange = snapshot.absolutePositionRange(of: range) - let selectionPositionRange = snapshot.absolutePositionRange(of: selection) - - // Record MARK comments on the node's leading and trailing trivia in `result` not as a child of `node`. - visit(node.leadingTrivia, position: node.position) - - let children = DocumentSymbolsFinder.find( - in: node.children(viewMode: .sourceAccurate), - snapshot: snapshot, - range: node.positionAfterSkippingLeadingTrivia.. SyntaxVisitorContinueKind { - guard let node = node.asProtocol(NamedDeclSyntax.self) else { - return .visitChildren - } - let symbolKind: SymbolKind? = - switch node.kind { - case .actorDecl: .class - case .associatedTypeDecl: .typeParameter - case .classDecl: .class - case .enumDecl: .enum - case .macroDecl: .function // LSP doesn't have a macro symbol kind. `function`` is closest. - case .operatorDecl: .operator - case .precedenceGroupDecl: .operator // LSP doesn't have a precedence group symbol kind. `operator` is closest. - case .protocolDecl: .interface - case .structDecl: .struct - case .typeAliasDecl: .typeParameter // LSP doesn't have a typealias symbol kind. `typeParameter` is closest. - default: nil - } - - guard let symbolKind else { - return .visitChildren - } - return record( - node: node, - name: node.name.text, - symbolKind: symbolKind, - range: node.rangeWithoutTrivia, - selection: node.name.rangeWithoutTrivia - ) - } - - private func visit(_ trivia: Trivia, position: AbsolutePosition) { - let markPrefix = "MARK: " - var position = position - for piece in trivia.pieces { - defer { - position = position.advanced(by: piece.sourceLength.utf8Length) - } - switch piece { - case .lineComment(let commentText), .blockComment(let commentText): - let trimmedComment = commentText.trimmingCharacters(in: CharacterSet(["/", "*"]).union(.whitespaces)) - if trimmedComment.starts(with: markPrefix) { - let markText = trimmedComment.dropFirst(markPrefix.count) - let range = snapshot.absolutePositionRange( - of: position.. SyntaxVisitorContinueKind { - if self.range.overlaps(node.position.. SyntaxVisitorContinueKind { - // LSP doesn't have a destructor kind. constructor is the closest match and also what clangd for destructors. - return record( - node: node, - name: node.deinitKeyword.text, - symbolKind: .constructor, - range: node.rangeWithoutTrivia, - selection: node.deinitKeyword.rangeWithoutTrivia - ) - } - - override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind { - let rangeEnd = - if let parameterClause = node.parameterClause { - parameterClause.endPositionBeforeTrailingTrivia - } else { - node.name.endPositionBeforeTrailingTrivia - } - - return record( - node: node, - name: node.declName, - symbolKind: .enumMember, - range: node.name.positionAfterSkippingLeadingTrivia.. SyntaxVisitorContinueKind { - return record( - node: node, - name: node.extendedType.trimmedDescription, - symbolKind: .namespace, - range: node.rangeWithoutTrivia, - selection: node.extendedType.rangeWithoutTrivia - ) - } - - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - let kind: SymbolKind = - if node.name.tokenKind.isOperator { - .operator - } else if node.parent?.is(MemberBlockItemSyntax.self) ?? false { - .method - } else { - .function - } - return record( - node: node, - name: node.declName, - symbolKind: kind, - range: node.rangeWithoutTrivia, - selection: node.name - .positionAfterSkippingLeadingTrivia.. SyntaxVisitorContinueKind { - return record( - node: node, - name: node.name.text, - symbolKind: .typeParameter, - range: node.rangeWithoutTrivia, - selection: node.rangeWithoutTrivia - ) - } - - override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - return record( - node: node, - name: node.declName, - symbolKind: .constructor, - range: node.rangeWithoutTrivia, - selection: node.initKeyword - .positionAfterSkippingLeadingTrivia.. SyntaxVisitorContinueKind { - // If there is only one pattern binding within the variable decl, consider the entire variable decl as the - // referenced range. If there are multiple, consider each pattern binding separately since the `var` keyword doesn't - // belong to any pattern binding in particular. - guard let variableDecl = node.parent?.parent?.as(VariableDeclSyntax.self), - variableDecl.isMemberOrTopLevelDeclaration - else { - return .visitChildren - } - let rangeNode: Syntax = variableDecl.bindings.count == 1 ? Syntax(variableDecl) : Syntax(node) - - return record( - node: node, - name: node.pattern.trimmedDescription, - symbolKind: variableDecl.parent?.is(MemberBlockItemSyntax.self) ?? false ? .property : .variable, - range: rangeNode.rangeWithoutTrivia, - selection: node.pattern.rangeWithoutTrivia - ) - } -} - -// MARK: - Syntax Utilities - -fileprivate extension EnumCaseElementSyntax { - var declName: String { - var result = self.name.text - if let parameterClause { - result += "(" - for parameter in parameterClause.parameters { - result += "\(parameter.firstName?.text ?? "_"):" - } - result += ")" - } - return result - } -} - -fileprivate extension FunctionDeclSyntax { - var declName: String { - var result = self.name.text - result += "(" - for parameter in self.signature.parameterClause.parameters { - result += "\(parameter.firstName.text):" - } - result += ")" - return result - } -} - -fileprivate extension InitializerDeclSyntax { - var declName: String { - var result = self.initKeyword.text - result += "(" - for parameter in self.signature.parameterClause.parameters { - result += "\(parameter.firstName.text):" - } - result += ")" - return result - } -} - -fileprivate extension SyntaxProtocol { - /// The position range of this node without its leading and trailing trivia. - var rangeWithoutTrivia: Range { - return positionAfterSkippingLeadingTrivia.. - - /// The text document related to the refactoring action. - package var textDocument: TextDocumentIdentifier - - package init(positionRange: Range, textDocument: TextDocumentIdentifier) { - self.positionRange = positionRange - self.textDocument = textDocument - } - - package init?(fromLSPDictionary dictionary: [String: LSPAny]) { - guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue], - case .string(let title)? = dictionary[CodingKeys.title.stringValue], - case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], - case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue] - else { - return nil - } - guard let positionRange = Range(fromLSPDictionary: rangeDict), - let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict) - else { - return nil - } - - self.init( - title: title, - actionString: actionString, - positionRange: positionRange, - textDocument: textDocument - ) - } - - package init( - title: String, - actionString: String, - positionRange: Range, - textDocument: TextDocumentIdentifier - ) { - self.title = title - self.actionString = actionString - self.positionRange = positionRange - self.textDocument = textDocument - } - - package func encodeToLSPAny() -> LSPAny { - return .dictionary([ - CodingKeys.title.stringValue: .string(title), - CodingKeys.actionString.stringValue: .string(actionString), - CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(), - CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(), - ]) - } -} diff --git a/Sources/SwiftLanguageService/FoldingRange.swift b/Sources/SwiftLanguageService/FoldingRange.swift deleted file mode 100644 index 0b04f8836..000000000 --- a/Sources/SwiftLanguageService/FoldingRange.swift +++ /dev/null @@ -1,292 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -import SKLogging -import SKUtilities -import SourceKitLSP -import SwiftSyntax - -private final class FoldingRangeFinder: SyntaxAnyVisitor { - private let snapshot: DocumentSnapshot - /// Some ranges might occur multiple times. - /// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call. - /// It doesn't make sense to report them multiple times, so use a `Set` here. - private var ranges: Set - /// The client-imposed limit on the number of folding ranges it would - /// prefer to receive from the LSP server. If the value is `nil`, there - /// is no preset limit. - private var rangeLimit: Int? - /// If `true`, the client is only capable of folding entire lines. If - /// `false` the client can handle folding ranges. - private var lineFoldingOnly: Bool - - init(snapshot: DocumentSnapshot, rangeLimit: Int?, lineFoldingOnly: Bool) { - self.snapshot = snapshot - self.ranges = [] - self.rangeLimit = rangeLimit - self.lineFoldingOnly = lineFoldingOnly - super.init(viewMode: .sourceAccurate) - } - - override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { - // Index comments, so we need to see at least '/*', or '//'. - if node.leadingTriviaLength.utf8Length > 2 { - self.addTrivia(from: node, node.leadingTrivia) - } - - if node.trailingTriviaLength.utf8Length > 2 { - self.addTrivia(from: node, node.trailingTrivia) - } - - return .visitChildren - } - - private func addTrivia(from node: TokenSyntax, _ trivia: Trivia) { - let pieces = trivia.pieces - var start = node.position - /// The index of the trivia piece we are currently inspecting. - var index = 0 - - while index < pieces.count { - let piece = pieces[index] - defer { - start = start.advanced(by: pieces[index].sourceLength.utf8Length) - index += 1 - } - switch piece { - case .blockComment: - _ = self.addFoldingRange( - start: start, - end: start.advanced(by: piece.sourceLength.utf8Length), - kind: .comment - ) - case .docBlockComment: - _ = self.addFoldingRange( - start: start, - end: start.advanced(by: piece.sourceLength.utf8Length), - kind: .comment - ) - case .lineComment, .docLineComment: - let lineCommentBlockStart = start - - // Keep scanning the upcoming trivia pieces to find the end of the - // block of line comments. - // As we find a new end of the block comment, we set `index` and - // `start` to `lookaheadIndex` and `lookaheadStart` resp. to - // commit the newly found end. - var lookaheadIndex = index - var lookaheadStart = start - var hasSeenNewline = false - LOOP: while lookaheadIndex < pieces.count { - let piece = pieces[lookaheadIndex] - defer { - lookaheadIndex += 1 - lookaheadStart = lookaheadStart.advanced(by: piece.sourceLength.utf8Length) - } - switch piece { - case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count): - if count > 1 || hasSeenNewline { - // More than one newline is separating the two line comment blocks. - // We have reached the end of this block of line comments. - break LOOP - } - hasSeenNewline = true - case .spaces, .tabs: - // We allow spaces and tabs because the comments might be indented - continue - case .lineComment, .docLineComment: - // We have found a new line comment in this block. Commit it. - index = lookaheadIndex - start = lookaheadStart - hasSeenNewline = false - default: - // We assume that any other trivia piece terminates the block - // of line comments. - break LOOP - } - } - _ = self.addFoldingRange( - start: lineCommentBlockStart, - end: start.advanced(by: pieces[index].sourceLength.utf8Length), - kind: .comment - ) - default: - break - } - } - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if let braced = node.asProtocol(BracedSyntax.self) { - return self.addFoldingRange( - start: braced.leftBrace.endPositionBeforeTrailingTrivia, - end: braced.rightBrace.positionAfterSkippingLeadingTrivia - ) - } - if let parenthesized = node.asProtocol(ParenthesizedSyntax.self) { - return self.addFoldingRange( - start: parenthesized.leftParen.endPositionBeforeTrailingTrivia, - end: parenthesized.rightParen.positionAfterSkippingLeadingTrivia - ) - } - return .visitChildren - } - - override func visit(_ node: ArrayExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.leftSquare.endPositionBeforeTrailingTrivia, - end: node.rightSquare.positionAfterSkippingLeadingTrivia - ) - } - - override func visit(_ node: DictionaryExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.leftSquare.endPositionBeforeTrailingTrivia, - end: node.rightSquare.positionAfterSkippingLeadingTrivia - ) - } - - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - if let leftParen = node.leftParen, let rightParen = node.rightParen { - return self.addFoldingRange( - start: leftParen.endPositionBeforeTrailingTrivia, - end: rightParen.positionAfterSkippingLeadingTrivia - ) - } - return .visitChildren - } - - override func visit(_ node: IfConfigClauseSyntax) -> SyntaxVisitorContinueKind { - guard let closePound = node.lastToken(viewMode: .sourceAccurate)?.nextToken(viewMode: .sourceAccurate) else { - return .visitChildren - } - - return self.addFoldingRange( - start: node.poundKeyword.positionAfterSkippingLeadingTrivia, - end: closePound.positionAfterSkippingLeadingTrivia - ) - } - - override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.leftSquare.endPositionBeforeTrailingTrivia, - end: node.rightSquare.positionAfterSkippingLeadingTrivia - ) - } - - override func visit(_ node: SwitchCaseSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.label.endPositionBeforeTrailingTrivia, - end: node.statements.endPosition - ) - } - - __consuming func finalize() -> Set { - return self.ranges - } - - private func addFoldingRange( - start: AbsolutePosition, - end: AbsolutePosition, - kind: FoldingRangeKind? = nil - ) -> SyntaxVisitorContinueKind { - if let limit = self.rangeLimit, self.ranges.count >= limit { - return .skipChildren - } - if start == end { - // Don't report empty ranges - return .visitChildren - } - - let start = snapshot.positionOf(utf8Offset: start.utf8Offset) - let end = snapshot.positionOf(utf8Offset: end.utf8Offset) - let range: FoldingRange - if lineFoldingOnly { - // If the folding range doesn't end at the end of the last line, exclude that line from the folding range since - // the end line gets folded away. This means if we reported `end.line`, we would eg. fold away the `}` that - // matches a `{`, which looks surprising. - // If the folding range does end at the end of the line we are in cases that don't have a closing indicator (like - // comments), so we can fold the last line as well. - let endLine: Int - if snapshot.lineTable.isAtEndOfLine(end) { - endLine = end.line - } else { - endLine = end.line - 1 - } - - // Since the client cannot fold less than a single line, if the - // fold would span 1 line there's no point in reporting it. - guard endLine > start.line else { - return .visitChildren - } - - // If the client only supports folding full lines, don't report - // the end of the range since there's nothing they could do with it. - range = FoldingRange( - startLine: start.line, - startUTF16Index: nil, - endLine: endLine, - endUTF16Index: nil, - kind: kind - ) - } else { - range = FoldingRange( - startLine: start.line, - startUTF16Index: start.utf16index, - endLine: end.line, - endUTF16Index: end.utf16index, - kind: kind - ) - } - ranges.insert(range) - return .visitChildren - } -} - -extension SwiftLanguageService { - package func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? { - let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange - let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri) - - let sourceFile = await syntaxTreeManager.syntaxTree(for: snapshot) - - try Task.checkCancellation() - - // If the limit is less than one, do nothing. - if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 { - return [] - } - - let rangeFinder = FoldingRangeFinder( - snapshot: snapshot, - rangeLimit: foldingRangeCapabilities?.rangeLimit, - lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false - ) - rangeFinder.walk(sourceFile) - let ranges = rangeFinder.finalize() - - return ranges.sorted() - } -} - -fileprivate extension LineTable { - func isAtEndOfLine(_ position: Position) -> Bool { - guard let line = self.line(at: position.line) else { - return false - } - guard let index = line.utf16.index(line.startIndex, offsetBy: position.utf16index, limitedBy: line.endIndex) else { - return false - } - return line[index...].allSatisfy(\.isNewline) - } -} diff --git a/Sources/SwiftLanguageService/GeneratedInterfaceManager.swift b/Sources/SwiftLanguageService/GeneratedInterfaceManager.swift deleted file mode 100644 index bedb73b67..000000000 --- a/Sources/SwiftLanguageService/GeneratedInterfaceManager.swift +++ /dev/null @@ -1,236 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging -import SKUtilities -import SourceKitD -import SourceKitLSP -import SwiftExtensions - -/// When information about a generated interface is requested, this opens the generated interface in sourcekitd and -/// caches the generated interface contents. -/// -/// It keeps the generated interface open in sourcekitd until the corresponding reference document is closed in the -/// editor. Additionally, it also keeps a few recently requested interfaces cached. This way we don't need to recompute -/// the generated interface contents between the initial generated interface request to find a USR's position in the -/// interface until the editor actually opens the reference document. -actor GeneratedInterfaceManager { - private struct OpenGeneratedInterfaceDocumentDetails { - let url: GeneratedInterfaceDocumentURLData - - /// The contents of the generated interface. - let snapshot: DocumentSnapshot - - /// The number of `GeneratedInterfaceManager` that are actively working with the sourcekitd document. If this value - /// is 0, the generated interface may be closed in sourcekitd. - /// - /// Usually, this value is 1, while the reference document for this generated interface is open in the editor. - var refCount: Int - } - - private weak var swiftLanguageService: SwiftLanguageService? - - /// The number of generated interface documents that are not in editor but should still be cached. - private let cacheSize = 2 - - /// Details about the generated interfaces that are currently open in sourcekitd. - /// - /// Conceptually, this is a dictionary with `url` being the key. To prevent excessive memory usage we only keep - /// `cacheSize` entries with a ref count of 0 in the array. Older entries are at the end of the list, newer entries - /// at the front. - private var openInterfaces: [OpenGeneratedInterfaceDocumentDetails] = [] - - init(swiftLanguageService: SwiftLanguageService) { - self.swiftLanguageService = swiftLanguageService - } - - /// If there are more than `cacheSize` entries in `openInterfaces` that have a ref count of 0, close the oldest ones. - private func purgeCache() { - var documentsToClose: [String] = [] - while openInterfaces.count(where: { $0.refCount == 0 }) > cacheSize, - let indexToPurge = openInterfaces.lastIndex(where: { $0.refCount == 0 }) - { - documentsToClose.append(openInterfaces[indexToPurge].url.sourcekitdDocumentName) - openInterfaces.remove(at: indexToPurge) - } - if !documentsToClose.isEmpty, let swiftLanguageService { - Task { - let sourcekitd = swiftLanguageService.sourcekitd - for documentToClose in documentsToClose { - await orLog("Closing generated interface") { - _ = try await swiftLanguageService.send( - sourcekitdRequest: \.editorClose, - sourcekitd.dictionary([ - sourcekitd.keys.name: documentToClose, - sourcekitd.keys.cancelBuilds: 0, - ]), - snapshot: nil - ) - } - } - } - } - } - - /// If we don't have the generated interface for the given `document` open in sourcekitd, open it, otherwise return - /// its details from the cache. - /// - /// If `incrementingRefCount` is `true`, then the document manager will keep the generated interface open in - /// sourcekitd, independent of the cache size. If `incrementingRefCount` is `true`, then `decrementRefCount` must be - /// called to allow the document to be closed again. - private func details( - for document: GeneratedInterfaceDocumentURLData, - incrementingRefCount: Bool - ) async throws -> OpenGeneratedInterfaceDocumentDetails { - func loadFromCache() -> OpenGeneratedInterfaceDocumentDetails? { - guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else { - return nil - } - if incrementingRefCount { - openInterfaces[cachedIndex].refCount += 1 - } - return openInterfaces[cachedIndex] - - } - if let cached = loadFromCache() { - return cached - } - - guard let swiftLanguageService else { - // `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - - let sourcekitd = swiftLanguageService.sourcekitd - - let keys = sourcekitd.keys - let skreq = sourcekitd.dictionary([ - keys.moduleName: document.moduleName, - keys.groupName: document.groupName, - keys.name: document.sourcekitdDocumentName, - keys.synthesizedExtension: 1, - keys.compilerArgs: await swiftLanguageService.compileCommand(for: try document.uri, fallbackAfterTimeout: false)? - .compilerArgs as [SKDRequestValue]?, - ]) - - let dict = try await swiftLanguageService.send(sourcekitdRequest: \.editorOpenInterface, skreq, snapshot: nil) - - guard let contents: String = dict[keys.sourceText] else { - throw ResponseError.unknown("sourcekitd response is missing sourceText") - } - - if let cached = loadFromCache() { - // Another request raced us to create the generated interface. Discard what we computed here and return the cached - // value. - await orLog("Closing generated interface created during race") { - _ = try await swiftLanguageService.send( - sourcekitdRequest: \.editorClose, - sourcekitd.dictionary([ - keys.name: document.sourcekitdDocumentName, - keys.cancelBuilds: 0, - ]), - snapshot: nil - ) - } - return cached - } - - let details = OpenGeneratedInterfaceDocumentDetails( - url: document, - snapshot: DocumentSnapshot( - uri: try document.uri, - language: .swift, - version: 0, - lineTable: LineTable(contents) - ), - refCount: incrementingRefCount ? 1 : 0 - ) - openInterfaces.insert(details, at: 0) - purgeCache() - return details - } - - private func decrementRefCount(for document: GeneratedInterfaceDocumentURLData) { - guard let cachedIndex = openInterfaces.firstIndex(where: { $0.url == document }) else { - logger.fault( - "Generated interface document for \(document.moduleName) is not open anymore. Unbalanced retain and releases?" - ) - return - } - if openInterfaces[cachedIndex].refCount == 0 { - logger.fault( - "Generated interface document for \(document.moduleName) is already 0. Unbalanced retain and releases?" - ) - return - } - openInterfaces[cachedIndex].refCount -= 1 - purgeCache() - } - - func position(ofUsr usr: String, in document: GeneratedInterfaceDocumentURLData) async throws -> Position { - guard let swiftLanguageService else { - // `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - - let details = try await details(for: document, incrementingRefCount: true) - defer { - decrementRefCount(for: document) - } - - let sourcekitd = swiftLanguageService.sourcekitd - let keys = sourcekitd.keys - let skreq = sourcekitd.dictionary([ - keys.sourceFile: document.sourcekitdDocumentName, - keys.usr: usr, - ]) - - let dict = try await swiftLanguageService.send( - sourcekitdRequest: \.editorFindUSR, - skreq, - snapshot: details.snapshot - ) - guard let offset: Int = dict[keys.offset] else { - throw ResponseError.unknown("Missing key 'offset'") - } - return details.snapshot.positionOf(utf8Offset: offset) - } - - func snapshot(of document: GeneratedInterfaceDocumentURLData) async throws -> DocumentSnapshot { - return try await details(for: document, incrementingRefCount: false).snapshot - } - - func open(document: GeneratedInterfaceDocumentURLData) async throws { - _ = try await details(for: document, incrementingRefCount: true) - } - - func close(document: GeneratedInterfaceDocumentURLData) async { - decrementRefCount(for: document) - } - - func reopen(interfacesWithBuildSettingsFrom buildSettingsFile: DocumentURI) async { - for openInterface in openInterfaces { - guard openInterface.url.buildSettingsFrom == buildSettingsFile else { - continue - } - await orLog("Reopening generated interface") { - // `MessageHandlingDependencyTracker` ensures that we don't handle a request for the generated interface while - // it is being re-opened because `documentUpdate` and `documentRequest` use the `buildSettingsFile` to determine - // their dependencies. - await close(document: openInterface.url) - openInterfaces.removeAll(where: { $0.url == openInterface.url }) - try await open(document: openInterface.url) - } - } - } -} diff --git a/Sources/SwiftLanguageService/MacroExpansion.swift b/Sources/SwiftLanguageService/MacroExpansion.swift deleted file mode 100644 index 38494d379..000000000 --- a/Sources/SwiftLanguageService/MacroExpansion.swift +++ /dev/null @@ -1,277 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Crypto -import Csourcekitd -import Foundation -import LanguageServerProtocol -import SKLogging -import SKOptions -import SKUtilities -import SourceKitD -import SourceKitLSP -import SwiftExtensions - -/// Caches the contents of macro expansions that were recently requested by the user. -actor MacroExpansionManager { - private struct CacheKey: Hashable { - let snapshotID: DocumentSnapshot.ID - let range: Range - let buildSettings: SwiftCompileCommand? - } - - init(swiftLanguageService: SwiftLanguageService?) { - self.swiftLanguageService = swiftLanguageService - } - - private weak var swiftLanguageService: SwiftLanguageService? - - /// The cache that stores reportTasks for a combination of uri, range and build settings. - /// - /// - Note: The capacity of this cache should be bigger than the maximum expansion depth of macros a user might - /// do to avoid re-generating all parent macros to a nested macro expansion's buffer. 10 seems to be big enough - /// for that because it's unlikely that a macro will expand to more than 10 levels. - private var cache = LRUCache(capacity: 10) - - /// Return the text of the macro expansion referenced by `macroExpansionURLData`. - func macroExpansion( - for macroExpansionURLData: MacroExpansionReferenceDocumentURLData - ) async throws -> String { - let expansions = try await macroExpansions( - in: macroExpansionURLData.parent, - at: macroExpansionURLData.parentSelectionRange - ) - guard let expansion = expansions.filter({ $0.bufferName == macroExpansionURLData.bufferName }).only else { - throw ResponseError.unknown("Failed to find macro expansion for \(macroExpansionURLData.bufferName).") - } - return expansion.newText - } - - func macroExpansions( - in uri: DocumentURI, - at range: Range - ) async throws -> [RefactoringEdit] { - guard let swiftLanguageService else { - // `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - - let snapshot = try await swiftLanguageService.latestSnapshot(for: uri) - let compileCommand = await swiftLanguageService.compileCommand(for: uri, fallbackAfterTimeout: false) - - let cacheKey = CacheKey(snapshotID: snapshot.id, range: range, buildSettings: compileCommand) - if let valueFromCache = cache[cacheKey] { - return valueFromCache - } - let macroExpansions = try await macroExpansionsImpl(in: snapshot, at: range, buildSettings: compileCommand) - cache[cacheKey] = macroExpansions - - return macroExpansions - } - - private func macroExpansionsImpl( - in snapshot: DocumentSnapshot, - at range: Range, - buildSettings: SwiftCompileCommand? - ) async throws -> [RefactoringEdit] { - guard let swiftLanguageService else { - // `SwiftLanguageService` has been destructed. We are tearing down the language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - let keys = swiftLanguageService.keys - - let line = range.lowerBound.line - let utf16Column = range.lowerBound.utf16index - let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) - let length = snapshot.utf8OffsetRange(of: range).count - - let skreq = swiftLanguageService.sourcekitd.dictionary([ - keys.cancelOnSubsequentRequest: 0, - // Preferred name for e.g. an extracted variable. - // Empty string means sourcekitd chooses a name automatically. - keys.name: "", - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - // LSP is zero based, but this request is 1 based. - keys.line: line + 1, - keys.column: utf8Column + 1, - keys.length: length, - keys.actionUID: swiftLanguageService.sourcekitd.api.uid_get_from_cstr("source.refactoring.kind.expand.macro")!, - keys.compilerArgs: buildSettings?.compilerArgs as [SKDRequestValue]?, - ]) - - let dict = try await swiftLanguageService.send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) - guard let expansions = [RefactoringEdit](dict, snapshot, keys) else { - throw SemanticRefactoringError.noEditsNeeded(snapshot.uri) - } - return expansions - } - - /// Remove all cached macro expansions for the given primary file, eg. because the macro's plugin might have changed. - func purge(primaryFile: DocumentURI) { - cache.removeAll { - $0.snapshotID.uri.primaryFile ?? $0.snapshotID.uri == primaryFile - } - } -} - -extension SwiftLanguageService { - /// Handles the `ExpandMacroCommand`. - /// - /// Makes a `PeekDocumentsRequest` or `ShowDocumentRequest`, containing the - /// location of each macro expansion, to the client depending on whether the - /// client supports the `experimental["workspace/peekDocuments"]` capability. - /// - /// - Parameters: - /// - expandMacroCommand: The `ExpandMacroCommand` that triggered this request. - func expandMacro( - _ expandMacroCommand: ExpandMacroCommand - ) async throws { - guard let sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the - // language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - - let parentFileDisplayName = - switch try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri) { - case .macroExpansion(let data): - data.bufferName - case .generatedInterface(let data): - data.displayName - case nil: - expandMacroCommand.textDocument.uri.fileURL?.lastPathComponent ?? expandMacroCommand.textDocument.uri.pseudoPath - } - - let expansions = try await macroExpansionManager.macroExpansions( - in: expandMacroCommand.textDocument.uri, - at: expandMacroCommand.positionRange - ) - - var completeExpansionFileContent = "" - var completeExpansionDirectoryName = "" - - var macroExpansionReferenceDocumentURLs: [ReferenceDocumentURL] = [] - for macroEdit in expansions { - if let bufferName = macroEdit.bufferName { - let macroExpansionReferenceDocumentURLData = - ReferenceDocumentURL.macroExpansion( - MacroExpansionReferenceDocumentURLData( - macroExpansionEditRange: macroEdit.range, - parent: expandMacroCommand.textDocument.uri, - parentSelectionRange: expandMacroCommand.positionRange, - bufferName: bufferName - ) - ) - - macroExpansionReferenceDocumentURLs.append(macroExpansionReferenceDocumentURLData) - - completeExpansionDirectoryName += "\(bufferName)-" - - let editContent = - """ - // \(parentFileDisplayName) @ \(macroEdit.range.lowerBound.line + 1):\(macroEdit.range.lowerBound.utf16index + 1) - \(macroEdit.range.upperBound.line + 1):\(macroEdit.range.upperBound.utf16index + 1) - \(macroEdit.newText) - - - """ - completeExpansionFileContent += editContent - } else if !macroEdit.newText.isEmpty { - logger.fault("Unable to retrieve some parts of macro expansion") - } - } - - if self.capabilityRegistry.clientHasExperimentalCapability(PeekDocumentsRequest.method), - self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method) - { - let expansionURIs = try macroExpansionReferenceDocumentURLs.map { try $0.uri } - - let uri = expandMacroCommand.textDocument.uri.primaryFile ?? expandMacroCommand.textDocument.uri - - let position = - switch try? ReferenceDocumentURL(from: expandMacroCommand.textDocument.uri) { - case .macroExpansion(let data): - data.primaryFileSelectionRange.lowerBound - case .generatedInterface, nil: - expandMacroCommand.positionRange.lowerBound - } - - Task { - let req = PeekDocumentsRequest( - uri: uri, - position: position, - locations: expansionURIs - ) - - let response = await orLog("Sending PeekDocumentsRequest to Client") { - try await sourceKitLSPServer.sendRequestToClient(req) - } - - if let response, !response.success { - logger.error("client refused to peek macro") - } - } - } else { - // removes superfluous newline - if completeExpansionFileContent.hasSuffix("\n\n") { - completeExpansionFileContent.removeLast() - } - - if completeExpansionDirectoryName.hasSuffix("-") { - completeExpansionDirectoryName.removeLast() - } - - var completeExpansionFilePath = - self.generatedMacroExpansionsPath.appending( - component: - Insecure.MD5.hash(data: Data(completeExpansionDirectoryName.utf8)) - .map { String(format: "%02hhx", $0) } // maps each byte of the hash to its hex equivalent `String` - .joined() - ) - - do { - try FileManager.default.createDirectory( - at: completeExpansionFilePath, - withIntermediateDirectories: true - ) - } catch { - throw ResponseError.unknown( - "Failed to create directory for complete macro expansion at \(completeExpansionFilePath.description)" - ) - } - - completeExpansionFilePath = - completeExpansionFilePath.appending(component: parentFileDisplayName) - do { - try completeExpansionFileContent.write(to: completeExpansionFilePath, atomically: true, encoding: .utf8) - } catch { - throw ResponseError.unknown( - "Unable to write complete macro expansion to \"\(completeExpansionFilePath.description)\"" - ) - } - - let completeMacroExpansionFilePath = completeExpansionFilePath - - Task { - let req = ShowDocumentRequest(uri: DocumentURI(completeMacroExpansionFilePath)) - - let response = await orLog("Sending ShowDocumentRequest to Client") { - try await sourceKitLSPServer.sendRequestToClient(req) - } - - if let response, !response.success { - logger.error("client refused to show document for macro expansion") - } - } - } - } -} diff --git a/Sources/SwiftLanguageService/OpenInterface.swift b/Sources/SwiftLanguageService/OpenInterface.swift deleted file mode 100644 index d6f9d402a..000000000 --- a/Sources/SwiftLanguageService/OpenInterface.swift +++ /dev/null @@ -1,64 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -package import SourceKitLSP - -extension SwiftLanguageService { - package func openGeneratedInterface( - document: DocumentURI, - moduleName: String, - groupName: String?, - symbolUSR symbol: String? - ) async throws -> GeneratedInterfaceDetails? { - // Include build settings context to distinguish different versions/configurations - let buildSettingsFileHash = "\(abs(document.buildSettingsFile.stringValue.hashValue))" - let sourcekitdDocumentName = [moduleName, groupName, buildSettingsFileHash].compactMap(\.self) - .joined(separator: ".") - - let urlData = GeneratedInterfaceDocumentURLData( - moduleName: moduleName, - groupName: groupName, - sourcekitdDocumentName: sourcekitdDocumentName, - primaryFile: document - ) - let position: Position? = - if let symbol { - await orLog("Getting position of USR") { - try await generatedInterfaceManager.position(ofUsr: symbol, in: urlData) - } - } else { - nil - } - - if self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method) { - return GeneratedInterfaceDetails(uri: try urlData.uri, position: position) - } - let interfaceFilePath = self.generatedInterfacesPath - .appending(components: buildSettingsFileHash, urlData.displayName) - try FileManager.default.createDirectory( - at: interfaceFilePath.deletingLastPathComponent(), - withIntermediateDirectories: true - ) - try await generatedInterfaceManager.snapshot(of: urlData).text.write( - to: interfaceFilePath, - atomically: true, - encoding: String.Encoding.utf8 - ) - return GeneratedInterfaceDetails( - uri: DocumentURI(interfaceFilePath), - position: position - ) - } -} diff --git a/Sources/SwiftLanguageService/RefactoringEdit.swift b/Sources/SwiftLanguageService/RefactoringEdit.swift deleted file mode 100644 index 341cbb2b8..000000000 --- a/Sources/SwiftLanguageService/RefactoringEdit.swift +++ /dev/null @@ -1,74 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -import SourceKitD - -/// Represents an edit from semantic refactor response. Notionally, a subclass of `TextEdit` -package struct RefactoringEdit: Hashable, Sendable, Codable { - /// The range of text to be replaced. - @CustomCodable - package var range: Range - - /// The new text. - package var newText: String - - /// If the new text of the edit should not be applied to the original source - /// file but to a separate buffer, a fake name for that buffer. For example - /// for expansion of macros, this is @ followed by the mangled name of the - /// macro expansion, followed by .swift. - package var bufferName: String? - - package init(range: Range, newText: String, bufferName: String?) { - self._range = CustomCodable(wrappedValue: range) - self.newText = newText - self.bufferName = bufferName - } -} - -extension RefactoringEdit: LSPAnyCodable { - package init?(fromLSPDictionary dictionary: [String: LSPAny]) { - guard case .dictionary(let rangeDict) = dictionary[CodingKeys.range.stringValue], - case .string(let newText) = dictionary[CodingKeys.newText.stringValue] - else { - return nil - } - - guard let range = Range(fromLSPDictionary: rangeDict) else { - return nil - } - - self._range = CustomCodable(wrappedValue: range) - self.newText = newText - - if case .string(let bufferName) = dictionary[CodingKeys.bufferName.stringValue] { - self.bufferName = bufferName - } else { - self.bufferName = nil - } - } - - package func encodeToLSPAny() -> LSPAny { - guard let bufferName = bufferName else { - return .dictionary([ - CodingKeys.range.stringValue: range.encodeToLSPAny(), - CodingKeys.newText.stringValue: .string(newText), - ]) - } - - return .dictionary([ - CodingKeys.range.stringValue: range.encodeToLSPAny(), - CodingKeys.newText.stringValue: .string(newText), - CodingKeys.bufferName.stringValue: .string(bufferName), - ]) - } -} diff --git a/Sources/SwiftLanguageService/RefactoringResponse.swift b/Sources/SwiftLanguageService/RefactoringResponse.swift deleted file mode 100644 index 45b1a5672..000000000 --- a/Sources/SwiftLanguageService/RefactoringResponse.swift +++ /dev/null @@ -1,145 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import LanguageServerProtocol -import SKLogging -import SKUtilities -import SourceKitD -import SourceKitLSP - -protocol RefactoringResponse { - init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit]) -} - -extension [RefactoringEdit] { - init?(_ dict: SKDResponseDictionary, _ snapshot: DocumentSnapshot, _ keys: sourcekitd_api_keys) { - guard let categorizedEdits: SKDResponseArray = dict[keys.categorizedEdits] else { - logger.fault("categorizedEdits doesn't exist in response dictionary") - return nil - } - - self = [] - - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - categorizedEdits.forEach { _, categorizedEdit in - guard let edits: SKDResponseArray = categorizedEdit[keys.edits] else { - logger.fault("edits doesn't exist in categorizedEdit dictionary") - return true - } - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - edits.forEach { _, edit in - guard let startLine: Int = edit[keys.line], - let startColumn: Int = edit[keys.column], - let endLine: Int = edit[keys.endLine], - let endColumn: Int = edit[keys.endColumn], - let text: String = edit[keys.text] - else { - logger.fault("Failed to deserialise edit dictionary containing values: \(edit)") - return true // continue - } - - // The LSP is zero based, but semantic_refactoring is one based. - let startPosition = snapshot.positionOf( - zeroBasedLine: startLine - 1, - utf8Column: startColumn - 1 - ) - let endPosition = snapshot.positionOf( - zeroBasedLine: endLine - 1, - utf8Column: endColumn - 1 - ) - // Snippets are only supported in code completion. - // Remove SourceKit placeholders in refactoring actions because they - // can't be represented in the editor properly. - let textWithSnippets = rewriteSourceKitPlaceholders(in: text, clientSupportsSnippets: false) - self.append( - RefactoringEdit( - range: startPosition.. SemanticRefactoring { - let keys = self.keys - - let uri = refactorCommand.textDocument.uri - let snapshot = try self.documentManager.latestSnapshot(uri) - let line = refactorCommand.positionRange.lowerBound.line - let utf16Column = refactorCommand.positionRange.lowerBound.utf16index - let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) - - let skreq = sourcekitd.dictionary([ - keys.cancelOnSubsequentRequest: 0, - // Preferred name for e.g. an extracted variable. - // Empty string means sourcekitd chooses a name automatically. - keys.name: "", - keys.sourceFile: uri.pseudoPath, - // LSP is zero based, but this request is 1 based. - keys.line: line + 1, - keys.column: utf8Column + 1, - keys.length: snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count, - keys.actionUID: self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)!, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)?.compilerArgs - as [SKDRequestValue]?, - ]) - - let dict = try await send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) - guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else { - throw SemanticRefactoringError.noEditsNeeded(uri) - } - return refactor - } -} diff --git a/Sources/SwiftLanguageService/RelatedIdentifiers.swift b/Sources/SwiftLanguageService/RelatedIdentifiers.swift deleted file mode 100644 index 3764487f3..000000000 --- a/Sources/SwiftLanguageService/RelatedIdentifiers.swift +++ /dev/null @@ -1,105 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP - -struct RelatedIdentifier { - let range: Range - let usage: RenameLocation.Usage -} - -extension RenameLocation.Usage { - fileprivate init?(_ uid: sourcekitd_api_uid_t?, _ values: sourcekitd_api_values) { - switch uid { - case values.definition: - self = .definition - case values.reference: - self = .reference - case values.call: - self = .call - case values.unknown: - self = .unknown - default: - return nil - } - } - - func uid(values: sourcekitd_api_values) -> sourcekitd_api_uid_t { - switch self { - case .definition: - return values.definition - case .reference: - return values.reference - case .call: - return values.call - case .unknown: - return values.unknown - } - } -} - -struct RelatedIdentifiersResponse { - let relatedIdentifiers: [RelatedIdentifier] - /// The compound decl name at the requested location. This can be used as `name` parameter to a - /// `find-syntactic-rename-ranges` request. - /// - /// `nil` if `sourcekitd` is too old and doesn't return the `name` as part of the related identifiers request or - /// `relatedIdentifiers` is empty (eg. when performing a related identifiers request on `self`). - let name: String? -} - -extension SwiftLanguageService { - func relatedIdentifiers( - at position: Position, - in snapshot: DocumentSnapshot, - includeNonEditableBaseNames: Bool - ) async throws -> RelatedIdentifiersResponse { - let skreq = sourcekitd.dictionary([ - keys.cancelOnSubsequentRequest: 0, - keys.offset: snapshot.utf8Offset(of: position), - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - keys.includeNonEditableBaseNames: includeNonEditableBaseNames ? 1 : 0, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)?.compilerArgs - as [SKDRequestValue]?, - ]) - - let dict = try await send(sourcekitdRequest: \.relatedIdents, skreq, snapshot: snapshot) - - guard let results: SKDResponseArray = dict[self.keys.results] else { - throw ResponseError.internalError("sourcekitd response did not contain results") - } - let name: String? = dict[self.keys.name] - - try Task.checkCancellation() - - var relatedIdentifiers: [RelatedIdentifier] = [] - - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - results.forEach { _, value in - guard let offset: Int = value[keys.offset], let length: Int = value[keys.length] else { - return true // continue - } - let start = snapshot.positionOf(utf8Offset: offset) - let end = snapshot.positionOf(utf8Offset: offset + length) - let usage = RenameLocation.Usage(value[keys.nameType], values) ?? .unknown - relatedIdentifiers.append(RelatedIdentifier(range: start.. Int` - case keywordBaseName - - /// The internal parameter name (aka. second name) inside a function declaration - /// - /// ### Examples - /// - ` b` in `func foo(a b: Int)` - case parameterName - - /// Same as `parameterName` but cannot be removed if it is the same as the parameter's first name. This only happens - /// for subscripts where parameters are unnamed by default unless they have both a first and second name. - /// - /// ### Examples - /// The second ` a` in `subscript(a a: Int)` - case noncollapsibleParameterName - - /// The external argument label of a function parameter - /// - /// ### Examples - /// - `a` in `func foo(a b: Int)` - /// - `a` in `func foo(a: Int)` - case declArgumentLabel - - /// The argument label inside a call. - /// - /// ### Examples - /// - `a` in `foo(a: 1)` - case callArgumentLabel - - /// The colon after an argument label inside a call. This is reported so it can be removed if the parameter becomes - /// unnamed. - /// - /// ### Examples - /// - `: ` in `foo(a: 1)` - case callArgumentColon - - /// An empty range that point to the position before an unnamed argument. This is used to insert the argument label - /// if an unnamed parameter becomes named. - /// - /// ### Examples - /// - An empty range before `1` in `foo(1)`, which could expand to `foo(a: 1)` - case callArgumentCombined - - /// The argument label in a compound decl name. - /// - /// ### Examples - /// - `a` in `foo(a:)` - case selectorArgumentLabel - - init?(_ uid: sourcekitd_api_uid_t, values: sourcekitd_api_values) { - switch uid { - case values.renameRangeBase: self = .baseName - case values.renameRangeCallArgColon: self = .callArgumentColon - case values.renameRangeCallArgCombined: self = .callArgumentCombined - case values.renameRangeCallArgLabel: self = .callArgumentLabel - case values.renameRangeDeclArgLabel: self = .declArgumentLabel - case values.renameRangeKeywordBase: self = .keywordBaseName - case values.renameRangeNoncollapsibleParam: self = .noncollapsibleParameterName - case values.renameRangeParam: self = .parameterName - case values.renameRangeSelectorArgLabel: self = .selectorArgumentLabel - default: return nil - } - } -} - -/// A single “piece” that is used for renaming a compound function name. -/// -/// See `SyntacticRenamePieceKind` for the different rename pieces that exist. -/// -/// ### Example -/// `foo(x: 1)` is represented by three pieces -/// - The base name `foo` -/// - The parameter name `x` -/// - The call argument colon `: `. -private struct SyntacticRenamePiece { - /// The range that represents this piece of the name - let range: Range - - /// The kind of the rename piece. - let kind: SyntacticRenamePieceKind - - /// If this piece belongs to a parameter, the index of that parameter (zero-based) or `nil` if this is the base name - /// piece. - let parameterIndex: Int? - - /// Create a `SyntacticRenamePiece` from a `sourcekitd` response. - init?( - _ dict: SKDResponseDictionary, - in snapshot: DocumentSnapshot, - keys: sourcekitd_api_keys, - values: sourcekitd_api_values - ) { - guard let line: Int = dict[keys.line], - let column: Int = dict[keys.column], - let endLine: Int = dict[keys.endLine], - let endColumn: Int = dict[keys.endColumn], - let kind: sourcekitd_api_uid_t = dict[keys.kind] - else { - return nil - } - let start = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: column - 1) - let end = snapshot.positionOf(zeroBasedLine: endLine - 1, utf8Column: endColumn - 1) - guard let kind = SyntacticRenamePieceKind(kind, values: values) else { - return nil - } - - self.range = start.., callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring { - let start = self.stringIndexOf( - line: range.lowerBound.line, - utf16Column: range.lowerBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - let end = self.stringIndexOf( - line: range.upperBound.line, - utf16Column: range.upperBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - return self.content[start..(as syntaxType: S.Type) -> S? { - return parent?.as(S.self) - } -} - -fileprivate extension RelatedIdentifiersResponse { - func renameLocations(in snapshot: DocumentSnapshot) -> [RenameLocation] { - return self.relatedIdentifiers.map { - (relatedIdentifier) -> RenameLocation in - let position = relatedIdentifier.range.lowerBound - let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index) - return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage) - } - } -} - -// MARK: - Name translation - -extension SwiftLanguageService: NameTranslatorService { - enum NameTranslationError: Error, CustomStringConvertible { - case malformedSwiftToClangTranslateNameResponse(SKDResponseDictionary) - case malformedClangToSwiftTranslateNameResponse(SKDResponseDictionary) - - var description: String { - switch self { - case .malformedSwiftToClangTranslateNameResponse(let response): - return """ - Malformed response for Swift to Clang name translation - - \(response.description) - """ - case .malformedClangToSwiftTranslateNameResponse(let response): - return """ - Malformed response for Clang to Swift name translation - - \(response.description) - """ - } - } - } - - /// Translate a Swift name to the corresponding C/C++/ObjectiveC name. - /// - /// This invokes the clang importer to perform the name translation, based on the `position` and `uri` at which the - /// Swift symbol is defined. - /// - /// - Parameters: - /// - position: The position at which the Swift name is defined - /// - uri: The URI of the document in which the Swift name is defined - /// - name: The Swift name of the symbol - package func translateSwiftNameToClang( - at symbolLocation: SymbolLocation, - in uri: DocumentURI, - name: String - ) async throws -> String { - let name = CompoundDeclName(name) - guard let snapshot = try? documentManager.latestSnapshotOrDisk(uri, language: .swift) else { - throw ResponseError.unknown("Failed to get contents of \(uri.forLogging) to translate Swift name to clang name") - } - - let req = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs - as [SKDRequestValue]?, - keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), - keys.nameKind: sourcekitd.values.nameSwift, - keys.baseName: name.baseName, - keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }), - ]) - - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) - - guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector], - let selectorPieces: SKDResponseArray = response[keys.selectorPieces] - else { - throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) - } - return - try selectorPieces - .map { (dict: SKDResponseDictionary) -> String in - guard var name: String = dict[keys.name] else { - throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response) - } - if isZeroArgSelector == 0 { - // Selector pieces in multi-arg selectors end with ":" - name.append(":") - } - return name - }.joined() - } - - /// Translates a C/C++/Objective-C symbol name to Swift. - /// - /// This requires the position at which the the symbol is referenced in Swift so sourcekitd can determine the - /// clang declaration that is being renamed and check if that declaration has a `SWIFT_NAME`. If it does, this - /// `SWIFT_NAME` is used as the name translation result instead of invoking the clang importer rename rules. - /// - /// - Parameters: - /// - position: A position at which this symbol is referenced from Swift. - /// - snapshot: The snapshot containing the `position` that points to a usage of the clang symbol. - /// - isObjectiveCSelector: Whether the name is an Objective-C selector. Cannot be inferred from the name because - /// a name without `:` can also be a zero-arg Objective-C selector. For such names sourcekitd needs to know - /// whether it is translating a selector to apply the correct renaming rule. - /// - name: The clang symbol name. - /// - Returns: - package func translateClangNameToSwift( - at symbolLocation: SymbolLocation, - in snapshot: DocumentSnapshot, - isObjectiveCSelector: Bool, - name: String - ) async throws -> String { - let req = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs - as [SKDRequestValue]?, - keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), - keys.nameKind: sourcekitd.values.nameObjc, - ]) - - if isObjectiveCSelector { - // Split the name into selector pieces, keeping the ':'. - let selectorPieces = name.split(separator: ":").map { String($0 + ":") } - req.set(keys.selectorPieces, to: sourcekitd.array(selectorPieces)) - } else { - req.set(keys.baseName, to: name) - } - - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) - - guard let baseName: String = response[keys.baseName] else { - throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) - } - let argNamesArray: SKDResponseArray? = response[keys.argNames] - let argNames = try argNamesArray?.map { (dict: SKDResponseDictionary) -> String in - guard var name: String = dict[keys.name] else { - throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) - } - if name.isEmpty { - // Empty argument names are represented by `_` in Swift. - name = "_" - } - return name + ":" - } - var result = baseName - if let argNames, !argNames.isEmpty { - result += "(" + argNames.joined() + ")" - } - return result - } -} - -// MARK: - Rename - -extension SwiftLanguageService { - /// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be - /// edited to rename a compound decl name. - /// - /// - Parameters: - /// - renameLocations: The locations to rename - /// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename - /// locations match that name. Eg. `myFunc(argLabel:otherLabel:)` or `myVar` - /// - snapshot: A `DocumentSnapshot` containing the contents of the file for which to compute the rename ranges. - private func getSyntacticRenameRanges( - renameLocations: [RenameLocation], - oldName: String, - in snapshot: DocumentSnapshot - ) async throws -> [SyntacticRenameName] { - let locations = sourcekitd.array( - renameLocations.map { renameLocation in - let location = sourcekitd.dictionary([ - keys.line: renameLocation.line, - keys.column: renameLocation.utf8Column, - keys.nameType: renameLocation.usage.uid(values: values), - ]) - return sourcekitd.dictionary([ - keys.locations: [location], - keys.name: oldName, - ]) - } - ) - - let skreq = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.pseudoPath, - // find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot. - // We need to send the source text again. - keys.sourceText: snapshot.text, - keys.renameLocations: locations, - ]) - - let syntacticRenameRangesResponse = try await send(sourcekitdRequest: \.findRenameRanges, skreq, snapshot: snapshot) - guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else { - throw ResponseError.internalError("sourcekitd did not return categorized ranges") - } - - return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) } - } - - /// If `position` is on an argument label or a parameter name, find the range from the function's base name to the - /// token that terminates the arguments or parameters of the function. Typically, this is the closing ')' but it can - /// also be a closing ']' for subscripts or the end of a trailing closure. - private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range? { - let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - guard let token = tree.token(at: snapshot.absolutePosition(of: position)) else { - return nil - } - - // The node that contains the function's base name. This might be an expression like `self.doStuff`. - // The start position of the last token in this node will be used as the base name position. - var startToken: TokenSyntax? = nil - var endToken: TokenSyntax? = nil - - switch token.keyPathInParent { - case \LabeledExprSyntax.label: - let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent - switch callLike?.as(SyntaxEnum.self) { - case .attribute(let attribute): - startToken = attribute.attributeName.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - endToken = attribute.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - case .functionCallExpr(let functionCall): - startToken = functionCall.calledExpression.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - endToken = functionCall.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - case .macroExpansionDecl(let macroExpansionDecl): - startToken = macroExpansionDecl.macroName - endToken = macroExpansionDecl.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - case .macroExpansionExpr(let macroExpansionExpr): - startToken = macroExpansionExpr.macroName - endToken = macroExpansionExpr.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - case .subscriptCallExpr(let subscriptCall): - startToken = subscriptCall.leftSquare - endToken = subscriptCall.lastToken(viewMode: SyntaxTreeViewMode.sourceAccurate) - default: - break - } - case \FunctionParameterSyntax.firstName: - let parameterClause = - token - .parent(as: FunctionParameterSyntax.self)? - .parent(as: FunctionParameterListSyntax.self)? - .parent(as: FunctionParameterClauseSyntax.self) - if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) { - switch functionSignature.parent?.as(SyntaxEnum.self) { - case .functionDecl(let functionDecl): - startToken = functionDecl.name - endToken = functionSignature.parameterClause.rightParen - case .initializerDecl(let initializerDecl): - startToken = initializerDecl.initKeyword - endToken = functionSignature.parameterClause.rightParen - case .macroDecl(let macroDecl): - startToken = macroDecl.name - endToken = functionSignature.parameterClause.rightParen - default: - break - } - } else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) { - startToken = subscriptDecl.subscriptKeyword - endToken = subscriptDecl.parameterClause.rightParen - } - case \DeclNameArgumentSyntax.name: - let declReference = - token - .parent(as: DeclNameArgumentSyntax.self)? - .parent(as: DeclNameArgumentListSyntax.self)? - .parent(as: DeclNameArgumentsSyntax.self)? - .parent(as: DeclReferenceExprSyntax.self) - startToken = declReference?.baseName - endToken = declReference?.argumentNames?.rightParen - default: - break - } - - if let startToken, let endToken { - return snapshot.absolutePositionRange( - of: startToken.positionAfterSkippingLeadingTrivia.. (position: Position?, usr: String?, functionLikeRange: Range?) { - let startOfIdentifierPosition = await adjustPositionToStartOfIdentifier(position, in: snapshot) - let symbolInfo = try? await self.symbolInfo( - SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: startOfIdentifierPosition) - ) - - guard let functionLikeRange = await findFunctionLikeRange(of: startOfIdentifierPosition, in: snapshot) else { - return (startOfIdentifierPosition, symbolInfo?.only?.usr, nil) - } - if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor { - // We have a rename like `MyStruct(x: 1)`, invoked from `x`. - if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri { - // If the initializer is declared within the same file, we can perform rename in the current file based on - // the declaration's location. - return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr, functionLikeRange) - } - // Otherwise, we don't have a reference to the base name of the initializer and we can't use related - // identifiers to perform the rename. - // Return `nil` for the position to perform a pure index-based rename. - return (nil, onlySymbol.usr, functionLikeRange) - } - // Adjust the symbol info to the symbol info of the base name. - // This ensures that we get the symbol info of the function's base instead of the parameter. - let baseNameSymbolInfo = try? await self.symbolInfo( - SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: functionLikeRange.lowerBound) - ) - return (functionLikeRange.lowerBound, baseNameSymbolInfo?.only?.usr, functionLikeRange) - } - - package func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) { - let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) - - let (renamePosition, usr, _) = await symbolToRename(at: request.position, in: snapshot) - guard let renamePosition else { - return (edits: WorkspaceEdit(), usr: usr) - } - - let relatedIdentifiersResponse = try await self.relatedIdentifiers( - at: renamePosition, - in: snapshot, - includeNonEditableBaseNames: true - ) - guard let oldNameString = relatedIdentifiersResponse.name else { - throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") - } - - let renameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot) - - try Task.checkCancellation() - - let oldName = CrossLanguageName(clangName: nil, swiftName: oldNameString, definitionLanguage: .swift) - let newName = CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift) - var edits = try await editsToRename( - locations: renameLocations, - in: snapshot, - oldName: oldName, - newName: newName - ) - if let compoundSwiftName = oldName.compoundSwiftName, !compoundSwiftName.parameters.isEmpty { - // If we are doing a function rename, run `renameParametersInFunctionBody` for every occurrence of the rename - // location within the current file. If the location is not a function declaration, it will exit early without - // invoking sourcekitd, so it's OK to do this performance-wise. - for renameLocation in renameLocations { - edits += await editsToRenameParametersInFunctionBody( - snapshot: snapshot, - renameLocation: renameLocation, - newName: newName - ) - } - } - edits = edits.filter { !$0.isNoOp(in: snapshot) } - - if edits.isEmpty { - return (edits: WorkspaceEdit(changes: [:]), usr: usr) - } - return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr) - } - - package func editsToRenameParametersInFunctionBody( - snapshot: DocumentSnapshot, - renameLocation: RenameLocation, - newName: CrossLanguageName - ) async -> [TextEdit] { - let position = snapshot.absolutePosition(of: renameLocation) - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - let token = syntaxTree.token(at: position) - let parameterClause: FunctionParameterClauseSyntax? - switch token?.keyPathInParent { - case \FunctionDeclSyntax.name: - parameterClause = token?.parent(as: FunctionDeclSyntax.self)?.signature.parameterClause - case \InitializerDeclSyntax.initKeyword: - parameterClause = token?.parent(as: InitializerDeclSyntax.self)?.signature.parameterClause - case \SubscriptDeclSyntax.subscriptKeyword: - parameterClause = token?.parent(as: SubscriptDeclSyntax.self)?.parameterClause - default: - parameterClause = nil - } - guard let parameterClause else { - // We are not at a function-like definition. Nothing to rename. - return [] - } - guard let newSwiftNameString = newName.swiftName else { - logger.fault( - "Cannot rename at \(renameLocation.line):\(renameLocation.utf8Column) because new name is not a Swift name" - ) - return [] - } - let newSwiftName = CompoundDeclName(newSwiftNameString) - - var edits: [TextEdit] = [] - for (index, parameter) in parameterClause.parameters.enumerated() { - guard parameter.secondName == nil else { - // The parameter has a second name. The function signature only renames the first name and the function body - // refers to the second name. Nothing to do. - continue - } - let oldParameterName = parameter.firstName.text - guard index < newSwiftName.parameters.count else { - // We don't have a new name for this parameter. Nothing to do. - continue - } - let newParameterName = newSwiftName.parameters[index].stringOrEmpty - guard !newParameterName.isEmpty else { - // We are changing the parameter to an empty name. This will retain the current external parameter name as the - // new second name, so nothing to do in the function body. - continue - } - guard newParameterName != oldParameterName else { - // This parameter wasn't modified. Nothing to do. - continue - } - - let oldCrossLanguageParameterName = CrossLanguageName( - clangName: nil, - swiftName: oldParameterName, - definitionLanguage: .swift - ) - let newCrossLanguageParameterName = CrossLanguageName( - clangName: nil, - swiftName: newParameterName, - definitionLanguage: .swift - ) - - let parameterRenameEdits = await orLog("Renaming parameter") { - let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) - // Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename. - // We know that the parameters are variables and thus there can't be overloads that need to be resolved by the - // type checker. - let relatedIdentifiers = try await self.relatedIdentifiers( - at: parameterPosition, - in: snapshot, - includeNonEditableBaseNames: false - ) - - // Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function - // declaration. - let filteredRelatedIdentifiers = RelatedIdentifiersResponse( - relatedIdentifiers: relatedIdentifiers.relatedIdentifiers.filter { !$0.range.contains(parameterPosition) }, - name: relatedIdentifiers.name - ) - - let parameterRenameLocations = filteredRelatedIdentifiers.renameLocations(in: snapshot) - - return try await editsToRename( - locations: parameterRenameLocations, - in: snapshot, - oldName: oldCrossLanguageParameterName, - newName: newCrossLanguageParameterName - ) - } - guard let parameterRenameEdits else { - continue - } - edits += parameterRenameEdits - } - return edits - } - - /// Return the edit that needs to be performed for the given syntactic rename piece to rename it from - /// `oldParameter` to `newParameter`. - /// Returns `nil` if no edit needs to be performed. - private func textEdit( - for piece: SyntacticRenamePiece, - in snapshot: DocumentSnapshot, - oldParameter: CompoundDeclName.Parameter, - newParameter: CompoundDeclName.Parameter - ) -> TextEdit? { - switch piece.kind { - case .parameterName: - if newParameter == .wildcard, piece.range.isEmpty, case .named(let oldParameterName) = oldParameter { - // We are changing a named parameter to an unnamed one. If the parameter didn't have an internal parameter - // name, we need to transfer the previously external parameter name to be the internal one. - // E.g. `func foo(a: Int)` becomes `func foo(_ a: Int)`. - return TextEdit(range: piece.range, newText: " " + oldParameterName) - } - if case .named(let newParameterLabel) = newParameter, - newParameterLabel.trimmingCharacters(in: .whitespaces) - == snapshot.lineTable[piece.range].trimmingCharacters(in: .whitespaces) - { - // We are changing the external parameter name to be the same one as the internal parameter name. The - // internal name is thus no longer needed. Drop it. - // Eg. an old declaration `func foo(_ a: Int)` becomes `func foo(a: Int)` when renaming the parameter to `a` - return TextEdit(range: piece.range, newText: "") - } - // In all other cases, don't touch the internal parameter name. It's not part of the public API. - return nil - case .noncollapsibleParameterName: - // Noncollapsible parameter names should never be renamed because they are the same as `parameterName` but - // never fall into one of the two categories above. - return nil - case .declArgumentLabel: - if piece.range.isEmpty { - // If we are inserting a new external argument label where there wasn't one before, add a space after it to - // separate it from the internal name. - // E.g. `subscript(a: Int)` becomes `subscript(a a: Int)`. - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard + " ") - } - // Otherwise, just update the name. - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) - case .callArgumentLabel: - // Argument labels of calls are just updated. - return TextEdit(range: piece.range, newText: newParameter.stringOrEmpty) - case .callArgumentColon: - if case .wildcard = newParameter { - // If the parameter becomes unnamed, remove the colon after the argument name. - return TextEdit(range: piece.range, newText: "") - } - return nil - case .callArgumentCombined: - if case .named(let newParameterName) = newParameter { - // If an unnamed parameter becomes named, insert the new name and a colon. - return TextEdit(range: piece.range, newText: newParameterName + ": ") - } - return nil - case .selectorArgumentLabel: - return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard) - case .baseName, .keywordBaseName: - preconditionFailure("Handled above") - } - } - - package func editsToRename( - locations renameLocations: [RenameLocation], - in snapshot: DocumentSnapshot, - oldName oldCrossLanguageName: CrossLanguageName, - newName newCrossLanguageName: CrossLanguageName - ) async throws -> [TextEdit] { - guard - let oldNameString = oldCrossLanguageName.swiftName, - let oldName = oldCrossLanguageName.compoundSwiftName, - let newName = newCrossLanguageName.compoundSwiftName - else { - throw ResponseError.unknown( - "Failed to rename \(snapshot.uri.forLogging) because the Swift name for rename is unknown" - ) - } - - let tree = await syntaxTreeManager.syntaxTree(for: snapshot) - - let compoundRenameRanges = try await getSyntacticRenameRanges( - renameLocations: renameLocations, - oldName: oldNameString, - in: snapshot - ) - - try Task.checkCancellation() - - return compoundRenameRanges.flatMap { (compoundRenameRange) -> [TextEdit] in - switch compoundRenameRange.category { - case .unmatched, .mismatch: - // The location didn't match. Don't rename it - return [] - case .activeCode, .inactiveCode, .selector: - // Occurrences in active code and selectors should always be renamed. - // Inactive code is currently never returned by sourcekitd. - break - case .string, .comment: - // We currently never get any results in strings or comments because the related identifiers request doesn't - // provide any locations inside strings or comments. We would need to have a textual index to find these - // locations. - return [] - } - return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in - if piece.kind == .baseName { - if let firstNameToken = tree.token(at: snapshot.absolutePosition(of: piece.range.lowerBound)), - firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName, - let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self), - parameterSyntax.secondName == nil // Should always be true because otherwise decl would be second name - { - // We are renaming a function parameter from inside the function body. - // This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new - // name as a second name. - return TextEdit( - range: Range(snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia)), - newText: " " + newName.baseName - ) - } - - return TextEdit(range: piece.range, newText: newName.baseName) - } else if piece.kind == .keywordBaseName { - // Keyword base names can't be renamed - return nil - } - - guard let parameterIndex = piece.parameterIndex, - parameterIndex < newName.parameters.count, - parameterIndex < oldName.parameters.count - else { - // Be lenient and just keep the old parameter names if the new name doesn't specify them, eg. if we are - // renaming `func foo(a: Int, b: Int)` and the user specified `bar(x:)` as the new name. - return nil - } - - return self.textEdit( - for: piece, - in: snapshot, - oldParameter: oldName.parameters[parameterIndex], - newParameter: newName.parameters[parameterIndex] - ) - } - } - } - - package func prepareRename( - _ request: PrepareRenameRequest - ) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? { - let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri) - - let (renamePosition, usr, functionLikeRange) = await symbolToRename(at: request.position, in: snapshot) - guard let renamePosition else { - return nil - } - - let response = try await self.relatedIdentifiers( - at: renamePosition, - in: snapshot, - includeNonEditableBaseNames: true - ) - guard var name = response.name else { - throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename") - } - if name.hasSuffix("()") { - name = String(name.dropLast(2)) - } - guard let relatedIdentRange = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range - else { - return nil - } - return (PrepareRenameResponse(range: functionLikeRange ?? relatedIdentRange, placeholder: name), usr) - } -} diff --git a/Sources/SwiftLanguageService/RewriteSourceKitPlaceholders.swift b/Sources/SwiftLanguageService/RewriteSourceKitPlaceholders.swift deleted file mode 100644 index 4c7444dfc..000000000 --- a/Sources/SwiftLanguageService/RewriteSourceKitPlaceholders.swift +++ /dev/null @@ -1,179 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -import SKLogging -@_spi(RawSyntax) import SwiftSyntax - -/// Translate SourceKit placeholder syntax — `<#foo#>` — in `input` to LSP -/// placeholder syntax: `${n:foo}`. -/// -/// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an -/// empty string, to prevent the client from inserting special placeholder -/// characters as if they were literal text. -@_spi(Testing) -public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippets: Bool) -> String { - var result = "" - var nextPlaceholderNumber = 1 - // Current stack of nested placeholders, most nested last. Each element needs - // to be rendered inside the element before it. - var placeholders: [(number: Int, contents: String)] = [] - let tokens = tokenize(input) - for token in tokens { - switch token { - case let .text(text): - if placeholders.isEmpty { - result += text - } else { - placeholders.latest.contents += text - } - - case .escapeInsidePlaceholder(let character): - if placeholders.isEmpty { - result.append(character) - } else { - // A closing brace is only escaped _inside_ a placeholder; otherwise the client would include the backslashes - // literally. - placeholders.latest.contents += [#"\"#, character] - } - - case .placeholderOpen: - placeholders.append((number: nextPlaceholderNumber, contents: "")) - nextPlaceholderNumber += 1 - - case .placeholderClose: - guard let (number, placeholderBody) = placeholders.popLast() else { - logger.fault("Invalid placeholder in \(input)") - return input - } - guard let displayName = nameForSnippet(placeholderBody) else { - logger.fault("Failed to decode placeholder \(placeholderBody) in \(input)") - return input - } - let placeholder = - clientSupportsSnippets - ? formatLSPPlaceholder(displayName, number: number) - : "" - if placeholders.isEmpty { - result += placeholder - } else { - placeholders.latest.contents += placeholder - } - } - } - - return result -} - -/// Scan `input` to identify special elements within: curly braces, which may -/// need to be escaped; and SourceKit placeholder open/close delimiters. -private func tokenize(_ input: String) -> [SnippetToken] { - var index = input.startIndex - var isAtEnd: Bool { index == input.endIndex } - func match(_ char: Character) -> Bool { - if isAtEnd || input[index] != char { - return false - } else { - input.formIndex(after: &index) - return true - } - } - func next() -> Character? { - guard !isAtEnd else { return nil } - defer { input.formIndex(after: &index) } - return input[index] - } - - var tokens: [SnippetToken] = [] - var text = "" - while let char = next() { - switch char { - case "<": - if match("#") { - tokens.append(.text(text)) - text.removeAll() - tokens.append(.placeholderOpen) - } else { - text.append(char) - } - - case "#": - if match(">") { - tokens.append(.text(text)) - text.removeAll() - tokens.append(.placeholderClose) - } else { - text.append(char) - } - - case "$", "}", "\\": - tokens.append(.text(text)) - text.removeAll() - tokens.append(.escapeInsidePlaceholder(char)) - - case let c: - text.append(c) - } - } - - tokens.append(.text(text)) - - return tokens -} - -/// A syntactical element inside a SourceKit snippet. -private enum SnippetToken { - /// A placeholder delimiter. - case placeholderOpen, placeholderClose - /// '$', '}' or '\', which need to be escaped when used inside a placeholder. - case escapeInsidePlaceholder(Character) - /// Any other consecutive run of characters from the input, which needs no - /// special treatment. - case text(String) -} - -/// Given the interior text of a SourceKit placeholder, extract a display name -/// suitable for a LSP snippet. -private func nameForSnippet(_ body: String) -> String? { - var text = rewrappedAsPlaceholder(body) - return text.withSyntaxText { - guard let data = RawEditorPlaceholderData(syntaxText: $0) else { - return nil - } - return String(syntaxText: data.typeForExpansionText ?? data.displayText) - } -} - -private let placeholderStart = "<#" -private let placeholderEnd = "#>" -private func rewrappedAsPlaceholder(_ body: String) -> String { - return placeholderStart + body + placeholderEnd -} - -/// Wrap `body` in LSP snippet placeholder syntax, using `number` as the -/// placeholder's index in the snippet. -private func formatLSPPlaceholder(_ body: String, number: Int) -> String { - "${\(number):\(body)}" -} - -private extension Array { - /// Mutable access to the final element of an array. - /// - /// - precondition: The array must not be empty. - var latest: Element { - get { self.last! } - _modify { - let index = self.index(before: self.endIndex) - yield &self[index] - } - } -} diff --git a/Sources/SwiftLanguageService/SemanticRefactorCommand.swift b/Sources/SwiftLanguageService/SemanticRefactorCommand.swift deleted file mode 100644 index 476f2039f..000000000 --- a/Sources/SwiftLanguageService/SemanticRefactorCommand.swift +++ /dev/null @@ -1,113 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Csourcekitd -package import LanguageServerProtocol -import SourceKitD - -package struct SemanticRefactorCommand: SwiftCommand { - typealias Response = SemanticRefactoring - - package static let identifier: String = "semantic.refactor.command" - - /// The name of this refactoring action. - package var title: String - - /// The sourcekitd identifier of the refactoring action. - package var actionString: String - - /// The range to refactor. - package var positionRange: Range - - /// The text document related to the refactoring action. - package var textDocument: TextDocumentIdentifier - - package init?(fromLSPDictionary dictionary: [String: LSPAny]) { - guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue], - case .string(let title)? = dictionary[CodingKeys.title.stringValue], - case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], - case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue] - else { - return nil - } - guard let positionRange = Range(fromLSPDictionary: rangeDict), - let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict) - else { - return nil - } - self.init( - title: title, - actionString: actionString, - positionRange: positionRange, - textDocument: textDocument - ) - } - - package init( - title: String, - actionString: String, - positionRange: Range, - textDocument: TextDocumentIdentifier - ) { - self.title = title - self.actionString = actionString - self.positionRange = positionRange - self.textDocument = textDocument - } - - package func encodeToLSPAny() -> LSPAny { - return .dictionary([ - CodingKeys.title.stringValue: .string(title), - CodingKeys.actionString.stringValue: .string(actionString), - CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(), - CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(), - ]) - } -} - -extension Array where Element == SemanticRefactorCommand { - init?( - array: SKDResponseArray?, - range: Range, - textDocument: TextDocumentIdentifier, - _ keys: sourcekitd_api_keys, - _ api: sourcekitd_api_functions_t - ) { - guard let results = array else { - return nil - } - var commands: [SemanticRefactorCommand] = [] - // swift-format-ignore: ReplaceForEachWithForLoop - // Reference is to `SKDResponseArray.forEach`, not `Array.forEach`. - results.forEach { _, value in - if let name: String = value[keys.actionName], - let actionuid: sourcekitd_api_uid_t = value[keys.actionUID], - let ptr = api.uid_get_string_ptr(actionuid) - { - let actionName = String(cString: ptr) - guard !actionName.hasPrefix("source.refactoring.kind.rename.") else { - return true - } - commands.append( - SemanticRefactorCommand( - title: name, - actionString: actionName, - positionRange: range, - textDocument: textDocument - ) - ) - } - return true - } - self = commands - } -} diff --git a/Sources/SwiftLanguageService/SemanticRefactoring.swift b/Sources/SwiftLanguageService/SemanticRefactoring.swift deleted file mode 100644 index 0b321faa1..000000000 --- a/Sources/SwiftLanguageService/SemanticRefactoring.swift +++ /dev/null @@ -1,97 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP - -/// Detailed information about the result of a specific refactoring operation. -/// -/// Wraps the information returned by sourcekitd's `semantic_refactoring` -/// request, such as the necessary edits and placeholder locations. -struct SemanticRefactoring: RefactoringResponse { - - /// The title of the refactoring action. - var title: String - - /// The resulting `WorkspaceEdit` of a `semantic_refactoring` request. - var edit: WorkspaceEdit - - init(_ title: String, _ edit: WorkspaceEdit) { - self.title = title - self.edit = edit - } - - init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit]) { - self.title = title - self.edit = WorkspaceEdit(changes: [ - uri: refactoringEdits.map { TextEdit(range: $0.range, newText: $0.newText) } - ]) - } -} - -/// An error from a semantic refactoring request. -enum SemanticRefactoringError: Error { - /// The underlying sourcekitd request failed with the given error. - case responseError(ResponseError) - - /// The underlying sourcekitd reported no edits for this action. - case noEditsNeeded(DocumentURI) -} - -extension SemanticRefactoringError: CustomStringConvertible { - var description: String { - switch self { - case .responseError(let error): - return "\(error)" - case .noEditsNeeded(let url): - return "no edits reported for semantic refactoring action for url \(url)" - } - } -} - -extension SwiftLanguageService { - - /// Handles the `SemanticRefactorCommand`. - /// - /// Sends a request to sourcekitd and wraps the result into a - /// `SemanticRefactoring` and then makes an `ApplyEditRequest` to the client - /// side for the actual refactoring. - /// - /// - Parameters: - /// - semanticRefactorCommand: The `SemanticRefactorCommand` that triggered this request. - func semanticRefactoring( - _ semanticRefactorCommand: SemanticRefactorCommand - ) async throws { - guard let sourceKitLSPServer else { - // `SourceKitLSPServer` has been destructed. We are tearing down the - // language server. Nothing left to do. - throw ResponseError.unknown("Connection to the editor closed") - } - - let semanticRefactor = try await self.refactoring(semanticRefactorCommand) - - let edit = semanticRefactor.edit - let req = ApplyEditRequest(label: semanticRefactor.title, edit: edit) - let response = try await sourceKitLSPServer.sendRequestToClient(req) - if !response.applied { - let reason: String - if let failureReason = response.failureReason { - reason = " reason: \(failureReason)" - } else { - reason = "" - } - logger.error("client refused to apply edit for \(semanticRefactor.title, privacy: .public) \(reason)") - } - } -} diff --git a/Sources/SwiftLanguageService/SemanticTokens.swift b/Sources/SwiftLanguageService/SemanticTokens.swift deleted file mode 100644 index 010d299eb..000000000 --- a/Sources/SwiftLanguageService/SemanticTokens.swift +++ /dev/null @@ -1,171 +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 https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP -import SwiftIDEUtils -import SwiftParser -import SwiftSyntax - -extension SwiftLanguageService { - /// Requests the semantic highlighting tokens for the given snapshot from sourcekitd. - private func semanticHighlightingTokens(for snapshot: DocumentSnapshot) async throws -> SyntaxHighlightingTokens? { - guard let compileCommand = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false), - !compileCommand.isFallback - else { - return nil - } - - let skreq = sourcekitd.dictionary([ - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - keys.compilerArgs: compileCommand.compilerArgs as [SKDRequestValue], - ]) - - let dict = try await send(sourcekitdRequest: \.semanticTokens, skreq, snapshot: snapshot) - - guard let skTokens: SKDResponseArray = dict[keys.semanticTokens] else { - return nil - } - - try Task.checkCancellation() - - return SyntaxHighlightingTokenParser(sourcekitd: sourcekitd).parseTokens(skTokens, in: snapshot) - } - - /// Computes an array of syntax highlighting tokens from the syntax tree that - /// have been merged with any semantic tokens from SourceKit. If the provided - /// range is non-empty, this function restricts its output to only those - /// tokens whose ranges overlap it. If no range is provided, tokens for the - /// entire document are returned. - /// - /// - Parameter range: The range of tokens to restrict this function to, if any. - /// - Returns: An array of syntax highlighting tokens. - private func mergedAndSortedTokens( - for snapshot: DocumentSnapshot, - in range: Range? = nil - ) async throws -> SyntaxHighlightingTokens { - try Task.checkCancellation() - - async let tree = syntaxTreeManager.syntaxTree(for: snapshot) - let semanticTokens = await orLog("Loading semantic tokens") { try await semanticHighlightingTokens(for: snapshot) } - - let range = - if let range = range.flatMap({ snapshot.byteSourceRange(of: $0) }) { - range - } else { - await tree.range - } - - try Task.checkCancellation() - - let tokens = - await tree - .classifications(in: range) - .map { $0.highlightingTokens(in: snapshot) } - .reduce(into: SyntaxHighlightingTokens(tokens: [])) { $0.tokens += $1.tokens } - - try Task.checkCancellation() - - return - tokens - .mergingTokens(with: semanticTokens ?? SyntaxHighlightingTokens(tokens: [])) - .sorted { $0.start < $1.start } - } - - package func documentSemanticTokens( - _ req: DocumentSemanticTokensRequest - ) async throws -> DocumentSemanticTokensResponse? { - let snapshot = try await self.latestSnapshot(for: req.textDocument.uri) - - let tokens = try await mergedAndSortedTokens(for: snapshot) - let encodedTokens = tokens.lspEncoded - - return DocumentSemanticTokensResponse(data: encodedTokens) - } - - package func documentSemanticTokensDelta( - _ req: DocumentSemanticTokensDeltaRequest - ) async throws -> DocumentSemanticTokensDeltaResponse? { - return nil - } - - package func documentSemanticTokensRange( - _ req: DocumentSemanticTokensRangeRequest - ) async throws -> DocumentSemanticTokensResponse? { - let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri) - let tokens = try await mergedAndSortedTokens(for: snapshot, in: req.range) - let encodedTokens = tokens.lspEncoded - - return DocumentSemanticTokensResponse(data: encodedTokens) - } -} - -extension SyntaxClassifiedRange { - fileprivate func highlightingTokens(in snapshot: DocumentSnapshot) -> SyntaxHighlightingTokens { - guard let (kind, modifiers) = self.kind.highlightingKindAndModifiers else { - return SyntaxHighlightingTokens(tokens: []) - } - - let multiLineRange = snapshot.absolutePositionRange(of: self.range) - let ranges = multiLineRange.splitToSingleLineRanges(in: snapshot) - - let tokens = ranges.map { - SyntaxHighlightingToken( - range: $0, - kind: kind, - modifiers: modifiers - ) - } - - return SyntaxHighlightingTokens(tokens: tokens) - } -} - -extension SyntaxClassification { - fileprivate var highlightingKindAndModifiers: (SemanticTokenTypes, SemanticTokenModifiers)? { - switch self { - case .none: - return nil - case .editorPlaceholder: - return nil - case .keyword: - return (.keyword, []) - case .identifier, .type, .dollarIdentifier: - return (.identifier, []) - case .operator: - return (.operator, []) - case .integerLiteral, .floatLiteral: - return (.number, []) - case .stringLiteral: - return (.string, []) - case .regexLiteral: - return (.regexp, []) - case .ifConfigDirective: - return (.macro, []) - case .attribute: - return (.modifier, []) - case .lineComment, .blockComment: - return (.comment, []) - case .docLineComment, .docBlockComment: - return (.comment, .documentation) - case .argumentLabel: - return (.function, []) - #if RESILIENT_LIBRARIES - @unknown default: - fatalError("Unknown case") - #endif - } - } -} diff --git a/Sources/SwiftLanguageService/SignatureHelp.swift b/Sources/SwiftLanguageService/SignatureHelp.swift deleted file mode 100644 index 1c74da66f..000000000 --- a/Sources/SwiftLanguageService/SignatureHelp.swift +++ /dev/null @@ -1,170 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation -package import LanguageServerProtocol -import SKLogging -import SourceKitD -import SourceKitLSP -import SwiftBasicFormat -import SwiftExtensions - -fileprivate extension String { - func utf16Offset(of utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Int { - guard - let stringIndex = self.utf8.index(self.startIndex, offsetBy: utf8Offset, limitedBy: self.endIndex) - else { - logger.fault( - """ - UTF-8 offset is past the end of the string while getting UTF-16 offset of \(utf8Offset) \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return self.utf16.count - } - return self.utf16.distance(from: self.startIndex, to: stringIndex) - } -} - -fileprivate extension ParameterInformation { - init?(_ parameter: SKDResponseDictionary, _ signatureLabel: String, _ keys: sourcekitd_api_keys) { - guard let nameOffset = parameter[keys.nameOffset] as Int?, - let nameLength = parameter[keys.nameLength] as Int? - else { - return nil - } - - let documentation: StringOrMarkupContent? = - if let docComment: String = parameter[keys.docComment] { - .markupContent(MarkupContent(kind: .markdown, value: docComment)) - } else { - nil - } - - let labelStart = signatureLabel.utf16Offset(of: nameOffset) - let labelEnd = signatureLabel.utf16Offset(of: nameOffset + nameLength) - - self.init( - label: .offsets(start: labelStart, end: labelEnd), - documentation: documentation - ) - } -} - -fileprivate extension SignatureInformation { - init?(_ signature: SKDResponseDictionary, _ keys: sourcekitd_api_keys) { - guard let label = signature[keys.name] as String?, - let skParameters = signature[keys.parameters] as SKDResponseArray? - else { - return nil - } - - let parameters = skParameters.compactMap { ParameterInformation($0, label, keys) } - - let activeParameter: Int? = - if let activeParam: Int = signature[keys.activeParameter] { - activeParam - } else if !parameters.isEmpty { - // If we have parameters and no active parameter is present, we return - // an out-of-range index that way editors don't show an active parameter. - // As of LSP 3.17, not returning an active parameter defaults to choosing - // the first parameter which isn't the desired behavior. - // LSP 3.17 states that out-of-range values are treated as 0 but this is - // not the case in VS Code so we rely on that as a workaround. - // LSP 3.18 defines a `noActiveParameterSupport` option which allows the - // active parameter to be `null` causing editors not to show an active - // parameter which would be the best solution here. - parameters.count - } else { - nil - } - - let documentation: StringOrMarkupContent? = - if let docComment: String = signature[keys.docComment] { - .markupContent(MarkupContent(kind: .markdown, value: docComment)) - } else { - nil - } - - self.init( - label: label, - documentation: documentation, - parameters: parameters, - activeParameter: activeParameter - ) - } - - /// Checks if two signatures are identical except for the active parameter. - /// This is used to match the active signature given a previously active signature. - func isSame(_ other: SignatureInformation) -> Bool { - self.label == other.label && self.documentation == other.documentation && self.parameters == other.parameters - } -} - -fileprivate extension SignatureHelp { - init?(_ dict: SKDResponseDictionary, _ keys: sourcekitd_api_keys) { - guard let skSignatures = dict[keys.signatures] as SKDResponseArray?, - let activeSignature = dict[keys.activeSignature] as Int? - else { - return nil - } - - let signatures = skSignatures.compactMap { SignatureInformation($0, keys) } - - guard !signatures.isEmpty else { - return nil - } - - self.init( - signatures: signatures, - activeSignature: activeSignature, - activeParameter: signatures[activeSignature].activeParameter - ) - } -} - -extension SwiftLanguageService { - package func signatureHelp(_ req: SignatureHelpRequest) async throws -> SignatureHelp? { - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - - let adjustedPosition = await adjustPositionToStartOfArgument(req.position, in: snapshot) - - let compileCommand = await compileCommand(for: snapshot.uri, fallbackAfterTimeout: false) - - let skreq = sourcekitd.dictionary([ - keys.offset: snapshot.utf8Offset(of: adjustedPosition), - keys.sourceFile: snapshot.uri.sourcekitdSourceFile, - keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, - keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?, - ]) - - let dict = try await send(sourcekitdRequest: \.signatureHelp, skreq, snapshot: snapshot) - - guard var signatureHelp = SignatureHelp(dict, keys) else { - return nil - } - - // Persist the active signature and parameter from the previous request if it exists. - guard let activeSignatureHelp = req.context?.activeSignatureHelp, - let activeSignatureIndex = activeSignatureHelp.activeSignature, - let activeSignature = activeSignatureHelp.signatures[safe: activeSignatureIndex], - let matchingSignatureIndex = signatureHelp.signatures.firstIndex(where: activeSignature.isSame) - else { - return signatureHelp - } - - signatureHelp.activeSignature = matchingSignatureIndex - signatureHelp.activeParameter = signatureHelp.signatures[matchingSignatureIndex].activeParameter - - return signatureHelp - } -} diff --git a/Sources/SwiftLanguageService/SwiftCodeLensScanner.swift b/Sources/SwiftLanguageService/SwiftCodeLensScanner.swift deleted file mode 100644 index 8c2647815..000000000 --- a/Sources/SwiftLanguageService/SwiftCodeLensScanner.swift +++ /dev/null @@ -1,101 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SourceKitLSP -import SwiftSyntax - -/// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them. -final class SwiftCodeLensScanner: SyntaxVisitor { - /// The document snapshot of the syntax tree that is being walked. - private let snapshot: DocumentSnapshot - - /// The collection of CodeLenses found in the document. - private var result: [CodeLens] = [] - - private let targetName: String? - - /// The map of supported commands and their client side command names - private let supportedCommands: [SupportedCodeLensCommand: String] - - private init( - snapshot: DocumentSnapshot, - targetName: String?, - supportedCommands: [SupportedCodeLensCommand: String] - ) { - self.snapshot = snapshot - self.targetName = targetName - self.supportedCommands = supportedCommands - super.init(viewMode: .fixedUp) - } - - /// Public entry point. Scans the syntax tree of the given snapshot for an `@main` annotation - /// and returns CodeLens's with Commands to run/debug the application. - public static func findCodeLenses( - in snapshot: DocumentSnapshot, - syntaxTreeManager: SyntaxTreeManager, - targetName: String? = nil, - supportedCommands: [SupportedCodeLensCommand: String] - ) async -> [CodeLens] { - guard snapshot.text.contains("@main") && !supportedCommands.isEmpty else { - // This is intended to filter out files that obviously do not contain an entry point. - return [] - } - - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - let visitor = SwiftCodeLensScanner(snapshot: snapshot, targetName: targetName, supportedCommands: supportedCommands) - visitor.walk(syntaxTree) - return visitor.result - } - - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - node.attributes.forEach(self.captureLensFromAttribute) - return .skipChildren - } - - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - node.attributes.forEach(self.captureLensFromAttribute) - return .skipChildren - } - - private func captureLensFromAttribute(attribute: AttributeListSyntax.Element) { - if attribute.trimmedDescription == "@main" { - let range = self.snapshot.absolutePositionRange(of: attribute.trimmedRange) - var targetNameToAppend: String = "" - var arguments: [LSPAny] = [] - if let targetName { - targetNameToAppend = " \(targetName)" - arguments.append(.string(targetName)) - } - - if let runCommand = supportedCommands[SupportedCodeLensCommand.run] { - // Return commands for running/debugging the executable. - // These command names must be recognized by the client and so should not be chosen arbitrarily. - self.result.append( - CodeLens( - range: range, - command: Command(title: "Run" + targetNameToAppend, command: runCommand, arguments: arguments) - ) - ) - } - - if let debugCommand = supportedCommands[SupportedCodeLensCommand.debug] { - self.result.append( - CodeLens( - range: range, - command: Command(title: "Debug" + targetNameToAppend, command: debugCommand, arguments: arguments) - ) - ) - } - } - } -} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift deleted file mode 100644 index 54677077f..000000000 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import LanguageServerProtocol - -/// A `Command` that should be executed by Swift's language server. -package protocol SwiftCommand: Codable, Hashable, LSPAnyCodable { - static var identifier: String { get } - var title: String { get set } -} - -extension SwiftCommand { - /// Converts this `SwiftCommand` to a generic LSP `Command` object. - package func asCommand() -> Command { - let argument = encodeToLSPAny() - return Command(title: title, command: Self.identifier, arguments: [argument]) - } -} - -extension ExecuteCommandRequest { - /// Attempts to convert the underlying `Command` metadata from this request - /// to a specific Swift language server `SwiftCommand`. - /// - /// - Parameters: - /// - type: The `SwiftCommand` metatype to convert to. - package func swiftCommand(ofType type: T.Type) -> T? { - guard type.identifier == command else { - return nil - } - guard let argument = arguments?.first else { - return nil - } - guard case let .dictionary(dictionary) = argument else { - return nil - } - return type.init(fromLSPDictionary: dictionary) - } -} - -extension SwiftLanguageService { - package static var builtInCommands: [String] { - [ - SemanticRefactorCommand.self, - ExpandMacroCommand.self, - ].map { (command: any SwiftCommand.Type) in - command.identifier - } - } -} diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift deleted file mode 100644 index d889d8335..000000000 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ /dev/null @@ -1,1321 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -package import BuildServerIntegration -import BuildServerProtocol -import Csourcekitd -import Dispatch -import Foundation -import IndexStoreDB -package import LanguageServerProtocol -import LanguageServerProtocolExtensions -import SKLogging -package import SKOptions -import SKUtilities -import SemanticIndex -package import SourceKitD -package import SourceKitLSP -import SwiftExtensions -import SwiftParser -import SwiftParserDiagnostics -package import SwiftSyntax -package import ToolchainRegistry - -#if os(Windows) -import WinSDK -#endif - -fileprivate extension Range { - /// Checks if this range overlaps with the other range, counting an overlap with an empty range as a valid overlap. - /// The standard library implementation makes `1..<3.overlaps(2..<2)` return false because the second range is empty and thus the overlap is also empty. - /// This implementation over overlap considers such an inclusion of an empty range as a valid overlap. - func overlapsIncludingEmptyRanges(other: Range) -> Bool { - switch (self.isEmpty, other.isEmpty) { - case (true, true): - return self.lowerBound == other.lowerBound - case (true, false): - return other.contains(self.lowerBound) - case (false, true): - return self.contains(other.lowerBound) - case (false, false): - return self.overlaps(other) - } - } -} - -/// Explicitly excluded `DocumentURI` schemes. -private let excludedDocumentURISchemes: [String] = [ - "git", - "hg", -] - -/// Returns true if diagnostics should be emitted for the given document. -/// -/// Some editors (like Visual Studio Code) use non-file URLs to manage source control diff bases -/// for the active document, which can lead to duplicate diagnostics in the Problems view. -/// As a workaround we explicitly exclude those URIs and don't emit diagnostics for them. -/// -/// Additionally, as of Xcode 11.4, sourcekitd does not properly handle non-file URLs when -/// the `-working-directory` argument is passed since it incorrectly applies it to the input -/// argument but not the internal primary file, leading sourcekitd to believe that the input -/// file is missing. -private func diagnosticsEnabled(for document: DocumentURI) -> Bool { - guard let scheme = document.scheme else { return true } - return !excludedDocumentURISchemes.contains(scheme) -} - -/// A swift compiler command derived from a `FileBuildSettingsChange`. -package struct SwiftCompileCommand: Sendable, Equatable, Hashable { - - /// The compiler arguments, including working directory. This is required since sourcekitd only - /// accepts the working directory via the compiler arguments. - package let compilerArgs: [String] - - /// Whether the compiler arguments are considered fallback - we withhold diagnostics for - /// fallback arguments and represent the file state differently. - package let isFallback: Bool - - package init(_ settings: FileBuildSettings) { - let baseArgs = settings.compilerArguments - // Add working directory arguments if needed. - if let workingDirectory = settings.workingDirectory, !baseArgs.contains("-working-directory") { - self.compilerArgs = baseArgs + ["-working-directory", workingDirectory] - } else { - self.compilerArgs = baseArgs - } - self.isFallback = settings.isFallback - } -} - -package actor SwiftLanguageService: LanguageService, Sendable { - /// The ``SourceKitLSPServer`` instance that created this `SwiftLanguageService`. - private(set) weak var sourceKitLSPServer: SourceKitLSPServer? - - private let sourcekitdPath: URL - - package let sourcekitd: SourceKitD - - /// Path to the swift-format executable if it exists in the toolchain. - let swiftFormat: URL? - - /// Queue on which notifications from sourcekitd are handled to ensure we are - /// handling them in-order. - let sourcekitdNotificationHandlingQueue = AsyncQueue() - - let capabilityRegistry: CapabilityRegistry - - let hooks: Hooks - - let options: SourceKitLSPOptions - - /// Directory where generated Swift interfaces will be stored. - var generatedInterfacesPath: URL { - options.generatedFilesAbsolutePath.appending(component: "GeneratedInterfaces") - } - - /// Directory where generated Macro expansions will be stored. - var generatedMacroExpansionsPath: URL { - options.generatedFilesAbsolutePath.appending(component: "GeneratedMacroExpansions") - } - - /// For each edited document, the last task that was triggered to send a `PublishDiagnosticsNotification`. - /// - /// This is used to cancel previous publish diagnostics tasks if an edit is made to a document. - /// - /// - Note: We only clear entries from the dictionary when a document is closed. The task that the document maps to - /// might have finished. This isn't an issue since the tasks do not retain `self`. - private var inFlightPublishDiagnosticsTasks: [DocumentURI: Task] = [:] - - let syntaxTreeManager = SyntaxTreeManager() - - /// The `semanticIndexManager` of the workspace this language service was created for. - private let semanticIndexManagerTask: Task - - nonisolated var keys: sourcekitd_api_keys { return sourcekitd.keys } - nonisolated var requests: sourcekitd_api_requests { return sourcekitd.requests } - nonisolated var values: sourcekitd_api_values { return sourcekitd.values } - - /// - Important: Use `setState` to change the state, which notifies the state change handlers - private var state: LanguageServerState - - private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = [] - - private let diagnosticReportManager: DiagnosticReportManager - - /// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`. - private(set) var macroExpansionManager: MacroExpansionManager! { - willSet { - // Must only be set once. - precondition(macroExpansionManager == nil) - precondition(newValue != nil) - } - } - - /// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`. - private(set) var generatedInterfaceManager: GeneratedInterfaceManager! { - willSet { - // Must only be set once. - precondition(generatedInterfaceManager == nil) - precondition(newValue != nil) - } - } - - var documentManager: DocumentManager { - get throws { - guard let sourceKitLSPServer else { - throw ResponseError.unknown("Connection to the editor closed") - } - return sourceKitLSPServer.documentManager - } - } - - /// The build settings that were used to open the given files. - /// - /// - Note: Not all documents open in `SwiftLanguageService` are necessarily in this dictionary because files where - /// `buildSettings(for:)` returns `nil` are not included. - private var buildSettingsForOpenFiles: [DocumentURI: SwiftCompileCommand] = [:] - - /// Calling `scheduleCall` on `refreshDiagnosticsAndSemanticTokensDebouncer` schedules a `DiagnosticsRefreshRequest` - /// and `WorkspaceSemanticTokensRefreshRequest` to be sent to to the client. - /// - /// We debounce these calls because the `DiagnosticsRefreshRequest` is a workspace-wide request. If we discover that - /// the client should update diagnostics for file A and then discover that it should also update diagnostics for file - /// B, we don't want to send two `DiagnosticsRefreshRequest`s. Instead, the two should be unified into a single - /// request. - private let refreshDiagnosticsAndSemanticTokensDebouncer: Debouncer - - /// Creates a language server for the given client using the sourcekitd dylib specified in `toolchain`. - /// `reopenDocuments` is a closure that will be called if sourcekitd crashes and the `SwiftLanguageService` asks its - /// parent server to reopen all of its documents. - /// Returns `nil` if `sourcekitd` couldn't be found. - package init( - sourceKitLSPServer: SourceKitLSPServer, - toolchain: Toolchain, - options: SourceKitLSPOptions, - hooks: Hooks, - workspace: Workspace - ) async throws { - guard let sourcekitd = toolchain.sourcekitd else { - throw ResponseError.unknown( - "Cannot create SwiftLanguage service because \(toolchain.identifier) does not contain sourcekitd" - ) - } - self.sourcekitdPath = sourcekitd - self.sourceKitLSPServer = sourceKitLSPServer - self.swiftFormat = toolchain.swiftFormat - let pluginPaths: PluginPaths? - if let clientPlugin = options.sourcekitdOrDefault.clientPlugin, - let servicePlugin = options.sourcekitdOrDefault.servicePlugin - { - pluginPaths = PluginPaths( - clientPlugin: URL(fileURLWithPath: clientPlugin), - servicePlugin: URL(fileURLWithPath: servicePlugin) - ) - } else if let clientPlugin = toolchain.sourceKitClientPlugin, let servicePlugin = toolchain.sourceKitServicePlugin { - pluginPaths = PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin) - } else { - logger.fault("Failed to find SourceKit plugin for toolchain at \(toolchain.path.path)") - pluginPaths = nil - } - self.sourcekitd = try await SourceKitD.getOrCreate(dylibPath: sourcekitd, pluginPaths: pluginPaths) - self.capabilityRegistry = workspace.capabilityRegistry - self.semanticIndexManagerTask = workspace.semanticIndexManagerTask - self.hooks = hooks - self.state = .connected - self.options = options - - // The debounce duration of 500ms was chosen arbitrarily without scientific research. - self.refreshDiagnosticsAndSemanticTokensDebouncer = Debouncer(debounceDuration: .milliseconds(500)) { - [weak sourceKitLSPServer] in - guard let sourceKitLSPServer else { - logger.fault( - "Not sending diagnostic and semantic token refresh request to client because sourceKitLSPServer has been deallocated" - ) - return - } - let clientCapabilities = await sourceKitLSPServer.capabilityRegistry?.clientCapabilities - if clientCapabilities?.workspace?.diagnostics?.refreshSupport ?? false { - _ = await orLog("Sending DiagnosticRefreshRequest to client after document dependencies updated") { - try await sourceKitLSPServer.sendRequestToClient(DiagnosticsRefreshRequest()) - } - } else { - logger.debug("Not sending DiagnosticRefreshRequest because the client doesn't support it") - } - - if clientCapabilities?.workspace?.semanticTokens?.refreshSupport ?? false { - _ = await orLog("Sending WorkspaceSemanticTokensRefreshRequest to client after document dependencies updated") { - try await sourceKitLSPServer.sendRequestToClient(WorkspaceSemanticTokensRefreshRequest()) - } - } else { - logger.debug("Not sending WorkspaceSemanticTokensRefreshRequest because the client doesn't support it") - } - } - - self.diagnosticReportManager = DiagnosticReportManager( - sourcekitd: self.sourcekitd, - options: options, - syntaxTreeManager: syntaxTreeManager, - documentManager: sourceKitLSPServer.documentManager, - clientHasDiagnosticsCodeDescriptionSupport: await capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport - ) - - self.macroExpansionManager = MacroExpansionManager(swiftLanguageService: self) - self.generatedInterfaceManager = GeneratedInterfaceManager(swiftLanguageService: self) - - // Create sub-directories for each type of generated file - try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: generatedMacroExpansionsPath, withIntermediateDirectories: true) - } - - /// - Important: For testing only - package func setReusedNodeCallback(_ callback: (@Sendable (_ node: Syntax) -> Void)?) async { - await self.syntaxTreeManager.setReusedNodeCallback(callback) - } - - /// Returns the latest snapshot of the given URI, generating the snapshot in case the URI is a reference document. - func latestSnapshot(for uri: DocumentURI) async throws -> DocumentSnapshot { - switch try? ReferenceDocumentURL(from: uri) { - case .macroExpansion(let data): - let content = try await self.macroExpansionManager.macroExpansion(for: data) - return DocumentSnapshot(uri: uri, language: .swift, version: 0, lineTable: LineTable(content)) - case .generatedInterface(let data): - return try await self.generatedInterfaceManager.snapshot(of: data) - case nil: - return try documentManager.latestSnapshot(uri) - } - } - - func compileCommand(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? { - guard let sourceKitLSPServer else { - logger.fault("Cannot retrieve build settings because SourceKitLSPServer is no longer alive") - return nil - } - guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: document.buildSettingsFile) else { - return nil - } - let settings = await workspace.buildServerManager.buildSettingsInferredFromMainFile( - for: document.buildSettingsFile, - language: .swift, - fallbackAfterTimeout: fallbackAfterTimeout - ) - - guard let settings else { - return nil - } - return SwiftCompileCommand(settings) - } - - func send( - sourcekitdRequest requestUid: KeyPath & Sendable, - _ request: SKDRequestDictionary, - snapshot: DocumentSnapshot? - ) async throws -> SKDResponseDictionary { - try await sourcekitd.send( - requestUid, - request, - timeout: options.sourcekitdRequestTimeoutOrDefault, - restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot?.uri.arbitrarySchemeURL, - fileContents: snapshot?.text - ) - } - - package nonisolated func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool { - return self.sourcekitdPath == toolchain.sourcekitd - } - - private func setState(_ newState: LanguageServerState) async { - let oldState = state - state = newState - for handler in stateChangeHandlers { - handler(oldState, newState) - } - - guard let sourceKitLSPServer else { - return - } - switch (oldState, newState) { - case (.connected, .connectionInterrupted), (.connected, .semanticFunctionalityDisabled): - await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.start() - case (.connectionInterrupted, .connected), (.semanticFunctionalityDisabled, .connected): - await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.end() - // We can provide diagnostics again now. Send a diagnostic refresh request to prompt the editor to reload - // diagnostics. - await refreshDiagnosticsAndSemanticTokensDebouncer.scheduleCall() - case (.connected, .connected), - (.connectionInterrupted, .connectionInterrupted), - (.connectionInterrupted, .semanticFunctionalityDisabled), - (.semanticFunctionalityDisabled, .connectionInterrupted), - (.semanticFunctionalityDisabled, .semanticFunctionalityDisabled): - break - } - } - - package func addStateChangeHandler( - handler: @Sendable @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void - ) { - self.stateChangeHandlers.append(handler) - } -} - -extension SwiftLanguageService { - - package func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult { - await sourcekitd.addNotificationHandler(self) - - return InitializeResult( - capabilities: ServerCapabilities( - textDocumentSync: .options( - TextDocumentSyncOptions( - openClose: true, - change: .incremental - ) - ), - hoverProvider: .bool(true), - completionProvider: CompletionOptions( - resolveProvider: true, - triggerCharacters: [".", "("] - ), - signatureHelpProvider: SignatureHelpOptions( - triggerCharacters: ["(", "["], - retriggerCharacters: [",", ":"] - ), - definitionProvider: nil, - implementationProvider: .bool(true), - referencesProvider: nil, - documentHighlightProvider: .bool(true), - documentSymbolProvider: .bool(true), - codeActionProvider: .value( - CodeActionServerCapabilities( - clientCapabilities: initialize.capabilities.textDocument?.codeAction, - codeActionOptions: CodeActionOptions(codeActionKinds: [.quickFix, .refactor]), - supportsCodeActions: true - ) - ), - codeLensProvider: CodeLensOptions(), - colorProvider: .bool(true), - foldingRangeProvider: .bool(true), - executeCommandProvider: ExecuteCommandOptions( - commands: Self.builtInCommands - ), - semanticTokensProvider: SemanticTokensOptions( - legend: SemanticTokensLegend.sourceKitLSPLegend, - range: .bool(true), - full: .bool(true) - ), - inlayHintProvider: .value(InlayHintOptions(resolveProvider: false)), - diagnosticProvider: DiagnosticOptions( - interFileDependencies: true, - workspaceDiagnostics: false - ) - ) - ) - } - - package func shutdown() async { - await self.sourcekitd.removeNotificationHandler(self) - } - - package func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? { - guard let snapshot = try? documentManager.latestSnapshot(uri) else { - return nil - } - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - let decl = syntaxTree.token(at: snapshot.absolutePosition(of: position))?.findParentOfSelf( - ofType: DeclSyntax.self, - stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } - ) - guard let decl else { - return nil - } - return snapshot.position(of: decl.positionAfterSkippingLeadingTrivia) - } - - /// Tell sourcekitd to crash itself. For testing purposes only. - package func crash() async { - _ = try? await send(sourcekitdRequest: \.crashWithExit, sourcekitd.dictionary([:]), snapshot: nil) - } - - // MARK: - Build Server Integration - - package func reopenDocument(_ notification: ReopenTextDocumentNotification) async { - switch try? ReferenceDocumentURL(from: notification.textDocument.uri) { - case .macroExpansion, .generatedInterface: - // Macro expansions and generated interfaces don't have document dependencies or build settings associated with - // their URI. We should thus not not receive any `ReopenDocument` notifications for them. - logger.fault("Unexpectedly received reopen document notification for reference document") - case nil: - let snapshot = orLog("Getting snapshot to re-open document") { - try documentManager.latestSnapshot(notification.textDocument.uri) - } - guard let snapshot else { - return - } - cancelInFlightPublishDiagnosticsTask(for: snapshot.uri) - await diagnosticReportManager.removeItemsFromCache(with: snapshot.uri) - - let closeReq = closeDocumentSourcekitdRequest(uri: snapshot.uri) - _ = await orLog("Closing document to re-open it") { - try await self.send(sourcekitdRequest: \.editorClose, closeReq, snapshot: nil) - } - - let buildSettings = await compileCommand(for: snapshot.uri, fallbackAfterTimeout: true) - let openReq = openDocumentSourcekitdRequest( - snapshot: snapshot, - compileCommand: buildSettings - ) - self.buildSettingsForOpenFiles[snapshot.uri] = buildSettings - _ = await orLog("Re-opening document") { - try await self.send(sourcekitdRequest: \.editorOpen, openReq, snapshot: snapshot) - } - - if await capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) { - await self.refreshDiagnosticsAndSemanticTokensDebouncer.scheduleCall() - } else { - await publishDiagnosticsIfNeeded(for: snapshot.uri) - } - } - } - - package func documentUpdatedBuildSettings(_ uri: DocumentURI) async { - guard (try? documentManager.openDocuments.contains(uri)) ?? false else { - return - } - let newBuildSettings = await self.compileCommand(for: uri, fallbackAfterTimeout: false) - if newBuildSettings != buildSettingsForOpenFiles[uri] { - // Close and re-open the document internally to inform sourcekitd to update the compile command. At the moment - // there's no better way to do this. - // Schedule the document re-open in the SourceKit-LSP server. This ensures that the re-open happens exclusively with - // no other request running at the same time. - sourceKitLSPServer?.handle(ReopenTextDocumentNotification(textDocument: TextDocumentIdentifier(uri))) - } - } - - package func documentDependenciesUpdated(_ uris: Set) async { - let uris = uris.filter { (try? documentManager.openDocuments.contains($0)) ?? false } - guard !uris.isEmpty else { - return - } - - await orLog("Sending dependencyUpdated request to sourcekitd") { - _ = try await self.send(sourcekitdRequest: \.dependencyUpdated, sourcekitd.dictionary([:]), snapshot: nil) - } - // Even after sending the `dependencyUpdated` request to sourcekitd, the code completion session has state from - // before the AST update. Close it and open a new code completion session on the next completion request. - CodeCompletionSession.close(sourcekitd: sourcekitd, uris: uris) - - for uri in uris { - await macroExpansionManager.purge(primaryFile: uri) - sourceKitLSPServer?.handle(ReopenTextDocumentNotification(textDocument: TextDocumentIdentifier(uri))) - } - } - - // MARK: - Text synchronization - - func openDocumentSourcekitdRequest( - snapshot: DocumentSnapshot, - compileCommand: SwiftCompileCommand? - ) -> SKDRequestDictionary { - return sourcekitd.dictionary([ - keys.name: snapshot.uri.pseudoPath, - keys.sourceText: snapshot.text, - keys.enableSyntaxMap: 0, - keys.enableStructure: 0, - keys.enableDiagnostics: 0, - keys.syntacticOnly: 1, - keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?, - ]) - } - - func closeDocumentSourcekitdRequest(uri: DocumentURI) -> SKDRequestDictionary { - return sourcekitd.dictionary([ - keys.name: uri.pseudoPath, - keys.cancelBuilds: 0, - ]) - } - - package func openDocument(_ notification: DidOpenTextDocumentNotification, snapshot: DocumentSnapshot) async { - switch try? ReferenceDocumentURL(from: notification.textDocument.uri) { - case .macroExpansion: - break - case .generatedInterface(let data): - await orLog("Opening generated interface") { - try await generatedInterfaceManager.open(document: data) - } - case nil: - cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri) - await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri) - - let buildSettings = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true) - buildSettingsForOpenFiles[snapshot.uri] = buildSettings - - let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings) - await orLog("Opening sourcekitd document") { - _ = try await self.send(sourcekitdRequest: \.editorOpen, req, snapshot: snapshot) - } - await publishDiagnosticsIfNeeded(for: notification.textDocument.uri) - } - } - - package func closeDocument(_ notification: DidCloseTextDocumentNotification) async { - cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri) - inFlightPublishDiagnosticsTasks[notification.textDocument.uri] = nil - await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri) - buildSettingsForOpenFiles[notification.textDocument.uri] = nil - await syntaxTreeManager.clearSyntaxTrees(for: notification.textDocument.uri) - switch try? ReferenceDocumentURL(from: notification.textDocument.uri) { - case .macroExpansion: - break - case .generatedInterface(let data): - await generatedInterfaceManager.close(document: data) - case nil: - let req = closeDocumentSourcekitdRequest(uri: notification.textDocument.uri) - await orLog("Closing sourcekitd document") { - _ = try await self.send(sourcekitdRequest: \.editorClose, req, snapshot: nil) - } - } - } - - package func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws { - _ = try await send( - sourcekitdRequest: \.editorOpen, - self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: SwiftCompileCommand(buildSettings)), - snapshot: snapshot - ) - } - - package func closeOnDiskDocument(uri: DocumentURI) async throws { - _ = try await send( - sourcekitdRequest: \.editorClose, - self.closeDocumentSourcekitdRequest(uri: uri), - snapshot: nil - ) - } - - /// Cancels any in-flight tasks to send a `PublishedDiagnosticsNotification` after edits. - private func cancelInFlightPublishDiagnosticsTask(for document: DocumentURI) { - if let inFlightTask = inFlightPublishDiagnosticsTasks[document] { - inFlightTask.cancel() - } - } - - /// If the client doesn't support pull diagnostics, compute diagnostics for the latest version of the given document - /// and send a `PublishDiagnosticsNotification` to the client for it. - private func publishDiagnosticsIfNeeded(for document: DocumentURI) async { - await withLoggingScope("publish-diagnostics") { - await publishDiagnosticsIfNeededImpl(for: document) - } - } - - private func publishDiagnosticsIfNeededImpl(for document: DocumentURI) async { - guard await !capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) else { - return - } - guard diagnosticsEnabled(for: document) else { - return - } - cancelInFlightPublishDiagnosticsTask(for: document) - inFlightPublishDiagnosticsTasks[document] = Task(priority: .medium) { [weak self] in - guard let self, let sourceKitLSPServer = await self.sourceKitLSPServer else { - logger.fault("Cannot produce PublishDiagnosticsNotification because sourceKitLSPServer was deallocated") - return - } - do { - // Sleep for a little bit until triggering the diagnostic generation. This effectively de-bounces diagnostic - // generation since any later edit will cancel the previous in-flight task, which will thus never go on to send - // the `DocumentDiagnosticsRequest`. - try await Task.sleep(for: sourceKitLSPServer.options.swiftPublishDiagnosticsDebounceDurationOrDefault) - } catch { - return - } - do { - let snapshot = try await self.latestSnapshot(for: document) - let buildSettings = await self.compileCommand(for: document, fallbackAfterTimeout: false) - let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport( - for: snapshot, - buildSettings: buildSettings - ) - let latestSnapshotID = try? await self.latestSnapshot(for: snapshot.uri).id - if latestSnapshotID != snapshot.id { - // Check that the document wasn't modified while we were getting diagnostics. This could happen because we are - // calling `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue` and thus a concurrent edit is - // possible while we are waiting for the sourcekitd request to return a result. - logger.log( - """ - Document was modified while loading diagnostics. \ - Loaded diagnostics for \(snapshot.id.version, privacy: .public), \ - latest snapshot is \((latestSnapshotID?.version).map(String.init) ?? "", privacy: .public) - """ - ) - throw CancellationError() - } - - sourceKitLSPServer.sendNotificationToClient( - PublishDiagnosticsNotification( - uri: document, - diagnostics: diagnosticReport.items - ) - ) - } catch is CancellationError { - } catch { - logger.fault( - """ - Failed to get diagnostics - \(error.forLogging) - """ - ) - } - } - } - - package func changeDocument( - _ notification: DidChangeTextDocumentNotification, - preEditSnapshot: DocumentSnapshot, - postEditSnapshot: DocumentSnapshot, - edits: [SourceEdit] - ) async { - cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri) - - let keys = self.keys - struct Edit { - let offset: Int - let length: Int - let replacement: String - } - - for edit in edits { - let req = sourcekitd.dictionary([ - keys.name: notification.textDocument.uri.pseudoPath, - keys.enableSyntaxMap: 0, - keys.enableStructure: 0, - keys.enableDiagnostics: 0, - keys.syntacticOnly: 1, - keys.offset: edit.range.lowerBound.utf8Offset, - keys.length: edit.range.length.utf8Length, - keys.sourceText: edit.replacement, - ]) - do { - _ = try await self.send(sourcekitdRequest: \.editorReplaceText, req, snapshot: nil) - } catch { - logger.fault( - """ - Failed to replace \(edit.range.lowerBound.utf8Offset):\(edit.range.upperBound.utf8Offset) by \ - '\(edit.replacement)' in sourcekitd - """ - ) - } - } - - let concurrentEdits = ConcurrentEdits( - fromSequential: edits - ) - await syntaxTreeManager.registerEdit( - preEditSnapshot: preEditSnapshot, - postEditSnapshot: postEditSnapshot, - edits: concurrentEdits - ) - - await publishDiagnosticsIfNeeded(for: notification.textDocument.uri) - } - - // MARK: - Language features - - package func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? { - throw ResponseError.unknown("unsupported method") - } - - package func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? { - throw ResponseError.unknown("unsupported method") - } - - package func hover(_ req: HoverRequest) async throws -> HoverResponse? { - let uri = req.textDocument.uri - let position = req.position - let cursorInfoResults = try await cursorInfo(uri, position.. String? in - if let documentation = cursorInfo.documentation { - var result = "" - if let annotatedDeclaration = cursorInfo.annotatedDeclaration { - let markdownDecl = - orLog("Convert XML declaration to Markdown") { - try xmlDocumentationToMarkdown(annotatedDeclaration) - } ?? annotatedDeclaration - result += "\(markdownDecl)\n" - } - result += documentation - return result - } else if let annotated: String = cursorInfo.annotatedDeclaration { - return """ - \(orLog("Convert XML to Markdown") { try xmlDocumentationToMarkdown(annotated) } ?? annotated) - """ - } else { - return nil - } - } - - if symbolDocumentations.isEmpty { - return nil - } - - let joinedDocumentation: String - if let only = symbolDocumentations.only { - joinedDocumentation = only - } else { - let documentationsWithSpacing = symbolDocumentations.enumerated().map { index, documentation in - // Work around a bug in VS Code that displays a code block after a horizontal ruler without any spacing - // (the pixels of the code block literally touch the ruler) by adding an empty line into the code block. - // Only do this for subsequent results since only those are preceeded by a ruler. - let prefix = "```swift\n" - if index != 0 && documentation.starts(with: prefix) { - return prefix + "\n" + documentation.dropFirst(prefix.count) - } else { - return documentation - } - } - joinedDocumentation = """ - ## Multiple results - - \(documentationsWithSpacing.joined(separator: "\n\n---\n\n")) - """ - } - - var tokenRange: Range? - - if let snapshot = try? await latestSnapshot(for: uri) { - let tree = await syntaxTreeManager.syntaxTree(for: snapshot) - if let token = tree.token(at: snapshot.absolutePosition(of: position)) { - tokenRange = snapshot.absolutePositionRange(of: token.trimmedRange) - } - } - - return HoverResponse( - contents: .markupContent(MarkupContent(kind: .markdown, value: joinedDocumentation)), - range: tokenRange - ) - } - - package func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] { - let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri) - - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - - class ColorLiteralFinder: SyntaxVisitor { - let snapshot: DocumentSnapshot - var result: [ColorInformation] = [] - - init(snapshot: DocumentSnapshot) { - self.snapshot = snapshot - super.init(viewMode: .sourceAccurate) - } - - override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { - guard node.macroName.text == "colorLiteral" else { - return .visitChildren - } - func extractArgument(_ argumentName: String, from arguments: LabeledExprListSyntax) -> Double? { - for argument in arguments { - if argument.label?.text == argumentName { - if let integer = argument.expression.as(IntegerLiteralExprSyntax.self) { - return Double(integer.literal.text) - } else if let integer = argument.expression.as(FloatLiteralExprSyntax.self) { - return Double(integer.literal.text) - } - } - } - return nil - } - guard let red = extractArgument("red", from: node.arguments), - let green = extractArgument("green", from: node.arguments), - let blue = extractArgument("blue", from: node.arguments), - let alpha = extractArgument("alpha", from: node.arguments) - else { - return .skipChildren - } - - result.append( - ColorInformation( - range: snapshot.absolutePositionRange(of: node.position.. [ColorPresentation] { - let color = req.color - // Empty string as a label breaks VSCode color picker - let label = "Color Literal" - let newText = "#colorLiteral(red: \(color.red), green: \(color.green), blue: \(color.blue), alpha: \(color.alpha))" - let textEdit = TextEdit(range: req.range, newText: newText) - let presentation = ColorPresentation(label: label, textEdit: textEdit, additionalTextEdits: nil) - return [presentation] - } - - package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? { - let snapshot = try await self.latestSnapshot(for: req.textDocument.uri) - - let relatedIdentifiers = try await self.relatedIdentifiers( - at: req.position, - in: snapshot, - includeNonEditableBaseNames: false - ) - return relatedIdentifiers.relatedIdentifiers.map { - DocumentHighlight( - range: $0.range, - kind: .read // unknown - ) - } - } - - package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? { - if (try? ReferenceDocumentURL(from: req.textDocument.uri)) != nil { - // Do not show code actions in reference documents - return nil - } - let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind?)] = [ - (retrieveSyntaxCodeActions, nil), - (retrieveRefactorCodeActions, .refactor), - (retrieveQuickFixCodeActions, .quickFix), - ] - let wantedActionKinds = req.context.only - let providers: [CodeActionProvider] = providersAndKinds.compactMap { - if let wantedActionKinds, let kind = $0.1, !wantedActionKinds.contains(kind) { - return nil - } - - return $0.provider - } - let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction - let codeActions = try await retrieveCodeActions(req, providers: providers) - let response = CodeActionRequestResponse( - codeActions: codeActions, - clientCapabilities: codeActionCapabilities - ) - return response - } - - func retrieveCodeActions( - _ req: CodeActionRequest, - providers: [CodeActionProvider] - ) async throws -> [CodeAction] { - guard providers.isEmpty == false else { - return [] - } - return await providers.concurrentMap { provider in - do { - return try await provider(req) - } catch { - // Ignore any providers that failed to provide refactoring actions. - return [] - } - } - .flatMap { $0 } - } - - func retrieveSyntaxCodeActions(_ request: CodeActionRequest) async throws -> [CodeAction] { - let uri = request.textDocument.uri - let snapshot = try documentManager.latestSnapshot(uri) - - let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) - guard let scope = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request) else { - return [] - } - return await allSyntaxCodeActions.concurrentMap { provider in - return provider.codeActions(in: scope) - }.flatMap { $0 } - } - - func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] { - let additionalCursorInfoParameters: ((SKDRequestDictionary) -> Void) = { skreq in - skreq.set(self.keys.retrieveRefactorActions, to: 1) - } - - let cursorInfoResponse = try await cursorInfo( - params.textDocument.uri, - params.range, - fallbackSettingsAfterTimeout: true, - additionalParameters: additionalCursorInfoParameters - ) - - var canInlineMacro = false - - var refactorActions = cursorInfoResponse.refactorActions.compactMap { - let lspCommand = $0.asCommand() - if !canInlineMacro { - canInlineMacro = $0.actionString == "source.refactoring.kind.inline.macro" - } - - return CodeAction(title: $0.title, kind: .refactor, command: lspCommand) - } - - if canInlineMacro { - let expandMacroCommand = ExpandMacroCommand(positionRange: params.range, textDocument: params.textDocument) - .asCommand() - - refactorActions.append(CodeAction(title: expandMacroCommand.title, kind: .refactor, command: expandMacroCommand)) - } - - return refactorActions - } - - func retrieveQuickFixCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] { - let snapshot = try await self.latestSnapshot(for: params.textDocument.uri) - let buildSettings = await self.compileCommand(for: params.textDocument.uri, fallbackAfterTimeout: true) - let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport( - for: snapshot, - buildSettings: buildSettings - ) - - let codeActions = diagnosticReport.items.flatMap { (diag) -> [CodeAction] in - let codeActions: [CodeAction] = - (diag.codeActions ?? []) + (diag.relatedInformation?.flatMap { $0.codeActions ?? [] } ?? []) - - if codeActions.isEmpty { - // The diagnostic doesn't have fix-its. Don't return anything. - return [] - } - - // Check if the diagnostic overlaps with the selected range. - guard params.range.overlapsIncludingEmptyRanges(other: diag.range) else { - return [] - } - - // Check if the set of diagnostics provided by the request contains this diagnostic. - // For this, only compare the 'basic' properties of the diagnostics, excluding related information and code actions since - // code actions are only defined in an LSP extension and might not be sent back to us. - guard - params.context.diagnostics.contains(where: { (contextDiag) -> Bool in - return contextDiag.range == diag.range && contextDiag.severity == diag.severity - && contextDiag.code == diag.code && contextDiag.source == diag.source && contextDiag.message == diag.message - }) - else { - return [] - } - - // Flip the attachment of diagnostic to code action instead of the code action being attached to the diagnostic - return codeActions.map({ - var codeAction = $0 - var diagnosticWithoutCodeActions = diag - diagnosticWithoutCodeActions.codeActions = nil - if let related = diagnosticWithoutCodeActions.relatedInformation { - diagnosticWithoutCodeActions.relatedInformation = related.map { - var withoutCodeActions = $0 - withoutCodeActions.codeActions = nil - return withoutCodeActions - } - } - codeAction.diagnostics = [diagnosticWithoutCodeActions] - return codeAction - }) - } - - return codeActions - } - - package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] { - let uri = req.textDocument.uri - let infos = try await variableTypeInfos(uri, req.range) - let hints = infos - .lazy - .filter { !$0.hasExplicitType } - .map { info -> InlayHint in - let position = info.range.upperBound - let label = ": \(info.printedType)" - let textEdits: [TextEdit]? - if info.canBeFollowedByTypeAnnotation { - textEdits = [TextEdit(range: position.. [CodeLens] { - let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) - var targetDisplayName: String? = nil - if let workspace = await sourceKitLSPServer?.workspaceForDocument(uri: req.textDocument.uri), - let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri), - let buildTarget = await workspace.buildServerManager.buildTarget(named: target) - { - targetDisplayName = buildTarget.displayName - } - return await SwiftCodeLensScanner.findCodeLenses( - in: snapshot, - syntaxTreeManager: self.syntaxTreeManager, - targetName: targetDisplayName, - supportedCommands: self.capabilityRegistry.supportedCodeLensCommands - ) - } - - package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport { - do { - switch try? ReferenceDocumentURL(from: req.textDocument.uri) { - case .generatedInterface: - // Generated interfaces don't have diagnostics associated with them. - return .full(RelatedFullDocumentDiagnosticReport(items: [])) - case .macroExpansion, nil: break - } - - await semanticIndexManagerTask.value?.prepareFileForEditorFunctionality( - req.textDocument.uri.buildSettingsFile - ) - let snapshot = try await self.latestSnapshot(for: req.textDocument.uri) - let buildSettings = await self.compileCommand(for: req.textDocument.uri, fallbackAfterTimeout: false) - try Task.checkCancellation() - let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport( - for: snapshot, - buildSettings: buildSettings - ) - return .full(diagnosticReport) - } catch { - // VS Code does not request diagnostics again for a document if the diagnostics request failed. - // Since sourcekit-lsp usually recovers from failures (e.g. after sourcekitd crashes), this is undesirable. - // Instead of returning an error, return empty results. - // Do forward cancellation because we don't want to clear diagnostics in the client if they cancel the diagnostic - // request. - if ResponseError(error) == .cancelled { - throw error - } - logger.error( - """ - Loading diagnostic failed with the following error. Returning empty diagnostics. - \(error.forLogging) - """ - ) - return .full(RelatedFullDocumentDiagnosticReport(items: [])) - } - } - - package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? { - throw ResponseError.unknown("unsupported method") - } - - package func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? { - if let command = req.swiftCommand(ofType: SemanticRefactorCommand.self) { - try await semanticRefactoring(command) - } else if let command = req.swiftCommand(ofType: ExpandMacroCommand.self) { - try await expandMacro(command) - } else { - throw ResponseError.unknown("unknown command \(req.command)") - } - - return nil - } - - package func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { - let referenceDocumentURL = try ReferenceDocumentURL(from: req.uri) - - switch referenceDocumentURL { - case let .macroExpansion(data): - return GetReferenceDocumentResponse( - content: try await macroExpansionManager.macroExpansion(for: data) - ) - case .generatedInterface(let data): - return GetReferenceDocumentResponse( - content: try await generatedInterfaceManager.snapshot(of: data).text - ) - } - } -} - -extension SwiftLanguageService: SKDNotificationHandler { - package nonisolated func notification(_ notification: SKDResponse) { - sourcekitdNotificationHandlingQueue.async { - await self.notificationImpl(notification) - } - } - - private func notificationImpl(_ notification: SKDResponse) async { - logger.debug( - """ - Received notification from sourcekitd - \(notification.forLogging) - """ - ) - // Check if we need to update our `state` based on the contents of the notification. - if notification.value?[self.keys.notification] == self.values.semaEnabledNotification { - await self.setState(.connected) - return - } - - if self.state == .connectionInterrupted { - // If we get a notification while we are restoring the connection, it means that the server has restarted. - // We still need to wait for semantic functionality to come back up. - await self.setState(.semanticFunctionalityDisabled) - - // Ask our parent to re-open all of our documents. - if let sourceKitLSPServer { - await sourceKitLSPServer.reopenDocuments(for: self) - } else { - logger.fault("Cannot reopen documents because SourceKitLSPServer is no longer alive") - } - } - - if notification.error == .connectionInterrupted { - await self.setState(.connectionInterrupted) - } - } -} - -// MARK: - Position conversion - -/// A line:column position as it is used in sourcekitd, using UTF-8 for the column index and using a one-based line and -/// column number. -struct SourceKitDPosition { - /// Line number within a document (one-based). - public var line: Int - - /// UTF-8 code-unit offset from the start of a line (1-based). - public var utf8Column: Int -} - -extension DocumentSnapshot { - func sourcekitdPosition( - of position: Position, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> SourceKitDPosition { - let utf8Column = lineTable.utf8ColumnAt( - line: position.line, - utf16Column: position.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - return SourceKitDPosition(line: position.line + 1, utf8Column: utf8Column + 1) - } -} - -extension sourcekitd_api_uid_t { - func isCommentKind(_ vals: sourcekitd_api_values) -> Bool { - switch self { - case vals.comment, vals.commentMarker, vals.commentURL: - return true - default: - return isDocCommentKind(vals) - } - } - - func isDocCommentKind(_ vals: sourcekitd_api_values) -> Bool { - return self == vals.docComment || self == vals.docCommentField - } - - func asCompletionItemKind(_ vals: sourcekitd_api_values) -> CompletionItemKind? { - switch self { - case vals.completionKindKeyword: - return .keyword - case vals.declModule: - return .module - case vals.declClass: - return .class - case vals.declStruct: - return .struct - case vals.declEnum: - return .enum - case vals.declEnumElement: - return .enumMember - case vals.declProtocol: - return .interface - case vals.declAssociatedType: - return .typeParameter - case vals.declTypeAlias: - return .typeParameter - case vals.declGenericTypeParam: - return .typeParameter - case vals.declConstructor: - return .constructor - case vals.declDestructor: - return .value - case vals.declSubscript: - return .method - case vals.declMethodStatic: - return .method - case vals.declMethodInstance: - return .method - case vals.declFunctionPrefixOperator, - vals.declFunctionPostfixOperator, - vals.declFunctionInfixOperator: - return .operator - case vals.declPrecedenceGroup: - return .value - case vals.declFunctionFree: - return .function - case vals.declVarStatic, vals.declVarClass: - return .property - case vals.declVarInstance: - return .property - case vals.declVarLocal, - vals.declVarGlobal, - vals.declVarParam: - return .variable - default: - return nil - } - } - - func asSymbolKind(_ vals: sourcekitd_api_values) -> SymbolKind? { - switch self { - case vals.declClass, vals.refClass, vals.declActor, vals.refActor: - return .class - case vals.declMethodInstance, vals.refMethodInstance, - vals.declMethodStatic, vals.refMethodStatic, - vals.declMethodClass, vals.refMethodClass: - return .method - case vals.declVarInstance, vals.refVarInstance, - vals.declVarStatic, vals.refVarStatic, - vals.declVarClass, vals.refVarClass: - return .property - case vals.declEnum, vals.refEnum: - return .enum - case vals.declEnumElement, vals.refEnumElement: - return .enumMember - case vals.declProtocol, vals.refProtocol: - return .interface - case vals.declFunctionFree, vals.refFunctionFree: - return .function - case vals.declVarGlobal, vals.refVarGlobal, - vals.declVarLocal, vals.refVarLocal: - return .variable - case vals.declStruct, vals.refStruct: - return .struct - case vals.declGenericTypeParam, vals.refGenericTypeParam: - return .typeParameter - case vals.declExtension: - // There are no extensions in LSP, so we return something vaguely similar - return .namespace - case vals.refModule: - return .module - case vals.declConstructor, vals.refConstructor: - return .constructor - default: - return nil - } - } -} diff --git a/Sources/SwiftLanguageService/SwiftTestingScanner.swift b/Sources/SwiftLanguageService/SwiftTestingScanner.swift deleted file mode 100644 index 67bac6124..000000000 --- a/Sources/SwiftLanguageService/SwiftTestingScanner.swift +++ /dev/null @@ -1,466 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import LanguageServerProtocol -import SKLogging -import SourceKitLSP -import SwiftParser -import SwiftSyntax - -// MARK: - Attribute parsing - -/// Get the traits applied to a testing attribute. -/// -/// - Parameters: -/// - testAttribute: The attribute to inspect. -/// -/// - Returns: An array of `ExprSyntax` instances representing the traits -/// applied to `testAttribute`. If the attribute has no traits, the empty -/// array is returned. -private func traits(ofTestAttribute testAttribute: AttributeSyntax) -> [ExprSyntax] { - guard let argument = testAttribute.arguments, case let .argumentList(argumentList) = argument else { - return [] - } - - // Skip the display name if present. - var traitArgumentsRange = argumentList.startIndex..