├── .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
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 | [](https://github.com/fireplusteam/ios_vs_code)
9 | [](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 | [](https://youtu.be/0dXQGY0IIEA)
21 |
22 | ## 🌳 File Tree Integration
23 |
24 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------