├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc.json ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── HOW_TO_BUILD.md ├── LICENSE.md ├── README.md ├── Tricks_and_tips.md ├── XCBBuildService.spec ├── esbuild.js ├── icons └── icon.png ├── make.sh ├── media ├── autocomplete.gif ├── project_tree.gif └── sidebar_tool.svg ├── package.json ├── resources ├── app_log.py ├── attach_lldb.py ├── fileLock.py ├── helper.py ├── lldb_exe_stub.c ├── open_xcode.sh ├── parent_xcodebuild.py ├── project_helper.rb ├── runtime_warning_database.py ├── update_debugger_launching.py ├── update_git_exclude_if_any.py ├── wait_debugger.py └── xcode_service_setup.py ├── sourcekit_build.sh ├── src ├── AutocompleteWatcher.ts ├── BuildTaskProvider.ts ├── CommandManagement │ ├── AtomicCommand.ts │ ├── BundlePath.ts │ └── CommandContext.ts ├── Debug │ ├── DebugAdapterTracker.ts │ ├── DebugAdapterTrackerFactory.ts │ ├── DebugConfigurationProvider.ts │ ├── LLDBDapDescriptorFactory.ts │ ├── ParentDebugAdapterTracker.ts │ ├── SimulatorFocus.ts │ └── XCTestRunInspector.ts ├── Executor.ts ├── LSP │ ├── DefinitionProvider.ts │ ├── LSPTestsProvider.ts │ ├── ReadOnlyDocumentProvider.ts │ ├── SourceKitLSPErrorHandler.ts │ ├── SwiftLSPClient.ts │ ├── WorkspaceContext.ts │ ├── getReferenceDocument.ts │ ├── lspExtension.ts │ ├── peekDocuments.ts │ └── uriConverters.ts ├── ProblemDiagnosticResolver.ts ├── ProjectManager │ ├── ProjectManager.ts │ ├── ProjectTree.ts │ ├── ProjectsCache.ts │ └── XcodeProjectFileProxy.ts ├── Services │ ├── BuildManager.ts │ ├── ProjectSettingsProvider.ts │ └── RunManager.ts ├── StatusBar │ └── StatusBar.ts ├── TerminalShell.ts ├── TestsProvider │ ├── CoverageProvider.ts │ ├── RawLogParsers │ │ ├── TestCaseAsyncParser.ts │ │ └── TestCaseProblemParser.ts │ ├── TestItemProvider │ │ ├── TestCase.ts │ │ ├── TestContainer.ts │ │ ├── TestFile.ts │ │ ├── TestHeading.ts │ │ ├── TestProject.ts │ │ ├── TestTarget.ts │ │ └── parseClass.ts │ ├── TestProvider.ts │ ├── TestResultProvider.ts │ └── TestTreeContext.ts ├── Tools │ ├── InteractiveTerminal.ts │ ├── ToolsManager.ts │ └── XCRunHelper.ts ├── XCBBuildServiceProxy │ ├── MessageReader.py │ ├── XCBBuildService.py │ ├── config.txt │ └── request_service.txt ├── XcodeSideTreePanel │ ├── ProjectConfigurationDataProvider.ts │ ├── RuntimeWarningsDataProvider.ts │ └── RuntimeWarningsLogWatcher.ts ├── buildCommands.ts ├── commands.ts ├── env.ts ├── extension.ts ├── inputPicker.ts ├── nonActiveExtension.ts ├── quickPickHistory.ts └── utils.ts ├── syntaxes ├── arm.disasm ├── arm64.disasm ├── disassembly.json └── x86.disasm ├── test └── extension │ ├── DefinitionProvider.test.ts │ ├── ProblemDiagnostic │ ├── ProblemDiagnosticResolver.test.ts │ └── mocks │ │ ├── build_log_empty.txt │ │ ├── build_log_success.txt │ │ ├── build_log_success_with_warnings.txt │ │ ├── build_log_with_errors.txt │ │ └── build_log_with_linker_errors.txt │ └── extension.test.ts ├── tsconfig.json └── vsc-extension-quickstart.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2020, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "curly": "error", 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "no-async-promise-executor": "off", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off", 18 | "@typescript-eslint/no-non-null-assertion": "off", 19 | "@typescript-eslint/semi": "error" 20 | }, 21 | "extends": [ 22 | "eslint:recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "prettier" 25 | ], 26 | "ignorePatterns": [ 27 | "out", 28 | "dist", 29 | "**/*.d.ts", 30 | "esbuild.js" 31 | ] 32 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.github 4 | tags 5 | vscode-test 6 | *.vsix 7 | !.eslintrc.json 8 | !.prettierignore 9 | !.prettierrc.json 10 | !.vscode-test.mjs 11 | !.vscode-test.js 12 | !.vscodeignore 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "xcode-build-server"] 2 | path = xcode-build-server 3 | url = https://github.com/fireplusteam/xcode-build-server.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 100, 4 | "tabWidth": 4, 5 | "arrowParens": "avoid", 6 | "overrides": [ 7 | { 8 | "files": "*.json", 9 | "options": { 10 | "tabWidth": 4 11 | } 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig([ 4 | { 5 | label: 'unitTests', 6 | files: 'out/test/**/*.test.js', 7 | version: 'insiders', 8 | mocha: { 9 | ui: 'tdd', 10 | timeout: 20000 11 | } 12 | } 13 | // you can specify additional test configurations, too 14 | ]); -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "ms-vscode.extension-test-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "testConfiguration": "${workspaceFolder}/.vscode-test.mjs", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: pretest" 34 | }, 35 | { 36 | "name": "Show IOS", 37 | "type": "debugpy", 38 | "request": "launch", 39 | "program": "${workspaceFolder}/resources/app_log.py", 40 | "stopOnEntry": false, 41 | "args": [ 42 | ".logs/app.log", 43 | "this.sessionID" 44 | ], 45 | "console": "internalConsole", 46 | "internalConsoleOptions": "neverOpen", 47 | "envFile": "/Users/Ievgenii_Mykhalevskyi/Desktop/git@github.com:fireplusteam/ios_vs_code/example/.vscode/.env", 48 | "cwd": "/Users/Ievgenii_Mykhalevskyi/Desktop/git@github.com:fireplusteam/ios_vs_code/example" 49 | }, 50 | { 51 | "name": "Populate Devices", 52 | "type": "debugpy", 53 | "request": "launch", 54 | "program": "${workspaceFolder}/resources/populate_devices.py", 55 | "stopOnEntry": false, 56 | "args": [ 57 | "Test_ios.xcodeproj", 58 | "Test_ios", 59 | "simulator", 60 | "-single", 61 | ], 62 | "console": "internalConsole", 63 | "internalConsoleOptions": "neverOpen", 64 | "cwd": "/Users/Ievgenii_Mykhalevskyi/tests/Test_ios/" 65 | }, 66 | { 67 | "name": "Run app on simulator", 68 | "type": "debugpy", 69 | "request": "launch", 70 | "program": "${workspaceFolder}/resources/launch.py", 71 | "stopOnEntry": false, 72 | "args": [ 73 | "A0B87EB7-B37A-4871-90BA-8F3649E3AB3F", 74 | "xcode.project.reader.Test-ios", 75 | "LLDB_DEBUG", 76 | "L1VzZXJzL0lldmdlbmlpX015a2hhbGV2c2t5aS90ZXN0cy9UZXN0X2lvcyBkZWJ1Z2dlcg==1" 77 | ], 78 | "console": "internalConsole", 79 | "internalConsoleOptions": "neverOpen", 80 | "cwd": "/Users/Ievgenii_Mykhalevskyi/tests/Test_ios/" 81 | }, 82 | { 83 | "type": "ruby_lsp", 84 | "request": "launch", 85 | "name": "Debug Ruby program", 86 | "program": "ruby resources/project_helper.rb '/Users/Ievgenii_Mykhalevskyi/Desktop/source8/AdidasAppSuite.xcodeproj'" 87 | }, 88 | { 89 | "name": "Debug: Install XCBBuildService", 90 | "type": "debugpy", 91 | "request": "launch", 92 | "program": "${file}", 93 | "args": [ 94 | "-install", 95 | "/Users/Ievgenii_Mykhalevskyi/Desktop/utils/XCBBuildServiceProxy/dist/XCBBuildService" 96 | ], 97 | "console": "integratedTerminal", 98 | "justMyCode": true 99 | }, 100 | { 101 | "name": "Debug: Attach XCBBuildService", 102 | "type": "debugpy", 103 | "request": "attach", 104 | "connect": { 105 | "host": "localhost", 106 | "port": 5678 107 | } 108 | } 109 | ] 110 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out/**": true, // set this to true to hide the "out" folder with the compiled JS files 5 | "**/.rbenv/**": true, 6 | "node_modules/**": true, 7 | "build/**": true, 8 | "**/__pycache__": true 9 | }, 10 | "search.exclude": { 11 | "out/**": true, // set this to false to include "out" folder in search results 12 | "**/.rbenv/**": true, 13 | "node_modules/**": true, 14 | "build/**": true, 15 | "**/__pycache__": true 16 | }, 17 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 18 | "typescript.tsc.autoDetect": "off", 19 | "cmake.configureOnOpen": false, 20 | "rubyLsp.rubyVersionManager": { 21 | "identifier": "rbenv" 22 | }, 23 | "rubyLsp.formatter": "syntax_tree", 24 | "editor.detectIndentation": false, 25 | "editor.formatOnSave": true, 26 | "cSpell.words": [ 27 | "appletvsimulator", 28 | "autowatcher", 29 | "connor", 30 | "Debuggee", 31 | "devicectl", 32 | "disasm", 33 | "fbchisellldb", 34 | "isysroot", 35 | "langclient", 36 | "libexec", 37 | "LLDBDAP", 38 | "Parens", 39 | "showdestinations", 40 | "sswg", 41 | "swiftinterface", 42 | "TESTROOT", 43 | "testrun", 44 | "typealias", 45 | "visionos", 46 | "vsdiag", 47 | "waitfor", 48 | "watchos", 49 | "xcresulttool", 50 | "xctestrun", 51 | "xrsimulator" 52 | ] 53 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": [ 9 | "npm: watch:tsc", 10 | "npm: watch:esbuild" 11 | ], 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "pretest", 23 | "group": "build", 24 | "problemMatcher": "$esbuild-watch", 25 | "isBackground": true, 26 | "label": "npm: pretest", 27 | "dependsOn": [ 28 | "npm: watch:tsc", 29 | "npm: watch:esbuild" 30 | ], 31 | "presentation": { 32 | "group": "watch", 33 | "reveal": "never" 34 | } 35 | }, 36 | { 37 | "type": "npm", 38 | "script": "watch:esbuild", 39 | "group": "build", 40 | "problemMatcher": "$esbuild-watch", 41 | "isBackground": true, 42 | "label": "npm: watch:esbuild", 43 | "presentation": { 44 | "group": "watch", 45 | "reveal": "never" 46 | } 47 | }, 48 | { 49 | "type": "npm", 50 | "script": "watch:tsc", 51 | "group": "build", 52 | "problemMatcher": "$tsc-watch", 53 | "isBackground": true, 54 | "label": "npm: watch:tsc", 55 | "presentation": { 56 | "group": "watch", 57 | "reveal": "never" 58 | } 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .* 2 | .ruby-lsp 3 | .vscode 4 | build 5 | **/__pycache__ 6 | node_modules 7 | out 8 | src 9 | !src/XCBBuildServiceProxy/dist 10 | dist/extension.js.map 11 | esbuild.js 12 | Gemfile 13 | Gemfile.lock 14 | make.sh 15 | tsconfig.json 16 | sourcekit_build.sh 17 | test 18 | HOW_TO_BUILD.md 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.5.9 - 2024-11-20 4 | 5 | ### Fixed 6 | 7 | - Build was looped at error 8 | 9 | ## 0.5.8 - 2024-11-20 10 | 11 | ### Fixed 12 | 13 | - Updated icon 14 | 15 | ## 0.5.7 - 2024-11-11 16 | 17 | ### Fixed 18 | 19 | - Autocompletion build was triggered on `build` instead of `build-for-testing` in case if test plan was autogenerated by Xcode 20 | 21 | ## 0.5.6 - 2024-11-11 22 | 23 | ### Added 24 | 25 | - Release an extension as a single bundle which increases the speed of js code execution 26 | 27 | ## 0.5.4 - 2024-11-8 28 | 29 | ### Added 30 | 31 | - Status Bar to better info a user about selected scheme, configuration, device, test plan 32 | - Added currently selected configuration panel to vscode side bar 33 | 34 | ## 0.5.3 - 2024-11-7 35 | 36 | ### Fixed 37 | 38 | - Kill sourcekit-lsp as it grows in memory usage rapidly in some cases (this should be fixed in swift 6.1 https://github.com/swiftlang/sourcekit-lsp/issues/1541) 39 | - Improved stability and bug fixes 40 | 41 | ## 0.5.0 - 2024-11-5 42 | 43 | ### Changed 44 | 45 | - Breaking change: `iOS:` renamed to `Xcode:` for all commands 46 | 47 | ### Added 48 | 49 | - Debug UI tests and multiple targets are now supported 50 | - Support Test Plan 51 | 52 | ### Fixed 53 | 54 | - Runtime warning communication between lldb and extension is done via fifo 55 | - Improved stability and bug fixes 56 | 57 | ## 0.4.0 - 2024-10-20 58 | 59 | ### Added 60 | 61 | - New breaking change. Removed .env file and moved everything to projectConfiguration.json file. 62 | - Get rid of old shell commands and replace with ts implementation 63 | - Own definition provider if sourcekit-lsp fails to provide location (Usually it stops working if there're errors in the file). As a workaround, symbol search works fine as a fallback (similar to Xcode). 64 | 65 | ### Fixed 66 | 67 | - Improved activation of extension logic 68 | - buildServer.json file for projects without workspace in was not properly generated 69 | - Fixed issue with relative path passing to sourcekit-lsp due to not correct workspace folder set 70 | - Open generated swiftinterface by sourcekit-slp 71 | - Fixed XCBBuildService in some cases when continueWithErrors environmental variable was set 72 | 73 | ## 0.3.0 - 2024-10-10 74 | 75 | ### Added 76 | 77 | - Support Swift Testing framework starting xCode 16. 78 | - Own lsp client support 79 | 80 | ### Fixed 81 | 82 | - Test debug session were treated as user cancelled on error 83 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | ruby File.read(File.join(__dir__, '.ruby-version')).strip 6 | 7 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 8 | gem 'pathname' 9 | gem 'xcodeproj' 10 | 11 | group :development do 12 | # linter 13 | #gem 'rubocop', require: false 14 | # formatter 15 | gem 'syntax_tree', require: true 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | atomos (0.1.3) 7 | claide (1.1.0) 8 | colored2 (3.1.2) 9 | nanaimo (0.3.0) 10 | pathname (0.3.0) 11 | prettier_print (1.2.1) 12 | rexml (3.2.5) 13 | syntax_tree (6.2.0) 14 | prettier_print (>= 1.2.0) 15 | xcodeproj (1.22.0) 16 | CFPropertyList (>= 2.3.3, < 4.0) 17 | atomos (~> 0.1.3) 18 | claide (>= 1.0.2, < 2.0) 19 | colored2 (~> 3.1) 20 | nanaimo (~> 0.3.0) 21 | rexml (~> 3.2.4) 22 | 23 | PLATFORMS 24 | arm64-darwin-23 25 | 26 | DEPENDENCIES 27 | pathname 28 | syntax_tree 29 | xcodeproj 30 | 31 | RUBY VERSION 32 | ruby 3.2.3p157 33 | 34 | BUNDLED WITH 35 | 2.4.14 36 | -------------------------------------------------------------------------------- /HOW_TO_BUILD.md: -------------------------------------------------------------------------------- 1 | ## How to build/install extension from a repo 2 | 3 | Open terminal to install required libraries (Also make sure you've installed Xcode, xcbeautify, xcodeproj): 4 | 5 | - install **pyinstaller** and **psutil** (needed to build Xcode proxy build service) 6 | 7 | ```bash 8 | pip install pyinstaller 9 | pip install psutil 10 | ``` 11 | 12 | - install **npm** 13 | 14 | ```bash 15 | brew install node 16 | ``` 17 | 18 | - clone git repo and update submodules: 19 | 20 | ```bash 21 | git clone https://github.com/fireplusteam/ios_vs_code.git 22 | git submodule update --init --recursive 23 | ``` 24 | 25 | - install vsce package 26 | 27 | ```bash 28 | brew install vsce 29 | ``` 30 | 31 | - 1. Open Visual Studio Code. 32 | 2. Press **Cmd+Shift+P** to open the Command Palette. 33 | 3. Type: **Shell Command: Install 'code' command in PATH**. 34 | 35 | - navigate to repo folder in your terminal and run: 36 | 37 | ```bash 38 | ./make.sh 39 | ``` 40 | 41 | If everything configured right, the extension should be built and installed to vs code automatically. 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 FirePlus Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xcode iOS Swift IDE Xcode iOS Swift IDE logo 2 | 3 | 📦[VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=FirePlusTeam.vscode-ios) | 🐞 4 | [Github Issues](https://github.com/fireplusteam/ios_vs_code/issues) 5 | 6 | You can support this project by giving a star on GitHub ⭐️ 7 | 8 | [![GitHub](https://img.shields.io/github/stars/fireplusteam/ios_vs_code?style=social)](https://github.com/fireplusteam/ios_vs_code) 9 | [![Github Sponsors](https://img.shields.io/badge/Github%20Sponsors-%E2%9D%A4-red?style=flat&logo=github)](https://github.com/sponsors/ios_vs_code) 10 | 11 |
12 | Develop/Build/Debug/Test your xCode projects in VS Code with your favorite extensions. 13 | 14 | Before use it make sure you've installed all **dependencies** required for this extension. 15 | 16 |
17 | 18 | ## ✅ Autocomplete 19 | 20 | [![Autocomplete](media/autocomplete.gif)](https://youtu.be/0dXQGY0IIEA) 21 | 22 | ## 🌳 File Tree Integration 23 | 24 | [![🌳 File Tree Integration](media/project_tree.gif)](https://youtu.be/3C-abUZGkgE) 25 | 26 | ### Also check [Tricks and Tips](Tricks_and_tips.md) 27 | 28 | ## Features 29 | 30 | - Supports iOS/MacOS/WatchOS/VisionOS/TvOS 31 | - Swift/Objective-C/C++ autocompletion 32 | - Compatibility with CodeLLDB/lldb-dap 33 | - Debug/Run app on Simulators 34 | - Debug/Run unit/snapshot/UI tests. Support running single/multiple tests for a class/target/set of classes 35 | - Support code coverage 36 | - Run an application on multiple simulator with a single command 37 | - Support project/workspace 38 | - Support launch configuration for app 39 | - Support diff snapshots testing 40 | - Add/Delete/Rename/Move files/folders inside vscode 41 | - VS Code workspace generation based on Xcode project/workspace 42 | - Parsing build/test logs and display in real time 43 | 44 | Instead of xCode preview you can use hot reloading [InjectionIII](https://github.com/johnno1962/InjectionIII) which works great with this extension: 45 | 46 | - HotReloading & Injection with [HotReloading](https://github.com/johnno1962/HotReloading) 47 | - SwiftUI injection property wrapper with [Inject](https://github.com/krzysztofzablocki/Inject) or [HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) 48 | 49 | To Debug View Hierarchy you can use this technique [How to debug your view hierarchy using recursiveDescription](https://www.hackingwithswift.com/articles/101/how-to-debug-your-view-hierarchy-using-recursivedescription) 50 | 51 | ## Dependencies 52 | 53 | Before an extension is activated, there's a automatic check if those dependencies are installed and if not, it's ask a user to install them automatically. 54 | Use the following guide to install them manually if any it doesn't work for you: 55 | 56 | **Required Dependencies**: 57 | 58 | - 🍏 **MacOS** — Other platforms are currently not supported 59 | - 📱 **Xcode** and simulators. Make sure that your Xcode is installed in `/Application/Xcode/` folder 60 | 61 | - **homebrew**: 62 | 63 | ```bash 64 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 65 | ``` 66 | 67 | - **xcbeautify** tool to prettify the building log output and : 68 | 69 | ```bash 70 | brew install xcbeautify 71 | ``` 72 | 73 | - **xcodeproj** gem library to make possible to add/delete/rename files in your Xcode project directly from vs code. 74 | 75 | ```bash 76 | brew install ruby 77 | gem install xcodeproj 78 | ``` 79 | 80 | **Note:** 81 | As [sourcekit-lsp](https://github.com/apple/sourcekit-lsp) updates indexes while building, If you want to have indexes updating even if you have compile errors, you need to give **a full disk control** to Visual Studio Code in Security Settings which allows to install a proxy service for Apple **XCBBuildService** automatically when an extension is activated. 82 | This's just needed to override the **continueBuildingAfterError** property when you build the app and gives you all errors in the project and compile flags possible used by sourcekit for indexing. 83 | 84 | ## How to use 85 | 86 | - Once you installed all the dependencies, you can open a folder which contains the iOS project. If project or workspace is located in the local folder then an extension will ask you if you want to configure it, otherwise you need to perform command **"Xcode: Select Project/Workspace"** and pick the right project/workspace to work with. You can also switch between multiple projects if they are located in the same folder/subfolders 87 | 88 | - There's ios launch configuration that can be added to `launch.json` file to run and debug ios project (there's also "Xcode: Run App & Debug" snippet) 89 | 90 | ```json 91 | "configurations": [ 92 | { 93 | "type": "xcode-lldb", 94 | "name": "Xcode: Run App & Debug", 95 | "request": "launch", 96 | "target": "app", 97 | "isDebuggable": true, 98 | "buildBeforeLaunch": "always", 99 | "lldbCommands": [] 100 | } 101 | ] 102 | ``` 103 | 104 | - Also there're automatically added build tasks which can be used by pressing standard "**Cmd+Shift+B**" shortcut. 105 | 106 | - To make autocompletion to work you may need to clean the project and build it entirely for the first time. 107 | 108 | ## Extension Settings 109 | 110 | This extension contributes the following settings: 111 | 112 | - `vscode-ios.watcher.singleModule`: Enable/disable the autocomplete watch build to update indexes whenever a you modified a file. 113 | - `vscode-ios.xcb.build.service`: if Enabled, it will ask a user sudo password to replace XCBBuildService with a proxy service which would enhance the Autocomplete feature. This's used to continue compile a project even if there's multiple errors, so all flags are updated 114 | 115 | ## Known Issues 116 | 117 | - You still need Xcode to use SwiftUI preview or edit storyboard/assets/project settings. 118 | - Running/debugging on device is not currently supported. 119 | - [sourcekit-lsp](https://github.com/apple/sourcekit-lsp) use indexing while build. if you find definition or references is not work correctly, just build it to update index or restart Swift LSP in VS Code. 120 | - When running for the first time, **you need to ensure that the log is complete**, otherwise some files cannot obtain the correct flags. 121 | - If Generating of project is not working as expected or generates some kind of errors if Xcode opens the same project file, you simply need to update **xcodeproj** lib and ruby library to the latest 122 | 123 | ```bash 124 | gem install xcodeproj 125 | ``` 126 | 127 | ## Release Notes 128 | 129 | ### 0.5.10 130 | 131 | It's still under development, so you can face some bugs 132 | -------------------------------------------------------------------------------- /Tricks_and_tips.md: -------------------------------------------------------------------------------- 1 | # Improve Build Time 2 | 3 | xcodebuild CLI tool is slower than Xcode. This section provides a workaround to improve the build time. 4 | 5 | The issue is caused by the fact that xcodebuild tries to connect to the Apple servers before building the project, which can take 20 seconds or more. Usually, those requests are not necessary, but they slow down each build. 6 | 7 | The workaround blocks developerservices2.apple.com domain when the xcodebuild tool is running by modifying /etc/hosts. Below you can find three ways to enable the workaround. 8 | 9 | ### Warning 10 | 11 | Keep in mind that disabling access to developerservices2.apple.com for xcodebuild may cause some issues with the build process. It will disable things like registering devices, capabilities, and other network-related features. Therefore, it's best to use it when you are working just on the code and don't need updating project settings. 12 | 13 | ## 1. Manual (script) 14 | 15 | Enable workaround: 16 | 17 | ```bash 18 | sudo bash -c "echo '127.0.0.1 developerservices2.apple.com' >>/etc/hosts" 19 | ``` 20 | 21 | Disable workaround: 22 | 23 | ```bash 24 | sudo sed -i '' '/developerservices2\.apple\.com/d' /etc/hosts 25 | ``` 26 | 27 | ## 2. Manual (network sniffer) 28 | 29 | If you use some tool to sniff network traffic like Proxyman or Charles Proxy, you can block requests to https://developerservices2.apple.com/* and automatically return some error like 999 status code. It will prevent xcodebuild from further calls. 30 | -------------------------------------------------------------------------------- /XCBBuildService.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['src/XCBBuildServiceProxy/XCBBuildService.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | ) 16 | pyz = PYZ(a.pure) 17 | 18 | exe = EXE( 19 | pyz, 20 | a.scripts, 21 | a.binaries, 22 | a.datas, 23 | [], 24 | name='XCBBuildService', 25 | debug=False, 26 | bootloader_ignore_signals=False, 27 | strip=False, 28 | upx=True, 29 | upx_exclude=[], 30 | runtime_tmpdir=None, 31 | console=True, 32 | disable_windowed_traceback=False, 33 | argv_emulation=False, 34 | target_arch=None, 35 | codesign_identity=None, 36 | entitlements_file=None, 37 | ) 38 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes("--production"); 4 | const watch = process.argv.includes("--watch"); 5 | 6 | async function main() { 7 | const ctx = await esbuild.context({ 8 | entryPoints: ["src/extension.ts"], 9 | bundle: true, 10 | format: "cjs", 11 | minify: production, 12 | sourcemap: !production, 13 | sourcesContent: false, 14 | platform: "node", 15 | outfile: "dist/extension.js", 16 | external: ["vscode"], 17 | logLevel: "silent", 18 | plugins: [ 19 | /* add to the end of plugins array */ 20 | esbuildProblemMatcherPlugin, 21 | ], 22 | }); 23 | if (watch) { 24 | await ctx.watch(); 25 | } else { 26 | await ctx.rebuild(); 27 | await ctx.dispose(); 28 | } 29 | } 30 | 31 | /** 32 | * @type {import('esbuild').Plugin} 33 | */ 34 | const esbuildProblemMatcherPlugin = { 35 | name: "esbuild-problem-matcher", 36 | 37 | setup(build) { 38 | build.onStart(() => { 39 | console.log("[watch] build started"); 40 | }); 41 | build.onEnd(result => { 42 | result.errors.forEach(({ text, location }) => { 43 | console.error(`✘ [ERROR] ${text}`); 44 | console.error(` ${location.file}:${location.line}:${location.column}:`); 45 | }); 46 | console.log("[watch] build finished"); 47 | }); 48 | }, 49 | }; 50 | 51 | main().catch(e => { 52 | console.error(e); 53 | process.exit(1); 54 | }); 55 | -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireplusteam/ios-swift-for-vs-code/4834827531a6d5abc835993a6a3b905f4cb83537/icons/icon.png -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # insall dependencies or update them 4 | npm install # resolve dependencies 5 | npm install --save @types/ps-tree 6 | npm install --save @types/find-process 7 | npm install --save @types/lockfile 8 | npm install vscode-languageserver-protocol 9 | npm install vscode-languageclient 10 | npm install @vscode/test-cli 11 | 12 | pip install psutil 13 | pyinstaller --onefile src/XCBBuildServiceProxy/XCBBuildService.py 14 | 15 | npm run compile 16 | npm run test 17 | 18 | vsce package 19 | 20 | code --install-extension vscode-ios-0.5.11.vsix 21 | -------------------------------------------------------------------------------- /media/autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireplusteam/ios-swift-for-vs-code/4834827531a6d5abc835993a6a3b905f4cb83537/media/autocomplete.gif -------------------------------------------------------------------------------- /media/project_tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireplusteam/ios-swift-for-vs-code/4834827531a6d5abc835993a6a3b905f4cb83537/media/project_tree.gif -------------------------------------------------------------------------------- /media/sidebar_tool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | xcode 5 | 6 | -------------------------------------------------------------------------------- /resources/app_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | import sys 4 | import helper 5 | 6 | 7 | class AppLogger: 8 | def __init__(self, file_path, printer=print) -> None: 9 | self.file_path = file_path 10 | self.printer = printer 11 | self.enabled = True 12 | 13 | def print_new_lines(self, file): 14 | try: 15 | while True: 16 | try: 17 | line = helper.binary_readline(file, b"\n") 18 | if not line: 19 | break 20 | if not line.endswith(b"\n"): 21 | break 22 | 23 | if self.enabled: 24 | line = line.decode(errors="replace") 25 | self.printer(line, end="", flush=True) 26 | except: 27 | # cut utf-8 characters as code lldb console can not print such characters and generates an error 28 | if self.enabled: 29 | to_print = "" 30 | for i in line: 31 | if ord(i) < 128: 32 | to_print += i 33 | else: 34 | to_print += "?" 35 | self.printer(to_print, end="") 36 | 37 | except: # no such file 38 | pass 39 | 40 | def _watch_file(self, file): 41 | while True: 42 | self.print_new_lines(file) 43 | time.sleep(1) 44 | 45 | def watch_app_log(self): 46 | with open(self.file_path, "rb") as file: 47 | self._watch_file(file) 48 | 49 | 50 | if __name__ == "__main__": 51 | file_path = sys.argv[1] 52 | session_id = sys.argv[2] 53 | 54 | logger = AppLogger(file_path) 55 | # Watch for changes in the file 56 | logger.watch_app_log() 57 | -------------------------------------------------------------------------------- /resources/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | import json 4 | import os 5 | import time 6 | import fileLock 7 | 8 | def get_list_of_pids(process_name: str): 9 | proc = subprocess.run(["ps", "aux"], capture_output=True, text=True) 10 | 11 | #print(proc.stdout) 12 | 13 | # Split the output into lines 14 | lines = proc.stdout.split('\n') 15 | 16 | # get list of pids by process name 17 | result = set() 18 | for line in lines[1:]: # Skip the header line 19 | columns = line.split() 20 | if len(line) == 0: 21 | break 22 | 23 | proc_start = line.find(columns[9]) + len(columns[9]) 24 | proc_line = line[proc_start:].strip() 25 | if len(columns) >= 2 and process_name in proc_line: 26 | pid = columns[1] 27 | result.add(pid) 28 | 29 | return result 30 | 31 | 32 | # --------GIT------------------------------------- 33 | 34 | def update_git_exclude(file_to_exclude): 35 | if not os.path.exists(".git"): 36 | return 37 | os.makedirs(".git/info", exist_ok=True) 38 | content = None 39 | try: 40 | with open(".git/info/exclude", 'r') as file: 41 | content = file.readlines() 42 | except: pass 43 | #print(f"Updating git ignore: {content}") 44 | if content is None: 45 | content = [] 46 | if len([x for x in content if f"{file_to_exclude}".strip() == x.strip()]) == 0: 47 | content.insert(0, f"{file_to_exclude}\n") 48 | #print(f"CHANGED: {content}") 49 | try: 50 | with open(".git/info/exclude", "w+") as file: 51 | file.write(''.join(content)) 52 | except Exception as e: 53 | print(f"Git ignore update exception: {str(e)}") 54 | 55 | 56 | #---------DEBUGGER-------------------------------- 57 | debugger_config_file = ".vscode/xcode/debugger.launching" 58 | def wait_debugger_to_action(session_id, actions: list[str]): 59 | while True: 60 | with fileLock.FileLock(debugger_config_file): 61 | with open(debugger_config_file, 'r') as file: 62 | config = json.load(file) 63 | if config is not None and not session_id in config: 64 | break 65 | 66 | if config is not None and config[session_id]["status"] in actions: 67 | break 68 | 69 | time.sleep(1) 70 | 71 | 72 | def is_debug_session_valid(session_id) -> bool: 73 | try: 74 | with fileLock.FileLock(debugger_config_file): 75 | with open(debugger_config_file, 'r') as file: 76 | config = json.load(file) 77 | if not session_id in config: 78 | return False 79 | if config[session_id]["status"] == "stopped": 80 | return False 81 | 82 | return True 83 | except: # no file or a key, so the session is valid 84 | return True 85 | 86 | 87 | def get_debugger_launch_config(session_id, key): 88 | with fileLock.FileLock(debugger_config_file): 89 | with open(debugger_config_file, 'r') as file: 90 | config = json.load(file) 91 | if config is not None and not session_id in config: 92 | return None 93 | 94 | if config is not None and config[session_id][key]: 95 | return config[session_id][key] 96 | 97 | 98 | def update_debugger_launch_config(session_id, key, value): 99 | config = {} 100 | try: 101 | with fileLock.FileLock(debugger_config_file): 102 | if os.path.exists(debugger_config_file): 103 | try: 104 | with open(debugger_config_file, "r+") as file: 105 | config = json.load(file) 106 | except: 107 | pass 108 | 109 | if session_id in config: 110 | # stopped can not be updated once reported 111 | if key == "status" and key in config[session_id] and config[session_id][key] == 'stopped': 112 | return 113 | config[session_id][key] = value; 114 | else: 115 | config[session_id] = {} 116 | config[session_id][key] = value 117 | 118 | with open(debugger_config_file, "w+") as file: 119 | json.dump(config, file, indent=2) 120 | except: 121 | pass # config is empty 122 | 123 | 124 | if __name__ == "__main__": 125 | print("ok") 126 | 127 | #---------------------BINARY READER HELPER---------------------------- 128 | 129 | def binary_readline(file, newline=b'\r\n'): 130 | line = bytearray() 131 | while True: 132 | x = file.read(1) 133 | if x: 134 | line += x 135 | else: 136 | if len(line) == 0: 137 | return None 138 | else: 139 | return line 140 | 141 | if line.endswith(newline): 142 | return line -------------------------------------------------------------------------------- /resources/lldb_exe_stub.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, const char *argv[]) 4 | { 5 | sleep(2); 6 | 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /resources/open_xcode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | jPath="$1" 4 | 5 | exec osascript - "$jPath" <(); 21 | 22 | private buildId = 0; 23 | 24 | constructor( 25 | atomicCommand: AtomicCommand, 26 | problemResolver: ProblemDiagnosticResolver, 27 | projectManager: ProjectManager 28 | ) { 29 | this.atomicCommand = atomicCommand; 30 | this.disposable.push( 31 | vscode.workspace.onDidOpenTextDocument(doc => { 32 | if (!doc || !this.isWatcherEnabledAnyFile()) { 33 | return; 34 | } 35 | if (this.isValidFile(doc.uri.fsPath) === false) { 36 | return; 37 | } 38 | this.changedFiles.set(doc.uri.fsPath, removeAllWhiteSpaces(doc.getText())); 39 | }) 40 | ); 41 | this.disposable.push( 42 | vscode.workspace.onDidSaveTextDocument(doc => { 43 | if (!doc || !this.isWatcherEnabledAnyFile()) { 44 | return; 45 | } 46 | if (this.isValidFile(doc.uri.fsPath) === false) { 47 | return; 48 | } 49 | 50 | const val = this.changedFiles.get(doc.uri.fsPath); 51 | const textOfDoc = removeAllWhiteSpaces(doc.getText()); 52 | if (val === textOfDoc) { 53 | return; 54 | } 55 | this.changedFiles.set(doc.uri.fsPath, textOfDoc); 56 | this.triggerIncrementalBuild().catch(() => {}); 57 | }) 58 | ); 59 | this.problemResolver = problemResolver; 60 | this.projectManager = projectManager; 61 | } 62 | 63 | async triggerIncrementalBuild() { 64 | if ((await this.isWatcherEnabledAnyFile()) === false) { 65 | return; 66 | } 67 | this.buildId++; 68 | await this.incrementalBuild(this.buildId); 69 | } 70 | 71 | terminate() { 72 | this.terminatingExtension = true; 73 | } 74 | 75 | private async isWatcherEnabledAnyFile() { 76 | if ((await isActivated()) === false) { 77 | return false; 78 | } 79 | const isWatcherEnabled = vscode.workspace 80 | .getConfiguration("vscode-ios", getWorkspaceFolder()) 81 | .get("watcher.singleModule"); 82 | if (!isWatcherEnabled || this.terminatingExtension) { 83 | return false; 84 | } 85 | return true; 86 | } 87 | 88 | private isValidFile(filePath: string | undefined) { 89 | if ( 90 | filePath && 91 | (filePath.endsWith(".swift") || 92 | filePath.endsWith(".m") || 93 | filePath.endsWith(".mm") || 94 | filePath.endsWith(".cpp") || 95 | filePath.endsWith(".c") || 96 | filePath.endsWith(".h") || 97 | filePath.endsWith(".hpp")) 98 | ) { 99 | return true; 100 | } 101 | return false; 102 | } 103 | 104 | private async incrementalBuild(buildId: number): Promise { 105 | try { 106 | await this.atomicCommand.autoWatchCommand(async context => { 107 | if (this.buildId !== buildId || (await this.isWatcherEnabledAnyFile()) === false) { 108 | return; 109 | } 110 | 111 | emptyAutobuildLog(); 112 | const fileLog = ".logs/autocomplete.log"; 113 | const rawParser = this.problemResolver.parseAsyncLogs(fileLog, context.buildEvent); 114 | try { 115 | const buildManager = new BuildManager(); 116 | await buildManager.buildAutocomplete(context, fileLog); 117 | } finally { 118 | this.problemResolver.end(rawParser, false); 119 | } 120 | }); 121 | } catch (err) { 122 | if (err === UserCommandIsExecuting) { 123 | await sleep(1000); 124 | if (buildId === this.buildId) { 125 | // still valid 126 | this.incrementalBuild(buildId).catch(() => {}); 127 | } // do nothing 128 | } else { 129 | throw err; 130 | } 131 | } 132 | } 133 | 134 | private async getModuleNameByFileName(path: string) { 135 | try { 136 | return await this.projectManager.listTargetsForFile(path); 137 | } catch (err) { 138 | console.log(`Error on determine the file module: ${err}`); 139 | return []; 140 | } 141 | } 142 | } 143 | 144 | function removeAllWhiteSpaces(str: string) { 145 | return str.replace(/\s/g, ""); 146 | } 147 | -------------------------------------------------------------------------------- /src/BuildTaskProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { buildSelectedTarget, buildTestsForCurrentFile, cleanDerivedData } from "./buildCommands"; 3 | import { isActivated } from "./env"; 4 | import { ProblemDiagnosticResolver } from "./ProblemDiagnosticResolver"; 5 | import { AtomicCommand } from "./CommandManagement/AtomicCommand"; 6 | import { CommandContext } from "./CommandManagement/CommandContext"; 7 | 8 | interface BuildTaskDefinition extends vscode.TaskDefinition { 9 | taskBuild: string; 10 | } 11 | 12 | export async function executeTask(name: string) { 13 | const tasks = await vscode.tasks.fetchTasks(); 14 | for (const task of tasks) { 15 | if (task.name === name && task.definition.type === BuildTaskProvider.BuildScriptType) { 16 | let disposable: vscode.Disposable; 17 | await new Promise(async (resolve, reject) => { 18 | disposable = vscode.tasks.onDidEndTaskProcess(e => { 19 | if (e.execution.task.name === name) { 20 | disposable.dispose(); 21 | if (e.exitCode !== 0) { 22 | reject(Error(`Task ${name} failed with ${e.exitCode}`)); 23 | return; 24 | } 25 | resolve(true); 26 | } 27 | }); 28 | try { 29 | await vscode.tasks.executeTask(task); 30 | } catch (err) { 31 | reject(err); 32 | } 33 | }); 34 | } 35 | } 36 | } 37 | 38 | export class BuildTaskProvider implements vscode.TaskProvider { 39 | static BuildScriptType = "vscode-ios-tasks"; 40 | 41 | private problemResolver: ProblemDiagnosticResolver; 42 | private atomicCommand: AtomicCommand; 43 | 44 | constructor(problemResolver: ProblemDiagnosticResolver, atomicCommand: AtomicCommand) { 45 | this.problemResolver = problemResolver; 46 | this.atomicCommand = atomicCommand; 47 | } 48 | 49 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 50 | public async provideTasks(token?: vscode.CancellationToken): Promise { 51 | if ((await isActivated()) === false) { 52 | return []; 53 | } 54 | 55 | const buildSelectedTargetTask = this.createBuildTask( 56 | "Build", 57 | vscode.TaskGroup.Build, 58 | async context => { 59 | await buildSelectedTarget(context, this.problemResolver); 60 | } 61 | ); 62 | 63 | const buildTestsTask = this.createBuildTask( 64 | "Build Tests", 65 | vscode.TaskGroup.Build, 66 | async context => { 67 | await buildTestsForCurrentFile(context, this.problemResolver, [], false); 68 | } 69 | ); 70 | 71 | const cleanTask = this.createBuildTask( 72 | "Clean Derived Data", 73 | vscode.TaskGroup.Clean, 74 | async context => { 75 | await cleanDerivedData(context); 76 | } 77 | ); 78 | 79 | return [buildTestsTask, buildSelectedTargetTask, cleanTask]; 80 | } 81 | 82 | private createBuildTask( 83 | title: string, 84 | group: vscode.TaskGroup, 85 | commandClosure: (context: CommandContext) => Promise 86 | ) { 87 | const def: BuildTaskDefinition = { 88 | type: BuildTaskProvider.BuildScriptType, 89 | taskBuild: title, 90 | }; 91 | const buildTask = new vscode.Task( 92 | def, 93 | vscode.TaskScope.Workspace, 94 | title, 95 | "iOS", 96 | this.customExecution(`Xcode: ${title}`, commandClosure) 97 | ); 98 | buildTask.group = group; 99 | buildTask.presentationOptions = { 100 | reveal: vscode.TaskRevealKind.Never, 101 | close: true, 102 | }; 103 | if (group === vscode.TaskGroup.Build) { 104 | buildTask.problemMatchers = ["$xcode"]; 105 | } else { 106 | buildTask.isBackground = true; 107 | } 108 | return buildTask; 109 | } 110 | 111 | public resolveTask(_task: vscode.Task) { 112 | const taskBuild = _task.definition.taskBuild; 113 | if (taskBuild) { 114 | // TODO: Implement resolver so a user can add tasks in his Task.json file 115 | //const definition: BuildTaskDefinition = _task.definition; 116 | //return this.getTask(definition.flavor, definition.flags ? definition.flags : [], definition); 117 | console.log(taskBuild); 118 | } 119 | return undefined; 120 | } 121 | 122 | private customExecution( 123 | successMessage: string, 124 | commandClosure: (context: CommandContext) => Promise 125 | ) { 126 | return new vscode.CustomExecution(() => { 127 | return new Promise(resolved => { 128 | const writeEmitter = new vscode.EventEmitter(); 129 | const closeEmitter = new vscode.EventEmitter(); 130 | let commandContext: CommandContext | undefined = undefined; 131 | const pty: vscode.Pseudoterminal = { 132 | open: async () => { 133 | closeEmitter.fire(0); // this's a workaround to hide a task terminal as soon as possible to let executor terminal to do the main job. That has a side effect if that task would be used in a chain of tasks, then it's finished before process actually finishes 134 | try { 135 | await this.atomicCommand.userCommand(async context => { 136 | commandContext = context; 137 | await commandClosure(context); 138 | }, successMessage); 139 | } catch (err) { 140 | /* empty */ 141 | } 142 | }, 143 | onDidWrite: writeEmitter.event, 144 | onDidClose: closeEmitter.event, 145 | close: async () => { 146 | commandContext?.cancel(); 147 | }, 148 | }; 149 | resolved(pty); 150 | }); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/CommandManagement/BundlePath.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { Executor } from "../Executor"; 3 | import { deleteFile } from "../utils"; 4 | import { getFilePathInWorkspace } from "../env"; 5 | 6 | let globalId = 0; 7 | function generateGlobalId() { 8 | return ++globalId; 9 | } 10 | 11 | export class BundlePath { 12 | private number: number; 13 | private allBundles: number[] = []; 14 | 15 | private name: string; 16 | 17 | constructor(name: string) { 18 | this.name = name; 19 | this.deleteExistingFilesIfAny(); 20 | this.number = generateGlobalId(); 21 | this.allBundles.push(this.number); 22 | } 23 | 24 | private BundlePath(number: number): string { 25 | return `.vscode/xcode/bundles/.${this.name}_${number}`; 26 | } 27 | private BundleResultPath(number: number): string { 28 | return `${this.BundlePath(number)}.xcresult`; 29 | } 30 | 31 | private deleteExistingFilesIfAny() { 32 | deleteFile(getFilePathInWorkspace(this.bundlePath())); 33 | deleteFile(getFilePathInWorkspace(this.bundleResultPath())); 34 | } 35 | 36 | generateNext() { 37 | this.number = generateGlobalId(); 38 | this.deleteExistingFilesIfAny(); 39 | this.allBundles.push(this.number); 40 | } 41 | 42 | bundlePath() { 43 | return this.BundlePath(this.number); 44 | } 45 | 46 | bundleResultPath() { 47 | return this.BundleResultPath(this.number); 48 | } 49 | 50 | async merge() { 51 | const resultBundles: string[] = []; 52 | for (const i of this.allBundles) { 53 | const filePath = getFilePathInWorkspace(this.BundleResultPath(i)); 54 | if (fs.existsSync(filePath)) { 55 | resultBundles.push(filePath); 56 | } 57 | } 58 | if (resultBundles.length <= 1) { 59 | return; // nothing to merge 60 | } 61 | // generate next bundle id to merge all results 62 | this.allBundles = []; 63 | this.generateNext(); 64 | await new Executor().execShell({ 65 | scriptOrCommand: { command: "xcrun xcresulttool" }, 66 | args: [ 67 | "merge", 68 | ...resultBundles, 69 | "--output-path", 70 | getFilePathInWorkspace(this.bundleResultPath()), 71 | ], 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/CommandManagement/CommandContext.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { 3 | Executor, 4 | ExecutorMode, 5 | ShellCommand, 6 | ShellExec, 7 | ShellFileScript, 8 | ShellResult, 9 | } from "../Executor"; 10 | import { ProjectSettingsProvider } from "../Services/ProjectSettingsProvider"; 11 | import { TerminalShell } from "../TerminalShell"; 12 | import { LSPClientContext } from "../LSP/lspExtension"; 13 | import { CustomError } from "../utils"; 14 | import { ProjectEnv } from "../env"; 15 | import { BundlePath } from "./BundlePath"; 16 | 17 | export const UserTerminatedError = new CustomError("User Terminated"); 18 | export const UserTerminalCloseError = new CustomError("User Closed Terminal"); 19 | 20 | interface CommandOptions { 21 | scriptOrCommand: ShellCommand | ShellFileScript; 22 | cwd?: string; 23 | args?: string[]; 24 | env?: { [name: string]: string }; 25 | mode?: ExecutorMode; 26 | pipeToDebugConsole?: boolean; 27 | pipeToParseBuildErrors?: boolean; 28 | pipe?: CommandOptions; 29 | } 30 | 31 | export class CommandContext { 32 | /// project environment 33 | readonly projectEnv: ProjectEnv; 34 | /// Xcode project settings provider 35 | private _projectSettingsProvider: ProjectSettingsProvider; 36 | get projectSettingsProvider(): ProjectSettingsProvider { 37 | return this._projectSettingsProvider; 38 | } 39 | 40 | readonly bundle: BundlePath; 41 | 42 | /// debug logs emitter 43 | private _debugConsoleEmitter = new vscode.EventEmitter(); 44 | get debugConsoleEvent(): vscode.Event { 45 | return this._debugConsoleEmitter.event; 46 | } 47 | 48 | /// build logs emitter 49 | private _buildEmitter = new vscode.EventEmitter(); 50 | get buildEvent(): vscode.Event { 51 | return this._buildEmitter.event; 52 | } 53 | 54 | readonly lspClient: LSPClientContext; 55 | 56 | private _terminal?: TerminalShell; 57 | public get terminal(): TerminalShell | undefined { 58 | return this._terminal; 59 | } 60 | 61 | private _cancellationTokenSource: vscode.CancellationTokenSource; 62 | public get cancellationToken(): vscode.CancellationToken { 63 | return this._cancellationTokenSource.token; 64 | } 65 | 66 | constructor( 67 | cancellationToken: vscode.CancellationTokenSource, 68 | terminal: TerminalShell | undefined, 69 | lspClient: LSPClientContext, 70 | bundle: BundlePath 71 | ) { 72 | this.bundle = bundle; 73 | this._cancellationTokenSource = cancellationToken; 74 | this._projectSettingsProvider = new ProjectSettingsProvider(this); 75 | this.projectEnv = new ProjectEnv(this._projectSettingsProvider); 76 | this._projectSettingsProvider.projectEnv = new WeakRef(this.projectEnv); 77 | this._terminal = terminal; 78 | this.lspClient = lspClient; 79 | } 80 | 81 | private convertToExeParams(shell: CommandOptions, attachTerminal: boolean) { 82 | const shellExe = shell as ShellExec; 83 | shellExe.cancellationToken = this._cancellationTokenSource.token; 84 | const stdoutCallback = 85 | shell.pipeToDebugConsole === true || shell.pipeToParseBuildErrors === true 86 | ? (out: string) => { 87 | if (shell.pipeToDebugConsole === true) { 88 | this._debugConsoleEmitter.fire(out); 89 | } 90 | if (shell.pipeToParseBuildErrors === true) { 91 | this._buildEmitter.fire(out); 92 | } 93 | } 94 | : undefined; 95 | shellExe.stdoutCallback = stdoutCallback; 96 | if (attachTerminal) { 97 | shellExe.terminal = this.terminal; 98 | } 99 | 100 | if (shell.pipe) { 101 | shellExe.pipe = this.convertToExeParams(shell.pipe, attachTerminal); 102 | } 103 | return shell; 104 | } 105 | 106 | public async execShellWithOptions(shell: CommandOptions): Promise { 107 | const shellExe = this.convertToExeParams(shell, true); 108 | return await new Executor().execShell(shellExe); 109 | } 110 | 111 | public async execShell( 112 | terminalName: string, 113 | scriptOrCommand: ShellCommand | ShellFileScript, 114 | args: string[] = [], 115 | mode: ExecutorMode = ExecutorMode.verbose 116 | ): Promise { 117 | return await new Executor().execShell({ 118 | cancellationToken: this._cancellationTokenSource.token, 119 | scriptOrCommand: scriptOrCommand, 120 | args: args, 121 | mode: mode, 122 | terminal: this._terminal, 123 | }); 124 | } 125 | 126 | public async execShellParallel(shell: CommandOptions): Promise { 127 | const shellExe = this.convertToExeParams(shell, false); 128 | return new Executor().execShell(shellExe); 129 | } 130 | 131 | public waitToCancel() { 132 | const disLocalCancel: vscode.Disposable[] = []; 133 | const finishToken = new vscode.EventEmitter(); 134 | const rejectToken = new vscode.EventEmitter(); 135 | return { 136 | wait: new Promise((resolve, reject) => { 137 | if (this.cancellationToken.isCancellationRequested) { 138 | resolve(); 139 | return; 140 | } 141 | disLocalCancel.push( 142 | this.cancellationToken.onCancellationRequested(() => { 143 | disLocalCancel.forEach(e => e.dispose()); 144 | reject(UserTerminatedError); 145 | }) 146 | ); 147 | disLocalCancel.push( 148 | finishToken.event(() => { 149 | disLocalCancel.forEach(e => e.dispose()); 150 | resolve(); 151 | }) 152 | ); 153 | disLocalCancel.push( 154 | rejectToken.event(error => { 155 | disLocalCancel.forEach(e => e.dispose()); 156 | reject(error); 157 | }) 158 | ); 159 | }), 160 | token: finishToken, 161 | rejectToken: rejectToken, 162 | }; 163 | } 164 | 165 | public cancel() { 166 | this._cancellationTokenSource.cancel(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Debug/DebugAdapterTrackerFactory.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ProblemDiagnosticResolver } from "../ProblemDiagnosticResolver"; 3 | import { DebugAdapterTracker } from "./DebugAdapterTracker"; 4 | import { ParentDebugAdapterTracker } from "./ParentDebugAdapterTracker"; 5 | 6 | export class DebugAdapterTrackerFactory implements vscode.DebugAdapterTrackerFactory { 7 | private problemResolver: ProblemDiagnosticResolver; 8 | 9 | constructor(problemResolver: ProblemDiagnosticResolver) { 10 | this.problemResolver = problemResolver; 11 | } 12 | 13 | createDebugAdapterTracker( 14 | session: vscode.DebugSession 15 | ): vscode.ProviderResult { 16 | if ( 17 | session.type === "debugpy" && 18 | session.configuration.sessionId !== undefined && 19 | session.configuration.target === "parent" 20 | ) { 21 | return new ParentDebugAdapterTracker(session); 22 | } 23 | if ( 24 | (session.type === "xcode-lldb" || session.type === "lldb") && 25 | session.configuration.sessionId 26 | ) { 27 | return new DebugAdapterTracker(session, this.problemResolver); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Debug/LLDBDapDescriptorFactory.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { XCRunHelper } from "../Tools/XCRunHelper"; 3 | import { getWorkspaceFolder } from "../env"; 4 | 5 | function useLLDB_DAP() { 6 | const isEnabled = vscode.workspace 7 | .getConfiguration("vscode-ios", getWorkspaceFolder()) 8 | .get("debug.lldb-dap"); 9 | if (!isEnabled) { 10 | return false; 11 | } 12 | return true; 13 | } 14 | 15 | /** 16 | * This class defines a factory used to find the lldb-dap binary to use 17 | * depending on the session configuration. 18 | */ 19 | export class LLDBDapDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { 20 | constructor() {} 21 | 22 | static async isValidDebugAdapterPath(pathUri: vscode.Uri): Promise { 23 | try { 24 | const fileStats = await vscode.workspace.fs.stat(pathUri); 25 | if (!(fileStats.type & vscode.FileType.File)) { 26 | return false; 27 | } 28 | } catch (err) { 29 | return false; 30 | } 31 | return true; 32 | } 33 | 34 | async createDebugAdapterDescriptor( 35 | session: vscode.DebugSession, 36 | executable: vscode.DebugAdapterExecutable | undefined 37 | ): Promise { 38 | if (session.configuration.isDummy === true) { 39 | // dummy session 40 | return new vscode.DebugAdapterExecutable("", []); 41 | } 42 | const path = await LLDBDapDescriptorFactory.getXcodeDebuggerExePath(); 43 | if (path === null) { 44 | LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(); 45 | return undefined; 46 | } 47 | 48 | const log_path = session.configuration.logPath + ".lldb"; 49 | const env: { [key: string]: string } = {}; 50 | if (log_path) { 51 | // Uncomment it for Debug purposes 52 | // env["LLDBDAP_LOG"] = getFilePathInWorkspace(log_path); 53 | } 54 | 55 | // const configEnvironment = config.get<{ [key: string]: string }>("lldb.environment") || {}; 56 | if (path) { 57 | const dbgOptions = { 58 | env: { 59 | // ...configEnvironment, 60 | ...env, 61 | }, 62 | }; 63 | return new vscode.DebugAdapterExecutable(path, [], dbgOptions); 64 | } else if (executable) { 65 | return new vscode.DebugAdapterExecutable(executable.command, executable.args, { 66 | ...executable.options, 67 | env: { 68 | ...executable.options?.env, 69 | // ...configEnvironment, 70 | ...env, 71 | }, 72 | }); 73 | } else { 74 | return undefined; 75 | } 76 | } 77 | 78 | static async getXcodeDebuggerExePath() { 79 | try { 80 | const path = await XCRunHelper.getLLDBDapPath(); 81 | const fileUri = vscode.Uri.file(path); 82 | const majorSwiftVersion = Number((await XCRunHelper.swiftToolchainVersion())[0]); 83 | // starting swift 6, lldb-dap is included in swift toolchain, so use is 84 | if ( 85 | majorSwiftVersion >= 6 && 86 | useLLDB_DAP() && 87 | (await LLDBDapDescriptorFactory.isValidDebugAdapterPath(fileUri)) 88 | ) { 89 | return path; 90 | } 91 | return null; 92 | } catch { 93 | return null; 94 | } 95 | } 96 | /** 97 | * Shows a message box when the debug adapter's path is not found 98 | */ 99 | static async showLLDBDapNotFoundMessage() { 100 | const openSettingsAction = "Reload VS Code"; 101 | const callbackValue = await vscode.window.showErrorMessage( 102 | `Xcode Debug adapter is not valid. Please make sure that Xcode is installed and restart VS Code!`, 103 | openSettingsAction 104 | ); 105 | 106 | if (openSettingsAction === callbackValue) { 107 | vscode.commands.executeCommand("workbench.action.reloadWindow"); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Debug/ParentDebugAdapterTracker.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DebugConfigurationProvider } from "./DebugConfigurationProvider"; 3 | import { DebugAdapterTracker } from "./DebugAdapterTracker"; 4 | 5 | export class ParentDebugAdapterTracker implements vscode.DebugAdapterTracker { 6 | private debugSession: vscode.DebugSession; 7 | private isTerminated = false; 8 | 9 | private get sessionID(): string { 10 | return this.debugSession.configuration.sessionId; 11 | } 12 | private get context() { 13 | return DebugConfigurationProvider.getContextForSession(this.sessionID)!; 14 | } 15 | 16 | private dis?: vscode.Disposable; 17 | 18 | constructor(debugSession: vscode.DebugSession) { 19 | this.debugSession = debugSession; 20 | } 21 | 22 | onWillStartSession() { 23 | try { 24 | this.dis = this.context.commandContext.cancellationToken.onCancellationRequested(() => { 25 | this.terminateCurrentSession(); 26 | }); 27 | } catch { 28 | /* empty */ 29 | this.terminateCurrentSession(); 30 | } 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars 34 | onDidSendMessage(_message: any) {} 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars 37 | onWillReceiveMessage(_message: any) {} 38 | 39 | onWillStopSession() { 40 | console.log("Session will stop"); 41 | this.terminateCurrentSession(); 42 | } 43 | 44 | onError(error: Error) { 45 | console.log("Error:", error); 46 | } 47 | 48 | onExit(code: number | undefined, signal: string | undefined) { 49 | console.log(`Exited with code ${code} and signal ${signal}`); 50 | } 51 | 52 | private async terminateCurrentSession() { 53 | if (this.isTerminated) { 54 | return; 55 | } 56 | try { 57 | this.dis?.dispose(); 58 | this.dis = undefined; 59 | this.isTerminated = true; 60 | await DebugAdapterTracker.updateStatus(this.sessionID, "stopped"); 61 | } finally { 62 | try { 63 | this.context.commandContext.cancel(); 64 | } catch { 65 | /* empty */ 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Debug/SimulatorFocus.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import { DeviceID, ProjectEnv } from "../env"; 3 | import path from "path"; 4 | 5 | export class SimulatorFocus { 6 | private deviceID?: DeviceID; 7 | private productName?: string; 8 | 9 | constructor() {} 10 | 11 | async init(projectEnv: ProjectEnv, processExe: string) { 12 | this.deviceID = await projectEnv.debugDeviceID; 13 | this.productName = processExe.split(path.sep).at(0); 14 | if (this.productName === undefined) { 15 | this.productName = await projectEnv.productName; 16 | } else if (this.productName.endsWith(".app")) { 17 | this.productName = this.productName.slice(0, -".app".length); 18 | } 19 | } 20 | 21 | focus() { 22 | if (this.productName === undefined || this.deviceID === undefined) { 23 | return; 24 | } 25 | 26 | try { 27 | if (this.deviceID?.platform === "macOS") { 28 | // eslint-disable-next-line no-useless-escape 29 | execSync(`osascript -e \"tell application \\"${this.productName}\\" to activate\"`); 30 | } else { 31 | execSync(`open -a Simulator --args -CurrentDeviceUDID ${this.deviceID.id}`); 32 | } 33 | } catch (error) { 34 | console.log(`Simulator was not focused. Error: ${error}`); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Debug/XCTestRunInspector.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { glob } from "glob"; 3 | import { buildTestsForCurrentFile } from "../buildCommands"; 4 | import { CommandContext } from "../CommandManagement/CommandContext"; 5 | import { ProblemDiagnosticResolver } from "../ProblemDiagnosticResolver"; 6 | import { getProductDir } from "../env"; 7 | import { XCRunHelper } from "../Tools/XCRunHelper"; 8 | 9 | type XCTestRunFile = { 10 | file: string; 11 | stat: Thenable; 12 | }; 13 | 14 | export type XCTestTarget = { 15 | target: string; 16 | host: string; 17 | testRun: string; 18 | }; 19 | 20 | export class XCTestRunInspector { 21 | constructor(private problemResolver: ProblemDiagnosticResolver) {} 22 | 23 | async build(context: CommandContext, tests: string[] | undefined, isCoverage: boolean) { 24 | const existingFiles = await this.getAllXCRunFiles(); 25 | if (tests) { 26 | await buildTestsForCurrentFile(context, this.problemResolver, tests, isCoverage); 27 | } else { 28 | await buildTestsForCurrentFile(context, this.problemResolver, [], isCoverage); 29 | } 30 | const changedFiles = await this.getChangedFiles(existingFiles); 31 | const selectedTestPlan = await this.getSelectedTestPlan(context); 32 | const targets = await this.parseXCRun(changedFiles, selectedTestPlan); 33 | if (tests) { 34 | const testsTargets = tests.map(test => test.split("/").at(0)); 35 | return targets.filter(target => testsTargets.includes(target.target)); 36 | } else { 37 | return targets; 38 | } 39 | } 40 | 41 | private async getAllXCRunFiles(): Promise { 42 | const files = await glob("*.xctestrun", { 43 | absolute: true, 44 | cwd: await getProductDir(), 45 | }); 46 | return files.map(file => { 47 | return { file: file, stat: vscode.workspace.fs.stat(vscode.Uri.file(file)) }; 48 | }); 49 | } 50 | 51 | private async getChangedFiles(beforeBuildFiles: XCTestRunFile[]) { 52 | const afterBuildFiles = await this.getAllXCRunFiles(); 53 | const changedFiles: string[] = []; 54 | for (const afterFile of afterBuildFiles) { 55 | const index = beforeBuildFiles.findIndex(value => value.file === afterFile.file); 56 | if (index !== -1) { 57 | // found, check if the file was changed during a build command 58 | if ((await afterFile.stat).mtime !== (await beforeBuildFiles[index].stat).mtime) { 59 | changedFiles.push(afterFile.file); 60 | } 61 | } else { 62 | // it's a new file 63 | changedFiles.push(afterFile.file); 64 | } 65 | } 66 | return changedFiles; 67 | } 68 | 69 | private async getSelectedTestPlan(context: CommandContext) { 70 | try { 71 | // check if it was pre selected by a user 72 | return await context.projectEnv.projectTestPlan; 73 | } catch { 74 | return undefined; 75 | } 76 | } 77 | 78 | private async parseXCRun( 79 | testRuns: string[], 80 | selectedTestPlan: string | undefined 81 | ): Promise { 82 | let selectedTestRun: any | null = null; 83 | let selectedFile: string = ""; 84 | for (const testRun of testRuns) { 85 | const stdout = await XCRunHelper.convertPlistToJson(testRun); 86 | const json = JSON.parse(stdout); 87 | if (testRuns.length === 1) { 88 | selectedTestRun = json; 89 | selectedFile = testRun; 90 | break; 91 | } else if (selectedTestPlan === undefined) { 92 | if (json.TestPlan?.IsDefault === true) { 93 | // set default as it was not preselected by a user 94 | selectedTestRun = json; 95 | selectedFile = testRun; 96 | break; 97 | } 98 | } else if (json.TestPlan?.Name === selectedTestPlan) { 99 | selectedFile = testRun; 100 | selectedTestRun = json; 101 | break; 102 | } 103 | } 104 | const result: XCTestTarget[] = []; 105 | if (selectedTestRun !== null) { 106 | // parse configs to targets 107 | let configurations = selectedTestRun.TestConfigurations; 108 | if (configurations === undefined) { 109 | const testTargets = []; 110 | for (const key in selectedTestRun) { 111 | if (selectedTestRun[key].BlueprintName !== undefined) { 112 | testTargets.push(selectedTestRun[key]); 113 | } 114 | } 115 | configurations = [{ TestTargets: testTargets }]; 116 | } 117 | for (const config of configurations) { 118 | console.log(config); 119 | for (const testTarget of config.TestTargets) { 120 | const hostPath = testTarget.TestHostPath.replace( 121 | "__TESTROOT__/", 122 | await getProductDir() 123 | ); 124 | 125 | result.push({ 126 | target: testTarget.BlueprintName, 127 | host: hostPath, 128 | testRun: selectedFile, 129 | }); 130 | } 131 | } 132 | } 133 | return result; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/LSP/LSPTestsProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { languageId, LSPTestItem, textDocumentTestsRequest } from "./lspExtension"; 3 | import { SwiftLSPClient } from "./SwiftLSPClient"; 4 | import * as lp from "vscode-languageserver-protocol"; 5 | import * as fs from "fs"; 6 | import { getFilePathInWorkspace } from "../env"; 7 | 8 | export class LSPTestsProvider { 9 | private version = 0; 10 | private dummyFile = getFilePathInWorkspace(".vscode/xcode/dummy.swift"); 11 | private request: Promise | undefined; 12 | 13 | constructor(private lspClient: SwiftLSPClient) { 14 | fs.writeFileSync(this.dummyFile, ""); 15 | } 16 | 17 | async fetchTests(document: vscode.Uri, content: string): Promise { 18 | if (this.request) { 19 | try { 20 | await this.request; 21 | } catch { 22 | /* empty */ 23 | } 24 | } 25 | 26 | const isRequested = this.performFetch(document, content); 27 | this.request = isRequested; 28 | return isRequested; 29 | } 30 | 31 | private async performFetch(document: vscode.Uri, content: string) { 32 | this.version++; 33 | const client = await this.lspClient.client(); 34 | const langId = languageId(document.fsPath); 35 | if (langId !== "swift") { 36 | return []; 37 | } 38 | 39 | const dummyUri = vscode.Uri.file(this.dummyFile); 40 | 41 | const didOpenParam: lp.DidOpenTextDocumentParams = { 42 | textDocument: { 43 | uri: dummyUri.toString(), 44 | languageId: langId, 45 | text: content, 46 | version: this.version, 47 | }, 48 | }; 49 | 50 | await client.sendNotification(lp.DidOpenTextDocumentNotification.method, didOpenParam); 51 | 52 | try { 53 | const testsInDocument = await ( 54 | await this.lspClient.client() 55 | ).sendRequest(textDocumentTestsRequest, { 56 | textDocument: { uri: dummyUri.toString() }, 57 | }); 58 | return testsInDocument; 59 | } finally { 60 | // const didCloseParam: lp.DidCloseTextDocumentParams = { 61 | // textDocument: { uri: dummyUri.toString() }, 62 | // }; 63 | // await client.sendNotification( 64 | // lp.DidCloseTextDocumentNotification.method, 65 | // didCloseParam 66 | // ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/LSP/ReadOnlyDocumentProvider.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the VS Code Swift open source project 4 | // 5 | // Copyright (c) 2023 the VS Code Swift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of VS Code Swift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import * as vscode from "vscode"; 16 | import * as fs from "fs/promises"; 17 | 18 | /** 19 | * Registers a {@link vscode.TextDocumentContentProvider TextDocumentContentProvider} that will display 20 | * a readonly version of a file 21 | */ 22 | export function getReadOnlyDocumentProvider(): vscode.Disposable { 23 | const provider = vscode.workspace.registerTextDocumentContentProvider("readonly", { 24 | provideTextDocumentContent: async uri => { 25 | try { 26 | const contents = await fs.readFile(uri.fsPath, "utf8"); 27 | return contents; 28 | } catch (error) { 29 | return `Failed to load swiftinterface ${uri.path}`; 30 | } 31 | }, 32 | }); 33 | return provider; 34 | } 35 | -------------------------------------------------------------------------------- /src/LSP/SourceKitLSPErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import * as langclient from "vscode-languageclient/node"; 2 | import * as vscode from "vscode"; 3 | 4 | /** 5 | * SourceKit-LSP error handler. Copy of the default error handler, except it includes 6 | * an error message that asks if you want to restart the sourcekit-lsp server again 7 | * after so many crashes 8 | */ 9 | export class SourceKitLSPErrorHandler implements langclient.ErrorHandler { 10 | private restarts: number[]; 11 | private enabled: boolean = false; 12 | 13 | constructor(private maxRestartCount: number) { 14 | this.restarts = []; 15 | } 16 | /** 17 | * Start listening for errors and requesting to restart the LSP server when appropriate. 18 | */ 19 | enable() { 20 | this.enabled = true; 21 | } 22 | /** 23 | * An error has occurred while writing or reading from the connection. 24 | * 25 | * @param error - the error received 26 | * @param message - the message to be delivered to the server if know. 27 | * @param count - a count indicating how often an error is received. Will 28 | * be reset if a message got successfully send or received. 29 | */ 30 | error( 31 | error: Error, 32 | message: langclient.Message | undefined, 33 | count: number | undefined 34 | ): langclient.ErrorHandlerResult | Promise { 35 | if (count && count <= 3) { 36 | return { action: langclient.ErrorAction.Continue }; 37 | } 38 | return { action: langclient.ErrorAction.Shutdown }; 39 | } 40 | /** 41 | * The connection to the server got closed. 42 | */ 43 | closed(): langclient.CloseHandlerResult | Promise { 44 | if (!this.enabled) { 45 | return { 46 | action: langclient.CloseAction.DoNotRestart, 47 | handled: true, 48 | }; 49 | } 50 | 51 | this.restarts.push(Date.now()); 52 | if (this.restarts.length <= this.maxRestartCount) { 53 | return { action: langclient.CloseAction.Restart }; 54 | } else { 55 | const diff = this.restarts[this.restarts.length - 1] - this.restarts[0]; 56 | if (diff <= 3 * 60 * 1000) { 57 | return new Promise(resolve => { 58 | vscode.window 59 | .showErrorMessage( 60 | `The SourceKit-LSP server crashed ${ 61 | this.maxRestartCount + 1 62 | } times in the last 3 minutes. See the output for more information. Do you want to restart it again.`, 63 | "Yes", 64 | "No" 65 | ) 66 | .then(result => { 67 | if (result === "Yes") { 68 | this.restarts = []; 69 | resolve({ action: langclient.CloseAction.Restart }); 70 | } else { 71 | resolve({ action: langclient.CloseAction.DoNotRestart }); 72 | } 73 | }); 74 | }); 75 | } else { 76 | this.restarts.shift(); 77 | return { action: langclient.CloseAction.Restart }; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/LSP/WorkspaceContext.ts: -------------------------------------------------------------------------------- 1 | import { getLSPWorkspacePath, getWorkspaceFolder } from "../env"; 2 | import { Executor } from "../Executor"; 3 | import { XCRunHelper } from "../Tools/XCRunHelper"; 4 | import { HandleProblemDiagnosticResolver } from "./lspExtension"; 5 | import * as vscode from "vscode"; 6 | 7 | export interface WorkspaceContext { 8 | readonly workspaceFolder: Promise; 9 | readonly problemDiagnosticResolver: HandleProblemDiagnosticResolver; 10 | setLLDBVersion: () => Promise; 11 | } 12 | 13 | export class WorkspaceContextImp implements WorkspaceContext { 14 | get workspaceFolder(): Promise { 15 | return getLSPWorkspacePath(); 16 | } 17 | readonly problemDiagnosticResolver: HandleProblemDiagnosticResolver; 18 | constructor(problemDiagnosticResolver: HandleProblemDiagnosticResolver) { 19 | this.problemDiagnosticResolver = problemDiagnosticResolver; 20 | } 21 | 22 | /** find LLDB version and setup path in CodeLLDB */ 23 | async setLLDBVersion() { 24 | // check we are using CodeLLDB 25 | try { 26 | const libPath = await getLLDBLibPath(); 27 | if (libPath === undefined) { 28 | throw Error("LLDB bin framework is not found."); 29 | } 30 | const lldbConfig = vscode.workspace.getConfiguration("lldb", getWorkspaceFolder()); 31 | const configLLDBPath = lldbConfig.get("library"); 32 | const expressions = lldbConfig.get("launch.expressions"); 33 | if (configLLDBPath === libPath && expressions === "native") { 34 | return; 35 | } 36 | 37 | // show dialog for setting up LLDB 38 | const result = await vscode.window.showInformationMessage( 39 | "The Xcode extension needs to update some CodeLLDB settings to enable debugging features. Do you want to set this up in your global settings or the workspace settings?", 40 | "Global", 41 | "Workspace", 42 | "Cancel" 43 | ); 44 | switch (result) { 45 | case "Global": 46 | await lldbConfig.update("library", libPath, vscode.ConfigurationTarget.Global); 47 | await lldbConfig.update( 48 | "launch.expressions", 49 | "native", 50 | vscode.ConfigurationTarget.Global 51 | ); 52 | // clear workspace setting 53 | await lldbConfig.update( 54 | "library", 55 | undefined, 56 | vscode.ConfigurationTarget.WorkspaceFolder 57 | ); 58 | // clear workspace setting 59 | await lldbConfig.update( 60 | "launch.expressions", 61 | undefined, 62 | vscode.ConfigurationTarget.WorkspaceFolder 63 | ); 64 | break; 65 | case "Workspace": 66 | await lldbConfig.update( 67 | "library", 68 | libPath, 69 | vscode.ConfigurationTarget.WorkspaceFolder 70 | ); 71 | await lldbConfig.update( 72 | "launch.expressions", 73 | "native", 74 | vscode.ConfigurationTarget.WorkspaceFolder 75 | ); 76 | break; 77 | } 78 | } catch (error) { 79 | const errorMessage = `Error: ${error}`; 80 | vscode.window.showErrorMessage( 81 | `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` 82 | ); 83 | throw error; 84 | } 85 | } 86 | } 87 | 88 | async function getLLDBLibPath() { 89 | const executable = await XCRunHelper.lldbBinPath(); 90 | const statement = `print('')`; 91 | const args = ["-b", "-O", `script ${statement}`]; 92 | const result = await new Executor().execShell({ 93 | scriptOrCommand: { command: executable }, 94 | args: args, 95 | }); 96 | if (result !== null) { 97 | const m = /^/m.exec(result.stdout); 98 | if (m) { 99 | return m[1]; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/LSP/getReferenceDocument.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the VS Code Swift open source project 4 | // 5 | // Copyright (c) 2024 the VS Code Swift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of VS Code Swift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import * as vscode from "vscode"; 16 | import * as langclient from "vscode-languageclient/node"; 17 | import { GetReferenceDocumentParams, GetReferenceDocumentRequest } from "./lspExtension"; 18 | 19 | export function activateGetReferenceDocument(client: langclient.LanguageClient): vscode.Disposable { 20 | const getReferenceDocument = vscode.workspace.registerTextDocumentContentProvider( 21 | "sourcekit-lsp", 22 | { 23 | provideTextDocumentContent: async (uri, token) => { 24 | const params: GetReferenceDocumentParams = { 25 | uri: client.code2ProtocolConverter.asUri(uri), 26 | }; 27 | 28 | const result = await client.sendRequest(GetReferenceDocumentRequest, params, token); 29 | 30 | if (result) { 31 | return result.content; 32 | } else { 33 | return "Unable to retrieve reference document"; 34 | } 35 | }, 36 | } 37 | ); 38 | 39 | return getReferenceDocument; 40 | } 41 | -------------------------------------------------------------------------------- /src/LSP/lspExtension.ts: -------------------------------------------------------------------------------- 1 | import * as ls from "vscode-languageserver-protocol"; 2 | import * as langclient from "vscode-languageclient/node"; 3 | import * as vscode from "vscode"; 4 | 5 | // Test styles where test-target represents a test target that contains tests 6 | export type TestStyle = "XCTest" | "swift-testing" | "test-target"; 7 | 8 | export interface LSPClientContext { 9 | start: () => void; 10 | restart: () => void; 11 | } 12 | 13 | export type SourcePredicate = (source: string) => boolean; 14 | 15 | export interface HandleProblemDiagnosticResolver { 16 | handleDiagnostics: ( 17 | uri: vscode.Uri, 18 | isSourceKit: SourcePredicate, 19 | newDiagnostics: vscode.Diagnostic[] 20 | ) => void; 21 | } 22 | 23 | export interface LSPTestItem { 24 | /** 25 | * This identifier uniquely identifies the test case or test suite. It can be used to run an individual test (suite). 26 | */ 27 | id: string; 28 | 29 | /** 30 | * Display name describing the test. 31 | */ 32 | label: string; 33 | 34 | /** 35 | * Optional description that appears next to the label. 36 | */ 37 | description?: string; 38 | 39 | /** 40 | * A string that should be used when comparing this item with other items. 41 | * 42 | * When `undefined` the `label` is used. 43 | */ 44 | sortText?: string; 45 | 46 | /** 47 | * Whether the test is disabled. 48 | */ 49 | disabled: boolean; 50 | 51 | /** 52 | * The type of test, eg. the testing framework that was used to declare the test. 53 | */ 54 | style: TestStyle; 55 | 56 | /** 57 | * The location of the test item in the source code. 58 | */ 59 | location: ls.Location; 60 | 61 | /** 62 | * The children of this test item. 63 | * 64 | * For a test suite, this may contain the individual test cases or nested suites. 65 | */ 66 | children: LSPTestItem[]; 67 | 68 | /** 69 | * Tags associated with this test item. 70 | */ 71 | tags: { id: string }[]; 72 | } 73 | 74 | // Definitions for non-standard requests used by sourcekit-lsp 75 | 76 | // Peek Documents 77 | export interface PeekDocumentsParams { 78 | /** 79 | * The `DocumentUri` of the text document in which to show the "peeked" editor 80 | */ 81 | uri: langclient.DocumentUri; 82 | 83 | /** 84 | * The `Position` in the given text document in which to show the "peeked editor" 85 | */ 86 | position: vscode.Position; 87 | 88 | /** 89 | * An array `DocumentUri` of the documents to appear inside the "peeked" editor 90 | */ 91 | locations: langclient.DocumentUri[]; 92 | } 93 | 94 | /** 95 | * Response to indicate the `success` of the `PeekDocumentsRequest` 96 | */ 97 | export interface PeekDocumentsResult { 98 | success: boolean; 99 | } 100 | 101 | /** 102 | * Request from the server to the client to show the given documents in a "peeked" editor. 103 | * 104 | * This request is handled by the client to show the given documents in a "peeked" editor (i.e. inline with / inside the editor canvas). 105 | * 106 | * It requires the experimental client capability `"workspace/peekDocuments"` to use. 107 | */ 108 | export const PeekDocumentsRequest = new langclient.RequestType< 109 | PeekDocumentsParams, 110 | PeekDocumentsResult, 111 | unknown 112 | >("workspace/peekDocuments"); 113 | 114 | // Get Reference Document 115 | export interface GetReferenceDocumentParams { 116 | /** 117 | * The `DocumentUri` of the custom scheme url for which content is required 118 | */ 119 | uri: langclient.DocumentUri; 120 | } 121 | 122 | /** 123 | * Response containing `content` of `GetReferenceDocumentRequest` 124 | */ 125 | export interface GetReferenceDocumentResult { 126 | content: string; 127 | } 128 | 129 | /** 130 | * Request from the client to the server asking for contents of a URI having a custom scheme 131 | * For example: "sourcekit-lsp:" 132 | */ 133 | export const GetReferenceDocumentRequest = new langclient.RequestType< 134 | GetReferenceDocumentParams, 135 | GetReferenceDocumentResult, 136 | unknown 137 | >("workspace/getReferenceDocument"); 138 | 139 | interface DocumentTestsParams { 140 | textDocument: { 141 | uri: ls.URI; 142 | }; 143 | } 144 | 145 | export const textDocumentTestsRequest = new langclient.RequestType< 146 | DocumentTestsParams, 147 | LSPTestItem[], 148 | unknown 149 | >("textDocument/tests"); 150 | 151 | /** Language client errors */ 152 | export enum LanguageClientError { 153 | LanguageClientUnavailable, 154 | } 155 | 156 | export function getTestIDComponents(id: string) { 157 | const dotIndex = id.indexOf("."); 158 | let target = ""; 159 | if (dotIndex !== -1) { 160 | target = id.substring(0, dotIndex); 161 | id = id.substring(dotIndex + 1); 162 | } 163 | const components = id.split("/"); 164 | const suite = components.length <= 1 ? undefined : components.slice(0, -1).join("/"); 165 | const testName = components.at(-1); 166 | return { target: target, suite: suite, testName: testName }; 167 | } 168 | 169 | export function languageId(file: string) { 170 | if (file.endsWith(".swift")) { 171 | return "swift"; 172 | } 173 | if (file.endsWith(".m")) { 174 | return "objective-c"; 175 | } 176 | if (file.endsWith(".mm")) { 177 | return "objective-cpp"; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/LSP/peekDocuments.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the VS Code Swift open source project 4 | // 5 | // Copyright (c) 2024 the VS Code Swift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of VS Code Swift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import * as vscode from "vscode"; 16 | import * as langclient from "vscode-languageclient/node"; 17 | import { PeekDocumentsParams, PeekDocumentsRequest } from "./lspExtension"; 18 | 19 | /** 20 | * Opens a peeked editor in `uri` at `position` having contents from `locations`. 21 | * 22 | * **NOTE**: 23 | * - If the `uri` is not open in the editor, this opens the `uri` in the editor and then opens a peeked editor. 24 | * - This closes any previously displayed peeked editor in `uri` and then, reopens a peeked editor in `uri` at 25 | * the given `position` with contents from the new `locations`. 26 | * 27 | * @param uri The uri of the file in which a peeked editor is to be opened 28 | * @param position The position in the file in which a peeked editor is to be opened 29 | * @param locations The locations of the contents which has to be displayed by the peeked editor 30 | */ 31 | async function openPeekedEditorIn( 32 | uri: vscode.Uri, 33 | position: vscode.Position, 34 | locations: vscode.Location[] 35 | ) { 36 | // #### NOTE - Undocumented behaviour of invoking VS Code's built-in "editor.action.peekLocations" command: 37 | // 1. If the `uri` is not open in the editor, it opens the `uri` in the editor and then opens a peeked editor. 38 | // 2. It always closes the previous peeked editor (If any) 39 | // 3. And after closing, It opens a new peeked editor having the contents of `locations` in `uri` **if and only 40 | // if** the previous peeked editor was displayed at a *different* `position` in `uri`. 41 | // 4. If it happens to be that the previous peeked editor was displayed at the *same* `position` in `uri`, then it 42 | // doesn't open the peeked editor window having the contents of new `locations` at all. 43 | 44 | // As (4.) says above, if we invoke "editor.action.peekLocations" on a position in which another peeked editor 45 | // window is already being shown, it won't cause the new peeked editor window to show up at all. This is not the 46 | // ideal behaviour. 47 | // 48 | // For example: 49 | // If there's already a peeked editor window at the position (2, 2) in "main.swift", its impossible to close this 50 | // peeked editor window and open a new peeked editor window at the same position (2, 2) in "main.swift" by invoking 51 | // the "editor.action.peekLocations" command in a single call. 52 | // 53 | // *The ideal behaviour* is to close any previously opened peeked editor window and then open the new one without 54 | // any regard to its `position` in the `uri`. 55 | 56 | // In order to achieve *the ideal behaviour*, we manually close the peeked editor window by ourselves before 57 | // opening a new peeked editor window. 58 | // 59 | // Since there isn't any API available to close the previous peeked editor, as a **workaround**, we open a dummy 60 | // peeked editor at a different position, causing the previous one to close irrespective of where it is. After 61 | // which we can invoke the command again to show the actual peeked window having the contents of the `locations`. 62 | await vscode.commands.executeCommand( 63 | "editor.action.peekLocations", 64 | uri, 65 | new vscode.Position(position.line, position.character !== 0 ? position.character - 1 : 1), 66 | [new vscode.Location(vscode.Uri.parse(""), new vscode.Position(0, 0))], 67 | "peek" 68 | ); 69 | 70 | // Opens the actual peeked editor window 71 | await vscode.commands.executeCommand( 72 | "editor.action.peekLocations", 73 | uri, 74 | position, 75 | locations, 76 | "peek" 77 | ); 78 | } 79 | 80 | export function activatePeekDocuments(client: langclient.LanguageClient): vscode.Disposable { 81 | const peekDocuments = client.onRequest( 82 | PeekDocumentsRequest.method, 83 | async (params: PeekDocumentsParams) => { 84 | const peekURI = client.protocol2CodeConverter.asUri(params.uri); 85 | 86 | const peekPosition = new vscode.Position( 87 | params.position.line, 88 | params.position.character 89 | ); 90 | 91 | const peekLocations = params.locations.map( 92 | location => 93 | new vscode.Location( 94 | client.protocol2CodeConverter.asUri(location), 95 | new vscode.Position(0, 0) 96 | ) 97 | ); 98 | 99 | openPeekedEditorIn(peekURI, peekPosition, peekLocations); 100 | 101 | return { success: true }; 102 | } 103 | ); 104 | 105 | return peekDocuments; 106 | } 107 | -------------------------------------------------------------------------------- /src/LSP/uriConverters.ts: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the VS Code Swift open source project 4 | // 5 | // Copyright (c) 2024 the VS Code Swift project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of VS Code Swift project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import * as vscode from "vscode"; 16 | 17 | export const uriConverters = { 18 | protocol2Code: (value: string): vscode.Uri => { 19 | if (!value.startsWith("sourcekit-lsp:")) { 20 | // Use the default implementation for all schemes other than sourcekit-lsp, as defined here: 21 | // https://github.com/microsoft/vscode-languageserver-node/blob/14ddabfc22187b698e83ecde072247aa40727308/client/src/common/protocolConverter.ts#L286 22 | return vscode.Uri.parse(value); 23 | } 24 | 25 | // vscode.uri fails to round-trip URIs that have both a `=` and `%3D` (percent-encoded `=`) in the query component. 26 | // ```ts 27 | // vscode.Uri.parse("scheme://host?outer=inner%3Dvalue").toString() -> 'scheme://host?outer%3Dinner%3Dvalue' 28 | // vscode.Uri.parse("scheme://host?outer=inner%3Dvalue").toString(/*skipEncoding*/ true) -> 'scheme://host?outer=inner=value' 29 | // ``` 30 | // The SourceKit-LSP scheme relies heavily on encoding options in the query parameters, eg. for Swift macro 31 | // expansions and the values of those query parameters might contain percent-encoded `=` signs. 32 | // 33 | // To work around the round-trip issue, use the URL type from Node.js to parse the URI and then map the URL 34 | // components to the Uri components in VS Code. 35 | const url = new URL(value); 36 | let scheme = url.protocol; 37 | if (scheme.endsWith(":")) { 38 | // URL considers ':' part of the protocol, `vscode.URI` does not consider it part of the scheme. 39 | scheme = scheme.substring(0, scheme.length - 1); 40 | } 41 | 42 | let auth = url.username; 43 | if (url.password) { 44 | auth += ":" + url.password; 45 | } 46 | let host = url.host; 47 | if (auth) { 48 | host = auth + "@" + host; 49 | } 50 | 51 | let query = url.search; 52 | if (query.startsWith("?")) { 53 | // URL considers '?' not part of the search, `vscode.URI` does consider '?' part of the query. 54 | query = query.substring(1); 55 | } 56 | 57 | let fragment = url.hash; 58 | if (fragment.startsWith("#")) { 59 | // URL considers '#' not part of the hash, `vscode.URI` does consider '#' part of the fragment. 60 | fragment = fragment.substring(1); 61 | } 62 | 63 | return vscode.Uri.from({ 64 | scheme: scheme, 65 | authority: host, 66 | path: url.pathname, 67 | query: query, 68 | fragment: fragment, 69 | }); 70 | }, 71 | code2Protocol: (value: vscode.Uri): string => { 72 | if (value.scheme !== "sourcekit-lsp") { 73 | // Use the default implementation for all schemes other than sourcekit-lsp, as defined here: 74 | // https://github.com/microsoft/vscode-languageserver-node/blob/14ddabfc22187b698e83ecde072247aa40727308/client/src/common/codeConverter.ts#L155 75 | return value.toString(); 76 | } 77 | // Create a dummy URL. We set all the components below. 78 | const url = new URL(value.scheme + "://"); 79 | 80 | // Uri encodes username and password in `authority`. Url has its custom fields for those. 81 | let host: string; 82 | let username: string; 83 | let password: string; 84 | const atInAuthority = value.authority.indexOf("@"); 85 | if (atInAuthority !== -1) { 86 | host = value.authority.substring(atInAuthority + 1); 87 | const auth = value.authority.substring(0, atInAuthority); 88 | const colonInAuth = auth.indexOf(":"); 89 | if (colonInAuth === -1) { 90 | username = auth; 91 | password = ""; 92 | } else { 93 | username = auth.substring(0, colonInAuth); 94 | password = auth.substring(colonInAuth + 1); 95 | } 96 | } else { 97 | host = value.authority; 98 | username = ""; 99 | password = ""; 100 | } 101 | 102 | // Need to set host before username and password because otherwise setting username + password is a no-op (probably 103 | // because a URL can't have a username without a host). 104 | url.host = host; 105 | url.username = username; 106 | url.password = password; 107 | url.pathname = value.path; 108 | 109 | let search = value.query; 110 | if (search) { 111 | // URL considers '?' not part of the search, vscode.URI does '?' part of the query. 112 | search = "?" + search; 113 | } 114 | url.search = search; 115 | 116 | let hash = value.fragment; 117 | if (hash) { 118 | // URL considers '#' not part of the hash, vscode.URI does '#' part of the fragment. 119 | hash = "#" + hash; 120 | } 121 | url.hash = hash; 122 | 123 | return url.toString(); 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /src/ProjectManager/ProjectTree.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | type Node = { 4 | isVisible: boolean; 5 | isLeaf: boolean; 6 | edges: Map | null; 7 | }; 8 | 9 | export class ProjectTree { 10 | private root: Node; 11 | 12 | constructor() { 13 | this.root = { isVisible: false, isLeaf: false, edges: null }; 14 | } 15 | 16 | addExcluded(filePath: string) { 17 | const components = filePath.split(path.sep); 18 | if (components.length <= 1) { 19 | return; 20 | } 21 | this.add(this.root, components, false, 0, false); 22 | } 23 | 24 | addIncluded(filePath: string, includeSubfolders = true) { 25 | const components = filePath.split(path.sep); 26 | if (components.length <= 1) { 27 | return; 28 | } 29 | this.add(this.root, components, true, 0, includeSubfolders); 30 | } 31 | 32 | excludedFiles() { 33 | const list: string[] = []; 34 | function excludedFiles(node: Node | undefined, filePath: string) { 35 | if (node === undefined) { 36 | return; 37 | } 38 | if (node.isLeaf && node.isVisible) { 39 | return; 40 | } 41 | if (node.edges === null) { 42 | if (!node.isVisible) { 43 | list.push(filePath); 44 | } 45 | return; 46 | } 47 | if (!node.isVisible) { 48 | list.push(filePath); 49 | return; 50 | } 51 | for (const [, value] of node.edges) { 52 | excludedFiles(value[1], path.join(filePath, value[0])); 53 | } 54 | } 55 | 56 | excludedFiles(this.root, ""); 57 | return list; 58 | } 59 | 60 | private add( 61 | node: Node | undefined, 62 | components: string[], 63 | isVisible: boolean, 64 | index: number, 65 | includeSubfolders: boolean 66 | ) { 67 | if (node === undefined) { 68 | return; 69 | } 70 | if (index >= components.length) { 71 | if (!isVisible) { 72 | return; 73 | } 74 | if (includeSubfolders) { 75 | node.isLeaf = true; 76 | } // if it's visible, tells that's a leaf 77 | return; 78 | } 79 | if (isVisible) { 80 | node.isVisible = true; 81 | } 82 | if (!isVisible && node.isLeaf) { 83 | return; 84 | } 85 | const edges = node.edges || new Map(); 86 | if (!edges.has(components[index].toLowerCase())) { 87 | edges.set(components[index].toLowerCase(), [ 88 | components[index], 89 | { 90 | isVisible: isVisible, 91 | isLeaf: index === components.length - 1 && includeSubfolders ? true : false, 92 | edges: null, 93 | }, 94 | ]); 95 | } 96 | node.edges = edges; 97 | const key = edges.get(components[index].toLowerCase()); 98 | this.add(key?.[1], components, isVisible, index + 1, includeSubfolders); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/ProjectManager/XcodeProjectFileProxy.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { ChildProcess, spawn } from "child_process"; 3 | import { getScriptPath } from "../env"; 4 | import { createInterface, Interface } from "readline"; 5 | import * as vscode from "vscode"; 6 | 7 | export class XcodeProjectFileProxy { 8 | private process: ChildProcess | undefined; 9 | private commandQueue: Promise | undefined; 10 | private rl: Interface | undefined; 11 | private onEndRead = new vscode.EventEmitter(); 12 | private onEndReadWithError = new vscode.EventEmitter(); 13 | 14 | constructor(projectPath: string) { 15 | this.runProcess(projectPath); 16 | } 17 | 18 | private runProcess(projectPath: string) { 19 | this.process = spawn(`ruby '${getScriptPath("project_helper.rb")}' '${projectPath}'`, { 20 | shell: true, 21 | stdio: "pipe", 22 | }); 23 | let stderr = ""; 24 | this.process.stderr?.on("data", data => { 25 | stderr += data.toString(); 26 | }); 27 | this.process.on("exit", (code, signal) => { 28 | this.rl = undefined; 29 | console.log(`Return code: ${code}, signal: ${signal}, error: ${stderr}`); 30 | this.onEndReadWithError.fire(Error(`${projectPath} file failed: ${stderr}`)); 31 | if (fs.existsSync(projectPath)) { 32 | this.runProcess(projectPath); 33 | } 34 | }); 35 | this.read(); 36 | } 37 | 38 | private async read() { 39 | const process = this.process; 40 | try { 41 | if (this.process?.stdout) { 42 | this.rl = createInterface({ 43 | input: this.process.stdout, //or fileStream 44 | terminal: false, 45 | }); 46 | } 47 | let result = [] as string[]; 48 | if (this.rl === undefined) { 49 | throw Error("Stream is undefined"); 50 | } 51 | for await (const line of this.rl) { 52 | if (line === "EOF_REQUEST") { 53 | this.onEndRead.fire(result); 54 | result = []; 55 | } else { 56 | result.push(line); 57 | } 58 | } 59 | } catch (error) { 60 | this.rl = undefined; 61 | process?.kill(); 62 | this.onEndReadWithError.fire(error); 63 | } 64 | } 65 | 66 | async request(command: string): Promise { 67 | if (this.commandQueue === undefined) { 68 | let dis: vscode.Disposable | undefined; 69 | let disError: vscode.Disposable | undefined; 70 | this.commandQueue = new Promise((resolve, reject) => { 71 | if (this.rl === undefined) { 72 | reject(Error("Process is killed")); 73 | } 74 | dis = this.onEndRead.event(e => { 75 | dis?.dispose(); 76 | this.commandQueue = undefined; 77 | resolve(e); 78 | }); 79 | disError = this.onEndReadWithError.event(e => { 80 | disError?.dispose(); 81 | this.commandQueue = undefined; 82 | reject(e); 83 | }); 84 | if (this.process?.stdin?.writable) { 85 | this.process.stdin.write(`${command}\n`); 86 | this.process.stdin.uncork(); 87 | } 88 | }); 89 | return await this.commandQueue; 90 | } else { 91 | const wait = this.commandQueue; 92 | await wait; 93 | return await this.request(command); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Services/BuildManager.ts: -------------------------------------------------------------------------------- 1 | import { BundlePath } from "../CommandManagement/BundlePath"; 2 | import { CommandContext } from "../CommandManagement/CommandContext"; 3 | import { ProjectEnv } from "../env"; 4 | import { ExecutorMode } from "../Executor"; 5 | import { CustomError } from "../utils"; 6 | import { TestPlanIsNotConfigured } from "./ProjectSettingsProvider"; 7 | 8 | export class BuildManager { 9 | constructor() {} 10 | 11 | static async commonArgs(projectEnv: ProjectEnv, bundle: BundlePath) { 12 | return [ 13 | "-configuration", 14 | await projectEnv.projectConfiguration, 15 | "-destination", 16 | `id=${(await projectEnv.debugDeviceID).id},platform=${(await projectEnv.debugDeviceID).platform}`, 17 | "-resultBundlePath", 18 | bundle.bundlePath(), 19 | "-skipMacroValidation", 20 | "-skipPackageUpdates", // to speed up the build 21 | "-disableAutomaticPackageResolution", 22 | "-onlyUsePackageVersionsFromResolvedFile", 23 | "-showBuildTimingSummary", 24 | ]; 25 | } 26 | 27 | static async args(projectEnv: ProjectEnv, bundle: BundlePath) { 28 | return [ 29 | ...(await BuildManager.commonArgs(projectEnv, bundle)), 30 | await projectEnv.projectType, 31 | await projectEnv.projectFile, 32 | "-scheme", 33 | await projectEnv.projectScheme, 34 | ]; 35 | } 36 | 37 | async checkFirstLaunchStatus(context: CommandContext) { 38 | await context.execShellWithOptions({ 39 | scriptOrCommand: { command: "xcodebuild" }, 40 | args: [ 41 | await context.projectEnv.projectType, 42 | await context.projectEnv.projectFile, 43 | "-checkFirstLaunchStatus", 44 | ], 45 | mode: ExecutorMode.verbose, 46 | }); 47 | 48 | await context.execShellWithOptions({ 49 | scriptOrCommand: { command: "xcodebuild" }, 50 | args: [ 51 | "-resolvePackageDependencies", 52 | await context.projectEnv.projectType, 53 | await context.projectEnv.projectFile, 54 | "-scheme", 55 | await context.projectEnv.projectScheme, 56 | ], 57 | mode: ExecutorMode.resultOk | ExecutorMode.stderr | ExecutorMode.commandName, 58 | pipe: { 59 | scriptOrCommand: { command: "xcbeautify", labelInTerminal: "Build" }, 60 | mode: ExecutorMode.stdout, 61 | }, 62 | }); 63 | } 64 | 65 | async build(context: CommandContext, logFilePath: string) { 66 | context.bundle.generateNext(); 67 | await context.execShellWithOptions({ 68 | scriptOrCommand: { command: "xcodebuild" }, 69 | pipeToParseBuildErrors: true, 70 | args: await BuildManager.args(context.projectEnv, context.bundle), 71 | mode: ExecutorMode.resultOk | ExecutorMode.stderr | ExecutorMode.commandName, 72 | pipe: { 73 | scriptOrCommand: { command: "tee" }, 74 | args: [logFilePath], 75 | mode: ExecutorMode.none, 76 | pipe: { 77 | scriptOrCommand: { command: "xcbeautify", labelInTerminal: "Build" }, 78 | mode: ExecutorMode.stdout, 79 | }, 80 | }, 81 | }); 82 | } 83 | 84 | async buildAutocomplete(context: CommandContext, logFilePath: string) { 85 | context.bundle.generateNext(); 86 | let buildCommand = "build-for-testing"; 87 | try { 88 | await context.projectSettingsProvider.testPlans; 89 | } catch (error) { 90 | if (error instanceof CustomError && error.isEqual(TestPlanIsNotConfigured)) { 91 | buildCommand = "build"; 92 | } else { 93 | throw error; 94 | } 95 | } 96 | 97 | await context.execShellWithOptions({ 98 | scriptOrCommand: { command: "xcodebuild" }, 99 | pipeToParseBuildErrors: true, 100 | args: [ 101 | buildCommand, 102 | ...(await BuildManager.args(context.projectEnv, context.bundle)), 103 | "-skipUnavailableActions", // for autocomplete, skip if it fails 104 | "-jobs", 105 | "4", 106 | ], 107 | env: { 108 | continueBuildingAfterErrors: "True", // build even if there's an error triggered 109 | }, 110 | mode: ExecutorMode.resultOk | ExecutorMode.stderr | ExecutorMode.commandName, 111 | pipe: { 112 | scriptOrCommand: { command: "tee" }, 113 | args: [logFilePath], 114 | mode: ExecutorMode.none, 115 | }, 116 | }); 117 | } 118 | 119 | async buildForTestingWithTests( 120 | context: CommandContext, 121 | logFilePath: string, 122 | tests: string[], 123 | isCoverage: boolean 124 | ) { 125 | context.bundle.generateNext(); 126 | const extraArguments: string[] = []; 127 | if (isCoverage) { 128 | extraArguments.push(...["-enableCodeCoverage", "YES"]); 129 | } 130 | 131 | await context.execShellWithOptions({ 132 | scriptOrCommand: { command: "xcodebuild" }, 133 | pipeToParseBuildErrors: true, 134 | args: [ 135 | "build-for-testing", 136 | ...tests.map(test => { 137 | return `-only-testing:${test}`; 138 | }), 139 | ...(await BuildManager.args(context.projectEnv, context.bundle)), 140 | ...extraArguments, 141 | ], 142 | mode: ExecutorMode.resultOk | ExecutorMode.stderr | ExecutorMode.commandName, 143 | pipe: { 144 | scriptOrCommand: { command: "tee" }, 145 | args: [logFilePath], 146 | mode: ExecutorMode.none, 147 | pipe: { 148 | scriptOrCommand: { 149 | command: "xcbeautify", 150 | labelInTerminal: "Build For Testing", 151 | }, 152 | mode: ExecutorMode.stdout, 153 | }, 154 | }, 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/StatusBar/StatusBar.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { isActivated, ProjectEnv } from "../env"; 3 | 4 | export class StatusBar implements vscode.Disposable { 5 | private schemeStatusItem = vscode.window.createStatusBarItem( 6 | vscode.StatusBarAlignment.Left, 7 | 5.04 8 | ); 9 | 10 | private configurationStatusItem = vscode.window.createStatusBarItem( 11 | vscode.StatusBarAlignment.Left, 12 | 5.03 13 | ); 14 | 15 | private deviceStatusItem = vscode.window.createStatusBarItem( 16 | vscode.StatusBarAlignment.Left, 17 | 5.021 18 | ); 19 | 20 | private testPlanStatusItem = vscode.window.createStatusBarItem( 21 | vscode.StatusBarAlignment.Left, 22 | 5.02 23 | ); 24 | 25 | constructor() { 26 | this.schemeStatusItem.command = "vscode-ios.project.selectTarget"; 27 | this.schemeStatusItem.tooltip = "Click to select the Xcode Project Scheme"; 28 | 29 | this.configurationStatusItem.command = "vscode-ios.project.selectConfiguration"; 30 | this.configurationStatusItem.tooltip = 31 | "Click to select the Xcode Project Build Configuration"; 32 | 33 | this.deviceStatusItem.command = "vscode-ios.project.selectDevice"; 34 | this.deviceStatusItem.tooltip = "Click to select the Xcode Project Debug Device"; 35 | 36 | this.testPlanStatusItem.command = "vscode-ios.project.selectTestPlan"; 37 | this.testPlanStatusItem.tooltip = "Click to select Xcode Project Test Plan"; 38 | } 39 | 40 | public dispose() { 41 | this.schemeStatusItem.dispose(); 42 | this.configurationStatusItem.dispose(); 43 | this.deviceStatusItem.dispose(); 44 | this.testPlanStatusItem.dispose(); 45 | } 46 | 47 | public async update(projectEnv: ProjectEnv) { 48 | if ((await isActivated()) === false) { 49 | this.schemeStatusItem.hide(); 50 | this.configurationStatusItem.hide(); 51 | this.deviceStatusItem.hide(); 52 | this.testPlanStatusItem.hide(); 53 | return; 54 | } 55 | try { 56 | this.schemeStatusItem.text = `$(target):${await projectEnv.projectScheme}`; 57 | this.schemeStatusItem.show(); 58 | } catch { 59 | /// if the scheme is not yet set, display just busy icon 60 | this.schemeStatusItem.text = `$(target):$(busy)`; 61 | this.schemeStatusItem.show(); 62 | } 63 | 64 | try { 65 | this.configurationStatusItem.text = `$(database):${await projectEnv.projectConfiguration}`; 66 | this.configurationStatusItem.show(); 67 | } catch { 68 | this.configurationStatusItem.hide(); 69 | } 70 | 71 | try { 72 | const device = await projectEnv.debugDeviceID; 73 | this.deviceStatusItem.text = `$(device-mobile):${device.name}`; 74 | if (device.OS !== undefined) { 75 | this.deviceStatusItem.text += `,${device.OS}`; 76 | } 77 | this.deviceStatusItem.show(); 78 | } catch { 79 | this.deviceStatusItem.hide(); 80 | } 81 | 82 | try { 83 | this.testPlanStatusItem.text = `$(shield):${await projectEnv.projectTestPlan}`; 84 | this.testPlanStatusItem.show(); 85 | } catch { 86 | this.testPlanStatusItem.hide(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TerminalShell.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export enum TerminalMessageStyle { 4 | default, 5 | success, 6 | command, 7 | error, 8 | warning, 9 | } 10 | 11 | export class TerminalShell { 12 | private terminal?: Promise; 13 | private writeEmitter: vscode.EventEmitter | undefined; 14 | private changeNameEmitter: vscode.EventEmitter | undefined; 15 | private animationInterval: NodeJS.Timeout | undefined; 16 | private exitEmitter = new vscode.EventEmitter(); 17 | private _terminalName: string; 18 | 19 | set terminalName(name: string) { 20 | this._terminalName = name; 21 | this.terminal = this.getTerminal(name); 22 | } 23 | 24 | public get onExitEvent(): vscode.Event { 25 | return this.exitEmitter.event; 26 | } 27 | 28 | constructor(terminalName: string) { 29 | this._terminalName = terminalName; 30 | } 31 | 32 | error() { 33 | this.terminalName = "❌ " + this._terminalName; 34 | this.stop(); 35 | } 36 | 37 | success() { 38 | this.terminalName = "✅ " + this._terminalName; 39 | this.stop(); 40 | } 41 | 42 | cancel() { 43 | this.terminalName = "🚫 " + this._terminalName; 44 | this.stop(); 45 | } 46 | 47 | private stop() { 48 | clearInterval(this.animationInterval); 49 | this.animationInterval = undefined; 50 | } 51 | 52 | public show() { 53 | this.terminal?.then(terminal => terminal.show()); 54 | } 55 | 56 | private dataToPrint(data: string) { 57 | data = data.replaceAll("\n", "\n\r"); 58 | return data; 59 | } 60 | 61 | public write(data: string, style = TerminalMessageStyle.default) { 62 | this.terminal?.then(() => { 63 | const toPrint = this.dataToPrint(data); 64 | switch (style) { 65 | case TerminalMessageStyle.default: 66 | this.writeEmitter?.fire(toPrint); 67 | break; 68 | case TerminalMessageStyle.command: 69 | this.writeEmitter?.fire(`\x1b[100m${toPrint}\x1b[0m`); 70 | break; 71 | case TerminalMessageStyle.error: 72 | this.writeEmitter?.fire(`\x1b[41m${toPrint}\x1b[0m`); // BgRed 73 | break; 74 | case TerminalMessageStyle.warning: 75 | this.writeEmitter?.fire(`\x1b[43m${toPrint}\x1b[0m`); // BgYellow 76 | break; 77 | case TerminalMessageStyle.success: 78 | this.writeEmitter?.fire(`\x1b[42m${toPrint}\x1b[0m`); // BgGreen 79 | break; 80 | } 81 | }); 82 | } 83 | 84 | private createTitleAnimation(terminalId: string) { 85 | // animation steps 86 | const steps = ["\\", "|", "/", "-"]; 87 | let currentIndex = 0; 88 | // start the animation 89 | const animationInterval = setInterval(() => { 90 | currentIndex = (currentIndex + 1) % steps.length; 91 | this.changeNameEmitter?.fire(`${steps[currentIndex]} ${terminalId}`); 92 | }, 1000); // Change this to control animation speed 93 | return animationInterval; 94 | } 95 | 96 | private getTerminalName(id: string) { 97 | const terminalId = `Xcode: ${id}`; 98 | return terminalId; 99 | } 100 | 101 | private getTerminal(id: string): Promise { 102 | const terminalId = this.getTerminalName(id); 103 | clearInterval(this.animationInterval); 104 | this.animationInterval = this.createTitleAnimation(terminalId); 105 | if (this.terminal) { 106 | this.changeNameEmitter?.fire(`${terminalId}`); 107 | return this.terminal; 108 | } 109 | 110 | return new Promise((resolve, reject) => { 111 | this.writeEmitter = new vscode.EventEmitter(); 112 | this.changeNameEmitter = new vscode.EventEmitter(); 113 | let terminal: vscode.Terminal | undefined = undefined; 114 | const pty: vscode.Pseudoterminal = { 115 | onDidWrite: this.writeEmitter.event, 116 | onDidChangeName: this.changeNameEmitter.event, 117 | open: () => { 118 | this.writeEmitter?.fire(`\x1b[42m${terminalId}:\x1b[0m\r\n`); 119 | if (terminal) { 120 | resolve(terminal); 121 | } else { 122 | reject(Error("Terminal is not created")); 123 | } 124 | }, //BgGreen 125 | close: () => { 126 | this.terminal = undefined; 127 | this.exitEmitter.fire(); 128 | }, 129 | }; 130 | terminal = vscode.window.createTerminal({ 131 | name: terminalId, 132 | pty: pty, 133 | }); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/TestsProvider/CoverageProvider.ts: -------------------------------------------------------------------------------- 1 | import { BundlePath } from "../CommandManagement/BundlePath"; 2 | import { Executor } from "../Executor"; 3 | import { getFilePathInWorkspace } from "../env"; 4 | import * as vscode from "vscode"; 5 | 6 | class XCFileCoverage extends vscode.FileCoverage { 7 | lineCoverage: vscode.StatementCoverage[] | undefined; 8 | bundle: BundlePath | undefined; 9 | } 10 | 11 | export class CoverageProvider { 12 | public async getCoverageFiles(bundle: BundlePath): Promise { 13 | const tree = await this.getCoverageData(bundle); 14 | 15 | const allCoverages = [] as vscode.FileCoverage[]; 16 | 17 | for (const target of tree.targets) { 18 | for (const file of target.files) { 19 | const fileCoverage = new XCFileCoverage( 20 | vscode.Uri.file(file.path), 21 | new vscode.TestCoverageCount(file.coveredLines, file.executableLines) 22 | ); 23 | fileCoverage.bundle = bundle; 24 | allCoverages.push(fileCoverage); 25 | } 26 | } 27 | 28 | return allCoverages; 29 | } 30 | 31 | public async getStatementCoverageFor( 32 | fileCoverage: vscode.FileCoverage 33 | ): Promise { 34 | if (fileCoverage instanceof XCFileCoverage) { 35 | if (fileCoverage.bundle === undefined) { 36 | return []; 37 | } 38 | const command = `xcrun xccov view --archive --json --file '${fileCoverage.uri.fsPath}' '${this.xcresultPath(fileCoverage.bundle)}'`; 39 | const executor = new Executor(); 40 | const outFileCoverageStr = await executor.execShell({ 41 | scriptOrCommand: { command: command }, 42 | }); 43 | const coverage = JSON.parse(outFileCoverageStr.stdout); 44 | 45 | if (fileCoverage.lineCoverage) { 46 | return fileCoverage.lineCoverage; 47 | } 48 | const linesCoverage = [] as vscode.StatementCoverage[]; 49 | const lines = coverage[fileCoverage.uri.fsPath]; 50 | for (const line of lines) { 51 | if (line.isExecutable) { 52 | linesCoverage.push( 53 | new vscode.StatementCoverage( 54 | line.executionCount, 55 | new vscode.Range( 56 | new vscode.Position(line.line - 1, 0), 57 | new vscode.Position(line.line - 1, 10000) 58 | ) 59 | ) 60 | ); 61 | } 62 | } 63 | fileCoverage.lineCoverage = linesCoverage; 64 | return linesCoverage; 65 | } 66 | return []; 67 | } 68 | 69 | private async getCoverageData(bundle: BundlePath) { 70 | const shell = new Executor(); 71 | const command = `xcrun xccov view --report --json '${this.xcresultPath(bundle)}'`; 72 | const coverageJsonStr = await shell.execShell({ 73 | scriptOrCommand: { command: command }, 74 | }); 75 | 76 | return JSON.parse(coverageJsonStr.stdout); 77 | } 78 | 79 | private xcresultPath(bundle: BundlePath) { 80 | return getFilePathInWorkspace(bundle.bundleResultPath()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/TestsProvider/RawLogParsers/TestCaseAsyncParser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | // eslint-disable-next-line no-useless-escape 4 | const testCaseRe = 5 | /^(Test Case\s'-\[)(.*)?\.(.*)?\s(.*)?\](.*)?(started\.)([\s\S]*?)^((Test Suite)|(Test session results)|(Test Case).*?(failed|passed).*\((.*)? .*.$)/gm; 6 | 7 | export class RawTestParser { 8 | stdout: string; 9 | watcherDisposal?: vscode.Disposable; 10 | constructor(stdout: string) { 11 | this.stdout = stdout; 12 | } 13 | } 14 | export class TestCaseAsyncParser { 15 | disposable: vscode.Disposable[] = []; 16 | 17 | buildErrors = new Set(); 18 | 19 | constructor() {} 20 | 21 | parseAsyncLogs( 22 | runPipeEvent: vscode.Event, 23 | onMessage: ( 24 | result: string, 25 | rawMessage: string, 26 | target: string, 27 | className: string, 28 | testName: string, 29 | duration: number 30 | ) => void 31 | ) { 32 | const rawParser = new RawTestParser(""); 33 | rawParser.watcherDisposal = runPipeEvent(data => { 34 | rawParser.stdout += data; 35 | this.parseStdout(rawParser, onMessage); 36 | }); 37 | return rawParser; 38 | } 39 | 40 | public end(rawParser: RawTestParser) { 41 | rawParser.watcherDisposal?.dispose(); 42 | rawParser.watcherDisposal = undefined; 43 | } 44 | 45 | private parseStdout( 46 | rawParser: RawTestParser, 47 | onMessage: ( 48 | result: string, 49 | rawMessage: string, 50 | target: string, 51 | className: string, 52 | testName: string, 53 | duration: number 54 | ) => void 55 | ) { 56 | let lastErrorIndex = -1; 57 | const matches = [...rawParser.stdout.matchAll(testCaseRe)]; 58 | for (const match of matches) { 59 | const result = match[12] || "failed"; 60 | const rawMessage = match[7]; 61 | const target = match[2]; 62 | const className = match[3]; 63 | const testName = match[4]; 64 | 65 | const duration = Number(match[13]); 66 | 67 | onMessage(result, rawMessage, target, className, testName, duration); 68 | lastErrorIndex = (match.index || 0) + match[0].length; 69 | } 70 | 71 | if (lastErrorIndex !== -1) { 72 | rawParser.stdout = rawParser.stdout.substring(lastErrorIndex + 1); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/TestsProvider/RawLogParsers/TestCaseProblemParser.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as vscode from "vscode"; 3 | 4 | const problemPattern = 5 | /^(.*?):(\d+)(?::(\d+))?:\s+(warning|error|note):\s+([\s\S]*?)(error|warning|note):?/m; 6 | const diffPattern = /(XCTAssertEqual|XCTAssertNotEqual)\sfailed:\s\((.*?)\).*?\((.*?)\)/m; 7 | 8 | export class TestCaseProblemParser { 9 | async parseAsyncLogs(testCase: string, testItem: vscode.TestItem) { 10 | if (testItem.uri) { 11 | const problems = 12 | this.parseBuildLog( 13 | testCase, 14 | testItem.uri, 15 | testItem.id.split(path.sep).at(-1) || "" 16 | ) || []; 17 | return problems; 18 | } 19 | return []; 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | private column(_output: string, _messageEnd: number) { 24 | return [0, 10000]; 25 | } 26 | 27 | private parseBuildLog(stdout: string, uri: vscode.Uri, testName: string) { 28 | const files: vscode.TestMessage[] = []; 29 | stdout += "\nerror:"; 30 | try { 31 | let startIndex = 0; 32 | while (startIndex < stdout.length) { 33 | while (startIndex > 0) { 34 | // find the start of line for the next pattern search 35 | if (stdout[startIndex] === "\n") { 36 | break; 37 | } 38 | --startIndex; 39 | } 40 | 41 | const output = stdout.slice(startIndex); 42 | const match = output.match(problemPattern); 43 | if (!match) { 44 | return; 45 | } 46 | const line = Number(match[2]) - 1; 47 | const column = this.column(output, (match?.index || 0) + match[0].length); 48 | 49 | let message = match[5]; 50 | const end = message.lastIndexOf("\n"); 51 | if (end !== -1) { 52 | message = message.substring(0, end); 53 | } 54 | 55 | const expectedActualMatch = this.expectedActualValues(message); 56 | const fullErrorMessage = this.errorMessage(message); 57 | 58 | const diffName = `Diff: ${testName}`; 59 | let diagnostic: vscode.TestMessage; 60 | if (expectedActualMatch) { 61 | diagnostic = vscode.TestMessage.diff( 62 | fullErrorMessage, 63 | expectedActualMatch.expected, 64 | expectedActualMatch.actual 65 | ); 66 | } else { 67 | diagnostic = new vscode.TestMessage(this.markDown(fullErrorMessage, diffName)); 68 | } 69 | 70 | const range = new vscode.Range( 71 | new vscode.Position(line, column[0]), 72 | new vscode.Position(line, column[1]) 73 | ); 74 | 75 | diagnostic.location = new vscode.Location(uri, range); 76 | 77 | files.push(diagnostic); 78 | 79 | startIndex += (match.index || 0) + match[0].length; 80 | } 81 | } catch (err) { 82 | console.log(`TestCase parser error: ${err}`); 83 | } 84 | return files; 85 | } 86 | 87 | private expectedActualValues(message: string) { 88 | const expectedActualMatch = message.match(diffPattern); 89 | if (expectedActualMatch) { 90 | return { expected: expectedActualMatch[3], actual: expectedActualMatch[2] }; 91 | } 92 | } 93 | 94 | private errorMessage(message: string) { 95 | const index = message.indexOf(" failed: "); 96 | if (index === -1) { 97 | const indexDelimiter = message.indexOf(" : "); 98 | if (indexDelimiter !== -1) { 99 | return message.substring(indexDelimiter + " : ".length).trim(); 100 | } 101 | return message; 102 | } 103 | 104 | for (let i = index; i >= 0; --i) { 105 | if (message[i] === ":") { 106 | return message.substring(i + 1).trim(); 107 | } 108 | } 109 | return message.substring(index).trim(); 110 | } 111 | 112 | private markDown(message: string, name: string) { 113 | const mdString = new vscode.MarkdownString(""); 114 | mdString.isTrusted = true; 115 | // replace file links to be opened 116 | message = message.replaceAll(/^(.*?):(\d+):/gm, (str, p1, p2) => { 117 | return `${str}\n\r[View line](command:vscode-ios.openFile?${encodeURIComponent(JSON.stringify([p1, p2]))})`; 118 | }); 119 | if (message.includes("SnapshotTesting.diffTool")) { 120 | const list = message.split(/^To configure[\s\S]*?SnapshotTesting.diffTool.*"$/gm); 121 | 122 | for (const pattern of list) { 123 | const files = [...pattern.matchAll(/^@[\s\S]*?"(file:.*?)"$/gm)]; 124 | mdString.appendMarkdown("\n" + pattern); 125 | if (files.length === 2) { 126 | mdString.appendMarkdown( 127 | `\n[Compare](command:vscode-ios.ksdiff?${encodeURIComponent(JSON.stringify([name, files[0][1], files[1][1]]))})` 128 | ); 129 | } 130 | } 131 | } else { 132 | mdString.appendMarkdown(message); 133 | } 134 | 135 | return mdString; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestCase.ts: -------------------------------------------------------------------------------- 1 | import { TestStyle } from "../../LSP/lspExtension"; 2 | import { CustomError } from "../../utils"; 3 | 4 | const InvalidTestCase = new CustomError("Invalid Test Case"); 5 | 6 | export class TestCase { 7 | constructor( 8 | private readonly testName: string | undefined, 9 | private readonly suite: string | undefined, 10 | private readonly target: string | undefined, 11 | private readonly testStyle: TestStyle 12 | ) {} 13 | 14 | getLabel() { 15 | return this.testName as string; 16 | } 17 | 18 | getXCodeBuildTest() { 19 | if (this.target && this.testName) { 20 | const list = [this.target]; 21 | 22 | if (this.suite !== undefined && this.suite.length > 0) { 23 | list.push(this.suite); 24 | } 25 | 26 | // TODO: remove this once xcodebuild tool supports full test id path 27 | // for some reason for new swift testing framework it doesn't understand the last part of the path 28 | if (this.testStyle === "swift-testing") { 29 | const testName = this.testName.split("/").slice(0, -1).join("/"); 30 | if (testName !== undefined && testName.length > 0) { 31 | list.push(testName); 32 | } 33 | } else { 34 | list.push(this.testName); 35 | } 36 | 37 | return list.join("/"); 38 | } 39 | throw InvalidTestCase; 40 | } 41 | 42 | getTestId() { 43 | if (this.target && this.testName) { 44 | if (this.suite) { 45 | return `${this.target}/${this.suite}/${this.testName}`; 46 | } else { 47 | return `${this.target}/${this.testName}`; 48 | } 49 | } 50 | throw InvalidTestCase; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestContainer.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export interface TestContainer { 4 | didResolve: boolean; 5 | 6 | updateFromDisk(controller: vscode.TestController, item: vscode.TestItem): Promise; 7 | 8 | updateFromContents( 9 | controller: vscode.TestController, 10 | content: string, 11 | item: vscode.TestItem 12 | ): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestFile.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { parseSwiftSource } from "./parseClass"; 3 | import { TestContainer } from "./TestContainer"; 4 | import { TestHeading as TestSuite } from "./TestHeading"; 5 | import { TestTreeContext, getContentFromFilesystem } from "../TestTreeContext"; 6 | import { getTestIDComponents, LSPTestItem } from "../../LSP/lspExtension"; 7 | import { TestCase } from "./TestCase"; 8 | 9 | let generationCounter = 0; 10 | 11 | export class TestFile implements TestContainer { 12 | public didResolve = false; 13 | context: TestTreeContext; 14 | private target: string; 15 | 16 | constructor(context: TestTreeContext, target: string) { 17 | this.context = context; 18 | this.target = target; 19 | } 20 | 21 | private mapTestItems( 22 | parent: vscode.TestItem, 23 | target: string | undefined, 24 | lspTest: LSPTestItem, 25 | controller: vscode.TestController, 26 | suiteGeneration: number 27 | ): vscode.TestItem[] { 28 | const id = `${parent.uri}/${lspTest.id}`; 29 | const testItem = controller.createTestItem(id, lspTest.label, parent.uri); 30 | testItem.range = new vscode.Range( 31 | new vscode.Position( 32 | lspTest.location.range.start.line, 33 | lspTest.location.range.start.character 34 | ), 35 | new vscode.Position( 36 | lspTest.location.range.end.line, 37 | lspTest.location.range.end.character 38 | ) 39 | ); 40 | if (lspTest.children.length !== 0) { 41 | const test = new TestSuite(suiteGeneration); 42 | this.context.testData.set(testItem, test); 43 | } else { 44 | const idComponents = getTestIDComponents(lspTest.id); 45 | const test = new TestCase( 46 | idComponents.testName, 47 | idComponents.suite, 48 | target, 49 | lspTest.style 50 | ); 51 | this.context.testData.set(testItem, test); 52 | } 53 | const itemChildren: vscode.TestItem[] = []; 54 | for (const lspChild of lspTest.children) { 55 | itemChildren.push( 56 | ...this.mapTestItems(testItem, target, lspChild, controller, suiteGeneration + 1) 57 | ); 58 | } 59 | testItem.children.replace(itemChildren); 60 | return [testItem]; 61 | } 62 | 63 | public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { 64 | try { 65 | const content = await getContentFromFilesystem(item.uri!); 66 | item.error = undefined; 67 | await this.updateFromContents(controller, content, item); 68 | } catch (e) { 69 | item.error = (e as Error).stack; 70 | } 71 | } 72 | 73 | public async updateFromContents( 74 | controller: vscode.TestController, 75 | content: string, 76 | item: vscode.TestItem 77 | ) { 78 | try { 79 | const url = item.uri!; 80 | const tests = await this.context.lspTestProvider.fetchTests(url, content); 81 | 82 | const itemChildren: vscode.TestItem[] = []; 83 | const target = this.target; 84 | for (const lspChild of tests) { 85 | itemChildren.push(...this.mapTestItems(item, target, lspChild, controller, 1)); 86 | } 87 | item.children.replace(itemChildren); 88 | } catch { 89 | // legacy fallback 90 | const ancestors = [{ item, children: [] as vscode.TestItem[] }]; 91 | const thisGeneration = generationCounter++; 92 | 93 | const ascend = (depth: number) => { 94 | while (ancestors.length > depth) { 95 | const finished = ancestors.pop()!; 96 | finished.item.children.replace(finished.children); 97 | } 98 | }; 99 | 100 | parseSwiftSource(content, { 101 | onTest: (range: vscode.Range, testName: string) => { 102 | const parent = ancestors[ancestors.length - 1]; 103 | const data = new TestCase(testName, parent.item.label, this.target, "XCTest"); 104 | const id = `${item.uri}/${data.getLabel()}`; 105 | 106 | const tcase = controller.createTestItem(id, data.getLabel(), item.uri); 107 | this.context.testData.set(tcase, data); 108 | tcase.range = range; 109 | parent.children.push(tcase); 110 | }, 111 | 112 | onHeading: (range, name) => { 113 | const parent = ancestors[ancestors.length - 1]; 114 | const id = `${item.uri}/${name}`; 115 | 116 | const thead = controller.createTestItem(id, name, item.uri); 117 | thead.range = range; 118 | this.context.testData.set(thead, new TestSuite(thisGeneration)); 119 | parent.children.push(thead); 120 | ancestors.push({ item: thead, children: [] }); 121 | }, 122 | }); 123 | 124 | ascend(0); // finish and assign children for all remaining items 125 | } 126 | this.didResolve = true; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestHeading.ts: -------------------------------------------------------------------------------- 1 | export class TestHeading { 2 | constructor(public generation: number) {} 3 | } 4 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestProject.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import { TestTreeContext } from "../TestTreeContext"; 4 | import { TestContainer } from "./TestContainer"; 5 | import { getFilePathInWorkspace } from "../../env"; 6 | import { FSWatcher, watch } from "fs"; 7 | import path from "path"; 8 | import { TestTarget } from "./TestTarget"; 9 | 10 | export class TestProject implements TestContainer { 11 | public didResolve = false; 12 | 13 | public context: TestTreeContext; 14 | 15 | public targetProvider: () => Promise; 16 | public filesForTargetProvider: (target: string) => Promise; 17 | 18 | private fsWatcher: FSWatcher | undefined; 19 | private projectContent: Buffer | undefined; 20 | 21 | constructor( 22 | context: TestTreeContext, 23 | targetProvider: () => Promise, 24 | filesForTargetProvider: (target: string) => Promise 25 | ) { 26 | this.context = context; 27 | this.targetProvider = targetProvider; 28 | this.filesForTargetProvider = filesForTargetProvider; 29 | } 30 | 31 | public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { 32 | try { 33 | await this.updateFromContents(controller, "", item); 34 | } catch (e) { 35 | item.error = (e as Error).stack; 36 | } 37 | } 38 | 39 | public async updateFromContents( 40 | controller: vscode.TestController, 41 | content: string, 42 | item: vscode.TestItem 43 | ) { 44 | const parent = { item, children: [] as vscode.TestItem[] }; 45 | const targets = await this.targetProvider(); 46 | const weakRef = new WeakRef(this); 47 | 48 | for (const target of targets) { 49 | const url = TestTreeContext.getTargetFilePath(item.uri, target); 50 | const { file, data } = this.context.getOrCreateTest("target://", url, () => { 51 | return new TestTarget(this.context, async () => { 52 | return (await weakRef.deref()?.filesForTargetProvider(target)) || []; 53 | }); 54 | }); 55 | 56 | if (!data.didResolve) { 57 | await data.updateFromDisk(controller, file); 58 | } 59 | if ([...file.children].length > 0) { 60 | parent.children.push(file); 61 | } else { 62 | this.context.deleteItem(file.id); 63 | } 64 | } 65 | 66 | // watch to changes for a file, if it's changed, refresh unit tests 67 | const filePath = getFilePathInWorkspace( 68 | path.join(item.uri?.path || "", item.label === "Package.swift" ? "" : "project.pbxproj") 69 | ); 70 | this.watchFile(filePath, controller, item); 71 | 72 | this.didResolve = true; 73 | // finish 74 | 75 | this.context.replaceItemsChildren(item, parent.children); 76 | } 77 | 78 | private watchFile( 79 | filePath: string, 80 | controller: vscode.TestController, 81 | item: vscode.TestItem, 82 | contentFile: Buffer | undefined = undefined 83 | ) { 84 | const weakRef = new WeakRef(this); 85 | 86 | this.fsWatcher?.close(); 87 | this.fsWatcher = undefined; 88 | this.fsWatcher = watch(filePath); 89 | this.fsWatcher.on("change", () => { 90 | const content = fs.readFileSync(filePath); 91 | if (this.projectContent?.toString() === content.toString()) { 92 | this.watchFile(filePath, controller, item, content); 93 | return; 94 | } 95 | this.projectContent = content; 96 | setTimeout(() => { 97 | weakRef.deref()?.context.replaceItemsChildren(item, []); 98 | weakRef.deref()?.updateFromDisk(controller, item); 99 | }, 1000); 100 | }); 101 | if (contentFile === undefined) { 102 | this.projectContent = fs.readFileSync(filePath); 103 | } else { 104 | this.projectContent = contentFile; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/TestTarget.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { TestTreeContext } from "../TestTreeContext"; 3 | import { TestFile } from "./TestFile"; 4 | import { TestContainer } from "./TestContainer"; 5 | 6 | export class TestTarget implements TestContainer { 7 | public didResolve = false; 8 | public context: TestTreeContext; 9 | 10 | public filesForTargetProvider: () => Promise; 11 | 12 | constructor(context: TestTreeContext, filesForTargetProvider: () => Promise) { 13 | this.context = context; 14 | this.filesForTargetProvider = filesForTargetProvider; 15 | } 16 | 17 | public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { 18 | try { 19 | await this.updateFromContents(controller, "", item); 20 | } catch (e) { 21 | item.error = (e as Error).stack; 22 | } 23 | } 24 | 25 | public async updateFromContents( 26 | controller: vscode.TestController, 27 | content: string, 28 | item: vscode.TestItem 29 | ) { 30 | const parent = { item, children: [] as vscode.TestItem[] }; 31 | const files = await this.filesForTargetProvider(); 32 | for (const fileInTarget of files) { 33 | const url = vscode.Uri.file(fileInTarget); 34 | const { file, data } = this.context.getOrCreateTest("file://", url, () => { 35 | return new TestFile(this.context, item.label); 36 | }); 37 | 38 | try { 39 | if (!data.didResolve) { 40 | await data.updateFromDisk(controller, file); 41 | } 42 | if ([...file.children].length > 0) { 43 | parent.children.push(file); 44 | } else { 45 | this.context.deleteItem(file.id); 46 | } 47 | } catch (err) { 48 | console.log(`Tests for a file ${url} can not be updated: ${err}`); 49 | this.context.deleteItem(file.id); 50 | } 51 | } 52 | this.didResolve = true; 53 | // finish 54 | this.context.replaceItemsChildren(item, parent.children); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/TestsProvider/TestItemProvider/parseClass.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const testRe = /(func)([\s]+)(test?.*)(\(\s*\))/gm; 4 | const headingRe = /(class)([\s]+)([\S]*)([\s]*:\s*)(XCTest)/gm; 5 | 6 | function getScope(text: string, start: number, commented: boolean[]) { 7 | const stack = [] as string[]; 8 | for (let i = start; i < text.length; ++i) { 9 | if (commented[i]) { 10 | continue; 11 | } 12 | if (text[i] === "{") { 13 | stack.push(text[i]); 14 | } else if (text[i] === "}") { 15 | stack.pop(); 16 | if (stack.length === 0) { 17 | return i; 18 | } 19 | } 20 | } 21 | } 22 | 23 | export function preCalcLineNumbers(text: string) { 24 | const line = [] as number[]; 25 | let currentNumber = 0; 26 | for (let i = 0; i < text.length; ++i) { 27 | line.push(currentNumber); 28 | currentNumber += text[i] === "\n" ? 1 : 0; 29 | } 30 | return line; 31 | } 32 | 33 | enum Commented { 34 | notCommented, 35 | singleCommented, 36 | multiCommented, 37 | quoted, 38 | multiQuoted, 39 | } 40 | 41 | export function preCalcCommentedCode(text: string) { 42 | const line = [] as boolean[]; 43 | let commented = Commented.notCommented; 44 | let openQuote = ""; 45 | for (let i = 0; i < text.length - 1; ) { 46 | switch (commented) { 47 | case Commented.notCommented: 48 | if (text.slice(i, i + 2) === "//") { 49 | commented = Commented.singleCommented; 50 | line.push(true, true); 51 | } else if (text.slice(i, i + 2) === "/*") { 52 | commented = Commented.multiCommented; 53 | line.push(true, true); 54 | } else if (text.slice(i, i + 3) === '"""') { 55 | commented = Commented.multiQuoted; 56 | line.push(true, true, true); 57 | } else if (text[i] === '"' || text[i] === "'") { 58 | commented = Commented.quoted; 59 | openQuote = text[i]; 60 | line.push(true); 61 | } else { 62 | line.push(false); 63 | } 64 | break; 65 | case Commented.singleCommented: 66 | if (text[i] === "\n") { 67 | commented = Commented.notCommented; 68 | } 69 | line.push(true); 70 | break; 71 | case Commented.multiCommented: 72 | if (text.slice(i, i + 2) === "*/") { 73 | commented = Commented.notCommented; 74 | line.push(true, true); 75 | } else { 76 | line.push(true); 77 | } 78 | break; 79 | case Commented.quoted: 80 | if (text.slice(i, i + 2) === '\\"' || text.slice(i, i + 2) === "\\'") { 81 | line.push(true, true); 82 | } else if (text[i] === openQuote) { 83 | commented = Commented.notCommented; 84 | line.push(true); 85 | } else { 86 | line.push(true); 87 | } 88 | break; 89 | case Commented.multiQuoted: 90 | if (text.slice(i, i + 2) === '\\"') { 91 | line.push(true, true); 92 | } else if (text.slice(i, i + 3) === '"""') { 93 | commented = Commented.notCommented; 94 | line.push(true, true, true); 95 | } else { 96 | line.push(true); 97 | } 98 | break; 99 | } 100 | i = line.length; 101 | } 102 | return line; 103 | } 104 | 105 | export function isCommented(commented: boolean[], start: number, end: number) { 106 | for (let i = start; i < end; ++i) { 107 | if (commented[i]) { 108 | return true; 109 | } 110 | } 111 | return false; 112 | } 113 | 114 | export const parseSwiftSource = ( 115 | text: string, 116 | events: { 117 | onTest(range: vscode.Range, testName: string): void; 118 | onHeading(range: vscode.Range, name: string): void; 119 | } 120 | ) => { 121 | const lineNumbers = preCalcLineNumbers(text); 122 | const commented = preCalcCommentedCode(text); 123 | 124 | const classes = [...text.matchAll(headingRe)]; 125 | for (const classRef of classes) { 126 | const classStartInd = classRef.index || 0; 127 | const classEndInd = classStartInd + classRef[0].length; 128 | if (isCommented(commented, classStartInd, classEndInd)) { 129 | continue; 130 | } 131 | 132 | const [, , , name] = classRef; 133 | const endScope = getScope(text, classStartInd, commented) || classEndInd; 134 | 135 | const range = new vscode.Range( 136 | new vscode.Position(lineNumbers[classStartInd], 0), 137 | new vscode.Position(lineNumbers[endScope], 10000) 138 | ); 139 | events.onHeading(range, name); 140 | 141 | const tests = [...text.slice(classStartInd, endScope).matchAll(testRe)]; 142 | for (const test of tests) { 143 | const testStartInd = (test.index || 0) + classStartInd; 144 | const testEndInd = testStartInd + test[0].length; 145 | if (isCommented(commented, testStartInd, testEndInd)) { 146 | continue; 147 | } 148 | 149 | const [, , , testName] = test; 150 | const endTestScope = getScope(text, testStartInd, commented) || testEndInd; 151 | 152 | const range = new vscode.Range( 153 | new vscode.Position(lineNumbers[testStartInd], 0), 154 | new vscode.Position(lineNumbers[endTestScope], 10000) 155 | ); 156 | events.onTest(range, testName); 157 | } 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /src/TestsProvider/TestTreeContext.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder } from "util"; 2 | import * as vscode from "vscode"; 3 | import { TestContainer } from "./TestItemProvider/TestContainer"; 4 | import { TestCase } from "./TestItemProvider/TestCase"; 5 | import { TestHeading } from "./TestItemProvider/TestHeading"; 6 | import { CoverageProvider } from "./CoverageProvider"; 7 | import { LSPTestsProvider } from "../LSP/LSPTestsProvider"; 8 | import { TestResultProvider } from "./TestResultProvider"; 9 | import { AtomicCommand } from "../CommandManagement/AtomicCommand"; 10 | 11 | const textDecoder = new TextDecoder("utf-8"); 12 | 13 | export type MarkdownTestData = TestHeading | TestCase | TestContainer; 14 | 15 | type TestNodeId = "file://" | "target://" | "project://"; 16 | 17 | export class TestTreeContext { 18 | readonly testData = new WeakMap(); 19 | readonly ctrl: vscode.TestController = vscode.tests.createTestController( 20 | "iOSTestController", 21 | "iOS Tests" 22 | ); 23 | readonly coverage: CoverageProvider = new CoverageProvider(); 24 | readonly testResult: TestResultProvider = new TestResultProvider(); 25 | readonly lspTestProvider: LSPTestsProvider; 26 | readonly atomicCommand: AtomicCommand; 27 | 28 | constructor(lspTestProvider: LSPTestsProvider, atomicCommand: AtomicCommand) { 29 | this.lspTestProvider = lspTestProvider; 30 | this.atomicCommand = atomicCommand; 31 | } 32 | 33 | static TestID(id: TestNodeId, uri: vscode.Uri) { 34 | return `${id}/${uri.toString()}`; 35 | } 36 | 37 | static getTargetFilePath(projectPath: vscode.Uri | undefined, target: string) { 38 | return vscode.Uri.file(`${projectPath?.toString() || ""}/${target}`); 39 | } 40 | 41 | getOrCreateTest(id: TestNodeId, uri: vscode.Uri, provider: () => any) { 42 | const uniqueId = TestTreeContext.TestID(id, uri); 43 | const existing = this.get(uniqueId, this.ctrl.items); 44 | if (existing) { 45 | return { file: existing, data: this.testData.get(existing) }; 46 | } 47 | 48 | const file = this.ctrl.createTestItem(uniqueId, uri.path.split("/").pop()!, uri); 49 | this.ctrl.items.add(file); 50 | 51 | const data = provider(); 52 | this.testData.set(file, data); 53 | 54 | file.canResolveChildren = true; 55 | return { file, data }; 56 | } 57 | 58 | private get(key: string, items: vscode.TestItemCollection) { 59 | for (const [id, item] of items) { 60 | if (id === key) { 61 | return item; 62 | } 63 | const value = this.getImp(key, item); 64 | if (value !== undefined) { 65 | return value; 66 | } 67 | } 68 | } 69 | 70 | private getImp(key: string, item: vscode.TestItem): vscode.TestItem | undefined { 71 | if (item.id === key) { 72 | return item; 73 | } 74 | 75 | for (const [, child] of item.children) { 76 | const value = this.getImp(key, child); 77 | if (value !== undefined) { 78 | return value; 79 | } 80 | } 81 | return undefined; 82 | } 83 | 84 | addItem(item: vscode.TestItem, shouldAdd: (root: vscode.TestItem) => boolean) { 85 | let res = false; 86 | this.ctrl.items.forEach(childItem => { 87 | if (res) { 88 | return; 89 | } 90 | res = res || this.addItemImp(item, childItem, shouldAdd); 91 | }); 92 | return res; 93 | } 94 | 95 | private addItemImp( 96 | item: vscode.TestItem, 97 | root: vscode.TestItem, 98 | shouldAdd: (root: vscode.TestItem) => boolean 99 | ) { 100 | if (shouldAdd(root)) { 101 | // found 102 | root.children.add(item); 103 | return true; 104 | } else { 105 | let res = false; 106 | root.children.forEach(childItem => { 107 | if (res) { 108 | return; 109 | } 110 | res = this.addItemImp(item, childItem, shouldAdd); 111 | }); 112 | return res; 113 | } 114 | } 115 | 116 | deleteItem(id: string | vscode.TestItem) { 117 | let tree: vscode.TestItem | undefined; 118 | if (typeof id === "string") { 119 | tree = this.get(id, this.ctrl.items); 120 | } else { 121 | tree = id; 122 | } 123 | if (tree) { 124 | this.testData.delete(tree); 125 | if (tree.parent) { 126 | tree.parent.children.delete(tree.id); 127 | } else { 128 | this.ctrl.items.delete(tree.id); 129 | } 130 | } 131 | } 132 | 133 | public replaceItemsChildren(item: vscode.TestItem, itemsChildren: vscode.TestItem[]) { 134 | const children: vscode.TestItem[] = []; 135 | for (const child of item.children) { 136 | children.push(child[1]); 137 | } 138 | for (const child of children) { 139 | this.deleteItem(child); 140 | } 141 | item.children.replace(itemsChildren); 142 | } 143 | 144 | allTestItems() { 145 | const list = [] as vscode.TestItem[]; 146 | this.ctrl.items.forEach(item => { 147 | this.allTestItemsImp(list, item); 148 | }); 149 | return list; 150 | } 151 | 152 | private allTestItemsImp(list: vscode.TestItem[], root: vscode.TestItem) { 153 | list.push(root); 154 | root.children.forEach(item => { 155 | this.allTestItemsImp(list, item); 156 | }); 157 | } 158 | } 159 | 160 | export const getContentFromFilesystem = async (uri: vscode.Uri) => { 161 | try { 162 | const rawContent = await vscode.workspace.fs.readFile(uri); 163 | return textDecoder.decode(rawContent); 164 | } catch (e) { 165 | console.warn(`Error providing tests for ${uri.fsPath}`, e); 166 | return ""; 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/Tools/InteractiveTerminal.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class InteractiveTerminal { 4 | private terminal: vscode.Terminal; 5 | private log: vscode.OutputChannel; 6 | private closeDisposal: vscode.Disposable; 7 | 8 | private async shellIntegration(): Promise { 9 | return new Promise(resolve => { 10 | if (this.terminal.shellIntegration !== undefined) { 11 | resolve(this.terminal.shellIntegration); 12 | return; 13 | } 14 | let disposal: vscode.Disposable | undefined = undefined; 15 | disposal = vscode.window.onDidChangeTerminalShellIntegration(eventShellCreated => { 16 | if (eventShellCreated.terminal !== this.terminal) { 17 | return; 18 | } 19 | resolve(eventShellCreated.shellIntegration); 20 | disposal?.dispose(); 21 | disposal = undefined; 22 | }); 23 | }); 24 | } 25 | 26 | constructor(log: vscode.OutputChannel, name: string) { 27 | this.log = log; 28 | this.terminal = vscode.window.createTerminal({ name: name, hideFromUser: true }); 29 | this.closeDisposal = vscode.window.onDidCloseTerminal(event => { 30 | if (event === this.terminal) { 31 | this.terminal = vscode.window.createTerminal({ name: name, hideFromUser: true }); 32 | } 33 | }); 34 | } 35 | 36 | show() { 37 | this.terminal.show(); 38 | } 39 | 40 | async executeCommand(installScript: string): Promise { 41 | this.log.appendLine(installScript); 42 | return new Promise(async (resolver, reject) => { 43 | try { 44 | const command = (await this.shellIntegration()).executeCommand(installScript); 45 | let dispose: vscode.Disposable | undefined = undefined; 46 | const localTerminal = this.terminal; 47 | dispose = vscode.window.onDidEndTerminalShellExecution(async event => { 48 | if (command === event.execution) { 49 | for await (const data of event.execution.read()) { 50 | this.log.append(data); 51 | } 52 | if (event.exitCode === 0) { 53 | this.log.appendLine("Successfully installed"); 54 | resolver(); 55 | } else { 56 | this.log.appendLine(`Is not installed, error: ${event.exitCode}`); 57 | reject(event.exitCode); 58 | } 59 | dispose?.dispose(); 60 | dispose = undefined; 61 | } 62 | }); 63 | let closeDisposal: vscode.Disposable | undefined; 64 | closeDisposal = vscode.window.onDidCloseTerminal(event => { 65 | if (event === localTerminal) { 66 | this.log.appendLine(`Terminal is Closed`); 67 | closeDisposal?.dispose(); 68 | closeDisposal = undefined; 69 | reject(Error("Terminal is closed")); 70 | } 71 | }); 72 | } catch (err) { 73 | reject(err); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Tools/XCRunHelper.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import path from "path"; 3 | 4 | export class XCRunHelper { 5 | private static async getStdOut(command: string): Promise { 6 | return new Promise((resolve, reject) => { 7 | exec(command, (error, stdout) => { 8 | if (error) { 9 | reject(error); 10 | } else { 11 | resolve(stdout.trim()); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | public static async getSdkPath(): Promise { 18 | return this.getStdOut("xcrun --show-sdk-path"); 19 | } 20 | 21 | public static async checkIfXCodeInstalled() { 22 | return this.getStdOut("xcodebuild -version"); 23 | } 24 | 25 | public static async getClangCompilerPath(): Promise { 26 | return this.getStdOut("xcrun -f clang"); 27 | } 28 | 29 | public static async sourcekitLSPPath() { 30 | return this.getStdOut("xcrun -f sourcekit-lsp"); 31 | } 32 | public static async swiftToolchainPath() { 33 | const stdout = await this.getStdOut("xcrun --find swift"); 34 | const swift = stdout.trimEnd(); 35 | return path.dirname(path.dirname(swift)); 36 | } 37 | 38 | private static lldbDapPath?: string; 39 | public static async getLLDBDapPath(): Promise { 40 | if (this.lldbDapPath === undefined) { 41 | this.lldbDapPath = await this.getStdOut("xcrun -find lldb-dap"); 42 | } 43 | return this.lldbDapPath; 44 | } 45 | 46 | public static async swiftToolchainVersion(): Promise<[string, string, string]> { 47 | const stdout = await this.getStdOut("xcrun swift --version"); 48 | const versionPattern = /swiftlang-([0-9]+)?.([0-9]+)?.([0-9]+)?/g; 49 | const version = [...stdout.matchAll(versionPattern)]?.[0]; 50 | if (version) { 51 | return [version[1], version[2], version[3]]; 52 | } else { 53 | throw Error("swift lang is not determined"); 54 | } 55 | } 56 | 57 | public static async lldbBinPath() { 58 | return await this.getStdOut("xcrun --find lldb"); 59 | } 60 | 61 | public static async convertPlistToJson(file: string) { 62 | return this.getStdOut(`plutil -convert json -o - "${file}"`); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/XCBBuildServiceProxy/MessageReader.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class MsgStatus(Enum): 4 | FirstByte = 1 5 | DetermineStart = 2 6 | MsgReadingLen = 3 7 | MsgReadingBody = 4 8 | MsgEnd = 5 9 | 10 | 11 | class MessageReader: 12 | 13 | def __init__(self) -> None: 14 | self.status = MsgStatus.FirstByte 15 | self.buffer = bytearray() 16 | self.read_index = 0 17 | self.msg_len = 0 18 | self.offset = 0 19 | 20 | 21 | # feed with only a single byte 22 | # message form: 00 00 00 00 00 00 00 xx xx xx xx message 23 | # where xx is little endian 4 bytes int to indicate the length of message 24 | # json data starts with c5 yy yy, where c5 indicates starts of json and yy yy the length of json 25 | def feed(self, byte): 26 | self.offset += 1 27 | self.buffer += byte 28 | match self.status: 29 | case MsgStatus.FirstByte: 30 | self.read_index = 0 31 | self.status = MsgStatus.DetermineStart 32 | case MsgStatus.DetermineStart: 33 | self.read_index += 1 34 | if self.read_index == 7: 35 | self.status = MsgStatus.MsgReadingLen 36 | self.read_index = 0 37 | self.msg_len = 0 38 | 39 | case MsgStatus.MsgReadingLen: 40 | self.msg_len += int.from_bytes(byte) << (8 * self.read_index) 41 | self.read_index += 1 42 | if self.read_index == 4: 43 | self.read_index = 0 44 | self.status = MsgStatus.MsgReadingBody 45 | 46 | case MsgStatus.MsgReadingBody: 47 | self.read_index += 1 48 | if self.read_index == self.msg_len: 49 | self.status = MsgStatus.MsgEnd 50 | 51 | 52 | def reset(self): 53 | self.read_index = 0 54 | self.buffer = bytearray() 55 | self.msg_len = 0 56 | self.status = MsgStatus.FirstByte 57 | 58 | 59 | def modify_body(self, new_content, start_pos: int,end_pos: int = -1,): 60 | if end_pos == -1: 61 | end_pos = len(self.buffer) 62 | assert(start_pos >= 12) 63 | self.buffer[start_pos:end_pos] = new_content 64 | self.msg_len -= end_pos - start_pos 65 | self.msg_len += len(new_content) 66 | self.buffer[8:8 + 4] = self.msg_len.to_bytes(4, "little") -------------------------------------------------------------------------------- /src/XCBBuildServiceProxy/config.txt: -------------------------------------------------------------------------------- 1 | # to generate XCBBuildService 2 | pyinstaller --onefile XCBBuildService.py 3 | 4 | # to create a symlink 5 | 6 | ln -s /Users/Ievgenii_Mykhalevskyi/Desktop/utils/XCBBuildServiceProxy/dist/XCBBuildService /Applications/Xcode.app/Contents/SharedFrameworks/XCBuild.framework/Versions/A/PlugIns/XCBBuildService.bundle/Contents/MacOS/XCBBuildService -------------------------------------------------------------------------------- /src/XcodeSideTreePanel/ProjectConfigurationDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ProjectEnv } from "../env"; 3 | 4 | export class ProjectConfigurationNode extends vscode.TreeItem { 5 | constructor(message: string, command: string, icon: string, tooltip: string, id: string) { 6 | const label: vscode.TreeItemLabel = { 7 | label: message, 8 | highlights: [[0, message.indexOf(":") + 1]], 9 | }; 10 | super(label, vscode.TreeItemCollapsibleState.Collapsed); 11 | this.tooltip = tooltip; 12 | this.id = id; 13 | this.collapsibleState = vscode.TreeItemCollapsibleState.None; 14 | this.command = { 15 | title: "Open location", 16 | command: command, 17 | }; 18 | this.iconPath = new vscode.ThemeIcon(icon); 19 | } 20 | } 21 | 22 | export class ProjectConfigurationDataProvider implements vscode.TreeDataProvider { 23 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 24 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 25 | 26 | private config: ProjectConfigurationNode[] = []; 27 | 28 | public async refresh(projectEnv: ProjectEnv) { 29 | this.config = []; 30 | 31 | try { 32 | const project = `Xcode Project: ${await projectEnv.projectFile}`; 33 | this.config.push( 34 | new ProjectConfigurationNode( 35 | project, 36 | "vscode-ios.project.select", 37 | "workspace-trusted", 38 | "Click to select Xcode Project File", 39 | "Project:ProjectFile" 40 | ) 41 | ); 42 | } catch { 43 | /// if the scheme is not yet set, display just busy icon 44 | const project = `Xcode Project: (Tap To Select One)`; 45 | this.config.push( 46 | new ProjectConfigurationNode( 47 | project, 48 | "vscode-ios.project.select", 49 | "workspace-trusted", 50 | "Click to select Xcode Project File", 51 | "Project:ProjectFile " 52 | ) 53 | ); 54 | } 55 | 56 | try { 57 | const scheme = `Scheme: ${await projectEnv.projectScheme}`; 58 | this.config.push( 59 | new ProjectConfigurationNode( 60 | scheme, 61 | "vscode-ios.project.selectTarget", 62 | "target", 63 | "Click to select Xcode Project Scheme", 64 | "Project:Scheme" 65 | ) 66 | ); 67 | } catch { 68 | /// if the scheme is not yet set, display just busy icon 69 | const scheme = `Scheme: (Tap To Select One)`; 70 | this.config.push( 71 | new ProjectConfigurationNode( 72 | scheme, 73 | "vscode-ios.project.selectTarget", 74 | "target", 75 | "Click to select Xcode Project Scheme", 76 | "Project:Scheme" 77 | ) 78 | ); 79 | } 80 | 81 | try { 82 | const configuration = `Configuration: ${await projectEnv.projectConfiguration}`; 83 | this.config.push( 84 | new ProjectConfigurationNode( 85 | configuration, 86 | "vscode-ios.project.selectConfiguration", 87 | "database", 88 | "Click to select Xcode Project Configuration", 89 | "Project:Configuration" 90 | ) 91 | ); 92 | } catch { 93 | const configuration = `Configuration: (Tap To Select One)`; 94 | this.config.push( 95 | new ProjectConfigurationNode( 96 | configuration, 97 | "vscode-ios.project.selectConfiguration", 98 | "database", 99 | "Click to select Xcode Project Configuration", 100 | "Project:Configuration" 101 | ) 102 | ); 103 | } 104 | 105 | try { 106 | const device = await projectEnv.debugDeviceID; 107 | const configuration = `Debug Device: ${device.name}, OS: ${device.OS}`; 108 | this.config.push( 109 | new ProjectConfigurationNode( 110 | configuration, 111 | "vscode-ios.project.selectDevice", 112 | "device-mobile", 113 | "Click to select Xcode Project Debug Device", 114 | "Project:DebugDevice" 115 | ) 116 | ); 117 | } catch { 118 | const configuration = `Debug Device: (Tap To Select One)`; 119 | this.config.push( 120 | new ProjectConfigurationNode( 121 | configuration, 122 | "vscode-ios.project.selectDevice", 123 | "device-mobile", 124 | "Click to select Xcode Project Debug Device", 125 | "Project:DebugDevice" 126 | ) 127 | ); 128 | } 129 | 130 | try { 131 | const configuration = `Test Plan: ${await projectEnv.projectTestPlan}`; 132 | this.config.push( 133 | new ProjectConfigurationNode( 134 | configuration, 135 | "vscode-ios.project.selectTestPlan", 136 | "shield", 137 | "Click to select Xcode Project Test Plan", 138 | "Project:TestPlan" 139 | ) 140 | ); 141 | } catch { 142 | const configuration = `Test Plan: (Tap to select different one)`; 143 | this.config.push( 144 | new ProjectConfigurationNode( 145 | configuration, 146 | "vscode-ios.project.selectTestPlan", 147 | "shield", 148 | "Click to select Xcode Project Test Plan", 149 | "Project:TestPlan" 150 | ) 151 | ); 152 | } 153 | 154 | this._onDidChangeTreeData.fire(undefined); 155 | } 156 | 157 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 158 | return element; 159 | } 160 | 161 | getChildren(element?: vscode.TreeItem): Thenable { 162 | if (element) { 163 | return Promise.resolve([]); 164 | } else { 165 | // return root elements 166 | return Promise.resolve(this.config); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/XcodeSideTreePanel/RuntimeWarningsDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class RuntimeWarningStackNode extends vscode.TreeItem { 4 | functionName: string; 5 | filePath: string; 6 | line: number; 7 | 8 | constructor(functionName: string, line: number, filePath: string) { 9 | super(`${functionName} + ${line}`, vscode.TreeItemCollapsibleState.None); 10 | this.functionName = functionName; 11 | this.line = line; 12 | this.filePath = filePath; 13 | this.command = { 14 | title: "Open location", 15 | command: "vscode-ios.openFile", 16 | arguments: [filePath, line], 17 | }; 18 | } 19 | } 20 | 21 | export class RuntimeWarningMessageNode extends vscode.TreeItem { 22 | count: number; 23 | stack: RuntimeWarningStackNode[] = []; 24 | 25 | constructor(message: string, count: number, id: string) { 26 | let startIndex = message.lastIndexOf("] "); 27 | if (startIndex === -1) { 28 | startIndex = 0; 29 | } else { 30 | startIndex += "] ".length; 31 | } 32 | 33 | super( 34 | `(${count})${message.substring(startIndex)}`, 35 | vscode.TreeItemCollapsibleState.Collapsed 36 | ); 37 | this.description = message; 38 | this.id = id; 39 | this.count = count; 40 | } 41 | } 42 | 43 | export class RuntimeWarningsDataProvider implements vscode.TreeDataProvider { 44 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 45 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 46 | 47 | private warnings: RuntimeWarningMessageNode[] = []; 48 | private used = new Map(); 49 | 50 | public refresh(elements: RuntimeWarningMessageNode[]): any { 51 | const newComingElements = new Set(); 52 | for (const elem of elements) { 53 | newComingElements.add(elem.id || ""); 54 | const used = this.used.get(elem.id || ""); 55 | if (used) { 56 | used.label = elem.label; 57 | used.count = elem.count; 58 | used.stack = elem.stack; 59 | used.description = elem.description; 60 | } else { 61 | this.warnings.push(elem); 62 | this.used.set(elem.id || "", elem); 63 | } 64 | } 65 | 66 | this.used.clear(); 67 | this.warnings = this.warnings.filter(value => { 68 | return newComingElements.has(value.id || ""); 69 | }); 70 | this.warnings.forEach(e => { 71 | this.used.set(e.id || "", e); 72 | }); 73 | 74 | this._onDidChangeTreeData.fire(undefined); 75 | } 76 | 77 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 78 | return element; 79 | } 80 | 81 | getChildren(element?: vscode.TreeItem): Thenable { 82 | if (element) { 83 | if (element instanceof RuntimeWarningMessageNode) { 84 | return Promise.resolve(element.stack); 85 | } 86 | return Promise.resolve([]); 87 | } else { 88 | // return root elements 89 | return Promise.resolve(this.warnings); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/XcodeSideTreePanel/RuntimeWarningsLogWatcher.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { createFifo } from "../utils"; 3 | import { getWorkspacePath } from "../env"; 4 | import path from "path"; 5 | import { 6 | RuntimeWarningMessageNode, 7 | RuntimeWarningStackNode, 8 | RuntimeWarningsDataProvider, 9 | } from "./RuntimeWarningsDataProvider"; 10 | import { error } from "console"; 11 | import { createInterface, Interface } from "readline"; 12 | 13 | export class RuntimeWarningsLogWatcher { 14 | private static LogPath = ".vscode/xcode/fifo/.app_runtime_warnings.fifo"; 15 | 16 | private panel: RuntimeWarningsDataProvider; 17 | private rl?: Interface; 18 | private stream?: fs.ReadStream; 19 | 20 | private cachedContent: string = ""; 21 | 22 | static get logPath(): string { 23 | return path.join(getWorkspacePath(), RuntimeWarningsLogWatcher.LogPath); 24 | } 25 | 26 | constructor(panel: RuntimeWarningsDataProvider) { 27 | this.panel = panel; 28 | } 29 | 30 | public async startWatcher() { 31 | try { 32 | // await deleteFifo(RuntimeWarningsLogWatcher.logPath); 33 | } catch (error) { 34 | console.log(`Error deleting fifo file: ${error}`); 35 | } 36 | await createFifo(RuntimeWarningsLogWatcher.logPath); 37 | try { 38 | this.panel.refresh([]); 39 | this.cachedContent = ""; 40 | this.updateTree(""); 41 | } catch { 42 | /* empty */ 43 | } 44 | 45 | if (this.rl === undefined || this.stream?.closed === true) { 46 | this.startWatcherImp(); 47 | } 48 | } 49 | 50 | private async startWatcherImp() { 51 | try { 52 | const stream = fs.createReadStream(RuntimeWarningsLogWatcher.logPath, { flags: "r" }); 53 | this.stream = stream; 54 | 55 | const rl = createInterface({ input: stream, crlfDelay: Infinity }); 56 | this.rl = rl; 57 | for await (const line of rl) { 58 | this.readContent(line); 59 | } 60 | } catch (error) { 61 | console.log(`FIFO file for warnings log got error: ${error}`); 62 | } 63 | } 64 | 65 | private readContent(data: string) { 66 | try { 67 | this.updateTree(data); 68 | } catch { 69 | /* empty */ 70 | } 71 | } 72 | 73 | disposeWatcher() { 74 | this.rl?.close(); 75 | this.stream?.close(); 76 | this.rl = undefined; 77 | this.stream = undefined; 78 | } 79 | 80 | private updateTree(content: string) { 81 | if (content === this.cachedContent) { 82 | return; 83 | } 84 | // convert to html 85 | const elements: RuntimeWarningMessageNode[] = []; 86 | try { 87 | const root = JSON.parse(content); 88 | 89 | for (const element in root) { 90 | const value = root[element]; 91 | const warning = new RuntimeWarningMessageNode(value.message, value.count, element); 92 | const stacks = value.data; 93 | for (const frame of stacks) { 94 | if ( 95 | frame.file && 96 | frame.file.length > 0 && 97 | frame.file.indexOf("") === -1 98 | ) { 99 | const frameNode = new RuntimeWarningStackNode( 100 | frame.function, 101 | frame.line, 102 | frame.file 103 | ); 104 | warning.stack.push(frameNode); 105 | } 106 | } 107 | 108 | elements.push(warning); 109 | } 110 | 111 | this.panel.refresh(elements); 112 | } catch { 113 | console.log(`Error of parsing runtime errors data: ${error}`); 114 | throw error; 115 | } finally { 116 | this.cachedContent = content; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/buildCommands.ts: -------------------------------------------------------------------------------- 1 | import { checkWorkspace } from "./commands"; 2 | import { ProblemDiagnosticResolver } from "./ProblemDiagnosticResolver"; 3 | import { getBuildRootPath } from "./env"; 4 | import path from "path"; 5 | import { CommandContext } from "./CommandManagement/CommandContext"; 6 | import { BuildManager } from "./Services/BuildManager"; 7 | import { ExecutorMode } from "./Executor"; 8 | import { handleValidationErrors } from "./extension"; 9 | 10 | export function getFileNameLog() { 11 | const fileName = path.join(".logs", "build.log"); 12 | return fileName; 13 | } 14 | 15 | export async function cleanDerivedData(context: CommandContext) { 16 | await context.execShellWithOptions({ 17 | scriptOrCommand: { command: "rm" }, 18 | args: ["-rf", await getBuildRootPath()], 19 | mode: ExecutorMode.onlyCommandNameAndResult, 20 | }); 21 | } 22 | 23 | export async function buildSelectedTarget( 24 | context: CommandContext, 25 | problemResolver: ProblemDiagnosticResolver 26 | ) { 27 | await checkWorkspace(context); 28 | const buildManager = new BuildManager(); 29 | const filePath = getFileNameLog(); 30 | const rawParser = problemResolver.parseAsyncLogs(filePath, context.buildEvent); 31 | try { 32 | const build = async () => { 33 | try { 34 | await buildManager.build(context, filePath); 35 | } catch (error) { 36 | await handleValidationErrors(context, error, async () => { 37 | await checkWorkspace(context); 38 | await build(); 39 | }); 40 | } 41 | }; 42 | await build(); 43 | } finally { 44 | problemResolver.end(rawParser); 45 | } 46 | } 47 | 48 | // TESTS 49 | 50 | export async function buildTestsForCurrentFile( 51 | context: CommandContext, 52 | problemResolver: ProblemDiagnosticResolver, 53 | tests: string[], 54 | isCoverage: boolean 55 | ) { 56 | await checkWorkspace(context); 57 | const buildManager = new BuildManager(); 58 | const filePath = getFileNameLog(); 59 | const rawParser = problemResolver.parseAsyncLogs(filePath, context.buildEvent); 60 | try { 61 | const build = async () => { 62 | try { 63 | await buildManager.buildForTestingWithTests(context, filePath, tests, isCoverage); 64 | } catch (error) { 65 | await handleValidationErrors(context, error, async () => { 66 | await checkWorkspace(context); 67 | await build(); 68 | }); 69 | } 70 | }; 71 | await build(); 72 | } finally { 73 | problemResolver.end(rawParser); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/inputPicker.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { quickPickWithHistory } from "./quickPickHistory"; 3 | import { UserTerminatedError } from "./CommandManagement/CommandContext"; 4 | 5 | export async function askIfDebuggable() { 6 | const items: QuickPickItem[] = [ 7 | { label: "Debug", value: "Debug" }, 8 | { label: "Run", value: "Run" }, 9 | ]; 10 | const option = await showPicker(items, "Debug?", "", false, false, true); 11 | return option === "Debug"; 12 | } 13 | 14 | export async function askIfBuild() { 15 | const items: QuickPickItem[] = [ 16 | { label: "Yes", value: "Yes" }, 17 | { label: "No", value: "No" }, 18 | ]; 19 | const option = await showPicker( 20 | items, 21 | "Prebuild before launch?", 22 | "(Esc) to cancel", 23 | false, 24 | false, 25 | true 26 | ); 27 | if (option === undefined) { 28 | throw UserTerminatedError; 29 | } 30 | return option === "Yes"; 31 | } 32 | 33 | export async function initializeWithError(error: unknown) { 34 | const option = await vscode.window.showErrorMessage( 35 | `Projects can not be initialized. ${error}. Open in Xcode and fix it first!`, 36 | "Open in Xcode", 37 | "Cancel" 38 | ); 39 | if (option === "Open in Xcode") { 40 | vscode.commands.executeCommand("vscode-ios.env.open.xcode"); 41 | } 42 | } 43 | 44 | let extContext: vscode.ExtensionContext; 45 | 46 | export function setContext(context: vscode.ExtensionContext) { 47 | extContext = context; 48 | } 49 | 50 | export interface QuickPickItem extends vscode.QuickPickItem { 51 | value: string | any; 52 | } 53 | 54 | export async function showPicker( 55 | json: string | QuickPickItem[], 56 | title: string, 57 | placeholder: string, 58 | canPickMany = false, 59 | ignoreFocusOut = false, 60 | useHistory = false 61 | ) { 62 | let items: QuickPickItem[]; 63 | if (typeof json === "string" || json instanceof String) { 64 | items = JSON.parse(json as string); 65 | } else { 66 | items = json; 67 | } 68 | 69 | const selectionClosure = async (items: QuickPickItem[]) => { 70 | return await vscode.window.showQuickPick(items, { 71 | title: title, 72 | placeHolder: placeholder, 73 | ignoreFocusOut: ignoreFocusOut, 74 | canPickMany: canPickMany, 75 | }); 76 | }; 77 | let selection: vscode.QuickPickItem | undefined; 78 | if (useHistory) { 79 | selection = await quickPickWithHistory(items, extContext, title, selectionClosure); 80 | } else { 81 | selection = await selectionClosure(items); 82 | } 83 | 84 | if (selection === undefined) { 85 | return undefined; 86 | } 87 | 88 | let value: string | any | undefined; 89 | 90 | if (typeof selection === "string") { 91 | value = selection as string; 92 | } 93 | 94 | if (typeof selection === "object") { 95 | if (selection === null) { 96 | value = undefined; 97 | } else { 98 | if (canPickMany) { 99 | const array = (selection as unknown as { [key: string]: any }[]).map(e => { 100 | return e["value"]; 101 | }); 102 | 103 | value = array; 104 | } else { 105 | const dict = selection as { [key: string]: any }; 106 | value = dict["value"]; 107 | } 108 | } 109 | } 110 | 111 | return value; 112 | } 113 | -------------------------------------------------------------------------------- /src/nonActiveExtension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | function showErrorOnPerformingExtensionCommand() { 4 | vscode.window 5 | .showErrorMessage( 6 | "To use this command you need to select Xcode project. Try It?", 7 | "Select Xcode Project", 8 | "Cancel" 9 | ) 10 | .then(option => { 11 | if (option === "Select Xcode Project") { 12 | vscode.commands.executeCommand("vscode-ios.project.select"); 13 | } 14 | }); 15 | } 16 | 17 | export function activateNotActiveExtension(context: vscode.ExtensionContext) { 18 | context.subscriptions.push( 19 | vscode.commands.registerCommand( 20 | "vscode-ios.tools.install", 21 | showErrorOnPerformingExtensionCommand 22 | ) 23 | ); 24 | context.subscriptions.push( 25 | vscode.commands.registerCommand( 26 | "vscode-ios.tools.update", 27 | showErrorOnPerformingExtensionCommand 28 | ) 29 | ); 30 | 31 | context.subscriptions.push( 32 | vscode.commands.registerCommand( 33 | "vscode-ios.lsp.restart", 34 | showErrorOnPerformingExtensionCommand 35 | ) 36 | ); 37 | 38 | context.subscriptions.push( 39 | vscode.commands.registerCommand( 40 | "vscode-ios.env.open.xcode", 41 | showErrorOnPerformingExtensionCommand 42 | ) 43 | ); 44 | 45 | context.subscriptions.push( 46 | vscode.commands.registerCommand( 47 | "vscode-ios.project.selectTarget", 48 | showErrorOnPerformingExtensionCommand 49 | ) 50 | ); 51 | 52 | context.subscriptions.push( 53 | vscode.commands.registerCommand( 54 | "vscode-ios.project.selectConfiguration", 55 | showErrorOnPerformingExtensionCommand 56 | ) 57 | ); 58 | 59 | context.subscriptions.push( 60 | vscode.commands.registerCommand( 61 | "vscode-ios.project.selectTestPlan", 62 | showErrorOnPerformingExtensionCommand 63 | ) 64 | ); 65 | 66 | context.subscriptions.push( 67 | vscode.commands.registerCommand( 68 | "vscode-ios.project.selectDevice", 69 | showErrorOnPerformingExtensionCommand 70 | ) 71 | ); 72 | 73 | context.subscriptions.push( 74 | vscode.commands.registerCommand( 75 | "vscode-ios.check.workspace", 76 | showErrorOnPerformingExtensionCommand 77 | ) 78 | ); 79 | 80 | context.subscriptions.push( 81 | vscode.commands.registerCommand( 82 | "vscode-ios.check.generateXcodeServer", 83 | showErrorOnPerformingExtensionCommand 84 | ) 85 | ); 86 | 87 | context.subscriptions.push( 88 | vscode.commands.registerCommand( 89 | "vscode-ios.build.clean", 90 | showErrorOnPerformingExtensionCommand 91 | ) 92 | ); 93 | context.subscriptions.push( 94 | vscode.commands.registerCommand( 95 | "vscode-ios.build.selectedTarget", 96 | showErrorOnPerformingExtensionCommand 97 | ) 98 | ); 99 | 100 | context.subscriptions.push( 101 | vscode.commands.registerCommand( 102 | "vscode-ios.build.tests", 103 | showErrorOnPerformingExtensionCommand 104 | ) 105 | ); 106 | 107 | context.subscriptions.push( 108 | vscode.commands.registerCommand( 109 | "vscode-ios.run.app.multiple.devices", 110 | showErrorOnPerformingExtensionCommand 111 | ) 112 | ); 113 | 114 | context.subscriptions.push( 115 | vscode.commands.registerCommand( 116 | "vscode-ios.run.app.debug", 117 | showErrorOnPerformingExtensionCommand 118 | ) 119 | ); 120 | 121 | context.subscriptions.push( 122 | vscode.commands.registerCommand( 123 | "vscode-ios.project.file.add", 124 | showErrorOnPerformingExtensionCommand 125 | ) 126 | ); 127 | 128 | context.subscriptions.push( 129 | vscode.commands.registerCommand( 130 | "vscode-ios.project.delete.reference", 131 | showErrorOnPerformingExtensionCommand 132 | ) 133 | ); 134 | 135 | context.subscriptions.push( 136 | vscode.commands.registerCommand( 137 | "vscode-ios.project.file.edit.targets", 138 | showErrorOnPerformingExtensionCommand 139 | ) 140 | ); 141 | 142 | context.subscriptions.push( 143 | vscode.commands.registerCommand( 144 | "vscode-ios.run.project.reload", 145 | showErrorOnPerformingExtensionCommand 146 | ) 147 | ); 148 | context.subscriptions.push( 149 | vscode.commands.registerCommand( 150 | "vscode-ios.run.project.update.deps", 151 | showErrorOnPerformingExtensionCommand 152 | ) 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /src/quickPickHistory.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getWorkspacePath } from "./env"; 3 | import { QuickPickItem } from "./inputPicker"; 4 | 5 | export async function quickPickWithHistory( 6 | items: QuickPickItem[] | string[], 7 | context: vscode.ExtensionContext, 8 | keyStorage: string, 9 | showPickClosure: (items: QuickPickItem[]) => Promise 10 | ) { 11 | const qItems: QuickPickItem[] = items.map(e => { 12 | if (typeof e === "string") { 13 | return { label: e, value: e }; 14 | } 15 | return e; 16 | }); 17 | return await quickPickWithHistoryImp(qItems, context, keyStorage, showPickClosure); 18 | } 19 | 20 | async function quickPickWithHistoryImp( 21 | items: QuickPickItem[], 22 | context: vscode.ExtensionContext, 23 | keyStorage: string, 24 | showPickClosure: (items: QuickPickItem[]) => Promise 25 | ) { 26 | const key = `${keyStorage}${getWorkspacePath()}`; 27 | let cache = context.globalState.get(key)?.filter((e: any) => { 28 | return e.title !== undefined; 29 | }); 30 | 31 | if ( 32 | cache === undefined || 33 | cache 34 | .map(e => { 35 | return e.title; 36 | }) 37 | .sort() 38 | .toString() !== 39 | items 40 | .map(v => { 41 | return v.value; 42 | }) 43 | .sort() 44 | .toString() 45 | ) { 46 | const oldCache = cache; 47 | cache = []; 48 | let i = 0; 49 | const date = Date.now(); 50 | for (const item of items) { 51 | const foundIndex = 52 | oldCache 53 | ?.map(e => { 54 | return e.title; 55 | }) 56 | .indexOf(item.value) || -1; 57 | cache.push({ 58 | title: item.value, 59 | order: i, 60 | date: foundIndex === -1 ? date : oldCache?.at(foundIndex)?.date || date, 61 | }); 62 | ++i; 63 | } 64 | } else { 65 | cache.sort((a, b) => { 66 | if (a.date !== b.date) { 67 | return b.date - a.date; 68 | } 69 | return a.order - b.order; 70 | }); 71 | } 72 | const indexedCache = cache.map(e => { 73 | return e.title; 74 | }); 75 | const sortedItems = items.sort((a, b) => { 76 | return (indexedCache?.indexOf(a.value) || -1) - (indexedCache?.indexOf(b.value) || -1); 77 | }); 78 | 79 | const option = await showPickClosure(sortedItems); 80 | if (option === undefined) { 81 | return undefined; 82 | } 83 | if (Array.isArray(option)) { 84 | for (const opt of option) { 85 | for (const item of cache) { 86 | if (item.title === opt.value) { 87 | item.date = Date.now(); 88 | } 89 | } 90 | } 91 | } else { 92 | for (const item of cache) { 93 | if (item.title === option.value) { 94 | item.date = Date.now(); 95 | } 96 | } 97 | } 98 | context.globalState.update(key, cache); 99 | 100 | return option; 101 | } 102 | 103 | interface QuickPickHistory { 104 | title: string; 105 | order: number; 106 | date: number; 107 | } 108 | -------------------------------------------------------------------------------- /syntaxes/arm.disasm: -------------------------------------------------------------------------------- 1 | (lldb) 2 | libIGL.so`igl::RenderPipelineDesc::TargetDesc::ColorAttachment::operator==: 3 | libIGL.so[0x7694] <+0>: ldr r2, [r1] 4 | libIGL.so[0x7696] <+2>: ldr r3, [r0] 5 | libIGL.so[0x7698] <+4>: cmp r3, r2 6 | libIGL.so[0x769a] <+6>: bne 0x76da ; <+70> at RenderPipelineState.cpp 7 | libIGL.so[0x769c] <+8>: ldrb r2, [r1, #0x5] 8 | libIGL.so[0x769e] <+10>: ldrb r3, [r0, #0x5] 9 | libIGL.so[0x76a0] <+12>: cmp r3, r2 10 | libIGL.so[0x76a2] <+14>: bne 0x76da ; <+70> at RenderPipelineState.cpp 11 | libIGL.so[0x76a4] <+16>: ldr r2, [r1, #0x8] 12 | libIGL.so[0x76a6] <+18>: ldr r3, [r0, #0x8] 13 | libIGL.so[0x76a8] <+20>: cmp r3, r2 14 | libIGL.so[0x76aa] <+22>: bne 0x76da ; <+70> at RenderPipelineState.cpp 15 | libIGL.so[0x76ac] <+24>: ldr r2, [r1, #0xc] 16 | libIGL.so[0x76ae] <+26>: ldr r3, [r0, #0xc] 17 | libIGL.so[0x76b0] <+28>: cmp r3, r2 18 | libIGL.so[0x76b2] <+30>: bne 0x76da ; <+70> at RenderPipelineState.cpp 19 | libIGL.so[0x76b4] <+32>: ldr r2, [r1, #0x10] 20 | libIGL.so[0x76b6] <+34>: ldr r3, [r0, #0x10] 21 | libIGL.so[0x76b8] <+36>: cmp r3, r2 22 | libIGL.so[0x76ba] <+38>: bne 0x76da ; <+70> at RenderPipelineState.cpp 23 | libIGL.so[0x76bc] <+40>: ldr r2, [r1, #0x14] 24 | libIGL.so[0x76be] <+42>: ldr r3, [r0, #0x14] 25 | libIGL.so[0x76c0] <+44>: cmp r3, r2 26 | libIGL.so[0x76c2] <+46>: bne 0x76da ; <+70> at RenderPipelineState.cpp 27 | libIGL.so[0x76c4] <+48>: ldr r2, [r1, #0x18] 28 | libIGL.so[0x76c6] <+50>: ldr r3, [r0, #0x18] 29 | libIGL.so[0x76c8] <+52>: cmp r3, r2 30 | libIGL.so[0x76ca] <+54>: bne 0x76da ; <+70> at RenderPipelineState.cpp 31 | libIGL.so[0x76cc] <+56>: ldr r1, [r1, #0x1c] 32 | libIGL.so[0x76ce] <+58>: ldr r0, [r0, #0x1c] 33 | libIGL.so[0x76d0] <+60>: subs r0, r0, r1 34 | libIGL.so[0x76d2] <+62>: clz r0, r0 35 | libIGL.so[0x76d6] <+66>: lsrs r0, r0, #0x5 36 | libIGL.so[0x76d8] <+68>: bx lr 37 | libIGL.so[0x76da] <+70>: movs r0, #0x0 38 | libIGL.so[0x76dc] <+72>: bx lr 39 | (lldb) disassemble --name _ZN3igl20VertexInputStateDesc28sizeForVertexAttributeFormatENS_21VertexAttributeFormatE 40 | libIGL.so`igl::VertexInputStateDesc::sizeForVertexAttributeFormat: 41 | libIGL.so[0x787c] <+0>: ldr r1, [pc, #0x8] ; <+12> at VertexInputState.cpp 42 | libIGL.so[0x787e] <+2>: add r1, pc 43 | libIGL.so[0x7880] <+4>: ldr.w r0, [r1, r0, lsl #2] 44 | libIGL.so[0x7884] <+8>: bx lr 45 | libIGL.so[0x7886] <+10>: nop -------------------------------------------------------------------------------- /syntaxes/disassembly.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Disassembly", 3 | "scopeName": "source.disassembly", 4 | "uuid": "9ade615f-5d82-4ac5-b22f-a1998c356ebe", 5 | "patterns": [ 6 | { 7 | "comment": "x86 Address, bytes and opcode", 8 | "name": "meta.instruction", 9 | "match": "^([A-Za-z0-9]+):\\s([A-Z0-9]{2}\\s)+>?\\s+(\\w+)", 10 | "captures": { 11 | "1": {"name": "constant.numeric"}, 12 | "3": {"name": "keyword.opcode"} 13 | } 14 | }, 15 | { 16 | "comment": "ARM Address, bytes and opcode", 17 | "name": "meta.instruction", 18 | "match": "^libIGL.so\\[([A-Za-z0-9]+)\\]\\s+(\\<\\+[0-9]*\\>):\\s+([A-Za-z]+.?[A-Za-z]*)", 19 | "captures": { 20 | "1": {"name": "constant.numeric"}, 21 | "3": {"name": "keyword.opcode"} 22 | } 23 | }, 24 | { 25 | "comment": "ARM64 Address, bytes and opcode", 26 | "name": "meta.instruction", 27 | "match": "^liblog.so\\[([A-Za-z0-9]+)\\]\\s+(\\<\\+[0-9]*\\>):\\s+([A-Za-z]+.?[A-Za-z]*)", 28 | "captures": { 29 | "1": {"name": "constant.numeric"}, 30 | "3": {"name": "keyword.opcode"} 31 | } 32 | }, 33 | { 34 | "comment": "Numeric constant", 35 | "name": "constant.numeric", 36 | "match": "(\\$|\\b)((0x)|[0-9])[A-Za-z0-9]+\\b" 37 | }, 38 | { 39 | "comment": "x86 Register", 40 | "name": "variable.language", 41 | "match": "%[A-Za-z][A-Za-z0-9]*" 42 | }, 43 | { 44 | "comment": "ARM Register", 45 | "name": "variable.language", 46 | "match": "r\\d+" 47 | }, 48 | { 49 | "comment": "ARM Register Shortnames", 50 | "name": "variable.language", 51 | "match": "(fp|sp|lr|pc|wzr|xzr)" 52 | }, 53 | { 54 | "comment": "ARM64 Register", 55 | "name": "variable.language", 56 | "match": "(x|w)[0-9]+" 57 | }, 58 | { 59 | "comment": "End of line comment", 60 | "name": "comment.line.semicolon", 61 | "match": ";.*$" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /syntaxes/x86.disasm: -------------------------------------------------------------------------------- 1 | 0x100008000: <0> popq %rdi 2 | 0x100008001: <1> pushq $0x0 3 | 0x100008003: <3> movq %rsp, %rbp 4 | 0x100008006: <6> andq $-0x10, %rsp 5 | 0x10000800A: <10> subq $0x10, %rsp 6 | 0x10000800E: <14> movl 0x8(%rbp), %esi 7 | 0x100008011: <17> leaq 0x10(%rbp), %rdx 8 | 0x100008015: <21> leaq -0x101c(%rip), %rcx 9 | 0x10000801C: <28> leaq -0x8(%rbp), %r8 10 | 0x100008020: <32> callq 0x100008062 # dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) 11 | 0x100008025: <37> movq -0x8(%rbp), %rdi 12 | 0x100008029: <41> cmpq $0x0, %rdi 13 | 0x10000802D: <45> jne 0x10000803f # <+63> 14 | 0x10000802F: <47> movq %rbp, %rsp 15 | 0x100008032: <50> addq $0x8, %rsp 16 | 0x100008036: <54> movq $0x0, %rbp 17 | 0x10000803D: <61> jmpq *%rax 18 | 0x10000803F: <63> addq $0x10, %rsp 19 | 0x100008043: <67> pushq %rdi 20 | 0x100008044: <68> movq 0x8(%rbp), %rdi 21 | 0x100008048: <72> leaq 0x10(%rbp), %rsi 22 | 0x10000804C: <76> leaq 0x8(%rsi,%rdi,8), %rdx 23 | 0x100008051: <81> movq %rdx, %rcx 24 | 0x100008054: <84> movq (%rcx), %r8 25 | 0x100008057: <87> addq $0x8, %rcx 26 | 0x10000805B: <91> testq %r8, %r8 27 | 0x10000805E: <94> jne 0x100008054 # <+84> 28 | 0x100008060: <96> jmpq *%rax 29 | -------------------------------------------------------------------------------- /test/extension/ProblemDiagnostic/mocks/build_log_empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireplusteam/ios-swift-for-vs-code/4834827531a6d5abc835993a6a3b905f4cb83537/test/extension/ProblemDiagnostic/mocks/build_log_empty.txt -------------------------------------------------------------------------------- /test/extension/ProblemDiagnostic/mocks/build_log_with_linker_errors.txt: -------------------------------------------------------------------------------- 1 | 2 | xcodebuild log random prefix 3 | 4 | 5 | error: TCA.framework library is not found. 6 | 7 | ok, this is working well 8 | 9 | clang: error: failed to link. Firebase.framework is not not found 10 | 11 | 12 | some random text -------------------------------------------------------------------------------- /test/extension/extension.test.ts: -------------------------------------------------------------------------------- 1 | // import * as assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | // assert.strictEqual([1, 2, 3].indexOf(5), -1); 13 | // assert.strictEqual([1, 2, 3].indexOf(0), -1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2022" 8 | ], 9 | "rootDir": "./", 10 | "sourceMap": true, 11 | "strict": true /* enable all strict type-checking options */, 12 | "skipLibCheck": true /// THIS'S JUST 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ] 22 | } -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 31 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 32 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 33 | * See the output of the test result in the Test Results view. 34 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 41 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 42 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 43 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 44 | --------------------------------------------------------------------------------