├── .editorconfig ├── .gitignore ├── .nvmrc ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── preview.gif ├── icons └── icon.png ├── package.json ├── src ├── clientMain.ts ├── helpers │ └── AbsolutePath.ts ├── sourcekites-server │ ├── current.ts │ ├── package.ts │ ├── packages │ │ ├── available-packages.ts │ │ ├── config-package.ts │ │ ├── debug-yaml-package.ts │ │ ├── description-package.ts │ │ ├── package-helpers.spec.ts │ │ ├── package-helpers.ts │ │ └── swift-file-package.ts │ ├── path-helpers.ts │ ├── server.ts │ ├── sourcekit-xml.ts │ ├── sourcekites.ts │ └── thenable.d.ts ├── toolchain │ └── SwiftTools.ts └── vscode │ ├── config-helpers.ts │ ├── lsp-interop.ts │ ├── lsp-preconditions.ts │ ├── output-channels.ts │ └── status-bar.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | coverage 4 | 5 | logs 6 | *.log 7 | *.vsix 8 | npm-debug.log* 9 | 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/dubnium 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | os: 3 | - linux 4 | - osx 5 | language: node_js 6 | node_js: 7 | - 12 8 | - 14 9 | before_install: 10 | - "npm i -g typescript" 11 | - "npm i -g vsce" 12 | script: 13 | - vsce package 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceRoot}/out/src/**/*.js" 17 | ], 18 | "preLaunchTask": "npm" 19 | }, 20 | { 21 | "name": "Launch Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceRoot}", 27 | "--extensionTestsPath=${workspaceRoot}/out/test" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/out/test" 33 | ], 34 | "preLaunchTask": "npm" 35 | }, 36 | { 37 | "name": "Launch SanityTest", 38 | "type": "node", 39 | "request": "launch", 40 | "program": "${workspaceRoot}/test/SanityTest.ts", 41 | "stopOnEntry": false, 42 | "args": [], 43 | "cwd": "${workspaceRoot}", 44 | "preLaunchTask": null, 45 | "runtimeExecutable": null, 46 | "runtimeArgs": [ 47 | "--nolazy" 48 | ], 49 | "env": { 50 | "NODE_ENV": "development" 51 | }, 52 | "console": "internalConsole", 53 | "sourceMaps": true, 54 | "outFiles": [ 55 | "${workspaceRoot}/out/test/**/*.js" 56 | ] 57 | }, 58 | { 59 | "name": "Launch benchmark test", 60 | "type": "node", 61 | "request": "launch", 62 | "program": "${workspaceRoot}/test/benchmarks.ts", 63 | "stopOnEntry": false, 64 | "args": [], 65 | "cwd": "${workspaceRoot}", 66 | "preLaunchTask": null, 67 | "runtimeExecutable": null, 68 | "runtimeArgs": [ 69 | "--nolazy" 70 | ], 71 | "env": { 72 | "NODE_ENV": "development" 73 | }, 74 | "console": "internalConsole", 75 | "sourceMaps": true, 76 | "outFiles": [ 77 | "${workspaceRoot}/out/test/**/*.js" 78 | ] 79 | }, 80 | { 81 | "type": "node", 82 | "request": "launch", 83 | "name": "Run Debug Server", 84 | "cwd": "${workspaceRoot}", 85 | "program": "${workspaceRoot}/src/debug/debugs.ts", 86 | "args": [ 87 | "--server=4711" 88 | ], 89 | "sourceMaps": true, 90 | "outFiles": [ 91 | "${workspaceRoot}/out/src/debug/**/*.js" 92 | ] 93 | }, 94 | { 95 | "name": "Attach to LSP server", 96 | "type": "node", 97 | "request": "attach", 98 | "port": 6004, 99 | "sourceMaps": true, 100 | "outFiles": [ 101 | "${workspaceRoot}/out/sourcekites-server/**/*.js" 102 | ] 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib", 10 | "swift.targets": [ 11 | { 12 | "name": "Watch", 13 | "path": "projects/Watch", 14 | "sources": ["Watch/*.swift"], 15 | "compilerArguments": [ 16 | "-sdk", 17 | "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk" 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | // A task runner that calls a custom npm script that compiles the extension. 9 | { 10 | "version": "2.0.0", 11 | // we want to run npm 12 | "command": "npm", 13 | // we run the custom script "compile" as defined in package.json 14 | "args": [ 15 | "run", 16 | "build", 17 | "--loglevel", 18 | "silent" 19 | ], 20 | // The tsc compiler is started in watching mode 21 | "isWatching": true, 22 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 23 | "problemMatcher": "$tsc-watch", 24 | "tasks": [ 25 | { 26 | "label": "npm", 27 | "type": "shell", 28 | "command": "npm", 29 | "args": [ 30 | "run", 31 | "build", 32 | "--loglevel", 33 | "silent" 34 | ], 35 | "isBackground": true, 36 | "problemMatcher": "$tsc-watch", 37 | "group": { 38 | "_id": "build", 39 | "isDefault": false 40 | } 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | docs/** 7 | **/*.map 8 | *.vsix 9 | .gitignore 10 | tsconfig.json 11 | vsc-extension-quickstart.md 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 2.12.3 6 | 7 | - upgrade dependencies 8 | 9 | ## 2.12.2 10 | 11 | - fix crashes in non-workspace usages 12 | 13 | ## 2.12.1 14 | 15 | [CVE-2021-28792](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-28792): Fixes vulnerability which allowed malicous workspaces to execute code when opened by providing. Now the vulnerable configs cannot be overrided in workspaces anymore: 16 | `sourcekit-lsp.serverPath`, `swift.languageServerPath`, `swift.path.sourcekite`, `swift.path.sourcekiteDockerMode`, `swift.path.swift_driver_bin`, `swift.path.shell`. Reported by [@Ry0taK](https://github.com/Ry0taK). 17 | 18 | ## 2.12.0 19 | 20 | - Better and more helpful error messages on first start 21 | - Upgraded dependencies 22 | 23 | ## 2.11.2 24 | 25 | - Running default target did not work #94 26 | 27 | ## 2.11.1 28 | 29 | - [Disable C and C++ for sourcekit-lsp](https://forums.swift.org/t/disable-sourcekit-lsp-for-c-files/30717/3) 30 | 31 | ## 2.11.0 32 | 33 | - Allow lang server restart #82 #85 by @clayreimann 34 | - Commands for stop, clean and run #82 #85 by @clayreimann 35 | - Added tables for all configurations and commands to README 36 | 37 | ## 2.10.1 38 | 39 | - Did not respect default toolchain path 40 | 41 | ## 2.10.0 42 | 43 | - Default to Xcode's sourcekit-lsp on macOS instead of sourcekite 44 | - README.md instruction improvements #70 by [@fxn](https://github.com/fxn). 45 | 46 | ## 2.9.1 47 | 48 | - Support iOS for sourcekit-lsp #64 by [@haifengkao](https://github.com/haifengkao) 49 | 50 | ## 2.9.0 51 | 52 | - `sourcekit-lsp` with different toolchains failed #63 53 | - Drop broken debugger of SDE #22 54 | 55 | If you are curious about how to set debugging up, see [Debugging Swift in VS Code](https://www.vknabel.com/pages/Debugging-Swift-in-VS-Code/). 56 | 57 | ## 2.8.3 58 | 59 | - Fixed ignored Toolchain Path [vknabel/sourcekite#9](https://github.com/vknabel/sourcekite/issues/9) 60 | 61 | ## 2.8.2 62 | 63 | - Fixed diagnostics not being shown [#58](https://github.com/vknabel/vscode-swift-development-environment/issues/58) [#57](https://github.com/vknabel/vscode-swift-development-environment/issues/57) 64 | 65 | ## 2.8.1 66 | 67 | - Fixed an issue preventing autocompletion to work reliably on Linux, fixes [#54](https://github.com/vknabel/vscode-swift-development-environment/issues/54) 68 | - Installation instructions now correctly link `/usr/lib/libsourcekitdInProc.so`, noticed by [@kennethz3](https://github.com/kennethz3) 69 | - Support quoted arguments in settings, fixed by [@haifengkao](https://github.com/haifengkao) 70 | 71 | ## 2.8.0 72 | 73 | - Now LSP-mode `sourcekite` supports `sourcekit-lsp.toolchainPath` after updating to [sourcekite@0.6.0](https://github.com/vknabel/sourcekite/releases/tag/0.6.0) 74 | 75 | ## 2.7.1 76 | 77 | - `sourcekit-lsp.serverPath` wasn't read correctly, fixed by [M1xA @AnyCPU](https://github.com/AnyCPU) 78 | 79 | ## 2.7.0 80 | 81 | - Latest sourcekite is now compatible with Swift 5. 82 | - Added options for sourcekit-lsp: `sourcekit-lsp.serverPath` and `sourcekit-lsp.toolchainPath`. [#39](https://github.com/vknabel/vscode-swift-development-environment/issues/39) 83 | - `sde.languageServerMode` now explicitly offers `sourcekit-lsp`. 84 | - Updated installation instructions with a stronger emphasize on sourcekit-lsp. 85 | 86 | _More details at [SDE 2.7.0 released](https://www.vknabel.com/pages/SDE-2-7-0-released/)_ 87 | 88 | ## 2.6.0 89 | 90 | - Add support for alternative language servers like [RLovelett/langserver-swift](https://github.com/RLovelett/langserver-swift) #21 91 | 92 | Probably Apple's recently announced language server will be supported, too. See https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024 for more infos. 93 | 94 | If you prefer RLovelett's LangserverSwift, SDE will read your old `swift.languageServerPath`-config. In order to actually use it your need to set `sde.languageServerMode` to `langserver`. 95 | 96 | ## 2.5.2 97 | 98 | - Warnings indicated a build failure 99 | 100 | ## 2.5.1 101 | 102 | - Resolve `~` to the home dir #30 103 | - Removed unuseful data from hover docs 104 | - Remove code formatter, use [vknabel/vscode-swiftformat](https://github.com/vknabel/vscode-swiftformat) instead 105 | - Installation instructions had wrong argument order for `ln -s` (thanks to [@mijo-gracanin](https://github.com/mijo-gracanin)) 106 | - Added note about installing `libcurl4-openssl-dev` (thanks to [@mijo-gracanin](https://github.com/mijo-gracanin)) 107 | 108 | ## 2.5.0 109 | 110 | - Autocompletion for SPM dependencies #27 (thanks to [@yeswolf](https://github.com/yeswolf)) 111 | - Better support for vscode workspaces 112 | - New setting `swift.targets` for supporting autocompletion if SDE can't. 113 | 114 | Especially when using Xcode projects SDE cannot infer the correct compiler arguments. Now you can fix this by explicitly supplying targets with their sources and compiler arguments. SDE will still detect other targets automatically. 115 | 116 | ```json 117 | { 118 | "swift.targets": [ 119 | { 120 | "name": "YourWatchExtension", 121 | "path": "YourProject/YourWatchExtension", 122 | "sources": ["**/*.swift"], 123 | "compilerArguments": [ 124 | "-sdk", 125 | "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk", 126 | "-target", 127 | "armv7k-apple-watchos4.0" 128 | ] 129 | } 130 | ] 131 | } 132 | ``` 133 | 134 | ## 2.4.4 135 | 136 | - Hotfix release: outdated vscode dependencies #31 (thanks to [@akdor1154](https://github.com/akdor1154)) 137 | 138 | ## 2.4.3 139 | 140 | - Hotfix release: fixes autocompletion 141 | 142 | ## 2.4.2 143 | 144 | _Broken release_ 145 | 146 | - Dummy module did always precede real ones leading to bad completion behavior 147 | 148 | ## 2.4.1 149 | 150 | - Extension did not work correctly 151 | - Can now be disabled by `"sde.enable": false` 152 | 153 | ## 2.4.0 154 | 155 | - Bumped internal dependencies to be more reliable on newer vscode versions 156 | - New setting `sde.swiftBuildingParams` allows run other commands than `swift build` #24 jinmingjian/sde#32 157 | 158 | ### Building Params 159 | 160 | It is now possible to run different commands when building swift code. 161 | 162 | - `"sde.swiftBuildingParams": ["build"]`: default setting 163 | - `"sde.swiftBuildingParams": ["build", "--build-path", ".vscode-build"]`: build in different directory, see #24 164 | - `"sde.swiftBuildingParams": ["build", "--build-tests"]`: compile tests, but do not run them 165 | - `"sde.swiftBuildingParams": ["test"]`: runs unit tests jinmingjian/sde#32 166 | 167 | ## 2.3.2 168 | 169 | - Code format did fail #19 170 | - Code format always indented by 4 spaces. Now configurable. 171 | 172 | ### Tabwidth 173 | 174 | By default `editor.tabSize` will be used. As this setting is global and affects all code, you can optionally override it using `"[swift]": { "tabSize": 2 }`. 175 | 176 | ## 2.3.1 177 | 178 | - Accidentially logged SourceKit's `key.kind` and `key.description` 179 | - Removed unused config `editor.quickSuggestions` 180 | - Will no longer write `sde.buildOnSave` or `editor.quickSuggestions` to workspace settings 181 | - `#` will now trigger completions 182 | - `-target` will now be detected for `UIKit`, `AppKit`, `WatchKit` and `Foundation` on macOS and linux #15 183 | - Index all swift files together when no `Package.swift` defined #14 184 | 185 | ## 2.3.0 186 | 187 | - Fixes autocompletion for methods and invocations leading to invalid syntax #9 188 | - Fixes a bug thats lead the extension to stop working #10 189 | - Display documentation on Hover #11 190 | 191 | ## 2.2.0 192 | 193 | - Autocompletion for external libraries like AppKit and UIKit after restart #8 194 | - Display short documentation on autocompletion 195 | - More reliable autocompletion, especially for global namespace 196 | - New `"sde.sourcekit.compilerOptions"` setting 197 | 198 | ### How do I get autocompletion for UIKit? 199 | 200 | Just add `"sde.sourcekit.compilerOptions": ["-target", "arm64-apple-ios11.0"]` to your workspace settings in Visual Studio Code and restart it. 201 | 202 | ## 2.1.3 203 | 204 | - Improved new README 205 | - Deprecated debugger, use [LLDB Debugger](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) instead 206 | 207 | An example config of using LLDB Debugger can be seen below. `program` should contain the path to your built executable as before, the `preLaunchTask` is optional, but will run `swift build` before each debug session to keep your binaries up to date. 208 | 209 | > **Note:** Currently I don't know of any reliable solution to debug your Swift tests. 210 | > If you do, please file an issue or write me an [email](mailto:dev@vknabel.com). 211 | 212 | ```js 213 | // .vscode.json/launch.json 214 | { 215 | "version": "0.2.0", 216 | "configurations": [ 217 | { 218 | "type": "lldb", 219 | "request": "launch", 220 | "name": "Run your Executable", 221 | "program": "${workspaceFolder}/.build/debug/your-executable", 222 | "args": [], 223 | "cwd": "${workspaceFolder}", 224 | "preLaunchTask": "swift-build" 225 | } 226 | } 227 | ``` 228 | 229 | ```js 230 | // .vscode.json/tasks.json 231 | { 232 | "version": "2.0.0", 233 | "tasks": [ 234 | { 235 | "label": "swift-build", 236 | "type": "shell", 237 | "command": "swift build" 238 | } 239 | } 240 | ``` 241 | 242 | ## 2.1.2 243 | 244 | ## 2.1.1 245 | 246 | - Did not work with latest vscode #2 and #3 247 | 248 | ## 2.1.0 249 | 250 | - Initial Swift 4 support jinmingjian/sde#38. 251 | 252 | ## 2.0.20170209 253 | 254 | - make a sourcekite docker image and add a new experimental setting "swift.path.sourcekiteDockerMode" for easier adoption for Linux users (issue: #26) (MacOS users do not need to update to this version in that there is no other additions in this version) 255 | 256 | ## 2.0.20170206 257 | 258 | - release 2.0 ships a Swift language server backend and a new simple, async, pipe driven language server frontend (issue: #9). This new backend solves the unicode problem of original tool sourcekit-repl. This new frontend improves the code logic and the performance which leave the room for future messaging optimization when needed as well. Futhermore, it is not needed to build whole things from Swift's sources any more. 259 | 260 | ### 2.0 Release Broadcast 261 | 262 | The `2.0` release introduces a new tool, [SourceKite](https://github.com/jinmingjian/sourcekite), as the interface to **SourceKit** library. Since the Swift `ABI` is not stable, you need to build it if you want to use SDE. Go to [SourceKite](https://github.com/jinmingjian/sourcekite) for further instructions. 263 | 264 | Also because the Swift ABI **is not stable**, you may find that the _Hover Help_ or the _Code Completion_ don't display the right information after you upgrade your Swift toolchain. This is because the SourceKit library you linked with the [SourceKite](https://github.com/jinmingjian/sourcekite) tool can't understand the sources or binaries of your project. To fix this, **rebuild your project** and **restart vscode**. 265 | 266 | #### Want to downgrade? 267 | 268 | If the release broke your current experience or if you accidentally upgraded, you can go back to the previous releases like this: 269 | 270 | 1. Download the 1.x vsix from [the release page](https://github.com/vknabel/swift-development-environment/releases) 271 | 2. Remove the installed version in your vscode 272 | 3. Install the local `.vsix` package in your vscode 273 | 274 | ## 1.0.20170129 275 | 276 | - serveral fixs for release 1.x and we want to release great new 2.0 277 | 278 | ## 1.0.20170118 279 | 280 | - experimental built-in sourcekit interface (only for macOS) 281 | 282 | ## 1.0.20170114 283 | 284 | - add an config option for shell exec path (issue: #15) 285 | 286 | ## 1.0.20170113 287 | 288 | - fix hard-coded shell exec path for macOS (issue: #14) 289 | 290 | ## 1.0.20170112 291 | 292 | - add container type info in hover (issue: #6) 293 | 294 | ## 1.0 295 | 296 | - Initial public release. 297 | 298 | You can read a [hands-on introduction](http://blog.dirac.io/2017/01/11/get_started_sde.html) for a detailed explanation. 299 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | First of all: thank you for your interest in contributing to this project and open source in general. 3 | 4 | In order to display autocompletions to users, this project actually consists of several parts: 5 | * The user-facing Visual Studio Code extension SDE. It contributes commands, settings and keyboard shortcuts. It will compile your swift code. This part is written in Typescript and is located at [src/](./src). 6 | * The SDE Language Server serves requests of your VS Code using the [Language Server Protocol](https://langserver.org/). It‘s job is to translate LSP requests to SourceKit (which powers Xcode) requests and to transform SourceKit‘s responses back to a valid LSP response. It communicates with SourceKit using Sourcekite. The language server is written in Typescript and is located at [src/server](./src/server). 7 | * The last part is Sourcekite. It’s only purpose is to link against SourceKit and to provide a command line interface. It is written in Swift and located at [vknabel/sourcekite](https://github.com/vknabel/sourcekite). 8 | 9 | There are many ways you could contribute to this project: 10 | * Opening issues: most contributions start with an issue. Bug reports and feature requests are always welcome. If you have problems using this extension, just open an issue and we can improve the documentation together. 11 | * We don’t know everything and you don’t have to in order to help others. You can read through some issues. You will probably be able to help others that way. 12 | * If you want to get your hands dirty, you could try finding a starter issue. Though most parts are written in Typescript. 13 | * Improve the ecosystem. If you want to get your hands dirty writing Swift code there is another [Language Server](https://github.com/RLovelett/langserver-swift) written in Swift! If you know Typescript, why not creating a new plugin? Ideas can be found on [vknabel/All-in-One-Swift-for-vscode](https://github.com/vknabel/All-in-One-Swift-for-vscode). Additionally [Apple announced](https://forums.swift.org/t/new-lsp-language-service-supporting-swift-and-c-family-languages-for-any-editor-and-platform/17024) to provide an official implementation in the future. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | 205 | ## Runtime Library Exception to the Apache 2.0 License: ## 206 | 207 | 208 | As an exception, if you use this Software to compile your source code and 209 | portions of this Software are embedded into the binary product as a result, 210 | you may redistribute such product without providing attribution as would 211 | otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Development Environment 2 | 3 | [![Build Status](https://travis-ci.org/vknabel/swift-development-environment.svg?branch=master)](https://travis-ci.org/vknabel/swift-development-environment) ![Visual Studio Code Version](https://img.shields.io/badge/Visual%20Studio%20Code-1.17.0-6193DF.svg) ![Swift Version](https://img.shields.io/badge/Swift-3.1.0–5-orange.svg) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) ![Platforms](https://img.shields.io/badge/Platform-Linux|macOS-lightgrey.svg) ![License Apache](https://img.shields.io/badge/License-Apache%20v2-lightgrey.svg) 4 | 5 | > **Deprecation:** this extension is no longer maintained and has been removed from the Visual Studio Code Marketplace. 6 | 7 | **SDE** adds Swift code completion and hover help to Visual Studio Code on macOS and Linux. 8 | 9 | If possible, always favor [SSWG Swift VS Code extension](https://github.com/swift-server/vscode-swift) over this one. This will only receive maintenance updates, but no new features or other improvements. 10 | 11 | > **Fork Notice:** This is the new home of SDE initially been developed by [@jinmingjian](https://github.com/jinmingjian). It is now maintained by [@vknabel](https://github.com/vknabel). [jinmingjian/sde](https://github.com/jinmingjian/sde) is no longer maintained and does only support Swift 3. This fork supports Swift 3.1, 4 and 5. 12 | 13 | ![preview](docs/preview.gif) 14 | 15 | ## Installation 16 | 17 | You have the choice between three different language server implementations. 18 | 19 | | `sde.languageServerMode` | Comments | Swift Versions | Install | 20 | | ------------------------- | ----------------------------------------- | ------------------------------ | -------------------------------------------------- | 21 | | `sourcekit-lsp` _default_ | Apple's official one. Activley developed. | 4 and 5 | [#Using sourcekit-lsp](#Using-sourcekit-lsp) | 22 | | `sourcekite` | SDE's one. Actively maintained. | 5 and older versions 3.1 and 4 | [#Using sourcekite](#Using-sourcekite) | 23 | | `langserver` | RLovelett's LSP. Not maintained. | 4.1, macOS only | [#Using Langserver Swift](#Using-Langserver-Swift) | 24 | 25 | sourcekit-lsp is easier to install and will be updated more frequently. On the other hand sourcekite treats standalone files, Xcode projects and SwiftPM modules differently and is more configurable. If you can't decide, you can install both and swap out the used LSP by setting `sde.languageServerMode` to `sourcekite`, `sourcekit-lsp` or `langserver`. 26 | 27 | ### Using sourcekit-lsp 28 | 29 | > **Note:** on macOS SDE defaults to using your Xcode's Swift and sourcekit-lsp. In that case, [SDE](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swift-development-environment) should work out of the box! 30 | 31 | 1. Install [SDE](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swift-development-environment). 32 | 2. Recent versions of Xcode ship with `sourcekit-lsp`, you can check its path running `xcrun -f sourcekit-lsp`. If not found, please [install sourcekit-lsp](https://github.com/apple/sourcekit-lsp#building-sourcekit-lsp). 33 | 3. Set `"sourcekit-lsp.serverPath": "absolute path to the sourcekit-lsp executable"` and `"sde.languageServerMode": "sourcekit-lsp"`. 34 | 35 | ### Using sourcekite 36 | 37 | 1. sourcekite does only work with [SDE](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swift-development-environment). Make sure you have it installed. 38 | 2. Install the companion project [sourcekite](https://github.com/vknabel/sourcekite). 39 | 40 | ```bash 41 | $ git clone https://github.com/vknabel/sourcekite 42 | $ cd sourcekite 43 | 44 | # For Linux 45 | # Ensure you have libcurl4-openssl-dev installed (not pre-installed) 46 | # $ apt-get update && apt-get install libcurl4-openssl-dev 47 | # Ensure LD_LIBRARY_PATH contains /your/swift/usr/lib 48 | # And have $ sudo ln -s /your/swift/usr/lib/libsourcekitdInProc.so /usr/lib/libsourcekitdInProc.so 49 | $ make install PREFIX=/usr/local 50 | 51 | # For macOS 52 | $ make install PREFIX=/usr/local 53 | ``` 54 | 55 | 3. Add the _absolute_ path to your compiled sourcekite binary `swift.path.sourcekite` to your vscode settings as `/usr/local/sourcekite`. 56 | 57 | If you experience any problems during installation, file an issue. All kind of feedback helps especially when trying to automate this. 58 | 59 | ### Using Langserver Swift 60 | 61 | Besides sourcekit-lsp and sourcekite SDE allows you to use [RLovelett/langserver-swift](https://github.com/RLovelett/langserver-swift). 62 | 63 | If you prefer using an alternative language server, set set `sde.languageServerMode` to `langserver` and let `swift.languageServerPath` point to your installed language server. 64 | 65 | Though in most cases sourcekit-lsp and sourcekite should produce better results and performance. 66 | 67 | ## Configuration 68 | 69 | | Config | Type | Default | Description | 70 | | ---------------------------------- | ---------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 71 | | `sourcekit-lsp.serverPath` | `string` | | The path of the sourcekit-lsp executable. In SDE: defaults to the toolchain's sourcekit-lsp. | 72 | | `sourcekit-lsp.toolchainPath` | `string` | | The path of the swift toolchain. In SDE: defaults to Xcode's default toolchain. | 73 | | `swift.languageServerPath` | `string` | `/usr/local/bin/langserver-swift` | [DEPRECATED] The fully qualified path to the Swift Language Server executable. | 74 | | `swift.path.sourcekite` | `string` | | The fully path to the sourcekite(SDE's LS backend). | 75 | | `swift.path.swift_driver_bin` | `string` | `/usr/bin/swift` | The fully path to the swift driver binary. | 76 | | `swift.path.shell` | `string` | `/bin/sh` | The fully path to the shell binary. | 77 | | `sde.sourcekit.compilerOptions` | `string[]` | `[]` | Optional compiler options like the target or search paths. Will only be used as default. `(debug \| release).yaml` builds will override these settings. | 78 | | `swift.targets` | `object[]` | `[]` | If SDE cannot reliably detect all targets, you can manually configure them. | 79 | | `sde.enable` | `boolean` | `true` | Enable SDE functionality | 80 | | `sde.languageServerMode` | `string` | `sourcekite` | Decides which language server should be used. `sourcekite` is the default LSP for SDE, `sourcekit-lsp` is Apple's official one and `langserver` is RLovelett's Langserver. | 81 | | `sde.enableTracing.client` | `boolean` | `false` | Enable tracing output for SDE client | 82 | | `sde.enableTracing.LSPServer` | `boolean` | `false` | Enable tracing output for SDE LS server | 83 | | `sde.buildOnSave` | `boolean` | `true` | Indicates wether SDE shall build the project on save. | 84 | | `sde.swiftBuildingParams` | `string[]` | `["build"]` | The params that shall be passed to the swift command. | 85 | | `swift.diagnosis.max_num_problems` | `number` | `100` | Controls the maximum number of problems produced by the server. NOET: Not used now. | 86 | 87 | ## Commands 88 | 89 | | Title | Command | 90 | | ----------------------- | ------------------------------------ | 91 | | Build Package | `sde.commands.build` | 92 | | Restart Language Server | `sde.commands.restartLanguageServer` | 93 | | Run Default Target | `sde.commands.run` | 94 | | Run Target… | `sde.commands.selectRun` | 95 | | Restart Target | `sde.commands.restartRun` | 96 | | Stop Running Target | `sde.commands.stop` | 97 | | Clean Package | `sde.commands.clean` | 98 | 99 | ## Contributors 100 | 101 | - Valentin Knabel, [@vknabel](https://github.com/vknabel), [twitter](https://twitter.com/vknabel), _maintainer_ 102 | - Clay Jensen-Reimann, [@clayreimann](https://github.com/clayreimann), [twitter](https://twitter.com/clayreimann) 103 | - Jin Mingjian, [@JinMingjian](https://github.com/JinMingjian), [twitter](https://twitter.com/JinMingjian), _author_, not involved anymore 104 | - Felix Fischer, [@felix91gr](https://github.com/felix91gr), [twitter](https://twitter.com/FelixFischer91) 105 | - Mijo Gračanin, [@mijo-gracanin](https://github.com/mijo-gracanin) 106 | 107 | ## FAQ 108 | 109 | ### How to contribute to this project? 110 | 111 | There are a lot of ways you could contribute to either this project or the Swift on VS Code itself. For more information head over to [CONTRIBUTING.md](./CONTRIBUTING.md). 112 | 113 | ### How can I debug my SwiftPM project? 114 | 115 | Debugging your Swift targets requires a different extension [LLDB Debugger](https://github.com/vadimcn/vscode-lldb). You can follow this tutorial: [vknabel.com/pages/Debugging-Swift-in-VS-Code](https://www.vknabel.com/pages/Debugging-Swift-in-VS-Code/). 116 | 117 | ### How do I get autocompletion when using TensorFlow? 118 | 119 | You can add the following configs. This will improve your autocompletion. Though currently the `TensorFlow` module will not be indexed yet. 120 | 121 | ```json 122 | // .vscode/settings.json example for TensorFlow 123 | { 124 | "sde.swiftBuildingParams": ["build", "-Xlinker", "-ltensorflow"], 125 | "sde.languageServerMode": "sourcekite", 126 | "sourcekit-lsp.toolchainPath": "/Library/Developer/Toolchains/swift-tensorflow-RELEASE-0.3.1.xctoolchain", 127 | "swift.path.swift_driver_bin": "/Library/Developer/Toolchains/swift-tensorflow-RELEASE-0.3.1.xctoolchain/usr/bin/swift" 128 | } 129 | ``` 130 | 131 | > In case you find a way to get autocompletion for the `TensorFlow` module to work, please submit a PR or open an issue. 132 | 133 | ### How do I get autocompletion for UIKit? 134 | 135 | With sourcekite, you can add new autocompletion targets through your configuration. 136 | 137 | ```json 138 | // .vscode/settings.json example for iOS and WatchOS 139 | { 140 | "swift.targets": [ 141 | { 142 | "name": "YourApp", 143 | "path": "YourApp/YourApp", 144 | "sources": ["**/*.swift"], 145 | "compilerArguments": [ 146 | "-sdk", 147 | "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk", 148 | "-target", 149 | "arm64-apple-ios11.0" 150 | ] 151 | }, 152 | { 153 | "name": "YourWatchApp", 154 | "path": "YourApp/YourWatchExtension", 155 | "sources": ["**/*.swift"], 156 | "compilerArguments": [ 157 | "-sdk", 158 | "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk", 159 | "-target", 160 | "armv7k-apple-watchos4.0" 161 | ] 162 | } 163 | ] 164 | } 165 | ``` 166 | 167 | Since Xcode 11.4, you may use its built-in support for sourcekit-lsp 168 | 169 | ```json 170 | // .vscode/settings.json example for iOS 171 | { 172 | "sde.languageServerMode": "sourcekit-lsp", 173 | "sourcekit-lsp.serverPath": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp", 174 | "sourcekit-lsp.toolchainPath": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", 175 | "sde.swiftBuildingParams": [ 176 | "build", 177 | "-Xswiftc", 178 | "-sdk", 179 | "-Xswiftc", 180 | "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk", 181 | "-Xswiftc", 182 | "-target", 183 | "-Xswiftc", 184 | "arm64-apple-ios11.0" 185 | ] 186 | } 187 | ``` 188 | 189 | ```json 190 | // .vscode/settings.json example for WatchOS 191 | { 192 | "sde.languageServerMode": "sourcekit-lsp", 193 | "sourcekit-lsp.serverPath": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp", 194 | "sourcekit-lsp.toolchainPath": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", 195 | "sde.swiftBuildingParams": [ 196 | "build", 197 | "-Xswiftc", 198 | "-sdk", 199 | "-Xswiftc", 200 | "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk", 201 | "-Xswiftc", 202 | "-target", 203 | "-Xswiftc", 204 | "armv7k-apple-watchos4.0" 205 | ] 206 | } 207 | ``` 208 | 209 | ### Build failed! What should I do? 210 | 211 | Go to vscode `OUTPUT` window, then select `SPM`. The `OUTPUT` window will tell you what's wrong. 212 | 213 | ### It says `Build error: root manifest not found`?? 214 | 215 | The Root Manifest refers to a Package.swift file of the Swift Package Manager (short SwiftPM, SPM). And this extension requires a Swift Package (a project for the SwiftPM) to function. Projects created by Xcode rely on a different format (`*.xcodeproj` and `*.xcodeworkspace`) and the extension cannot handle these. 216 | 217 | You'll need to create a new Swift Package and "wire it up" with your Xcode Project. There are some tips and guides linked in [this SO question](https://stackoverflow.com/questions/41900749/use-swift-package-manager-on-existing-xcode-project). 218 | 219 | ### I'd like to have a different build paths than usually. How can I achieve that? 220 | 221 | You can compile your app using a command like `swift build --build-path "./.build-macos"` on macOS and `swift build --build-path "./.build-linux"` on Linux, e.g. from within a docker container, you just need to add the appropriate building parameter: 222 | 223 | ``` 224 | // .vscode/settings.json 225 | { 226 | "sde.swiftBuildingParams": [ 227 | "build", 228 | "--build-path", 229 | "./.build-macos" 230 | ] 231 | } 232 | ``` 233 | 234 | ### Other questions? 235 | 236 | If so, file an [issue](https://github.com/vknabel/vscode-swift-development-environment/issues), please :) 237 | 238 | ## License 239 | 240 | Apache License v2. 241 | 242 | ## 3rd-party Sources Thanks 243 | 244 | 1. [dbgmits](https://github.com/enlight/dbgmits), very nice structure of sources, but of which in my heavy modification to support non-MI and much more 245 | -------------------------------------------------------------------------------- /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vknabel/vscode-swift-development-environment/487b75064e870511f847767d1295debfcda5b920/docs/preview.gif -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vknabel/vscode-swift-development-environment/487b75064e870511f847767d1295debfcda5b920/icons/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-swift-development-environment", 3 | "displayName": "Maintained Swift Development Environment", 4 | "description": "New home of Swift Development Environment for VS Code", 5 | "author": { 6 | "name": "Valentin Knabel", 7 | "email": "dev@vknabel.com", 8 | "url": "https://github.com/vknabel" 9 | }, 10 | "license": "Apache-2.0", 11 | "version": "2.12.3", 12 | "publisher": "vknabel", 13 | "icon": "icons/icon.png", 14 | "galleryBanner": { 15 | "color": "#FC823F", 16 | "theme": "light" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/vknabel/vscode-swift-development-environment" 21 | }, 22 | "engines": { 23 | "vscode": "^1.54.0" 24 | }, 25 | "categories": [ 26 | "Programming Languages" 27 | ], 28 | "keywords": [ 29 | "swift", 30 | "sde", 31 | "linux", 32 | "lldb", 33 | "development" 34 | ], 35 | "preview": false, 36 | "activationEvents": [ 37 | "onLanguage:swift", 38 | "workspaceContains:**/*swift", 39 | "onCommand:sde.commands.build", 40 | "onCommand:sde.commands.run", 41 | "onCommand:sde.commands.clean", 42 | "onCommand:sde.commands.selectRun" 43 | ], 44 | "main": "./out/clientMain", 45 | "contributes": { 46 | "commands": [ 47 | { 48 | "command": "sde.commands.build", 49 | "title": "Build Package", 50 | "category": "SDE" 51 | }, 52 | { 53 | "command": "sde.commands.restartLanguageServer", 54 | "title": "Restart Language Server", 55 | "category": "SDE" 56 | }, 57 | { 58 | "command": "sde.commands.run", 59 | "title": "Run Default Target", 60 | "category": "SDE", 61 | "enablement": "sde:running == false" 62 | }, 63 | { 64 | "command": "sde.commands.selectRun", 65 | "title": "Run Target…", 66 | "category": "SDE", 67 | "enablement": "sde:running == false" 68 | }, 69 | { 70 | "command": "sde.commands.restartRun", 71 | "title": "Restart Target", 72 | "category": "SDE", 73 | "enablement": "sde:running == true" 74 | }, 75 | { 76 | "command": "sde.commands.stop", 77 | "title": "Stop Running Target", 78 | "category": "SDE", 79 | "enablement": "sde:running == true" 80 | }, 81 | { 82 | "command": "sde.commands.clean", 83 | "title": "Clean Package", 84 | "category": "SDE" 85 | } 86 | ], 87 | "keybindings": [ 88 | { 89 | "command": "sde.commands.build", 90 | "key": "alt+b" 91 | }, 92 | { 93 | "command": "sde.commands.run", 94 | "key": "alt+r" 95 | }, 96 | { 97 | "command": "sde.commands.selectRun", 98 | "key": "alt+shift+r" 99 | }, 100 | { 101 | "command": "sde.commands.stop", 102 | "key": "alt+s" 103 | } 104 | ], 105 | "configuration": { 106 | "type": "object", 107 | "title": "Swift Development Environment", 108 | "properties": { 109 | "sourcekit-lsp.serverPath": { 110 | "type": "string", 111 | "description": "The path of the sourcekit-lsp executable\nIn SDE: defaults to the toolchain's sourcekit-lsp. Only available in global config for security reasons.", 112 | "scope": "machine" 113 | }, 114 | "sourcekit-lsp.toolchainPath": { 115 | "type": "string", 116 | "description": "The path of the swift toolchain.\nIn SDE: defaults to Xcode's default toolchain." 117 | }, 118 | "swift.languageServerPath": { 119 | "type": "string", 120 | "default": "/usr/local/bin/langserver-swift", 121 | "description": "[DEPRECATED] The fully qualified path to the Swift Language Server executable. Only available in global config for security reasons.", 122 | "scope": "machine" 123 | }, 124 | "swift.path.sourcekite": { 125 | "type": "string", 126 | "description": "The fully path to the sourcekite(SDE's LS backend). Only available in global config for security reasons.", 127 | "scope": "machine" 128 | }, 129 | "swift.path.sourcekiteDockerMode": { 130 | "type": "boolean", 131 | "default": false, 132 | "description": "[DEPRECATED] (Experimental)Enable to run dedicated docker shell command. It is the responsibility of user to guarantee that the docker cmd 'docker run --rm -i jinmingjian/docker-sourcekite' works in your system.", 133 | "scope": "machine" 134 | }, 135 | "swift.path.swift_driver_bin": { 136 | "type": "string", 137 | "default": "/usr/bin/swift", 138 | "description": "The fully path to the swift driver binary. Only available in global config for security reasons.", 139 | "scope": "machine" 140 | }, 141 | "swift.path.shell": { 142 | "type": "string", 143 | "default": "/bin/sh", 144 | "description": "The fully path to the shell binary. Only available in global config for security reasons.", 145 | "scope": "machine" 146 | }, 147 | "sde.sourcekit.compilerOptions": { 148 | "type": "array", 149 | "description": "Optional compiler options like the target or search paths. Will only be used as default. `(debug|release).yaml` builds will override these settings.", 150 | "default": [], 151 | "items": { 152 | "type": "string" 153 | } 154 | }, 155 | "swift.targets": { 156 | "type": "array", 157 | "description": "If SDE cannot reliably detect all targets, you can manually configure them.", 158 | "default": [], 159 | "items": { 160 | "type": "object", 161 | "properties": { 162 | "name": { 163 | "type": "string", 164 | "required": true, 165 | "description": "The module name of the target. Will be passed as -module-name." 166 | }, 167 | "path": { 168 | "type": "string", 169 | "required": true, 170 | "description": "The reference path." 171 | }, 172 | "sources": { 173 | "required": false, 174 | "type": "array", 175 | "description": "An array of globs to determine all sources within the current target.", 176 | "default": [ 177 | "**/*.swift" 178 | ], 179 | "items": { 180 | "type": "string" 181 | } 182 | }, 183 | "compilerArguments": { 184 | "type": "array", 185 | "required": false, 186 | "description": "Additional compiler arguments to be passed.", 187 | "items": { 188 | "type": "string" 189 | } 190 | } 191 | } 192 | } 193 | }, 194 | "sde.enable": { 195 | "type": "boolean", 196 | "default": true, 197 | "description": "Enable SDE functionality" 198 | }, 199 | "sde.languageServerMode": { 200 | "type": "string", 201 | "default": "sourcekite", 202 | "enum": [ 203 | "sourcekite", 204 | "langserver", 205 | "sourcekit-lsp" 206 | ], 207 | "description": "Decides which language server should be used. `sourcekite` is the default LSP for SDE, `sourcekit-lsp` is Apple's official one and `langserver` is RLovelett's Langserver." 208 | }, 209 | "sde.enableTracing.client": { 210 | "type": "boolean", 211 | "default": false, 212 | "description": "Enable tracing output for SDE client" 213 | }, 214 | "sde.enableTracing.LSPServer": { 215 | "type": "boolean", 216 | "default": false, 217 | "description": "Enable tracing output for SDE LS server" 218 | }, 219 | "sde.buildOnSave": { 220 | "type": "boolean", 221 | "default": true, 222 | "description": "Indicates wether SDE shall build the project on save." 223 | }, 224 | "sde.swiftBuildingParams": { 225 | "type": "array", 226 | "description": "The params that shall be passed to the swift command.", 227 | "default": [ 228 | "build" 229 | ], 230 | "items": { 231 | "type": "string" 232 | } 233 | }, 234 | "swift.diagnosis.max_num_problems": { 235 | "type": "number", 236 | "default": 100, 237 | "description": "Controls the maximum number of problems produced by the server. NOET: Not used now." 238 | } 239 | } 240 | }, 241 | "languages": [ 242 | { 243 | "id": "swift", 244 | "extensions": [ 245 | ".swift" 246 | ], 247 | "aliases": [ 248 | "Swift" 249 | ], 250 | "firstLine": "^#!/.*\\bswift[0-9.-]*\\b" 251 | } 252 | ], 253 | "breakpoints": [ 254 | { 255 | "language": "swift" 256 | } 257 | ] 258 | }, 259 | "prettier": { 260 | "trailingComma": "es5", 261 | "tabWidth": 2, 262 | "printWidth": 100 263 | }, 264 | "scripts": { 265 | "vscode:prepublish": "npm run build", 266 | "build": "npm run compile", 267 | "compile": "npx tsc", 268 | "format": "npx prettier CHANGELOG.md README.md src/*.ts src/sourcekites-server/**/*.ts tsconfig.json --write", 269 | "test": "npx jest" 270 | }, 271 | "devDependencies": { 272 | "@types/bunyan": "^1.8.4", 273 | "@types/glob": "^7.1.3", 274 | "@types/jest": "^27.0.0", 275 | "@types/js-yaml": "^4.0.0", 276 | "@types/node": "^14.14.33", 277 | "@types/vscode": "1.54.0", 278 | "@types/xml-js": "^1.0.0", 279 | "jest": "^27.0.0", 280 | "prettier": "^2.2.1", 281 | "ts-jest": "^27.0.0", 282 | "tsc": "^1.20150623.0", 283 | "typescript": "^4.2.3" 284 | }, 285 | "dependencies": { 286 | "bunyan": "^1.8.5", 287 | "fs-promise": "^2.0.3", 288 | "glob": "^7.1.4", 289 | "js-yaml": "^4.0.0", 290 | "vscode-languageclient": "^7.0.0", 291 | "vscode-languageserver": "^7.0.0", 292 | "vscode-languageserver-textdocument": "^1.0.1", 293 | "xml-js": "^1.6.11" 294 | }, 295 | "jest": { 296 | "transform": { 297 | "^.+\\.tsx?$": "ts-jest" 298 | }, 299 | "testMatch": [ 300 | "**/*.spec.ts" 301 | ], 302 | "moduleFileExtensions": [ 303 | "ts", 304 | "js" 305 | ], 306 | "collectCoverage": true 307 | } 308 | } -------------------------------------------------------------------------------- /src/clientMain.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { commands, DiagnosticCollection, ExtensionContext, Uri, window, workspace } from "vscode"; 4 | import { absolutePath } from "./helpers/AbsolutePath"; 5 | import * as tools from "./toolchain/SwiftTools"; 6 | import lsp from "./vscode/lsp-interop"; 7 | import output from "./vscode/output-channels"; 8 | 9 | let swiftBinPath: string | null = null; 10 | let swiftBuildParams: string[] = ["build"]; 11 | export let diagnosticCollection: DiagnosticCollection; 12 | 13 | let mostRecentRunTarget = ""; 14 | 15 | export async function activate(context: ExtensionContext) { 16 | output.init(context); 17 | 18 | if (workspace.getConfiguration().get("sde.enable") === false) { 19 | output.build.log("SDE Disabled", false); 20 | return; 21 | } 22 | tools.setRunning(false); 23 | output.build.log("Activating SDE"); 24 | 25 | initConfig(); 26 | await lsp.startLSPClient(context); 27 | 28 | //commands 29 | let toolchain = new tools.Toolchain( 30 | swiftBinPath, 31 | workspace.workspaceFolders && workspace.workspaceFolders[0] 32 | ? workspace.workspaceFolders[0].uri.fsPath 33 | : "/", 34 | swiftBuildParams 35 | ); 36 | context.subscriptions.push(toolchain.diagnostics); 37 | context.subscriptions.push(toolchain.start()); 38 | context.subscriptions.push( 39 | commands.registerCommand("sde.commands.build", () => toolchain.build()), 40 | commands.registerCommand( 41 | "sde.commands.restartLanguageServer", 42 | async () => await lsp.restartLSPClient(context) 43 | ), 44 | commands.registerCommand("sde.commands.run", () => toolchain.runStart()), 45 | commands.registerCommand("sde.commands.selectRun", () => { 46 | window 47 | .showInputBox({ prompt: "Run which target?", value: mostRecentRunTarget }) 48 | .then((target) => { 49 | if (!target) { 50 | return; 51 | } 52 | mostRecentRunTarget = target; 53 | toolchain.runStart(target); 54 | }); 55 | }), 56 | commands.registerCommand("sde.commands.restart", () => { 57 | toolchain.runStop(); 58 | toolchain.runStart(mostRecentRunTarget); 59 | }), 60 | commands.registerCommand("sde.commands.stop", () => toolchain.runStop()), 61 | commands.registerCommand("sde.commands.clean", () => toolchain.clean()) 62 | ); 63 | 64 | workspace.onDidSaveTextDocument( 65 | (document) => { 66 | if (tools.shouldBuildOnSave() && document.languageId === "swift") { 67 | toolchain.build(); 68 | } 69 | }, 70 | null, 71 | context.subscriptions 72 | ); 73 | 74 | // respond to settings changes 75 | workspace.onDidChangeConfiguration(async (event) => { 76 | if ( 77 | event.affectsConfiguration("sde") || 78 | event.affectsConfiguration("swift") || 79 | event.affectsConfiguration("sourcekit-lsp") 80 | ) { 81 | await lsp.restartLSPClient(context); 82 | } 83 | }); 84 | 85 | // build on startup 86 | toolchain.build(); 87 | } 88 | 89 | function initConfig() { 90 | swiftBinPath = absolutePath(workspace.getConfiguration().get("swift.path.swift_driver_bin")); 91 | swiftBuildParams = workspace.getConfiguration().get("sde.swiftBuildingParams") || [ 92 | "build", 93 | ]; 94 | } 95 | -------------------------------------------------------------------------------- /src/helpers/AbsolutePath.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as path from "path"; 3 | 4 | export function absolutePath(userDefinedPath: string) { 5 | return path.normalize(userDefinedPath.replace(/^~/, os.homedir() + "/")); 6 | } 7 | -------------------------------------------------------------------------------- /src/sourcekites-server/current.ts: -------------------------------------------------------------------------------- 1 | export interface Current { 2 | log(label: string, ...details: any[]); 3 | report(label: string, ...details: any[]); 4 | spawn(command: string): Promise; 5 | swift(inPath: string, command: string): Promise; 6 | defaultCompilerArguments(): string[]; 7 | config: { 8 | isTracingOn: boolean; 9 | swiftPath: string; 10 | sourcekitePath: string; 11 | shellPath: string; 12 | sourceKitCompilerOptions: string[]; 13 | workspacePaths: string[]; 14 | toolchainPath: string | null; 15 | targets: Array<{ 16 | name: string; 17 | path: string; 18 | sources?: string[]; 19 | compilerArguments?: string[]; 20 | }>; 21 | }; 22 | } 23 | 24 | import * as childProcess from "child_process"; 25 | 26 | async function spawn(cmd: string) { 27 | let buffer = ""; 28 | return new Promise((resolve, reject) => { 29 | const sp = childProcess.spawn(Current.config.shellPath, ["-c", cmd]); 30 | sp.stdout.on("data", data => { 31 | buffer += data; 32 | }); 33 | sp.on("exit", code => { 34 | if (code === 0) { 35 | resolve(buffer); 36 | } else { 37 | reject(code); 38 | } 39 | }); 40 | }); 41 | } 42 | async function swift(inPath: string, cmd: string) { 43 | return await this.spawn(`cd ${inPath} && ${Current.config.swiftPath} ${cmd}`); 44 | } 45 | function log(label: string, ...details: any[]) { 46 | if (Current.config.isTracingOn) { 47 | console.log(`[${label}]`, ...details); 48 | } 49 | } 50 | function report(label: string, ...details: any[]) { 51 | if (Current.config.isTracingOn) { 52 | console.log(`[ERROR][${label}]`, ...details); 53 | } 54 | } 55 | function defaultCompilerArguments() { 56 | return [ 57 | "-sdk", 58 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", 59 | "-sdk", 60 | "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk", 61 | "-sdk", 62 | "/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk", 63 | "-sdk", 64 | "/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk", 65 | "-I", 66 | "/System/Library/Frameworks/", 67 | "-I", 68 | "/usr/lib/swift/pm/", 69 | ...Current.config.sourceKitCompilerOptions 70 | ]; 71 | } 72 | 73 | export let Current: Current = { 74 | log, 75 | report, 76 | spawn, 77 | swift, 78 | defaultCompilerArguments, 79 | config: { 80 | workspacePaths: [], 81 | isTracingOn: false, 82 | swiftPath: "$(which swift)", 83 | sourcekitePath: "$(which sourcekite)", 84 | shellPath: "/bin/bash", 85 | sourceKitCompilerOptions: [], 86 | targets: [], 87 | toolchainPath: null 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/sourcekites-server/package.ts: -------------------------------------------------------------------------------- 1 | export type Path = string; 2 | 3 | export type Package = (fromPath: Path) => Promise; 4 | 5 | export interface Target { 6 | name: string; 7 | path: Path; 8 | sources: Set; 9 | compilerArguments: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/available-packages.ts: -------------------------------------------------------------------------------- 1 | import { descriptionPackage } from "./description-package"; 2 | import { Package } from "../package"; 3 | import { swiftFilePackage } from "./swift-file-package"; 4 | import { debugYamlPackage } from "./debug-yaml-package"; 5 | import { configPackage } from "./config-package"; 6 | import { flatteningTargetsWithUniqueSources } from "./package-helpers"; 7 | 8 | export const availablePackages: Package = async (fromPath) => { 9 | const [ 10 | configTargets, 11 | debugYamlTargets, 12 | descriptionTargets, 13 | swiftFileTargets, 14 | ] = await Promise.all([ 15 | configPackage(fromPath), 16 | debugYamlPackage(fromPath), 17 | descriptionPackage(fromPath), 18 | swiftFilePackage(fromPath), 19 | ]); 20 | return flatteningTargetsWithUniqueSources( 21 | configTargets, 22 | debugYamlTargets, 23 | descriptionTargets, 24 | swiftFileTargets 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/config-package.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Current } from "../current"; 4 | import { Package } from "../package"; 5 | import { expandingSourceGlob } from "../path-helpers"; 6 | 7 | export const configPackage: Package = async (fromPath) => { 8 | const targets = Current.config.targets 9 | .filter( 10 | ({ path: targetPath }) => 11 | path.isAbsolute(targetPath) || fs.existsSync(path.resolve(fromPath, targetPath)) 12 | ) 13 | .map(async (configTarget) => { 14 | const targetPath = path.normalize(path.resolve(fromPath, configTarget.path)); 15 | const expandedSources = (configTarget.sources || ["**/*.swift"]).map( 16 | expandingSourceGlob(fromPath, targetPath) 17 | ); 18 | const sources = await Promise.all(expandedSources); 19 | return { 20 | ...configTarget, 21 | path: targetPath, 22 | sources: new Set([].concat(...sources)), 23 | compilerArguments: configTarget.compilerArguments || [], 24 | }; 25 | }); 26 | return await Promise.all(targets); 27 | }; 28 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/debug-yaml-package.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as yaml from "js-yaml"; 3 | import * as path from "path"; 4 | import { Package, Path, Target } from "../package"; 5 | import { compilerArgumentsForImportPath } from "../path-helpers"; 6 | 7 | interface DebugYaml { 8 | client: { 9 | name: "swift-build"; 10 | }; 11 | tools: {}; 12 | targets: { [llTarget: string]: string[] }; 13 | default: string; 14 | commands: { 15 | [command: string]: LLCommand; 16 | }; 17 | } 18 | 19 | interface LLCommand { 20 | "module-name": string; 21 | sources?: string[]; 22 | "import-paths"?: string[]; 23 | "other-args"?: string[]; 24 | } 25 | 26 | export const debugYamlPackage: Package = async (fromPath) => { 27 | let debugContents: string; 28 | try { 29 | debugContents = await contentsOfDebugOrReleaseYaml(fromPath); 30 | } catch (error) { 31 | return []; 32 | } 33 | const debugYaml = yaml.load(debugContents) as DebugYaml; 34 | const targets: Target[] = []; 35 | for (const name in debugYaml.commands) { 36 | const command = debugYaml.commands[name]; 37 | if (command.sources == null || command.sources.length === 0) { 38 | continue; 39 | } 40 | targets.push({ 41 | name: command["module-name"] || name, 42 | path: fromPath, // actually a subfolder, but all paths are absolute 43 | sources: new Set( 44 | command.sources.map((toSource) => path.normalize(path.resolve(fromPath, toSource))) 45 | ), 46 | compilerArguments: compilerArgumentsForCommand(command), 47 | }); 48 | } 49 | return targets; 50 | }; 51 | 52 | function contentsOfFile(file: Path) { 53 | return new Promise((resolve, reject) => { 54 | fs.readFile(file, "utf8", (error, data) => { 55 | if (typeof data === "string") { 56 | resolve(data); 57 | } else { 58 | reject(error); 59 | } 60 | }); 61 | }); 62 | } 63 | 64 | function compilerArgumentsForCommand(command: LLCommand): string[] { 65 | const importPaths = command["import-paths"] || []; 66 | const otherArgs = command["other-args"] || []; 67 | const moduleNameArgs = 68 | (command["module-name"] && ["-module-name", command["module-name"], "-Onone"]) || []; 69 | const importPathArgs = importPaths.map(compilerArgumentsForImportPath); 70 | return otherArgs.concat(moduleNameArgs, ...importPathArgs); 71 | } 72 | 73 | function contentsOfDebugOrReleaseYaml(fromPath: Path) { 74 | return contentsOfFile(path.resolve(fromPath, ".build", "debug.yaml")).catch(() => 75 | contentsOfFile(path.resolve(fromPath, ".build", "release.yaml")) 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/description-package.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { Current } from "../current"; 3 | import { Package, Target, Path } from "../package"; 4 | const joinPath = path.join; 5 | 6 | type PackageDescription = 7 | | { targets: TargetDescription[]; modules: undefined } 8 | | { targets: undefined; modules: TargetDescription[] }; 9 | interface TargetDescription { 10 | name: string; 11 | path: string; 12 | sources: string[]; 13 | } 14 | 15 | export const descriptionPackage: Package = async (fromPath) => { 16 | try { 17 | const data = await Current.swift(fromPath, `package describe --type json`); 18 | const packageDescription = JSON.parse(data) as PackageDescription; 19 | const targetDescription = packageDescription.modules || packageDescription.targets; 20 | return targetDescription.map(targetFromDescriptionFromPath(fromPath)); 21 | } catch (error) { 22 | Current.report(error); 23 | return []; 24 | } 25 | }; 26 | 27 | function targetFromDescriptionFromPath(fromPath: Path) { 28 | return ({ name, path, sources }: TargetDescription): Target => { 29 | return { 30 | name, 31 | path, 32 | sources: new Set(sources), 33 | compilerArguments: [ 34 | "-I", 35 | joinPath(fromPath, ".build", "debug"), 36 | ...Current.defaultCompilerArguments(), 37 | ], 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/package-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { removingDuplicateSources, flatteningTargetsWithUniqueSources } from "./package-helpers"; 2 | import { Target } from "../package"; 3 | 4 | const uniqueTarget: Target = { 5 | name: "Unique", 6 | path: "Sources/Unique", 7 | sources: new Set(["Hello.swift", "main.swift"]), 8 | compilerArguments: [], 9 | }; 10 | 11 | const unrelatedTarget: Target = { 12 | name: "UnrelatedTarget", 13 | path: "Sources/UnrelatedTarget", 14 | sources: new Set(["Unrelated.swift"]), 15 | compilerArguments: [], 16 | }; 17 | 18 | describe("package helpers", () => { 19 | describe("removingDuplicateSources", () => { 20 | it("does not emit unique targets", () => { 21 | const emittedTargets = removingDuplicateSources([], [uniqueTarget]); 22 | expect(emittedTargets).toHaveLength(0); 23 | }); 24 | 25 | it("unrelated source sets will be kept", () => { 26 | const emittedTargets = removingDuplicateSources([unrelatedTarget], [uniqueTarget]); 27 | expect(emittedTargets).toEqual([unrelatedTarget]); 28 | }); 29 | 30 | it("unrelated source sets with differing paths will be kept for same file names", () => { 31 | const unrelatedTargetWithSameFileNames: Target = { 32 | ...unrelatedTarget, 33 | sources: uniqueTarget.sources, 34 | }; 35 | 36 | const emittedTargets = removingDuplicateSources( 37 | [unrelatedTargetWithSameFileNames], 38 | [uniqueTarget] 39 | ); 40 | expect(emittedTargets).toEqual([unrelatedTargetWithSameFileNames]); 41 | }); 42 | 43 | it("source sets with same paths but different file names are kept", () => { 44 | const samePathTargetWithDifferentSources = { 45 | ...unrelatedTarget, 46 | path: uniqueTarget.path, 47 | }; 48 | const emittedTargets = removingDuplicateSources( 49 | [samePathTargetWithDifferentSources], 50 | [uniqueTarget] 51 | ); 52 | expect(emittedTargets).toEqual([samePathTargetWithDifferentSources]); 53 | }); 54 | 55 | it("source sets with different paths but same files will be deuplicated", () => { 56 | const differentPathTargetWithSameSources = { 57 | ...unrelatedTarget, 58 | path: "./", 59 | sources: new Set( 60 | Array(uniqueTarget.sources.values()).map( 61 | (sourceFile) => `${uniqueTarget.path}/${sourceFile}` 62 | ) 63 | ), 64 | }; 65 | const emittedTargets = removingDuplicateSources( 66 | [differentPathTargetWithSameSources], 67 | [uniqueTarget] 68 | ); 69 | expect(emittedTargets).toEqual([differentPathTargetWithSameSources]); 70 | }); 71 | }); 72 | 73 | describe("flatteningTargetsWithUniqueSources", () => { 74 | it("bug: configs did not override global paths", () => { 75 | // see https://github.com/vknabel/vscode-swift-development-environment/issues/55 76 | const emittedTargets = flatteningTargetsWithUniqueSources( 77 | [ 78 | { 79 | name: "HiModuleFromConfigs", 80 | path: "/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi", 81 | sources: new Set(["Hi.swift"]), 82 | compilerArguments: [], 83 | }, 84 | ], 85 | [ 86 | { 87 | name: "HiModuleFromDebugYaml", 88 | path: "/Users/vknabel/Desktop/AutocompleteIos", 89 | sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi/Hi.swift"]), 90 | compilerArguments: [], 91 | }, 92 | ], 93 | [ 94 | { 95 | name: "AutocompleteIos", 96 | path: "/Users/vknabel/Desktop/AutocompleteIos", 97 | sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Package.swift"]), 98 | compilerArguments: [], 99 | }, 100 | ] 101 | ); 102 | expect(emittedTargets).toEqual([ 103 | { 104 | name: "HiModuleFromConfigs", 105 | path: "/Users/vknabel/Desktop/AutocompleteIos/Sources/Hi", 106 | sources: new Set(["Hi.swift"]), 107 | compilerArguments: [], 108 | }, 109 | { 110 | name: "HiModuleFromDebugYaml", 111 | path: "/Users/vknabel/Desktop/AutocompleteIos", 112 | sources: new Set([]), 113 | compilerArguments: [], 114 | }, 115 | { 116 | name: "AutocompleteIos", 117 | path: "/Users/vknabel/Desktop/AutocompleteIos", 118 | sources: new Set(["/Users/vknabel/Desktop/AutocompleteIos/Package.swift"]), 119 | compilerArguments: [], 120 | }, 121 | ]); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/package-helpers.ts: -------------------------------------------------------------------------------- 1 | import { Target } from "../package"; 2 | import * as path from "path"; 3 | 4 | export function flatteningTargetsWithUniqueSources(...targets: Target[][]): Target[] { 5 | return targets.reduce( 6 | (current, next) => [ 7 | ...current, 8 | ...removingDuplicateSources(next, current.map(normalizedTarget)), 9 | ], 10 | [] 11 | ); 12 | } 13 | 14 | export function removingDuplicateSources(fromTargets: Target[], uniqueTargets: Target[]): Target[] { 15 | return fromTargets.map((target) => { 16 | const swiftFilesWithoutTargets = Array.from(target.sources).filter( 17 | (source) => 18 | uniqueTargets.findIndex((desc) => desc.sources.has(path.resolve(target.path, source))) === 19 | -1 20 | ); 21 | return { ...target, sources: new Set(swiftFilesWithoutTargets) }; 22 | }); 23 | } 24 | 25 | function normalizedTarget(target: Target): Target { 26 | return { 27 | ...target, 28 | sources: mapSet(target.sources, (source) => path.resolve(target.path, source)), 29 | }; 30 | } 31 | function mapSet(set: Set, transform: (element: T) => R): Set { 32 | return new Set(Array.from(set.values()).map((element) => transform(element))); 33 | } 34 | -------------------------------------------------------------------------------- /src/sourcekites-server/packages/swift-file-package.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Package, Path } from "../package"; 4 | 5 | export const swiftFilePackage: Package = async (fromPath) => { 6 | return [ 7 | { 8 | name: path.basename(fromPath), 9 | path: fromPath, 10 | sources: new Set( 11 | allSwiftFilesInPath(fromPath).map((file) => path.normalize(path.resolve(fromPath, file))) 12 | ), 13 | compilerArguments: [], 14 | }, 15 | ]; 16 | }; 17 | 18 | function allSwiftFilesInPath(root: Path): Path[] { 19 | const result = new Array(); 20 | try { 21 | const dir = fs.readdirSync(root).filter((sub) => !sub.startsWith(".") && sub !== "Carthage"); 22 | for (const sub of dir) { 23 | if (path.extname(sub) === ".swift") { 24 | result.push(path.join(root, sub)); 25 | } else { 26 | result.push(...allSwiftFilesInPath(path.join(root, sub))); 27 | } 28 | } 29 | return result; 30 | } catch (error) { 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/sourcekites-server/path-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as glob from "glob"; 3 | 4 | export const expandingSourceGlob = (fromPath: string, targetPath: string) => ( 5 | sourceGlob: string 6 | ) => { 7 | return new Promise((resolve, reject) => { 8 | const options: glob.IOptions = { 9 | cwd: targetPath, 10 | root: fromPath 11 | }; 12 | glob(sourceGlob, options, (error, matches) => { 13 | if (error) { 14 | reject(error); 15 | } else { 16 | resolve( 17 | matches.map(match => path.normalize(path.resolve(targetPath, match))) 18 | ); 19 | } 20 | }); 21 | }); 22 | }; 23 | 24 | export const compilerArgumentsForImportPath = (importPath: string) => [ 25 | "-Xcc", 26 | "-I", 27 | "-Xcc", 28 | importPath, 29 | "-I", 30 | importPath, 31 | "-Xcc", 32 | "-F", 33 | "-Xcc", 34 | importPath, 35 | "-F", 36 | importPath 37 | ]; 38 | -------------------------------------------------------------------------------- /src/sourcekites-server/server.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as path from "path"; 4 | import { 5 | IPCMessageReader, 6 | IPCMessageWriter, 7 | createConnection, 8 | Connection, 9 | TextDocuments, 10 | InitializeParams, 11 | InitializeResult, 12 | CompletionItem, 13 | CompletionItemKind, 14 | InsertTextFormat, 15 | TextEdit, 16 | Hover, 17 | MarkedString, 18 | Definition, 19 | FileChangeType, 20 | Range, 21 | TextDocumentSyncKind, 22 | } from "vscode-languageserver/node"; 23 | import * as fs from "fs"; 24 | import * as sourcekitProtocol from "./sourcekites"; 25 | import * as childProcess from "child_process"; 26 | import { parseDocumentation } from "./sourcekit-xml"; 27 | import { Target } from "./package"; 28 | import { availablePackages } from "./packages/available-packages"; 29 | import { Current } from "./current"; 30 | import { TextDocument } from "vscode-languageserver-textdocument"; 31 | 32 | export const spawn = childProcess.spawn; 33 | 34 | // Create a connection for the server. The connection uses Node's IPC as a transport 35 | let connection: Connection = createConnection( 36 | new IPCMessageReader(process), 37 | new IPCMessageWriter(process) 38 | ); 39 | 40 | // Create a simple text document manager. The text document manager 41 | // supports full document sync only 42 | let documents: TextDocuments = new TextDocuments(TextDocument); 43 | // Make the text document manager listen on the connection 44 | // for open, change and close text document events 45 | documents.listen(connection); 46 | 47 | let targets: Target[] | null; 48 | export async function initializeModuleMeta() { 49 | const loadingTargets = Current.config.workspacePaths.map(availablePackages); 50 | const loadedTargets = await Promise.all(loadingTargets); 51 | targets = [].concat(...loadedTargets); 52 | } 53 | 54 | export function targetForSource(srcPath: string): Target { 55 | return ( 56 | (targets && targets.find((target) => target.sources.has(path.normalize(srcPath)))) || { 57 | name: path.basename(srcPath), 58 | path: srcPath, 59 | sources: new Set([srcPath]), 60 | compilerArguments: [], 61 | } 62 | ); 63 | } 64 | 65 | // After the server has started the client sends an initilize request. The server receives 66 | // in the passed params the root paths of the workspaces plus the client capabilites. 67 | connection.onInitialize((params: InitializeParams, cancellationToken): InitializeResult => { 68 | Current.config.isTracingOn = params.initializationOptions.isLSPServerTracingOn; 69 | Current.config.workspacePaths = params.workspaceFolders?.map(({ uri }) => 70 | uri.replace("file://", "") 71 | ) ?? ["/"]; 72 | skProtocolPath = params.initializationOptions.skProtocolProcess; 73 | Current.config.toolchainPath = params.initializationOptions.toolchainPath; 74 | skProtocolProcessAsShellCmd = params.initializationOptions.skProtocolProcessAsShellCmd; 75 | skCompilerOptions = params.initializationOptions.skCompilerOptions; 76 | Current.log( 77 | "-->onInitialize ", 78 | `isTracingOn=[${Current.config.isTracingOn}], 79 | skProtocolProcess=[${skProtocolPath}],skProtocolProcessAsShellCmd=[${skProtocolProcessAsShellCmd}]` 80 | ); 81 | return { 82 | capabilities: { 83 | // Tell the client that the server works in FULL text document sync mode 84 | textDocumentSync: TextDocumentSyncKind.Incremental, 85 | definitionProvider: true, 86 | hoverProvider: true, 87 | // referencesProvider: false, 88 | // documentSymbolProvider: false, 89 | // signatureHelpProvider: { 90 | // triggerCharacters: ['[', ','] 91 | // }, 92 | // We're providing completions. 93 | completionProvider: { 94 | resolveProvider: false, 95 | triggerCharacters: [ 96 | ".", 97 | ":", 98 | "(", 99 | "#", //' ', '<', //TODO 100 | ], 101 | }, 102 | }, 103 | }; 104 | }); 105 | 106 | // The settings interface describe the server relevant settings part 107 | interface Settings { 108 | swift: any; 109 | editor: { 110 | tabSize?: number; 111 | }; 112 | "[swift]": {}; 113 | } 114 | 115 | //external 116 | export let maxBytesAllowedForCodeCompletionResponse: number = 0; 117 | export let editorSettings: Settings["editor"] = {}; 118 | //internal 119 | export let skProtocolPath = null; 120 | export let skProtocolProcessAsShellCmd = false; 121 | export let skCompilerOptions: string[] = []; 122 | let maxNumProblems = null; 123 | // The settings have changed. Is send on server activation 124 | // as well. 125 | connection.onDidChangeConfiguration((change) => { 126 | Current.log("-->onDidChangeConfiguration"); 127 | const settings = change.settings; 128 | const sdeSettings = settings.swift; 129 | editorSettings = { ...settings.editor, ...settings["[swift]"] }; 130 | 131 | //FIXME does LS client support on-the-fly change? 132 | maxNumProblems = sdeSettings.diagnosis.max_num_problems; 133 | Current.config.sourcekitePath = sdeSettings.path.sourcekite; 134 | Current.config.swiftPath = sdeSettings.path.swift_driver_bin; 135 | Current.config.shellPath = sdeSettings.path.shell || "/bin/bash"; 136 | Current.config.targets = sdeSettings.targets || []; 137 | 138 | Current.log(`-->onDidChangeConfiguration tracing: 139 | swiftDiverBinPath=[${Current.config.swiftPath}], 140 | shellPath=[${Current.config.shellPath}]`); 141 | 142 | //FIXME reconfigure when configs haved 143 | sourcekitProtocol.initializeSourcekite(); 144 | if (!targets) { 145 | //FIXME oneshot? 146 | initializeModuleMeta(); 147 | } 148 | // Revalidate any open text documents 149 | documents.all().forEach(validateTextDocument); 150 | }); 151 | 152 | function validateTextDocument(textDocument: TextDocument): void { 153 | // let diagnostics: Diagnostic[] = []; 154 | // let lines = textDocument.getText().split(/\r?\n/g); 155 | // let problems = 0; 156 | // for (var i = 0; i < lines.length && problems < maxNumProblems; i++) { 157 | // let line = lines[i]; 158 | // let index = line.indexOf('typescript'); 159 | // if (index >= 0) { 160 | // problems++; 161 | // diagnostics.push({ 162 | // severity: DiagnosticSeverity.Warning, 163 | // range: { 164 | // start: { line: i, character: index }, 165 | // end: { line: i, character: index + 10 } 166 | // }, 167 | // message: `${line.substr(index, 10)} should be spelled TypeScript`, 168 | // source: 'ex' 169 | // }); 170 | // } 171 | // } 172 | // Send the computed diagnostics to VSCode. 173 | // connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); 174 | } 175 | 176 | // The content of a text document has changed. This event is emitted 177 | // when the text document first opened or when its content has changed. 178 | documents.onDidChangeContent((change) => { 179 | validateTextDocument(change.document); 180 | Current.log("---onDidChangeContent"); 181 | }); 182 | 183 | connection.onDidChangeWatchedFiles((watched) => { 184 | // trace('---','onDidChangeWatchedFiles'); 185 | watched.changes.forEach((e) => { 186 | let file: string; 187 | switch (e.type) { 188 | case FileChangeType.Created: 189 | file = fromUriString(e.uri); 190 | targetForSource(file).sources.add(file); 191 | break; 192 | case FileChangeType.Deleted: 193 | file = fromUriString(e.uri); 194 | targetForSource(file).sources.delete(file); 195 | break; 196 | default: 197 | //do nothing 198 | } 199 | }); 200 | }); 201 | 202 | // This handler provides the initial list of the completion items. 203 | connection.onCompletion(({ textDocument, position }): Thenable => { 204 | function removeSubstringFix(sub: string, replacement = ""): TextEdit[] { 205 | const prefixOffset = offset - Math.max(3, sub.length * 2 - 1); 206 | const prefix = srcText.slice(prefixOffset, offset); 207 | const lastOccurence = prefix.lastIndexOf(sub); 208 | if (lastOccurence === -1) return []; 209 | const duplicateKeywordRange = Range.create( 210 | document.positionAt(prefixOffset + lastOccurence), 211 | document.positionAt(prefixOffset + lastOccurence + sub.length) 212 | ); 213 | return [TextEdit.replace(duplicateKeywordRange, replacement)]; 214 | } 215 | function completionOfDuplicateFuncKeywordFix(kind: string): TextEdit[] { 216 | return (kind.includes("source.lang.swift.decl.function.") && removeSubstringFix("func ")) || []; 217 | } 218 | function completionOfDuplicateDotFix(kind: string): TextEdit[] { 219 | return ( 220 | (kind.includes("source.lang.swift.decl.function.") && removeSubstringFix("..", ".")) || [] 221 | ); 222 | } 223 | function combineFixes(kind: string, ...fixes: Array<(kind: string) => TextEdit[]>): TextEdit[] { 224 | return fixes.map((fix) => fix(kind)).reduce((all, next) => [...all, ...next], []); 225 | } 226 | 227 | const document: TextDocument = documents.get(textDocument.uri); 228 | const srcPath = document.uri.substring(7, document.uri.length); 229 | const srcText: string = document.getText(); //NOTE needs on-the-fly buffer 230 | const offset = document.offsetAt(position); //FIXME 231 | return sourcekitProtocol.codeComplete(srcText, srcPath, offset).then( 232 | function (completions: Object[] | null) { 233 | return (completions || []).map((c) => { 234 | let item = CompletionItem.create(c["key.description"]); 235 | item.kind = toCompletionItemKind(c["key.kind"]); 236 | item.detail = `${c["key.modulename"]}.${c["key.name"]}`; 237 | item.insertText = createSuggest(c["key.sourcetext"]); 238 | item.insertTextFormat = InsertTextFormat.Snippet; 239 | item.documentation = c["key.doc.brief"]; 240 | item.additionalTextEdits = combineFixes( 241 | c["key.kind"], 242 | completionOfDuplicateFuncKeywordFix, 243 | completionOfDuplicateDotFix 244 | ); 245 | return item; 246 | }); 247 | }, 248 | function (err) { 249 | //FIXME 250 | return err; 251 | } 252 | ); 253 | }); 254 | 255 | /** 256 | * ref: https://github.com/facebook/nuclide/blob/master/pkg/nuclide-swift/lib/sourcekitten/Complete.js#L57 257 | */ 258 | function createSuggest(sourcetext: string): string { 259 | let index = 1; 260 | let snp = sourcetext.replace(/<#T##(.+?)#>/g, (m, g) => { 261 | return "${" + index++ + ":" + g.split("##")[0] + "}"; 262 | }); 263 | const normalized = snp.replace("<#code#>", `\${${index++}}`); 264 | return normalized.startsWith(".") ? normalized.slice(1) : normalized; 265 | } 266 | 267 | //TODO more meanful CompletionItemKinds... 268 | function toCompletionItemKind(keyKind: string): CompletionItemKind { 269 | switch (keyKind) { 270 | case "source.lang.swift.decl.function.free": 271 | case "source.lang.swift.ref.function.free": 272 | return CompletionItemKind.Function; 273 | case "source.lang.swift.decl.function.method.instance": 274 | case "source.lang.swift.ref.function.method.instance": 275 | case "source.lang.swift.decl.function.method.static": 276 | case "source.lang.swift.ref.function.method.static": 277 | return CompletionItemKind.Method; 278 | case "source.lang.swift.decl.function.operator": 279 | case "source.lang.swift.ref.function.operator": 280 | case "source.lang.swift.decl.function.subscript": 281 | case "source.lang.swift.ref.function.subscript": 282 | return CompletionItemKind.Keyword; 283 | case "source.lang.swift.decl.function.constructor": 284 | case "source.lang.swift.ref.function.constructor": 285 | case "source.lang.swift.decl.function.destructor": 286 | case "source.lang.swift.ref.function.destructor": 287 | return CompletionItemKind.Constructor; 288 | case "source.lang.swift.decl.function.accessor.getter": 289 | case "source.lang.swift.ref.function.accessor.getter": 290 | case "source.lang.swift.decl.function.accessor.setter": 291 | case "source.lang.swift.ref.function.accessor.setter": 292 | return CompletionItemKind.Property; 293 | case "source.lang.swift.decl.class": 294 | case "source.lang.swift.ref.class": 295 | case "source.lang.swift.decl.struct": 296 | case "source.lang.swift.ref.struct": 297 | return CompletionItemKind.Class; 298 | case "source.lang.swift.decl.enum": 299 | case "source.lang.swift.ref.enum": 300 | return CompletionItemKind.Enum; 301 | case "source.lang.swift.decl.enumelement": 302 | case "source.lang.swift.ref.enumelement": 303 | return CompletionItemKind.Value; 304 | case "source.lang.swift.decl.protocol": 305 | case "source.lang.swift.ref.protocol": 306 | return CompletionItemKind.Interface; 307 | case "source.lang.swift.decl.typealias": 308 | case "source.lang.swift.ref.typealias": 309 | return CompletionItemKind.Reference; 310 | case "source.lang.swift.decl.var.instance": 311 | case "source.lang.swift.ref.var.instance": 312 | return CompletionItemKind.Field; 313 | case "source.lang.swift.decl.var.global": 314 | case "source.lang.swift.ref.var.global": 315 | case "source.lang.swift.decl.var.static": 316 | case "source.lang.swift.ref.var.static": 317 | case "source.lang.swift.decl.var.local": 318 | case "source.lang.swift.ref.var.local": 319 | return CompletionItemKind.Variable; 320 | 321 | case "source.lang.swift.decl.extension.struct": 322 | case "source.lang.swift.decl.extension.class": 323 | return CompletionItemKind.Class; 324 | case "source.lang.swift.decl.extension.enum": 325 | return CompletionItemKind.Enum; 326 | default: 327 | return CompletionItemKind.Text; //FIXME 328 | } 329 | } 330 | 331 | // This handler resolve additional information for the item selected in 332 | // the completion list. 333 | // connection.onCompletionResolve((item: CompletionItem): CompletionItem => { 334 | // if (item.data === 1) { 335 | // item.detail = 'TypeScript details', 336 | // item.documentation = 'TypeScript documentation' 337 | // } else if (item.data === 2) { 338 | // item.detail = 'JavaScript details', 339 | // item.documentation = 'JavaScript documentation' 340 | // } 341 | // return item; 342 | // }); 343 | 344 | connection.onHover(({ textDocument, position }): Promise => { 345 | const document: TextDocument = documents.get(textDocument.uri); 346 | const srcPath = document.uri.substring(7, document.uri.length); 347 | const srcText: string = document.getText(); //NOTE needs on-the-fly buffer 348 | const offset = document.offsetAt(position); //FIXME 349 | return sourcekitProtocol.cursorInfo(srcText, srcPath, offset).then( 350 | function (cursorInfo) { 351 | return extractHoverHelp(cursorInfo).then((mks) => { 352 | return { contents: mks || [] }; 353 | }); 354 | }, 355 | function (err) { 356 | //FIXME 357 | return err; 358 | } 359 | ); 360 | }); 361 | 362 | /** 363 | * sadasd 364 | * @param cursorInfo s 365 | */ 366 | async function extractHoverHelp(cursorInfo: Object): Promise { 367 | //local helper 368 | function extractText(elementName: string, full_as_xml: string) { 369 | let s = full_as_xml.indexOf(`<${elementName}>`); 370 | let e = full_as_xml.indexOf(``); 371 | let rt = full_as_xml.substring(s + elementName.length + 2, e); 372 | return rt; 373 | } 374 | //TODO wait vscode to support full html rendering... 375 | //stripe all sub elements 376 | function stripeOutTags(str) { 377 | return str.replace(/(<.[^(><.)]+>)/g, (m, c) => ""); 378 | } 379 | 380 | const keyKind = cursorInfo["key.kind"]; 381 | const keyName = cursorInfo["key.name"]; 382 | if (!keyName) { 383 | return null; 384 | } 385 | 386 | const full_as_xml = cursorInfo["key.doc.full_as_xml"]; 387 | const annotated_decl = cursorInfo["key.annotated_decl"]; 388 | const snippet = annotated_decl 389 | ? "```swift\n" + 390 | decode(stripeOutTags(extractText("Declaration", full_as_xml || annotated_decl))) + 391 | "\n```\n" 392 | : keyName; 393 | return [snippet, ...parseDocumentation(full_as_xml)]; //FIXME clickable keyTypename 394 | } 395 | 396 | connection.onDefinition(({ textDocument, position }): Promise => { 397 | const document: TextDocument = documents.get(textDocument.uri); 398 | const srcPath = document.uri.substring(7, document.uri.length); 399 | const srcText: string = document.getText(); //NOTE needs on-the-fly buffer 400 | const offset = document.offsetAt(position); //FIXME 401 | return sourcekitProtocol.cursorInfo(srcText, srcPath, offset).then( 402 | function (cursorInfo) { 403 | const filepath = cursorInfo["key.filepath"]; 404 | if (filepath) { 405 | const offset = cursorInfo["key.offset"]; 406 | const len = cursorInfo["key.length"]; 407 | const fileUri = `file://${filepath}`; 408 | let document: TextDocument = documents.get(fileUri); //FIXME 409 | //FIXME more here: https://github.com/Microsoft/language-server-protocol/issues/96 410 | if (!document) { 411 | //FIXME just make a temp doc to let vscode help us 412 | const content = fs.readFileSync(filepath, "utf8"); 413 | document = TextDocument.create(fileUri, "swift", 0, content); 414 | } 415 | return { 416 | uri: fileUri, 417 | range: { 418 | start: document.positionAt(offset), 419 | end: document.positionAt(offset + len), 420 | }, 421 | }; 422 | } else { 423 | return null; 424 | } 425 | }, 426 | function (err) { 427 | //FIXME 428 | return err; 429 | } 430 | ); 431 | }); 432 | 433 | function fromDocumentUri(document: { uri: string }): string { 434 | // return Files.uriToFilePath(document.uri); 435 | return fromUriString(document.uri); 436 | } 437 | 438 | function fromUriString(uri: string): string { 439 | return uri.substring(7, uri.length); 440 | } 441 | 442 | /* 443 | connection.onDidOpenTextDocument((params) => { 444 | // A text document got opened in VSCode. 445 | // params.uri uniquely identifies the document. For documents store on disk this is a file URI. 446 | // params.text the initial full content of the document. 447 | connection.console.log(`${params.uri} opened.`); 448 | }); 449 | 450 | connection.onDidChangeTextDocument((params) => { 451 | // The content of a text document did change in VSCode. 452 | // params.uri uniquely identifies the document. 453 | // params.contentChanges describe the content changes to the document. 454 | connection.console.log(`${params.uri} changed: ${JSON.stringify(params.contentChanges)}`); 455 | }); 456 | 457 | connection.onDidCloseTextDocument((params) => { 458 | // A text document got closed in VSCode. 459 | // params.uri uniquely identifies the document. 460 | connection.console.log(`${params.uri} closed.`); 461 | }); 462 | */ 463 | 464 | // Listen on the connection 465 | connection.listen(); 466 | 467 | //=== helper 468 | 469 | const xmlEntities = { 470 | "&": "&", 471 | """: '"', 472 | "<": "<", 473 | ">": ">", 474 | }; 475 | function decode(str) { 476 | return str.replace(/("|<|>|&)/g, (m, c) => xmlEntities[c]); 477 | } 478 | 479 | //FIX issue#15 480 | export function getShellExecPath() { 481 | return fs.existsSync(Current.config.shellPath) ? Current.config.shellPath : "/usr/bin/sh"; 482 | } 483 | -------------------------------------------------------------------------------- /src/sourcekites-server/sourcekit-xml.ts: -------------------------------------------------------------------------------- 1 | import * as convert from "xml-js"; 2 | 3 | interface Text { 4 | type: "cdata"; 5 | cdata: string; 6 | } 7 | interface Data { 8 | type: "text"; 9 | text: string; 10 | } 11 | 12 | interface ParentOf { 13 | type: "element"; 14 | elements: T[]; 15 | } 16 | 17 | interface NamedElement { 18 | type: "element"; 19 | name: Name; 20 | elements: Children; 21 | attributes: Attributes; 22 | } 23 | 24 | interface Root { 25 | elements: [ 26 | NamedElement< 27 | "Function" | "Class", 28 | Array 29 | > 30 | ]; 31 | } 32 | 33 | type Name = NamedElement<"Name", [Text]>; 34 | type Declaration = NamedElement<"Declaration", [Text]>; 35 | type USR = NamedElement<"USR", [Text]>; 36 | type CommentParts = NamedElement<"CommentParts", Array>; // TODO 37 | type Parameters = NamedElement<"Parameters", Array>; // TODO 38 | type Parameter = NamedElement<"Parameter", Array>; 39 | type Direction = NamedElement<"Direction", [Text]>; 40 | type Block = NamedElement< 41 | "Abstract" | "Discussion" | "Item" | "ResultDiscussion" | "ThrowsDiscussion", 42 | Array 43 | >; 44 | type CodeListing = NamedElement< 45 | "CodeListing", 46 | Array, 47 | { language?: string } 48 | >; 49 | type CodeLineNumbered = NamedElement<"zCodeLineNumbered", [Data]>; 50 | type List = NamedElement<"List-Bullet" | "List-Number", Array>; 51 | type Para = NamedElement<"Para", Array>; // TODO 52 | type CodeVoice = NamedElement<"codeVoice", [Text]>; 53 | 54 | type SkElement = 55 | | Name 56 | | Declaration 57 | | USR 58 | | CommentParts 59 | | Parameters 60 | | Parameter 61 | | Direction 62 | | Block 63 | | CodeListing 64 | | CodeLineNumbered 65 | | List 66 | | Para 67 | | CodeVoice; 68 | 69 | export function parseDocumentation(xml: string | null): string[] { 70 | if (xml == null) return []; 71 | const root = convert.xml2js(xml, { 72 | captureSpacesBetweenElements: true 73 | }) as Root; 74 | return root.elements.map(e => 75 | e.elements 76 | .filter(e => e.name === SkElementType.CommentParts) 77 | .map(sk2md) 78 | .join("") 79 | ); 80 | } 81 | 82 | enum SkElementType { 83 | cdata = "cdata", 84 | text = "text", 85 | element = "element", 86 | Name = "Name", 87 | Declaration = "Declaration", 88 | USR = "USR", 89 | CommentParts = "CommentParts", 90 | Parameters = "Parameters", 91 | Parameter = "Parameter", 92 | Direction = "Direction", 93 | Abstract = "Abstract", 94 | Discussion = "Discussion", 95 | Item = "Item", 96 | ResultDiscussion = "ResultDiscussion", 97 | ThrowsDiscussion = "ThrowsDiscussion", 98 | CodeListing = "CodeListing", 99 | zCodeLineNumbered = "zCodeLineNumbered", 100 | ListBullet = "List-Bullet", 101 | ListNumber = "List-Number", 102 | Para = "Para", 103 | codeVoice = "codeVoice" 104 | } 105 | 106 | function sk2md(element: SkElement | Text | Data): string { 107 | switch (element.type) { 108 | case SkElementType.cdata: 109 | return element.cdata; 110 | case SkElementType.text: 111 | return element.text; 112 | case "element": 113 | return skElement2md(element); 114 | } 115 | } 116 | 117 | function skElement2md(element: SkElement): string { 118 | const children = ( 119 | opt: { sep?: string; map?: (v: string, i: number) => string } = {}, 120 | els?: SkElement[] 121 | ) => 122 | (els || (element.elements as Array) || []) 123 | .map(sk2md) 124 | .map(opt.map || ((id: string) => id)) 125 | .join(opt.sep || ""); 126 | switch (element.name) { 127 | case SkElementType.Abstract: 128 | case SkElementType.CommentParts: 129 | return children(); 130 | case SkElementType.Discussion: 131 | return "\n\n**Discussion:**\n\n" + children() + "\n\n"; 132 | case SkElementType.ThrowsDiscussion: 133 | return "\n\n**Throws:**" + children() + "\n\n"; 134 | case SkElementType.ResultDiscussion: 135 | return "\n\n**Returns:**" + children() + "\n\n"; 136 | case SkElementType.CodeListing: 137 | return ( 138 | "\n```" + 139 | ((element as CodeListing).attributes.language || "") + 140 | "\n" + 141 | children({ sep: "\n" }) + 142 | "\n```\n" 143 | ); 144 | case SkElementType.codeVoice: 145 | return "`" + children() + "`"; 146 | case SkElementType.Declaration: 147 | return children(); 148 | case SkElementType.Item: 149 | return children(); 150 | case SkElementType.ListBullet: 151 | return "\n" + children({ sep: "\n", map: c => "* " + c }) + "\n"; 152 | case SkElementType.ListNumber: 153 | return ( 154 | "\n" + children({ sep: "\n", map: (c, i) => i + 1 + ". " + c }) + "\n" 155 | ); 156 | case SkElementType.Name: 157 | return "**" + children() + "**"; 158 | case SkElementType.Para: 159 | return "\n" + children() + "\n"; 160 | case SkElementType.Parameter: 161 | const name = children( 162 | {}, 163 | [].concat( 164 | ...element.elements 165 | .filter(e => e.name === SkElementType.Name) 166 | .map(e => e.elements) 167 | ) 168 | ); 169 | const discussion = children( 170 | {}, 171 | [].concat( 172 | ...element.elements 173 | .filter(e => e.name === SkElementType.Discussion) 174 | .map(e => e.elements) 175 | ) 176 | ); 177 | return discussion.length > 0 178 | ? `**${name}:** ${discussion}` 179 | : `**${name}**`; 180 | case SkElementType.Parameters: 181 | return ( 182 | "\n\n**Parameters:**" + children({ map: c => "\n* " + c }) + "\n\n" 183 | ); 184 | case SkElementType.zCodeLineNumbered: 185 | return children(); 186 | 187 | case SkElementType.Direction: // unknown 188 | return ""; 189 | default: 190 | return ( 191 | "**<" + 192 | element.name + 193 | ">** " + 194 | children() + 195 | "**<" + 196 | element.name + 197 | ">** " 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/sourcekites-server/sourcekites.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as server from "./server"; 4 | 5 | import * as yaml from "js-yaml"; 6 | import { ChildProcess } from "child_process"; 7 | import { Current } from "./current"; 8 | 9 | let skProtocolProcess: ChildProcess | null = null; 10 | let skeHandler: SourcekiteResponseHandler | null = null; 11 | export function initializeSourcekite() { 12 | if (skProtocolProcess == null) { 13 | initializeSKProtocolProcess(); 14 | } 15 | } 16 | 17 | function terminateSourcekite() { 18 | if (skProtocolProcess != null) { 19 | skProtocolProcess.kill(); 20 | } 21 | } 22 | 23 | function restartSourcekite() { 24 | terminateSourcekite(); 25 | initializeSourcekite(); 26 | } 27 | 28 | function createSkProtocolProcess() { 29 | const env = { 30 | ...process.env, 31 | TOOLCHAIN_DIR: Current.config.toolchainPath || process.env["TOOLCHAIN_DIR"] 32 | }; 33 | if (server.skProtocolProcessAsShellCmd) { 34 | const volumes = Current.config.workspacePaths.map( 35 | path => `-v '${path}:${path}'` 36 | ); 37 | return server.spawn( 38 | server.getShellExecPath(), 39 | ["-c", `docker run --rm ${volumes} -i jinmingjian/docker-sourcekite`], 40 | { env } 41 | ); 42 | } else { 43 | return server.spawn(server.skProtocolPath, [], { env }); 44 | } 45 | } 46 | 47 | function initializeSKProtocolProcess() { 48 | Current.log( 49 | "sourcekite", 50 | `***sourcekite initializing with skProtocolProcess at [${server.skProtocolPath}]` 51 | ); 52 | 53 | const pathSourcekite = Current.config.sourcekitePath; 54 | 55 | skProtocolProcess = createSkProtocolProcess(); 56 | skProtocolProcess.stderr.on("data", data => { 57 | Current.log("sourcekite", "***stderr***" + data); 58 | }); 59 | skProtocolProcess.on("exit", function(code, signal) { 60 | Current.log("sourcekite", "[exited]", `code: ${code}, signal: ${signal}`); 61 | //NOTE this is not guaranteed to reboot, but we just want it 'not guaranteed' 62 | skProtocolProcess = createSkProtocolProcess(); 63 | }); 64 | skProtocolProcess.on("error", function(err) { 65 | Current.log( 66 | "sourcekite", 67 | "***sourcekitd_repl error***" + (err).message 68 | ); 69 | if ((err).message.indexOf("ENOENT") > 0) { 70 | const msg = 71 | "The '" + 72 | pathSourcekite + 73 | "' command is not available." + 74 | " Please check your swift executable user setting and ensure it is installed."; 75 | Current.log("sourcekite", "***sourcekitd_repl not found***" + msg); 76 | } 77 | throw err; 78 | }); 79 | skeHandler = new SourcekiteResponseHandler(); 80 | } 81 | 82 | enum ParsingState { 83 | endResponse, 84 | startResponseContent 85 | } 86 | 87 | //assumption:single-thread 88 | class SourcekiteResponseHandler { 89 | private static nResponsesSlot = 64; //FIXME config options? 90 | // private static responseTimeoutMills = 15 * 1000 //FIXME 91 | 92 | private rids = new Array(SourcekiteResponseHandler.nResponsesSlot); //for checking 93 | private responses = new Array(SourcekiteResponseHandler.nResponsesSlot); 94 | private responsesProcessed = Array.from( 95 | new Array(SourcekiteResponseHandler.nResponsesSlot) 96 | ).map((_, i) => true); 97 | private responseResolves = new Array( 98 | SourcekiteResponseHandler.nResponsesSlot 99 | ); 100 | private responseRejects = new Array( 101 | SourcekiteResponseHandler.nResponsesSlot 102 | ); 103 | 104 | constructor() { 105 | skProtocolProcess.stdout.on("data", this.handleResponse.bind(this)); 106 | Current.log("-->SourcekiteResponseHandler constructor done"); 107 | } 108 | 109 | // private hasError = false 110 | private output = ""; 111 | // private rid = -1 112 | private handleResponse(data): void { 113 | this.output += data; 114 | if (Current.config.isTracingOn) { 115 | Current.log("SourcekiteResponseHandler", `${data}`); 116 | } 117 | if (this.output.endsWith("}\n\n")) { 118 | const idx = this.output.indexOf("\n"); 119 | const ridstr = this.output.substring(0, idx); 120 | const rid = parseInt(ridstr); 121 | if (isNaN(rid)) { 122 | throw new Error("wrong format for reqid"); 123 | } 124 | const res = this.output.substring(idx + 1); 125 | const slot = this.getSlot(rid); 126 | const resolve = this.responseResolves[slot]; 127 | const reject = this.responseRejects[slot]; 128 | this.output = ""; 129 | this.responsesProcessed[slot] = true; 130 | try { 131 | resolve(res); 132 | } catch (e) { 133 | Current.log(`---error: ${e}`); 134 | reject(e); 135 | } 136 | } 137 | } 138 | 139 | public getResponse(rid: number): Promise { 140 | // const start = new Date().getTime() //FIXME enable timeout? 141 | return new Promise((resolve, reject) => { 142 | const slot = this.getSlot(rid); 143 | //FIXME enable timeout?reject only when covered by next replacer 144 | if (!this.responsesProcessed[slot]) { 145 | const rjt = this.responseRejects[slot]; 146 | rjt(`fail to process the request[reqid=${this.rids[slot]}]`); 147 | } 148 | this.rids[slot] = rid; 149 | this.responseResolves[slot] = resolve; 150 | this.responseRejects[slot] = reject; 151 | this.responsesProcessed[slot] = false; 152 | }); 153 | } 154 | 155 | private getSlot(rid: number): number { 156 | return rid % SourcekiteResponseHandler.nResponsesSlot; 157 | } 158 | } 159 | 160 | let reqCount = 0; //FIXME 161 | 162 | type RequestType = "codecomplete" | "cursorinfo" | "demangle" | "editor.open"; 163 | 164 | function pluck(prop: K): (ofTarget: T) => T[K] { 165 | return target => target[prop]; 166 | } 167 | 168 | function typedResponse( 169 | request: string, 170 | requestType: RequestType, 171 | extraState: any = null, 172 | retries = 0 173 | ): Promise { 174 | function parseSkResponse(resp: string): any { 175 | return yaml.load(resp); 176 | } 177 | 178 | Current.log("request", request); 179 | const rid = reqCount++; 180 | skProtocolProcess.stdin.write(rid + "\n"); 181 | skProtocolProcess.stdin.write(request); 182 | return skeHandler 183 | .getResponse(rid) 184 | .catch(e => { 185 | console.log("Request did fail", requestType, e); 186 | if (retries > 5) { 187 | console.log("Request failed too many times. Abort."); 188 | throw "Request failed too many times. Abort."; 189 | } else { 190 | restartSourcekite(); 191 | return typedResponse(request, requestType, extraState, retries); 192 | } 193 | }) 194 | .then(parseSkResponse); 195 | } 196 | 197 | function request( 198 | requestType: RequestType, 199 | srcText: string, 200 | srcPath: string, 201 | offset: number 202 | ): Promise { 203 | function targetArgumentsForImport( 204 | lib: string, 205 | platform: string, 206 | target: string 207 | ): string[] | null { 208 | return loadedArgs.indexOf("-target") === -1 && 209 | srcText.includes(`import ${lib}`) 210 | ? [ 211 | "-target", 212 | target, 213 | "-sdk", 214 | `/Applications/Xcode.app/Contents/Developer/Platforms/${platform}.platform/Developer/SDKs/${platform}.sdk` 215 | ] 216 | : null; 217 | } 218 | function defaultTargetArguments() { 219 | if (loadedArgs.indexOf("-target") !== -1) { 220 | return []; 221 | } 222 | return process.platform === "linux" 223 | ? ["-target", "x86_64-unknown-linux"] 224 | : [ 225 | "-target", 226 | "x86_64-apple-macosx10.10", 227 | "-sdk", 228 | `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk` 229 | ]; 230 | } 231 | 232 | const target = server.targetForSource(srcPath); 233 | const sourcePaths = Array.from(target.sources); 234 | const loadedArgs = target.compilerArguments; 235 | /*const inferredOSArgs = process.platform === 'darwin' 236 | ? ["-target", "x86_64-apple-macosx10.10"]*/ 237 | const inferredTargetArgs = 238 | targetArgumentsForImport("UIKit", "iPhoneOS", "arm64-apple-ios11.0") || 239 | targetArgumentsForImport( 240 | "WatchKit", 241 | "WatchOS", 242 | "armv7k-apple-watchos4.0" 243 | ) || 244 | targetArgumentsForImport("AppKit", "MacOSX", "x86_64-apple-macosx10.10") || 245 | defaultTargetArguments(); 246 | const compilerargs = JSON.stringify([ 247 | ...target.compilerArguments, 248 | ...(sourcePaths || [srcPath]), 249 | ...inferredTargetArgs 250 | ]); 251 | 252 | srcText = JSON.stringify(srcText); 253 | let request = `{ 254 | key.request: source.request.${requestType}, 255 | key.sourcefile: "${srcPath}", 256 | key.offset: ${offset}, 257 | key.compilerargs: ${compilerargs}, 258 | key.sourcetext: ${srcText} 259 | } 260 | 261 | `; 262 | return typedResponse(request, requestType); 263 | } 264 | 265 | //== codeComplete 266 | export function codeComplete( 267 | srcText: string, 268 | srcPath: string, 269 | offset: number 270 | ): Promise { 271 | return request("codecomplete", srcText, srcPath, offset).then( 272 | pluck("key.results") 273 | ); 274 | } 275 | 276 | //== cursorInfo 277 | export function cursorInfo( 278 | srcText: string, 279 | srcPath: string, 280 | offset: number 281 | ): Promise { 282 | return request("cursorinfo", srcText, srcPath, offset); 283 | } 284 | 285 | //== demangle 286 | export function demangle(...demangledNames: string[]): Promise { 287 | const names = JSON.stringify(demangledNames.join(",")); 288 | let request = `{ 289 | key.request: source.request.demangle, 290 | key.names: [${names}] 291 | } 292 | 293 | `; 294 | return typedResponse(request, "demangle").then(pluck("key.results")); 295 | } 296 | -------------------------------------------------------------------------------- /src/sourcekites-server/thenable.d.ts: -------------------------------------------------------------------------------- 1 | //workaround for thenable:https://github.com/Microsoft/vscode-extension-vscode/issues/41 2 | interface Thenable extends PromiseLike {} 3 | -------------------------------------------------------------------------------- /src/toolchain/SwiftTools.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as cp from "child_process"; 4 | import * as fs from "fs"; 5 | import * as path from "path"; 6 | import { Duplex, Readable, Stream } from "stream"; 7 | import { 8 | commands, 9 | Diagnostic, 10 | DiagnosticCollection, 11 | DiagnosticSeverity, 12 | Disposable, 13 | languages, 14 | Range, 15 | Uri, 16 | workspace, 17 | } from "vscode"; 18 | import * as config from "../vscode/config-helpers"; 19 | import output, { LogStream } from "../vscode/output-channels"; 20 | import { statusBarItem } from "../vscode/status-bar"; 21 | 22 | type OnProcExit = (code: number, signal: NodeJS.Signals) => void; 23 | type ChildProc = cp.ChildProcessWithoutNullStreams; 24 | type ProcAndOutput = { proc: ChildProc; output: Promise }; 25 | 26 | const DiagnosticFirstLine = /(.+?):(\d+):(\d+): (error|warning|note|.+?): (.+)/; 27 | 28 | export function setRunning(isRunning: boolean) { 29 | commands.executeCommand("setContext", "sde:running", isRunning); 30 | } 31 | 32 | export function swiftPackageExists(): boolean { 33 | const manifestPath = workspace.workspaceFolders 34 | ? path.join(workspace.workspaceFolders[0].uri.fsPath, "Package.swift") 35 | : null; 36 | return manifestPath && fs.existsSync(manifestPath); 37 | } 38 | 39 | export function shouldBuildOnSave(): boolean { 40 | return config.buildOnSave() && swiftPackageExists(); 41 | } 42 | 43 | export class Toolchain { 44 | private swiftBinPath: string; 45 | private basePath: string; 46 | private buildArgs: string[]; 47 | private buildProc?: ChildProc; 48 | private runProc?: ChildProc; 49 | private _diagnostics?: DiagnosticCollection; 50 | 51 | constructor(swiftPath: string, pkgBasePath: string, args: string[]) { 52 | this.swiftBinPath = swiftPath; 53 | this.basePath = pkgBasePath; 54 | this.buildArgs = args; 55 | } 56 | 57 | // Getters 58 | get isRunning(): boolean { 59 | return this.runProc != undefined; 60 | } 61 | 62 | get diagnostics(): DiagnosticCollection { 63 | if (!this._diagnostics) { 64 | this._diagnostics = languages.createDiagnosticCollection("swift"); 65 | } 66 | return this._diagnostics; 67 | } 68 | 69 | // Public API 70 | /** 71 | * @returns A Disposable that can be used to stop this instance of the Toolchain 72 | */ 73 | start(): Disposable { 74 | return { 75 | dispose: () => this.stop(), 76 | }; 77 | } 78 | 79 | /** 80 | * Stops this instance of the Toolchain 81 | */ 82 | stop() { 83 | if (this.buildProc) { 84 | console.log("Stopping build proc"); 85 | } 86 | this.buildProc?.kill(); 87 | if (this.runProc) { 88 | console.log("Stopping run proc"); 89 | } 90 | this.runProc?.kill(); 91 | } 92 | 93 | private spawnSwiftProc(args: string[], logs: LogStream, onExit: OnProcExit): ProcAndOutput { 94 | // let oPipe = new Duplex({ highWaterMark: 1024, allowHalfOpen: false }); 95 | // let ePipe = new Duplex({ highWaterMark: 1024, allowHalfOpen: false }); 96 | const proc = cp.spawn(this.swiftBinPath, args, { 97 | cwd: this.basePath, 98 | // stdio: ["ignore", oPipe, ePipe], 99 | }); 100 | proc.stderr.on("data", data => { 101 | logs.write(`${data}`); 102 | }); 103 | let stdout = ""; 104 | proc.stdout.on("data", data => { 105 | stdout += data; 106 | logs.write(`${data}`); 107 | }); 108 | const promise = new Promise((resolve, reject) => { 109 | logs.log(`pid: ${proc.pid} - ${this.swiftBinPath} ${args.join(" ")}`); 110 | proc.on("error", err => { 111 | logs.log(`[Error] ${err.message}`); 112 | reject(err); 113 | }); 114 | proc.on("exit", (code, signal) => { 115 | resolve(stdout); 116 | onExit(code, signal); 117 | }); 118 | }); 119 | return { proc, output: promise }; 120 | } 121 | 122 | build(target: string = "") { 123 | output.build.clear(); 124 | output.build.log("-- Build Started --"); 125 | const start = Date.now(); 126 | const buildArgs = [...this.buildArgs]; 127 | if (target) { 128 | buildArgs.unshift(target); 129 | } 130 | if (!["build", "test"].includes(buildArgs[0])) { 131 | buildArgs.unshift("build"); 132 | } 133 | statusBarItem.start(); 134 | try { 135 | const { proc, output: buildOutput } = this.spawnSwiftProc(buildArgs, output.build, code => { 136 | const duration = Date.now() - start; 137 | if (code != 0) { 138 | statusBarItem.failed(); 139 | output.build.log(`-- Build Failed (${(duration / 1000).toFixed(1)}s) --`, true); 140 | } else { 141 | statusBarItem.succeeded(); 142 | output.build.log(`-- Build Succeeded (${(duration / 1000).toFixed(1)}s) --`); 143 | } 144 | this.buildProc = undefined; 145 | }); 146 | buildOutput.then(buildOutput => this.generateDiagnostics(buildOutput)); 147 | this.buildProc = proc; 148 | } catch (e) { 149 | console.log(e); 150 | } 151 | } 152 | 153 | private generateDiagnostics(buildOutput: string = "") { 154 | this._diagnostics.clear(); 155 | const newDiagnostics = new Map(); 156 | const lines = buildOutput.split("\n"); 157 | for (let i = 0; i < lines.length; i++) { 158 | const line = lines[i]; 159 | const match = line.match(DiagnosticFirstLine); 160 | if (!match) { 161 | // console.log(`line did not match - '${line}'`); 162 | continue; 163 | // } else { 164 | // console.log(`found diagnostic - 165 | // ${line} 166 | // ${lines[i + 1]} 167 | // ${lines[i + 2]} 168 | // -------`); 169 | // console.log(match); 170 | } 171 | const [_, file, lineNumStr, startColStr, swiftSev, message] = match; 172 | // vscode used 0 indexed lines and columns 173 | const lineNum = parseInt(lineNumStr, 10) - 1; 174 | const startCol = parseInt(startColStr, 10) - 1; 175 | const endCol = lines[i + 2].trimEnd().length - 1; 176 | const range = new Range(lineNum, startCol, lineNum, endCol); 177 | const diagnostic = new Diagnostic(range, message, toVSCodeSeverity(swiftSev)); 178 | diagnostic.source = "sourcekitd"; 179 | if (!newDiagnostics.has(file)) { 180 | newDiagnostics.set(file, []); 181 | } 182 | newDiagnostics.get(file).push(diagnostic); 183 | } 184 | for (const entry of newDiagnostics) { 185 | const [file, diagnostics] = entry; 186 | if (file.includes("checkouts")) { 187 | continue; 188 | } 189 | // TODO: check for overlapping diagnostic ranges and collapse into `diagnostic.relatedInformation` 190 | const uri = Uri.parse(file); 191 | // TODO: check to see if sourcekitd already has diagnostics for this file 192 | this._diagnostics.set(uri, diagnostics); 193 | } 194 | } 195 | 196 | runStart(target: string = "") { 197 | setRunning(true); 198 | output.run.clear(); 199 | output.run.log(`running ${target ? target : "package"}…`); 200 | const { proc } = this.spawnSwiftProc( 201 | target ? ["run", target] : ["run"], 202 | output.run, 203 | (code, signal) => { 204 | // handle termination here 205 | output.run.log(`Process exited. code=${code} signal=${signal}`); 206 | setRunning(false); 207 | this.runProc = undefined; 208 | } 209 | ); 210 | this.runProc = proc; 211 | } 212 | 213 | runStop() { 214 | setRunning(false); 215 | output.run.log(`stopping`); 216 | this.runProc.kill(); 217 | this.runProc = undefined; 218 | } 219 | 220 | clean() { 221 | statusBarItem.start("cleaning"); 222 | this.spawnSwiftProc(["package clean"], output.build, (code, signal) => { 223 | statusBarItem.succeeded("clean"); 224 | output.build.log("done"); 225 | }); 226 | } 227 | } 228 | 229 | function toVSCodeSeverity(sev: string) { 230 | switch (sev) { 231 | case "error": 232 | return DiagnosticSeverity.Error; 233 | case "warning": 234 | return DiagnosticSeverity.Warning; 235 | case "note": 236 | return DiagnosticSeverity.Information; 237 | default: 238 | return DiagnosticSeverity.Hint; //FIXME 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/vscode/config-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | import { workspace, ExtensionContext } from "vscode"; 4 | import { ServerOptions, TransportKind, Executable } from "vscode-languageclient/node"; 5 | 6 | export enum LangaugeServerMode { 7 | SourceKit = "sourcekit-lsp", 8 | LanguageServer = "langserver", 9 | SourceKite = "sourcekite", 10 | } 11 | 12 | /** 13 | * @returns which language server to use 14 | */ 15 | export function lsp(): LangaugeServerMode { 16 | return workspace 17 | .getConfiguration() 18 | .get("sde.languageServerMode", LangaugeServerMode.SourceKit); 19 | } 20 | 21 | /** 22 | * @returns if the project should be built when a file is saved 23 | */ 24 | export function buildOnSave(): boolean { 25 | return workspace.getConfiguration().get("sde.buildOnSave", true); 26 | } 27 | 28 | /** 29 | * @returns if build logging is enabled 30 | */ 31 | export function isBuildTracingOn(): boolean { 32 | return workspace.getConfiguration().get("sde.enableTracing.client"); 33 | } 34 | 35 | export function isLSPTracingOn(): boolean { 36 | return workspace.getConfiguration().get("sde.enableTracing.LSPServer"); 37 | } 38 | 39 | /** 40 | * get server options for 41 | * @param context the current extension context 42 | */ 43 | export function sourcekiteServerOptions(context: ExtensionContext): ServerOptions { 44 | // The server is implemented in node 45 | const serverModule = context.asAbsolutePath(path.join("out/sourcekites-server", "server.js")); 46 | // The debug options for the server 47 | const debugOptions = { 48 | execArgv: ["--nolazy", "--inspect=6004"], 49 | ...process.env, 50 | }; 51 | 52 | // If the extension is launched in debug mode then the debug server options are used 53 | // Otherwise the run options are used 54 | const serverOptions: ServerOptions = { 55 | run: { 56 | module: serverModule, 57 | transport: TransportKind.ipc, 58 | options: debugOptions, 59 | }, 60 | debug: { 61 | module: serverModule, 62 | transport: TransportKind.ipc, 63 | options: debugOptions, 64 | }, 65 | }; 66 | return serverOptions; 67 | } 68 | 69 | export function languageServerPath(): string { 70 | return workspace 71 | .getConfiguration("swift") 72 | .get("languageServerPath", "/usr/local/bin/LanguageServer"); 73 | } 74 | export function lspServerOptions(): ServerOptions { 75 | // Load the path to the language server from settings 76 | const executableCommand = languageServerPath(); 77 | 78 | const run: Executable = { 79 | command: executableCommand, 80 | options: process.env, 81 | }; 82 | const debug: Executable = run; 83 | const serverOptions: ServerOptions = { 84 | run: run, 85 | debug: debug, 86 | }; 87 | return serverOptions; 88 | } 89 | 90 | export function toolchainPath(): string { 91 | return workspace.getConfiguration("sourcekit-lsp").get("toolchainPath"); 92 | } 93 | export function sourcekitLspServerOptions(): ServerOptions { 94 | const toolchain = workspace.getConfiguration("sourcekit-lsp").get("toolchainPath"); 95 | 96 | const sourcekitPath = sourceKitLSPLocation(toolchain); 97 | 98 | // sourcekit-lsp takes -Xswiftc arguments like "swift build", but it doesn't need "build" argument 99 | const sourceKitArgs = workspace 100 | .getConfiguration() 101 | .get("sde.swiftBuildingParams", []) 102 | .filter(param => param !== "build"); 103 | 104 | const env: NodeJS.ProcessEnv = toolchain 105 | ? { ...process.env, SOURCEKIT_TOOLCHAIN_PATH: toolchain } 106 | : process.env; 107 | 108 | const run: Executable = { 109 | command: sourcekitPath, 110 | options: { env }, 111 | args: sourceKitArgs, 112 | }; 113 | const serverOptions: ServerOptions = run; 114 | return serverOptions; 115 | } 116 | 117 | export function sourceKitLSPLocation(toolchain: string | undefined): string { 118 | const explicit = workspace 119 | .getConfiguration("sourcekit-lsp") 120 | .get("serverPath", null); 121 | if (explicit) return explicit; 122 | 123 | const sourcekitLSPPath = path.resolve( 124 | toolchain || "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain", 125 | "usr/bin/sourcekit-lsp" 126 | ); 127 | const isPreinstalled = fs.existsSync(sourcekitLSPPath); 128 | if (isPreinstalled) { 129 | return sourcekitLSPPath; 130 | } 131 | 132 | return workspace 133 | .getConfiguration("swift") 134 | .get("languageServerPath", "/usr/local/bin/sourcekit-lsp"); 135 | } 136 | -------------------------------------------------------------------------------- /src/vscode/lsp-interop.ts: -------------------------------------------------------------------------------- 1 | import { workspace, ExtensionContext, Disposable } from "vscode"; 2 | import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node"; 3 | import { absolutePath } from "../helpers/AbsolutePath"; 4 | import * as config from "./config-helpers"; 5 | import { LangaugeServerMode } from "./config-helpers"; 6 | import { currentLspPreconditions } from "./lsp-preconditions"; 7 | 8 | function currentServerOptions(context: ExtensionContext): ServerOptions { 9 | switch (config.lsp()) { 10 | case LangaugeServerMode.LanguageServer: 11 | return config.lspServerOptions(); 12 | case LangaugeServerMode.SourceKit: 13 | return config.sourcekitLspServerOptions(); 14 | case LangaugeServerMode.SourceKite: 15 | return config.sourcekiteServerOptions(context); 16 | } 17 | } 18 | 19 | function currentClientOptions(): Partial { 20 | switch (config.lsp()) { 21 | case LangaugeServerMode.SourceKit: 22 | return { 23 | documentSelector: ["swift", "objective-c", "objective-cpp"], 24 | synchronize: undefined, 25 | }; 26 | case LangaugeServerMode.SourceKite: 27 | return { 28 | initializationOptions: { 29 | isLSPServerTracingOn: config.isLSPTracingOn(), 30 | skProtocolProcess: absolutePath( 31 | workspace.getConfiguration().get("swift.path.sourcekite") 32 | ), 33 | skProtocolProcessAsShellCmd: workspace 34 | .getConfiguration() 35 | .get("swift.path.sourcekiteDockerMode"), 36 | skCompilerOptions: workspace.getConfiguration().get("sde.sourcekit.compilerOptions"), 37 | toolchainPath: 38 | workspace.getConfiguration("sourcekit-lsp").get("toolchainPath") || null, 39 | }, 40 | }; 41 | default: 42 | return {}; 43 | } 44 | } 45 | 46 | let lspClient: LanguageClient | undefined; 47 | let clientDisposable: Disposable | undefined; 48 | 49 | /** 50 | * Starts the LSP client (which specifies how to start the LSP server), and registers 51 | * a dispoasble in the extension context. 52 | * @param context the SDE extension context 53 | */ 54 | async function startLSPClient(context: ExtensionContext) { 55 | await currentLspPreconditions(); 56 | let clientOptions: LanguageClientOptions = { 57 | // Register the server for plain text documentss 58 | documentSelector: [ 59 | { language: "swift", scheme: "file" }, 60 | { pattern: "*.swift", scheme: "file" }, 61 | ], 62 | synchronize: { 63 | configurationSection: ["swift", "editor", "[swift]"], 64 | // Notify the server about file changes to '.clientrc files contain in the workspace 65 | fileEvents: [ 66 | workspace.createFileSystemWatcher("**/*.swift"), 67 | workspace.createFileSystemWatcher(".build/*.yaml"), 68 | ], 69 | }, 70 | ...currentClientOptions(), 71 | }; 72 | // Create the language client and start the client. 73 | const lspOpts = currentServerOptions(context); 74 | lspClient = new LanguageClient("Swift", lspOpts, clientOptions); 75 | clientDisposable = lspClient.start(); 76 | context.subscriptions.push(clientDisposable); 77 | } 78 | 79 | /** 80 | * Stops the current LSP client and starts a new client. 81 | * The client is stopped using the disposable returned from `client.start()` 82 | * @param context the SDE extension context 83 | */ 84 | async function restartLSPClient(context: ExtensionContext) { 85 | clientDisposable.dispose(); 86 | await startLSPClient(context); 87 | } 88 | 89 | export default { 90 | startLSPClient, 91 | restartLSPClient, 92 | }; 93 | -------------------------------------------------------------------------------- /src/vscode/lsp-preconditions.ts: -------------------------------------------------------------------------------- 1 | import { commands, Uri, window, workspace } from "vscode"; 2 | import { absolutePath } from "../helpers/AbsolutePath"; 3 | import * as config from "./config-helpers"; 4 | import { LangaugeServerMode } from "./config-helpers"; 5 | import * as fs from "fs"; 6 | 7 | export async function currentLspPreconditions(): Promise { 8 | await generalPreconditions(); 9 | switch (config.lsp()) { 10 | case LangaugeServerMode.LanguageServer: 11 | return currentLanguageServerPreconditions(); 12 | case LangaugeServerMode.SourceKit: 13 | return currentSourcekitLspPreconditions(); 14 | case LangaugeServerMode.SourceKite: 15 | return currentSourcekitePreconditions(); 16 | } 17 | } 18 | 19 | enum PreconditionActions { 20 | Retry = "Retry", 21 | OpenSettings = "Settings", 22 | InstructionsSourcekitLsp = "Help for sourcekit-lsp", 23 | InstructionsSourcekite = "Help for sourcekite", 24 | InstructionsLangserver = "Help for LangserverSwift", 25 | } 26 | 27 | async function generalPreconditions(): Promise { 28 | const shellPath = absolutePath(workspace.getConfiguration().get("swift.path.shell")); 29 | if (!shellPath || !fs.existsSync(shellPath)) { 30 | await handlePreconditionAction( 31 | await window.showErrorMessage( 32 | `Wrong shell path ${shellPath} for setting swift.path.shell.`, 33 | PreconditionActions.Retry, 34 | PreconditionActions.OpenSettings 35 | ) 36 | ); 37 | } 38 | 39 | const swiftPath = absolutePath(workspace.getConfiguration().get("swift.path.swift_driver_bin")); 40 | if (!swiftPath || !fs.existsSync(swiftPath)) { 41 | await handlePreconditionAction( 42 | await window.showErrorMessage( 43 | `Swift not found at path ${swiftPath} for setting swift.path.swift_driver_bin`, 44 | PreconditionActions.Retry, 45 | PreconditionActions.OpenSettings 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | async function currentLanguageServerPreconditions(): Promise { 52 | const lspPath = config.languageServerPath(); 53 | if (!fs.existsSync(lspPath)) { 54 | await handlePreconditionAction( 55 | await window.showErrorMessage( 56 | `Langserver not found at \`${lspPath}\`. 57 | Install it and provide the path to \`swift.languageServerPath\`.`, 58 | PreconditionActions.Retry, 59 | PreconditionActions.OpenSettings, 60 | PreconditionActions.InstructionsLangserver 61 | ) 62 | ); 63 | } 64 | } 65 | 66 | async function currentSourcekitLspPreconditions(): Promise { 67 | const sourcekitLspPath = config.sourceKitLSPLocation(config.toolchainPath()); 68 | 69 | if (!fs.existsSync(sourcekitLspPath)) { 70 | await handlePreconditionAction( 71 | await window.showErrorMessage( 72 | `sourcekit-lsp not found at \`${sourcekitLspPath}\`. 73 | Install it and provide the path to \`sourcekit-lsp.serverPath\`.`, 74 | PreconditionActions.Retry, 75 | PreconditionActions.OpenSettings, 76 | PreconditionActions.InstructionsSourcekitLsp 77 | ) 78 | ); 79 | } 80 | } 81 | 82 | async function currentSourcekitePreconditions(): Promise { 83 | const isDockerMode = workspace 84 | .getConfiguration() 85 | .get("swift.path.sourcekiteDockerMode", false); 86 | const sourcekitePath = absolutePath(workspace.getConfiguration().get("swift.path.sourcekite")); 87 | 88 | if (!isDockerMode && !fs.existsSync(sourcekitePath)) { 89 | await handlePreconditionAction( 90 | await window.showErrorMessage( 91 | `\`sourcekite\` not found at \`${sourcekitePath}\`. 92 | Install it and provide the path to \`swift.path.sourcekite\`.`, 93 | PreconditionActions.Retry, 94 | PreconditionActions.OpenSettings, 95 | PreconditionActions.InstructionsSourcekite 96 | ) 97 | ); 98 | } 99 | } 100 | 101 | async function handlePreconditionAction( 102 | action: string | PreconditionActions | undefined 103 | ): Promise { 104 | switch (action) { 105 | case PreconditionActions.Retry: 106 | await currentLspPreconditions(); 107 | break; 108 | case PreconditionActions.OpenSettings: 109 | if (action === "Settings") { 110 | await commands.executeCommand("workbench.action.openSettings"); 111 | } 112 | break; 113 | case PreconditionActions.InstructionsSourcekite: 114 | await commands.executeCommand( 115 | "vscode.open", 116 | Uri.parse( 117 | "https://github.com/vknabel/vscode-swift-development-environment/tree/2.11.1#using-sourcekit-lsp" 118 | ) 119 | ); 120 | break; 121 | case PreconditionActions.InstructionsSourcekitLsp: 122 | await commands.executeCommand( 123 | "vscode.open", 124 | Uri.parse( 125 | "https://github.com/vknabel/vscode-swift-development-environment/tree/2.11.1#using-sourcekite" 126 | ) 127 | ); 128 | break; 129 | case PreconditionActions.InstructionsLangserver: 130 | await commands.executeCommand( 131 | "vscode.open", 132 | Uri.parse( 133 | "https://github.com/vknabel/vscode-swift-development-environment/tree/2.11.1#using-langserver-swift" 134 | ) 135 | ); 136 | break; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/vscode/output-channels.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, ExtensionContext, window } from "vscode"; 2 | 3 | export interface LogStream { 4 | write(msg: string, show?: boolean): void; 5 | log(msg: string, show?: boolean): void; 6 | clear(): void; 7 | } 8 | 9 | let disposables: Disposable[] = []; 10 | function makeChannel(name: string, showByDefault: boolean = true): LogStream { 11 | const _channel = window.createOutputChannel(`Swift - ${name}`); 12 | disposables.push(_channel); 13 | 14 | const retVal = { 15 | _channel, 16 | write(msg: string, show: boolean = showByDefault) { 17 | this._channel.append(msg); 18 | if (show) { 19 | this._channel.show(true); 20 | } 21 | }, 22 | log(msg: string, show: boolean = showByDefault) { 23 | this._channel.appendLine(msg); 24 | if (show) { 25 | this._channel.show(true); 26 | } 27 | }, 28 | clear() { 29 | this._channel.clear(); 30 | }, 31 | }; 32 | return retVal; 33 | } 34 | 35 | function init(context: ExtensionContext) { 36 | context.subscriptions.push(...disposables); 37 | } 38 | 39 | export default { 40 | init, 41 | noop: { 42 | log(msg: string, show?: boolean) {}, 43 | clear() {}, 44 | }, 45 | build: makeChannel("Build", false), 46 | run: makeChannel("Run"), 47 | }; 48 | -------------------------------------------------------------------------------- /src/vscode/status-bar.ts: -------------------------------------------------------------------------------- 1 | import { window, StatusBarItem, StatusBarAlignment, ThemeColor } from "vscode"; 2 | 3 | const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 4 | function buildAnimator() { 5 | let i = 0; 6 | return function() { 7 | i = (i + 1) % frames.length; 8 | return frames[i]; 9 | }; 10 | } 11 | 12 | let buildItem: StatusBarItem; 13 | let defaultColor: string | ThemeColor; 14 | let animationInterval: NodeJS.Timeout; 15 | 16 | const getItem = () => { 17 | if (!buildItem) { 18 | buildItem = window.createStatusBarItem(StatusBarAlignment.Left); 19 | defaultColor = buildItem.color; 20 | } 21 | buildItem.color = defaultColor; 22 | buildItem.show(); 23 | return buildItem; 24 | }; 25 | const stopAnimation = () => clearInterval(animationInterval); 26 | 27 | export const statusBarItem = { 28 | start(action: string = "building") { 29 | stopAnimation(); 30 | const item = getItem(); 31 | const nextFrame = buildAnimator(); 32 | animationInterval = setInterval(() => { 33 | item.text = `${nextFrame()} ${action}`; 34 | }, 100); 35 | }, 36 | succeeded(action: string = "build") { 37 | stopAnimation(); 38 | const item = getItem(); 39 | item.text = `$(check) ${action} succeeded`; 40 | item.color = defaultColor; 41 | setTimeout(() => item.hide(), 10000); 42 | }, 43 | failed(action: string = "build") { 44 | stopAnimation(); 45 | const item = getItem(); 46 | item.text = `$(issue-opened) ${action} failed`; 47 | item.color = "red"; 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6", 8 | "DOM" 9 | ], 10 | "rootDir": "src", 11 | "sourceMap": true 12 | // "strict": true, 13 | // "noUnusedLocals": true 14 | }, 15 | "compileOnSave": true 16 | } --------------------------------------------------------------------------------