├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── enhancement.md └── workflows │ ├── artifacts.yml │ ├── build_runner.yml │ ├── coverage.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── LICENSE-ZLS ├── README.md ├── build.zig ├── build.zig.zon ├── codecov.yml ├── schema.json ├── src ├── BuildAssociatedConfig.zig ├── Config.zig ├── DiagnosticsCollection.zig ├── DocumentScope.zig ├── DocumentStore.zig ├── Server.zig ├── analyser │ ├── InternPool.zig │ ├── README.md │ ├── analyser.zig │ ├── completions.zig │ ├── degibberish.zig │ ├── error_msg.zig │ └── string_pool.zig ├── analysis.zig ├── ast.zig ├── binned_allocator.zig ├── build_runner │ ├── 0.14.0.zig │ ├── BuildRunnerVersion.zig │ ├── legacy.zig │ └── shared.zig ├── configuration.zig ├── debug.zig ├── diff.zig ├── features │ ├── code_actions.zig │ ├── completions.zig │ ├── diagnostics.zig │ ├── document_symbol.zig │ ├── folding_range.zig │ ├── goto.zig │ ├── hover.zig │ ├── inlay_hints.zig │ ├── references.zig │ ├── selection_range.zig │ ├── semantic_tokens.zig │ └── signature_help.zig ├── main.zig ├── offsets.zig ├── snippets.zig ├── tools │ ├── config.json │ ├── config_gen.zig │ ├── langref_master.html.in │ └── publish_http_form.zig ├── tracy.zig ├── translate_c.zig ├── uri.zig ├── zig-components │ ├── Ast.zig │ ├── LICENSE │ └── Parse.zig └── zls.zig └── tests ├── ErrorBuilder.zig ├── add_analysis_cases.zig ├── analysis ├── array.zig ├── basic.zig ├── optional.zig ├── peer_type_resolution.zig ├── pointer.zig └── tuple.zig ├── analysis_check.zig ├── context.zig ├── helper.zig ├── language_features └── cimport.zig ├── lifecycle.zig ├── lsp_features ├── code_actions.zig ├── completion.zig ├── definition.zig ├── document_symbol.zig ├── folding_range.zig ├── hover.zig ├── inlay_hints.zig ├── references.zig ├── selection_range.zig ├── semantic_tokens.zig └── signature_help.zig ├── tests.zig └── utility ├── ast.zig ├── diff.zig └── position_context.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | *.zon text=auto eol=lf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | A bug is when something works differently than it is expected to. 9 | ## Remember to search before filing a new report 10 | Please search for this bug in the issue tracker, and use a bug report title that 11 | would have made your bug report turn up in the search results for your search query. 12 | - type: input 13 | id: zig-version 14 | attributes: 15 | label: Zig Version 16 | description: "The output of `zig version`" 17 | placeholder: "0.9.0-dev.1275+ac52e0056" 18 | validations: 19 | required: true 20 | - type: input 21 | id: project-version 22 | attributes: 23 | label: This Project's Version 24 | description: "a tagged release version or a commit hash" 25 | placeholder: "b21039d51261923c665d3bc58fadc4b4d5e221ea" 26 | validations: 27 | required: true 28 | - type: input 29 | id: editor 30 | attributes: 31 | label: Client / Code Editor / Extensions 32 | description: What client/code editor/extensions are you using, if any? 33 | placeholder: "nvim 0.9.4 with CoC and ziglang/zig.vim" 34 | validations: 35 | required: false 36 | - type: textarea 37 | id: repro 38 | attributes: 39 | label: Steps to Reproduce and Observed Behavior 40 | description: What exactly can someone else do, in order to observe the problem that you observed? 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: expected 45 | attributes: 46 | label: Expected Behavior 47 | description: What did you expect to happen? 48 | validations: 49 | required: true 50 | - type: textarea 51 | id: logs 52 | attributes: 53 | label: Relevant log output 54 | description: If applicable, include a log output [Guide](https://github.com/zigtools/zls/wiki/Guide:-How-to-view-ZLS-log-output). This will be automatically formatted into monospace, so no need for backticks. 55 | render: shell 56 | validations: 57 | required: false 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement 3 | about: Share your idea 4 | labels: enhancement 5 | --- 6 | 7 | ## Remember to search before filing a new report 8 | -------------------------------------------------------------------------------- /.github/workflows/artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Deploy release artifacts 2 | 3 | on: 4 | # push: 5 | # branches: 6 | # - dev 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | # if: github.repository_owner == 'llogick' && github.ref == 'refs/heads/dev' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # required to resolve the version string 17 | 18 | - uses: mlugg/setup-zig@v1 19 | with: 20 | version: master 21 | 22 | # - name: Install APT packages 23 | # run: | 24 | # sudo apt-get update 25 | # sudo apt-get install tar 7zip 26 | 27 | # - name: Install minisign 28 | # run: | 29 | # wget https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz 30 | # tar -xf minisign-0.11-linux.tar.gz --directory ${HOME} 31 | # echo "${HOME}/minisign-linux/x86_64/" >> $GITHUB_PATH 32 | 33 | - name: Build all targets 34 | run: | 35 | mkdir -p builds 36 | for target in "x86_64-linux" "aarch64-linux" "x86_64-windows" "aarch64-windows" "x86_64-macos" "aarch64-macos"; do 37 | zig build -Dcpu=baseline -Dtarget=${target} -Doptimize=ReleaseFast 38 | if [[ $target == *"windows"* ]]; then 39 | cp zig-out/bin/zigscient.exe builds/zigscient-${target}.exe 40 | else 41 | cp zig-out/bin/zigscient builds/zigscient-${target} 42 | fi 43 | done 44 | - uses: actions/upload-artifact@v4 45 | with: 46 | name: zigscient 47 | path: builds/ 48 | 49 | release: 50 | needs: build 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: write 54 | steps: 55 | - uses: actions/download-artifact@v4 56 | with: 57 | name: zigscient 58 | path: zigscient 59 | - name: Get project version 60 | run: | 61 | chmod +x zigscient/zigscient-x86_64-linux 62 | echo "PROJECT_VERSION=$(zigscient/zigscient-x86_64-linux version)" >> $GITHUB_ENV 63 | - name: Create zips 64 | run: | 65 | cd zigscient 66 | for file in *; do 67 | zip -j "../${file%.*}.zip" "$file" 68 | done 69 | - uses: softprops/action-gh-release@v2 70 | with: 71 | name: ${{ env.PROJECT_VERSION }} 72 | tag_name: ${{ env.PROJECT_VERSION }} 73 | draft: true 74 | make_latest: true 75 | files: "*.zip" 76 | -------------------------------------------------------------------------------- /.github/workflows/build_runner.yml: -------------------------------------------------------------------------------- 1 | name: BuildRunner 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | paths: 9 | - ".github/workflows/build_runner.yml" 10 | - "src/build_runner/**" 11 | # Ensure that the build runner checks get run when the minimum Zig version gets modified 12 | - "build.zig" 13 | - "build.zig.zon" 14 | workflow_dispatch: 15 | 16 | jobs: 17 | check_build_runner: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, windows-latest] 22 | zig: 23 | - version: master 24 | build-runner-file: 0.14.0.zig 25 | # - version: 0.13.0 26 | # build-runner-file: legacy.zig 27 | # - version: 0.12.0 28 | # build-runner-file: legacy.zig 29 | runs-on: ${{ matrix.os }} 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: mlugg/setup-zig@v1 35 | with: 36 | version: ${{ matrix.zig.version }} 37 | 38 | - name: Create temp zig project 39 | run: | 40 | mkdir $RUNNER_TEMP/TEMP_ZIG_PROJECT 41 | cd $RUNNER_TEMP/TEMP_ZIG_PROJECT 42 | zig init 43 | 44 | - name: Check build_runner builds 45 | run: | 46 | cd $RUNNER_TEMP/TEMP_ZIG_PROJECT 47 | zig build --build-runner ${{ github.workspace }}/src/build_runner/${{ matrix.zig.build-runner-file }} 48 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | coverage: 11 | if: github.repository_owner == 'zigtools' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: mlugg/setup-zig@v1 17 | with: 18 | version: master 19 | 20 | - run: zig env 21 | 22 | - name: Install kcov 23 | run: | 24 | wget https://github.com/SimonKagstrom/kcov/releases/download/v42/kcov-amd64.tar.gz 25 | sudo tar xf kcov-amd64.tar.gz -C / 26 | 27 | - name: Run Tests with kcov 28 | run: | 29 | kcov --version 30 | zig build test -Dcoverage --summary all 31 | 32 | - name: Upload to Codecov 33 | uses: codecov/codecov-action@v4 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | directory: zig-out/coverage/kcov-merged 37 | fail_ci_if_error: true 38 | verbose: true 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: mlugg/setup-zig@v1 21 | with: 22 | version: master 23 | 24 | - name: Run zig fmt 25 | if: matrix.os == 'ubuntu-latest' 26 | run: zig fmt --check . 27 | 28 | - name: Run Tests 29 | run: zig build test --summary all 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | result/ 4 | *.zlsreplay 5 | 6 | # Remove on the next lunar eclipse or whatever 7 | zig-cache 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) 2024 The Zigscient Contributors. 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | Includes other software related under the MIT license: 24 | - ZLS, Copyright (c) 2020 Auguste Rame and Alexandros Naskos. For licensing see /LICENSE-ZLS 25 | - Zig, Copyright (c) Zig contributors. For licensing see /src/zig-components/LICENSE 26 | -------------------------------------------------------------------------------- /LICENSE-ZLS: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Auguste Rame and Alexandros Naskos 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 | A drop-in alternative to the original Zig Language Server, with several enhancements. 2 | 3 | Key features and improvements: 4 | - Reworked Modules Collection and Lookup 5 | - Modules are grouped by CompileStep (root ID). See [How to set/switch `root_id`](https://github.com/llogick/zigscient/wiki/Modules:-Switching-%60root_id%60) 6 | - Improved parser performance: 7 | - Slightly better syntax error handling 8 | - Faster reparsing of large documents 9 | - Enhanced code completions: 10 | - Declaration literals: autocompletion and navigation support 11 | - Error and function return type completions, e.g. `return .`, `return error.`, and `switch(err) { error. }` 12 | 13 | 14 | > [!NOTE] 15 | > Remember to rename the executable or update your editor's configuration 16 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zigscient, 3 | // Must match the `proj_version` in `build.zig` 4 | .version = "0.15.0-dev", 5 | // Must match the `minimum_build_zig_version` in `build.zig` 6 | .minimum_zig_version = "0.14.0", 7 | .dependencies = .{ 8 | .known_folders = .{ 9 | .url = "https://github.com/ziglibs/known-folders/archive/aa24df42183ad415d10bc0a33e6238c437fc0f59.tar.gz", 10 | .hash = "known_folders-0.0.0-Fy-PJtLDAADGDOwYwMkVydMSTp_aN-nfjCZw6qPQ2ECL", 11 | }, 12 | .diffz = .{ 13 | .url = "https://github.com/ziglibs/diffz/archive/420fcb22306ffd4c9c3c761863dfbb6bdbb18a73.tar.gz", 14 | .hash = "diffz-0.0.1-AAAAAOLCAQA7XaOIAD5RPaSkjRxSqO9L9EAeJ-5UvU25", 15 | }, 16 | .lsp_codegen = .{ 17 | .url = "https://github.com/zigtools/zig-lsp-codegen/archive/063a98c13a2293d8654086140813bdd1de6501bc.tar.gz", 18 | .hash = "lsp_codegen-0.1.0-CMjjo0ZXCQB-rAhPYrlfzzpU0u0u2MeGvUucZ-_g32eg", 19 | }, 20 | .tracy = .{ 21 | .url = "https://github.com/wolfpld/tracy/archive/refs/tags/v0.11.1.tar.gz", 22 | .hash = "N-V-__8AAMeOlQEipHjcyu0TCftdAi9AQe7EXUDJOoVe0k-t", 23 | .lazy = true, 24 | }, 25 | }, 26 | .paths = .{""}, 27 | .fingerprint = 0xf531a4a2475aa009, 28 | } 29 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | github_checks: 10 | annotations: false 11 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "title": "ZLS Config", 4 | "description": "Configuration file for ZLS", 5 | "type": "object", 6 | "properties": { 7 | "enable_snippets": { 8 | "description": "Enables snippet completions when the client also supports them", 9 | "type": "boolean", 10 | "default": true 11 | }, 12 | "enable_argument_placeholders": { 13 | "description": "Whether to enable function argument placeholder completions", 14 | "type": "boolean", 15 | "default": true 16 | }, 17 | "enable_build_on_save": { 18 | "description": "Whether to enable build-on-save diagnostics. Will be automatically enabled if the `build.zig` has declared a 'check' step.", 19 | "type": "boolean", 20 | "default": null 21 | }, 22 | "build_on_save_args": { 23 | "description": "Specify which arguments should be passed to Zig when running build-on-save.\n\nIf the `build.zig` has declared a 'check' step, it will be preferred over the default 'install' step.", 24 | "type": "array", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "default": [] 29 | }, 30 | "enable_autofix": { 31 | "description": "Whether to automatically fix errors on save. Currently supports adding and removing discards.", 32 | "type": "boolean", 33 | "default": false 34 | }, 35 | "semantic_tokens": { 36 | "description": "Set level of semantic tokens. `partial` only includes information that requires semantic analysis.", 37 | "type": "string", 38 | "enum": [ 39 | "none", 40 | "partial", 41 | "full" 42 | ], 43 | "default": "full" 44 | }, 45 | "inlay_hints_show_variable_type_hints": { 46 | "description": "Enable inlay hints for variable types", 47 | "type": "boolean", 48 | "default": true 49 | }, 50 | "inlay_hints_show_struct_literal_field_type": { 51 | "description": "Enable inlay hints for fields in struct and union literals", 52 | "type": "boolean", 53 | "default": true 54 | }, 55 | "inlay_hints_show_parameter_name": { 56 | "description": "Enable inlay hints for parameter names", 57 | "type": "boolean", 58 | "default": true 59 | }, 60 | "inlay_hints_show_builtin": { 61 | "description": "Enable inlay hints for builtin functions", 62 | "type": "boolean", 63 | "default": true 64 | }, 65 | "inlay_hints_param_hint_kind": { 66 | "description": "Show the parameter's name or type as the hint", 67 | "type": "string", 68 | "enum": [ 69 | "name", 70 | "type" 71 | ], 72 | "default": "name" 73 | }, 74 | "inlay_hints_exclude_single_argument": { 75 | "description": "Don't show inlay hints for single argument calls", 76 | "type": "boolean", 77 | "default": true 78 | }, 79 | "inlay_hints_hide_redundant_param_names": { 80 | "description": "Hides inlay hints when parameter name matches the identifier (e.g. foo: foo)", 81 | "type": "boolean", 82 | "default": false 83 | }, 84 | "inlay_hints_hide_redundant_param_names_last_token": { 85 | "description": "Hides inlay hints when parameter name matches the last token of a parameter node (e.g. foo: bar.foo, foo: &foo)", 86 | "type": "boolean", 87 | "default": false 88 | }, 89 | "warn_style": { 90 | "description": "Enables warnings for style guideline mismatches", 91 | "type": "boolean", 92 | "default": false 93 | }, 94 | "highlight_global_var_declarations": { 95 | "description": "Whether to highlight global var declarations", 96 | "type": "boolean", 97 | "default": false 98 | }, 99 | "skip_std_references": { 100 | "description": "When true, skips searching for references in std. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is", 101 | "type": "boolean", 102 | "default": false 103 | }, 104 | "prefer_ast_check_as_child_process": { 105 | "description": "Favor using `zig ast-check` instead of ZLS's fork", 106 | "type": "boolean", 107 | "default": true 108 | }, 109 | "builtin_path": { 110 | "description": "Path to 'builtin;' useful for debugging, automatically set if let null", 111 | "type": "string", 112 | "default": null 113 | }, 114 | "zig_lib_path": { 115 | "description": "Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports", 116 | "type": "string", 117 | "default": null 118 | }, 119 | "zig_exe_path": { 120 | "description": "Zig executable path, e.g. `/path/to/zig/zig`, used to run the custom build runner. If `null`, zig is looked up in `PATH`. Will be used to infer the zig standard library path if none is provided", 121 | "type": "string", 122 | "default": null 123 | }, 124 | "build_runner_path": { 125 | "description": "Path to the `build_runner.zig` file provided by ZLS. null is equivalent to `${executable_directory}/build_runner.zig`", 126 | "type": "string", 127 | "default": null 128 | }, 129 | "global_cache_path": { 130 | "description": "Path to a directory that will be used as zig's cache. null is equivalent to `${KnownFolders.Cache}/zls`", 131 | "type": "string", 132 | "default": null 133 | }, 134 | "completion_label_details": { 135 | "description": "When false, the function signature of completion results is hidden. Improves readability in some editors", 136 | "type": "boolean", 137 | "default": true 138 | }, 139 | "ws_build_zig": { 140 | "description": "Internal; Override/specify the build.zig file to use.", 141 | "type": "string", 142 | "default": null 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/BuildAssociatedConfig.zig: -------------------------------------------------------------------------------- 1 | //! Configuration options related to a specific `BuildFile`. 2 | const std = @import("std"); 3 | 4 | const Self = @This(); 5 | 6 | pub const BuildOption = struct { 7 | name: []const u8, 8 | value: ?[]const u8 = null, 9 | 10 | /// Duplicates the `BuildOption`, copying internal strings. Caller owns returned option with contents 11 | /// allocated using `allocator`. 12 | pub fn dupe(self: BuildOption, allocator: std.mem.Allocator) !BuildOption { 13 | const copy_name = try allocator.dupe(u8, self.name); 14 | errdefer allocator.free(copy_name); 15 | const copy_value = if (self.value) |val| 16 | try allocator.dupe(u8, val) 17 | else 18 | null; 19 | return BuildOption{ 20 | .name = copy_name, 21 | .value = copy_value, 22 | }; 23 | } 24 | 25 | /// Formats the `BuildOption` as a command line parameter compatible with `zig build`. This will either be 26 | /// `-Dname=value` or `-Dname`. Caller owns returned slice allocated using `allocator`. 27 | pub fn formatParam(self: BuildOption, allocator: std.mem.Allocator) ![]const u8 { 28 | if (self.value) |val| { 29 | return try std.fmt.allocPrint(allocator, "-D{s}={s}", .{ self.name, val }); 30 | } else { 31 | return try std.fmt.allocPrint(allocator, "-D{s}", .{self.name}); 32 | } 33 | } 34 | }; 35 | 36 | /// If provided this path is used when resolving `@import("builtin")` 37 | /// It is relative to the directory containing the `build.zig` 38 | /// 39 | /// This file should contain the output of: 40 | /// `zig build-exe/build-lib/build-obj --show-builtin ` 41 | relative_builtin_path: ?[]const u8 = null, 42 | 43 | /// If provided, this list of options will be passed to `build.zig`. 44 | build_options: ?[]BuildOption = null, 45 | 46 | /// See `zig build --build-runner /path/to/zigscient/src/build_runner/0.12.0.zig --roots` 47 | root_id: ?u32 = null, 48 | -------------------------------------------------------------------------------- /src/Config.zig: -------------------------------------------------------------------------------- 1 | //! DO NOT EDIT 2 | //! Configuration options for ZLS. 3 | //! If you want to add a config option edit 4 | //! src/tools/config.json 5 | //! GENERATED BY src/tools/config_gen.zig 6 | 7 | /// Enables snippet completions when the client also supports them 8 | enable_snippets: bool = true, 9 | 10 | /// Whether to enable function argument placeholder completions 11 | enable_argument_placeholders: bool = true, 12 | 13 | /// Whether to enable build-on-save diagnostics. Will be automatically enabled if the `build.zig` has declared a 'check' step. 14 | enable_build_on_save: ?bool = null, 15 | 16 | /// Specify which arguments should be passed to Zig when running build-on-save. 17 | /// 18 | /// If the `build.zig` has declared a 'check' step, it will be preferred over the default 'install' step. 19 | build_on_save_args: []const []const u8 = &.{}, 20 | 21 | /// Whether to automatically fix errors on save. Currently supports adding and removing discards. 22 | enable_autofix: bool = false, 23 | 24 | /// Set level of semantic tokens. `partial` only includes information that requires semantic analysis. 25 | semantic_tokens: enum { 26 | none, 27 | partial, 28 | full, 29 | } = .full, 30 | 31 | /// Enable inlay hints for variable types 32 | inlay_hints_show_variable_type_hints: bool = true, 33 | 34 | /// Enable inlay hints for fields in struct and union literals 35 | inlay_hints_show_struct_literal_field_type: bool = true, 36 | 37 | /// Enable inlay hints for parameter names 38 | inlay_hints_show_parameter_name: bool = true, 39 | 40 | /// Enable inlay hints for builtin functions 41 | inlay_hints_show_builtin: bool = true, 42 | 43 | /// Show the parameter's name or type as the hint 44 | inlay_hints_param_hint_kind: enum { 45 | name, 46 | type, 47 | } = .name, 48 | 49 | /// Don't show inlay hints for single argument calls 50 | inlay_hints_exclude_single_argument: bool = true, 51 | 52 | /// Hides inlay hints when parameter name matches the identifier (e.g. foo: foo) 53 | inlay_hints_hide_redundant_param_names: bool = false, 54 | 55 | /// Hides inlay hints when parameter name matches the last token of a parameter node (e.g. foo: bar.foo, foo: &foo) 56 | inlay_hints_hide_redundant_param_names_last_token: bool = false, 57 | 58 | /// Enables warnings for style guideline mismatches 59 | warn_style: bool = false, 60 | 61 | /// Whether to highlight global var declarations 62 | highlight_global_var_declarations: bool = false, 63 | 64 | /// When true, skips searching for references in std. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is 65 | skip_std_references: bool = false, 66 | 67 | /// Favor using `zig ast-check` instead of ZLS's fork 68 | prefer_ast_check_as_child_process: bool = true, 69 | 70 | /// Path to 'builtin;' useful for debugging, automatically set if let null 71 | builtin_path: ?[]const u8 = null, 72 | 73 | /// Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports 74 | zig_lib_path: ?[]const u8 = null, 75 | 76 | /// Zig executable path, e.g. `/path/to/zig/zig`, used to run the custom build runner. If `null`, zig is looked up in `PATH`. Will be used to infer the zig standard library path if none is provided 77 | zig_exe_path: ?[]const u8 = null, 78 | 79 | /// Path to the `build_runner.zig` file provided by ZLS. null is equivalent to `${executable_directory}/build_runner.zig` 80 | build_runner_path: ?[]const u8 = null, 81 | 82 | /// Path to a directory that will be used as zig's cache. null is equivalent to `${KnownFolders.Cache}/zls` 83 | global_cache_path: ?[]const u8 = null, 84 | 85 | /// When false, the function signature of completion results is hidden. Improves readability in some editors 86 | completion_label_details: bool = true, 87 | 88 | /// Internal; Override/specify the build.zig file to use. 89 | ws_build_zig: ?[]const u8 = null, 90 | 91 | // DO NOT EDIT 92 | -------------------------------------------------------------------------------- /src/analyser/README.md: -------------------------------------------------------------------------------- 1 | This directory contains mostly future work on an improved analysis backend. Only a fractions of this code is being used. For the current analysis backend implementation, See `src/analysis.zig`. 2 | -------------------------------------------------------------------------------- /src/analyser/analyser.zig: -------------------------------------------------------------------------------- 1 | pub const completions = @import("completions.zig"); 2 | pub const InternPool = @import("InternPool.zig"); 3 | pub const StringPool = @import("string_pool.zig").StringPool; 4 | pub const degibberish = @import("degibberish.zig"); 5 | 6 | comptime { 7 | const std = @import("std"); 8 | std.testing.refAllDeclsRecursive(@This()); 9 | } 10 | -------------------------------------------------------------------------------- /src/analyser/degibberish.zig: -------------------------------------------------------------------------------- 1 | //! Converts a Zig type into a english description of the type. 2 | //! 3 | //! Example: 4 | //! `[*:0]const u8` -> 0 terminated many-item pointer to to const u8 5 | 6 | const std = @import("std"); 7 | const InternPool = @import("InternPool.zig"); 8 | 9 | const FormatDegibberishData = struct { 10 | ip: *InternPool, 11 | ty: InternPool.Index, 12 | }; 13 | 14 | pub fn fmtDegibberish(ip: *InternPool, ty: InternPool.Index) std.fmt.Formatter(formatDegibberish) { 15 | const data = FormatDegibberishData{ .ip = ip, .ty = ty }; 16 | return .{ .data = data }; 17 | } 18 | 19 | fn formatDegibberish(data: FormatDegibberishData, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { 20 | if (fmt.len != 0) std.fmt.invalidFmtError(fmt, data.ty); 21 | _ = options; 22 | 23 | const ip = data.ip; 24 | var ty = data.ty; 25 | 26 | while (ty != .none) { 27 | switch (ip.indexToKey(ty)) { 28 | .simple_type, 29 | .int_type, 30 | => try writer.print("{}", .{ty.fmt(ip)}), 31 | 32 | .pointer_type => |pointer_info| { 33 | // ignored attributes: 34 | // - address_space 35 | // - is_allowzero 36 | // - is_volatile 37 | // - packed_offset 38 | 39 | if (pointer_info.sentinel != .none) { 40 | try writer.print("{} terminated ", .{pointer_info.sentinel.fmt(ip)}); 41 | } 42 | 43 | // single pointer 44 | const size_prefix = switch (pointer_info.flags.size) { 45 | .one => "single-item pointer", 46 | .many => "many-item pointer", 47 | .slice => "slice (pointer + length)", 48 | .c => "C pointer", 49 | }; 50 | 51 | try writer.writeAll(size_prefix); 52 | 53 | if (pointer_info.flags.alignment != 0) { 54 | try writer.print(" with alignment {d}", .{pointer_info.flags.alignment}); 55 | } 56 | 57 | try writer.writeAll(" to "); 58 | 59 | if (pointer_info.flags.is_const) { 60 | try writer.writeAll("const "); 61 | } 62 | 63 | ty = pointer_info.elem_type; 64 | continue; 65 | }, 66 | .array_type => |array_info| { 67 | if (array_info.sentinel != .none) { 68 | try writer.print("{} terminated ", .{array_info.sentinel.fmt(ip)}); 69 | } 70 | try writer.print("array {d} of ", .{array_info.len}); 71 | ty = array_info.child; 72 | continue; 73 | }, 74 | .struct_type => try writer.print("struct {}", .{ty.fmt(ip)}), 75 | .optional_type => |optional_info| { 76 | try writer.writeAll("optional of "); 77 | ty = optional_info.payload_type; 78 | continue; 79 | }, 80 | .error_union_type => |error_union_info| { 81 | try writer.writeAll("error union with "); 82 | try writer.print("{}", .{fmtDegibberish(ip, error_union_info.error_set_type)}); 83 | try writer.writeAll(" and payload "); 84 | ty = error_union_info.payload_type; 85 | continue; 86 | }, 87 | .error_set_type => |error_set_info| { 88 | try writer.writeAll("error set of ("); 89 | for (0..error_set_info.names.len) |i| { 90 | if (i != 0) try writer.writeByte(','); 91 | const name = error_set_info.names.at(@intCast(i), ip); 92 | try writer.print("{}", .{InternPool.fmtId(ip, name)}); 93 | } 94 | try writer.writeAll(")"); 95 | }, 96 | .enum_type => try writer.print("enum {}", .{ty.fmt(ip)}), 97 | .function_type => |function_info| { 98 | try writer.writeAll("function ("); 99 | for (0..function_info.args.len) |i| { 100 | if (i != 0) try writer.writeAll(", "); 101 | const arg_ty = function_info.args.at(@intCast(i), ip); 102 | try writer.print("{}", .{fmtDegibberish(ip, arg_ty)}); 103 | } 104 | try writer.writeAll(") returning "); 105 | ty = function_info.return_type; 106 | continue; 107 | }, 108 | .union_type => try writer.print("union {}", .{ty.fmt(ip)}), 109 | .tuple_type => |tuple_info| { 110 | std.debug.assert(tuple_info.types.len == tuple_info.values.len); 111 | try writer.writeAll("tuple of ("); 112 | for (0..tuple_info.types.len) |i| { 113 | if (i != 0) try writer.writeAll(", "); 114 | const field_ty = tuple_info.types.at(@intCast(i), ip); 115 | try writer.print("{}", .{fmtDegibberish(ip, field_ty)}); 116 | } 117 | try writer.writeAll(")"); 118 | }, 119 | .vector_type => |vector_info| { 120 | try writer.print("vector {d} of ", .{vector_info.len}); 121 | ty = vector_info.child; 122 | continue; 123 | }, 124 | .anyframe_type => |anyframe_info| { 125 | try writer.writeAll("function frame returning "); 126 | ty = anyframe_info.child; 127 | continue; 128 | }, 129 | 130 | .simple_value, 131 | .int_u64_value, 132 | .int_i64_value, 133 | .int_big_value, 134 | .float_16_value, 135 | .float_32_value, 136 | .float_64_value, 137 | .float_80_value, 138 | .float_128_value, 139 | .float_comptime_value, 140 | .optional_value, 141 | .slice, 142 | .aggregate, 143 | .union_value, 144 | .error_value, 145 | .null_value, 146 | .undefined_value, 147 | .unknown_value, 148 | => unreachable, 149 | } 150 | break; 151 | } 152 | } 153 | 154 | test "degibberish - simple types" { 155 | const gpa = std.testing.allocator; 156 | var ip = try InternPool.init(gpa); 157 | defer ip.deinit(gpa); 158 | 159 | try std.testing.expectFmt("u32", "{}", .{fmtDegibberish(&ip, .u32_type)}); 160 | try std.testing.expectFmt("comptime_float", "{}", .{fmtDegibberish(&ip, .comptime_float_type)}); 161 | } 162 | 163 | test "degibberish - pointer types" { 164 | const gpa = std.testing.allocator; 165 | var ip = try InternPool.init(gpa); 166 | defer ip.deinit(gpa); 167 | 168 | try std.testing.expectFmt("many-item pointer to u8", "{}", .{fmtDegibberish(&ip, .manyptr_u8_type)}); 169 | try std.testing.expectFmt("many-item pointer to const u8", "{}", .{fmtDegibberish(&ip, .manyptr_const_u8_type)}); 170 | try std.testing.expectFmt("0 terminated many-item pointer to const u8", "{}", .{fmtDegibberish(&ip, .manyptr_const_u8_sentinel_0_type)}); 171 | try std.testing.expectFmt("single-item pointer to const comptime_int", "{}", .{fmtDegibberish(&ip, .single_const_pointer_to_comptime_int_type)}); 172 | try std.testing.expectFmt("slice (pointer + length) to const u8", "{}", .{fmtDegibberish(&ip, .slice_const_u8_type)}); 173 | try std.testing.expectFmt("0 terminated slice (pointer + length) to const u8", "{}", .{fmtDegibberish(&ip, .slice_const_u8_sentinel_0_type)}); 174 | } 175 | 176 | test "degibberish - array types" { 177 | const gpa = std.testing.allocator; 178 | var ip = try InternPool.init(gpa); 179 | defer ip.deinit(gpa); 180 | 181 | const @"[3:0]u8" = try ip.get(gpa, .{ .array_type = .{ .len = 3, .child = .u8_type, .sentinel = .zero_u8 } }); 182 | const @"[0]u32" = try ip.get(gpa, .{ .array_type = .{ .len = 0, .child = .u32_type } }); 183 | 184 | try std.testing.expectFmt("0 terminated array 3 of u8", "{}", .{fmtDegibberish(&ip, @"[3:0]u8")}); 185 | try std.testing.expectFmt("array 0 of u32", "{}", .{fmtDegibberish(&ip, @"[0]u32")}); 186 | } 187 | 188 | test "degibberish - optional types" { 189 | const gpa = std.testing.allocator; 190 | var ip = try InternPool.init(gpa); 191 | defer ip.deinit(gpa); 192 | 193 | const @"?u32" = try ip.get(gpa, .{ .optional_type = .{ .payload_type = .u32_type } }); 194 | 195 | try std.testing.expectFmt("optional of u32", "{}", .{fmtDegibberish(&ip, @"?u32")}); 196 | } 197 | 198 | test "degibberish - error union types" { 199 | const gpa = std.testing.allocator; 200 | var ip = try InternPool.init(gpa); 201 | defer ip.deinit(gpa); 202 | 203 | const foo_string = try ip.string_pool.getOrPutString(gpa, "foo"); 204 | const bar_string = try ip.string_pool.getOrPutString(gpa, "bar"); 205 | const baz_string = try ip.string_pool.getOrPutString(gpa, "baz"); 206 | 207 | const @"error{foo,bar,baz}" = try ip.get(gpa, .{ .error_set_type = .{ 208 | .names = try ip.getStringSlice(gpa, &.{ foo_string, bar_string, baz_string }), 209 | .owner_decl = .none, 210 | } }); 211 | 212 | const @"error{foo,bar,baz}!u32" = try ip.get(gpa, .{ .error_union_type = .{ 213 | .error_set_type = @"error{foo,bar,baz}", 214 | .payload_type = .u32_type, 215 | } }); 216 | 217 | try std.testing.expectFmt("error union with error set of (foo,bar,baz) and payload u32", "{}", .{fmtDegibberish(&ip, @"error{foo,bar,baz}!u32")}); 218 | } 219 | 220 | test "degibberish - error set types" { 221 | const gpa = std.testing.allocator; 222 | var ip = try InternPool.init(gpa); 223 | defer ip.deinit(gpa); 224 | 225 | const foo_string = try ip.string_pool.getOrPutString(gpa, "foo"); 226 | const bar_string = try ip.string_pool.getOrPutString(gpa, "bar"); 227 | const baz_string = try ip.string_pool.getOrPutString(gpa, "baz"); 228 | 229 | const @"error{foo,bar,baz}" = try ip.get(gpa, .{ .error_set_type = .{ 230 | .names = try ip.getStringSlice(gpa, &.{ foo_string, bar_string, baz_string }), 231 | .owner_decl = .none, 232 | } }); 233 | 234 | try std.testing.expectFmt("error set of (foo,bar,baz)", "{}", .{fmtDegibberish(&ip, @"error{foo,bar,baz}")}); 235 | } 236 | 237 | test "degibberish - function types" { 238 | const gpa = std.testing.allocator; 239 | var ip = try InternPool.init(gpa); 240 | defer ip.deinit(gpa); 241 | 242 | const @"fn(u32, void) type" = try ip.get(gpa, .{ .function_type = .{ 243 | .args = try ip.getIndexSlice(gpa, &.{ .u32_type, .void_type }), 244 | .return_type = .type_type, 245 | } }); 246 | 247 | try std.testing.expectFmt("function () returning noreturn", "{}", .{fmtDegibberish(&ip, .fn_noreturn_no_args_type)}); 248 | try std.testing.expectFmt("function () returning void", "{}", .{fmtDegibberish(&ip, .fn_void_no_args_type)}); 249 | try std.testing.expectFmt("function (u32, void) returning type", "{}", .{fmtDegibberish(&ip, @"fn(u32, void) type")}); 250 | } 251 | 252 | test "degibberish - tuple types" { 253 | const gpa = std.testing.allocator; 254 | var ip = try InternPool.init(gpa); 255 | defer ip.deinit(gpa); 256 | 257 | const @"struct{u32, comptime_float, c_int}" = try ip.get(gpa, .{ .tuple_type = .{ 258 | .types = try ip.getIndexSlice(gpa, &.{ .u32_type, .comptime_float_type, .c_int_type }), 259 | .values = try ip.getIndexSlice(gpa, &.{ .none, .none, .none }), 260 | } }); 261 | 262 | try std.testing.expectFmt("tuple of (u32, comptime_float, c_int)", "{}", .{fmtDegibberish(&ip, @"struct{u32, comptime_float, c_int}")}); 263 | } 264 | 265 | test "degibberish - vector types" { 266 | const gpa = std.testing.allocator; 267 | var ip = try InternPool.init(gpa); 268 | defer ip.deinit(gpa); 269 | 270 | const @"@Vector(3, u8)" = try ip.get(gpa, .{ .vector_type = .{ .len = 3, .child = .u8_type } }); 271 | const @"@Vector(0, u32)" = try ip.get(gpa, .{ .vector_type = .{ .len = 0, .child = .u32_type } }); 272 | 273 | try std.testing.expectFmt("vector 3 of u8", "{}", .{fmtDegibberish(&ip, @"@Vector(3, u8)")}); 274 | try std.testing.expectFmt("vector 0 of u32", "{}", .{fmtDegibberish(&ip, @"@Vector(0, u32)")}); 275 | } 276 | 277 | test "degibberish - anyframe types" { 278 | const gpa = std.testing.allocator; 279 | var ip = try InternPool.init(gpa); 280 | defer ip.deinit(gpa); 281 | 282 | const @"anyframe->u32" = try ip.get(gpa, .{ .anyframe_type = .{ .child = .u32_type } }); 283 | try std.testing.expectFmt("function frame returning u32", "{}", .{fmtDegibberish(&ip, @"anyframe->u32")}); 284 | } 285 | -------------------------------------------------------------------------------- /src/analyser/error_msg.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("lsp").types; 3 | const offsets = @import("../offsets.zig"); 4 | 5 | const InternPool = @import("InternPool.zig"); 6 | const Index = InternPool.Index; 7 | const Key = InternPool.Key; 8 | 9 | pub const ErrorMsg = union(enum) { 10 | /// zig: expected type '{}', found '{}' 11 | expected_type: struct { 12 | expected: Index, 13 | actual: Index, 14 | }, 15 | /// zig: expected optional type, found '{}' 16 | /// zig: expected error set type, found '{}' 17 | /// zig: expected pointer, found '{}' 18 | expected_tag_type: struct { 19 | expected_tag: std.builtin.TypeId, 20 | actual: Index, 21 | }, 22 | /// zig: comparison of '{}' with null 23 | compare_eq_with_null: struct { 24 | non_null_type: Index, 25 | }, 26 | /// zig: tried to unwrap optional of type `{}` which was '{}' 27 | invalid_optional_unwrap: struct { 28 | operand: Index, 29 | }, 30 | /// zig: type '{}' cannot represent integer value '{}' 31 | integer_out_of_range: struct { 32 | dest_ty: Index, 33 | actual: Index, 34 | }, 35 | /// zig: expected {d} array elements; found 0 36 | wrong_array_elem_count: struct { 37 | expected: u32, 38 | actual: u32, 39 | }, 40 | /// zig: type '{}' does not support indexing 41 | /// zig: operand must be an array, slice, tuple, or vector 42 | expected_indexable_type: struct { 43 | actual: Index, 44 | }, 45 | /// zig: duplicate struct field: '{}' 46 | duplicate_struct_field: struct { 47 | name: InternPool.StringPool.String, 48 | }, 49 | /// zig: `{}` has no member '{s}' 50 | /// zig: `{}` does not support field access 51 | unknown_field: struct { 52 | accessed: Index, 53 | field_name: []const u8, 54 | }, 55 | 56 | const FormatContext = struct { 57 | error_msg: ErrorMsg, 58 | ip: *InternPool, 59 | }; 60 | 61 | pub fn fmt(self: ErrorMsg, ip: *InternPool) std.fmt.Formatter(format) { 62 | return .{ .data = .{ .error_msg = self, .ip = ip } }; 63 | } 64 | 65 | pub fn format( 66 | ctx: FormatContext, 67 | comptime fmt_str: []const u8, 68 | options: std.fmt.FormatOptions, 69 | writer: anytype, 70 | ) @TypeOf(writer).Error!void { 71 | _ = options; 72 | const ip = ctx.ip; 73 | if (fmt_str.len != 0) std.fmt.invalidFmtError(fmt_str, ctx.error_msg); 74 | return switch (ctx.error_msg) { 75 | .expected_type => |info| std.fmt.format( 76 | writer, 77 | "expected type '{}', found '{}'", 78 | .{ info.expected.fmt(ip), ip.typeOf(info.actual).fmt(ip) }, 79 | ), 80 | .expected_tag_type => |info| blk: { 81 | const expected_tag_str = switch (info.expected_tag) { 82 | .type => "type", 83 | .void => "void", 84 | .bool => "bool", 85 | .noreturn => "noreturn", 86 | .int => "integer", 87 | .float => "float", 88 | .pointer => "pointer", 89 | .array => "array", 90 | .@"struct" => "struct", 91 | .comptime_float => "comptime_float", 92 | .comptime_int => "comptime_int", 93 | .undefined => "undefined", 94 | .null => "null", 95 | .optional => "optional", 96 | .error_union => "error union", 97 | .error_set => "error set", 98 | .@"enum" => "enum", 99 | .@"union" => "union", 100 | .@"fn" => "function", 101 | .@"opaque" => "opaque", 102 | .frame => "frame", 103 | .@"anyframe" => "anyframe", 104 | .vector => "vector", 105 | .enum_literal => "enum literal", 106 | }; 107 | break :blk std.fmt.format( 108 | writer, 109 | "expected {s} type, found '{}'", 110 | .{ expected_tag_str, info.actual.fmt(ip) }, 111 | ); 112 | }, 113 | .compare_eq_with_null => |info| std.fmt.format( 114 | writer, 115 | "comparison of '{}' with null", 116 | .{info.non_null_type.fmt(ip)}, 117 | ), 118 | .invalid_optional_unwrap => |info| blk: { 119 | const operand_ty = ip.typeOf(info.operand); 120 | const payload_ty = ip.indexToKey(operand_ty).optional_type.payload_type; 121 | break :blk std.fmt.format( 122 | writer, 123 | "tried to unwrap optional of type `{}` which was {}", 124 | .{ payload_ty.fmt(ip), info.operand.fmt(ip) }, 125 | ); 126 | }, 127 | .integer_out_of_range => |info| std.fmt.format( 128 | writer, 129 | "type '{}' cannot represent integer value '{}'", 130 | .{ info.dest_ty.fmt(ip), info.actual.fmt(ip) }, 131 | ), 132 | .wrong_array_elem_count => |info| std.fmt.format( 133 | writer, 134 | "expected {d} array elements; found {d}", 135 | .{ info.expected, info.actual }, 136 | ), 137 | .expected_indexable_type => |info| std.fmt.format( 138 | writer, 139 | "type '{}' does not support indexing", 140 | .{info.actual.fmt(ip)}, 141 | ), 142 | .duplicate_struct_field => |info| std.fmt.format( 143 | writer, 144 | "duplicate struct field: '{}'", 145 | .{info.name.fmt(&ip.string_pool)}, 146 | ), 147 | .unknown_field => |info| blk: { 148 | const accessed_ty = ip.typeOf(info.accessed); 149 | break :blk if (ip.canHaveFields(accessed_ty)) 150 | std.fmt.format( 151 | writer, 152 | "'{}' has no member '{s}'", 153 | .{ accessed_ty.fmt(ip), info.field_name }, 154 | ) 155 | else 156 | std.fmt.format( 157 | writer, 158 | "'{}' does not support field access", 159 | .{accessed_ty.fmt(ip)}, 160 | ); 161 | }, 162 | }; 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /src/analyser/string_pool.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const Allocator = std.mem.Allocator; 4 | const assert = std.debug.assert; 5 | 6 | pub const Config = struct { 7 | /// Whether the StringPool may be used simultaneously from multiple threads. 8 | thread_safe: bool = !builtin.single_threaded, 9 | 10 | /// What type of mutex you'd like to use, for thread safety. 11 | /// when specified, the mutex type must have the same shape as `std.Thread.Mutex` and 12 | /// `DummyMutex`, and have no required fields. Specifying this field causes 13 | /// the `thread_safe` field to be ignored. 14 | /// 15 | /// when null (default): 16 | /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled. 17 | /// * the mutex type defaults to `DummyMutex` otherwise. 18 | MutexType: ?type = null, 19 | }; 20 | 21 | /// The StringPool is a Data structure that stores only one copy of distinct and immutable strings i.e. `[]const u8`. 22 | /// 23 | /// The `getOrPutString` function will intern a given string and return a unique identifier 24 | /// that can then be used to retrieve the original string with the `stringToSlice*` functions. 25 | pub fn StringPool(comptime config: Config) type { 26 | return struct { 27 | const Pool = @This(); 28 | 29 | /// A unique number that identifier a interned string. 30 | /// 31 | /// Two interned string can be checked for equality simply by checking 32 | /// if this identifier are equal if they both come from the same StringPool. 33 | pub const String = enum(u32) { 34 | _, 35 | 36 | pub fn toOptional(self: String) OptionalString { 37 | return @enumFromInt(@intFromEnum(self)); 38 | } 39 | 40 | pub fn fmt(self: String, pool: *Pool) std.fmt.Formatter(print) { 41 | return .{ .data = .{ .string = self, .pool = pool } }; 42 | } 43 | }; 44 | 45 | pub const OptionalString = enum(u32) { 46 | none = std.math.maxInt(u32), 47 | _, 48 | 49 | pub fn unwrap(self: OptionalString) ?String { 50 | if (self == .none) return null; 51 | return @enumFromInt(@intFromEnum(self)); 52 | } 53 | }; 54 | 55 | /// Asserts that `str` contains no null bytes. 56 | pub fn getString(pool: *Pool, str: []const u8) ?String { 57 | assert(std.mem.indexOfScalar(u8, str, 0) == null); 58 | 59 | // precompute the hash before acquiring the lock 60 | const precomputed_key_hash = std.hash_map.hashString(str); 61 | 62 | pool.mutex.lock(); 63 | defer pool.mutex.unlock(); 64 | 65 | const adapter = PrecomputedStringIndexAdapter{ 66 | .bytes = &pool.bytes, 67 | .adapted_key = str, 68 | .precomputed_key_hash = precomputed_key_hash, 69 | }; 70 | 71 | const index = pool.map.getKeyAdapted(str, adapter) orelse return null; 72 | return @enumFromInt(index); 73 | } 74 | 75 | /// Asserts that `str` contains no null bytes. 76 | /// Returns `error.OutOfMemory` if adding this new string would increase the amount of allocated bytes above std.math.maxInt(u32) 77 | pub fn getOrPutString(pool: *Pool, allocator: Allocator, str: []const u8) error{OutOfMemory}!String { 78 | assert(std.mem.indexOfScalar(u8, str, 0) == null); 79 | 80 | const start_index = std.math.cast(u32, pool.bytes.items.len) orelse return error.OutOfMemory; 81 | 82 | // precompute the hash before acquiring the lock 83 | const precomputed_key_hash = std.hash_map.hashString(str); 84 | 85 | pool.mutex.lock(); 86 | defer pool.mutex.unlock(); 87 | 88 | const adapter = PrecomputedStringIndexAdapter{ 89 | .bytes = &pool.bytes, 90 | .adapted_key = str, 91 | .precomputed_key_hash = precomputed_key_hash, 92 | }; 93 | 94 | pool.bytes.ensureUnusedCapacity(allocator, str.len + 1) catch { 95 | // If allocation fails, try to do the lookup anyway. 96 | const index = pool.map.getKeyAdapted(str, adapter) orelse return error.OutOfMemory; 97 | return @enumFromInt(index); 98 | }; 99 | 100 | const gop = try pool.map.getOrPutContextAdapted( 101 | allocator, 102 | str, 103 | adapter, 104 | std.hash_map.StringIndexContext{ .bytes = &pool.bytes }, 105 | ); 106 | 107 | if (!gop.found_existing) { 108 | pool.bytes.appendSliceAssumeCapacity(str); 109 | pool.bytes.appendAssumeCapacity(0); 110 | gop.key_ptr.* = start_index; 111 | } 112 | return @enumFromInt(gop.key_ptr.*); 113 | } 114 | 115 | /// Caller owns the memory. 116 | pub fn stringToSliceAlloc(pool: *Pool, allocator: Allocator, index: String) Allocator.Error![]const u8 { 117 | pool.mutex.lock(); 118 | defer pool.mutex.unlock(); 119 | const string_bytes: [*:0]u8 = @ptrCast(pool.bytes.items.ptr); 120 | const start = @intFromEnum(index); 121 | return try allocator.dupe(u8, std.mem.sliceTo(string_bytes + start, 0)); 122 | } 123 | 124 | /// Caller owns the memory. 125 | pub fn stringToSliceAllocZ(pool: *Pool, allocator: Allocator, index: String) Allocator.Error![:0]const u8 { 126 | pool.mutex.lock(); 127 | defer pool.mutex.unlock(); 128 | const string_bytes: [*:0]u8 = @ptrCast(pool.bytes.items.ptr); 129 | const start = @intFromEnum(index); 130 | return try allocator.dupeZ(u8, std.mem.sliceTo(string_bytes + start, 0)); 131 | } 132 | 133 | /// storage a slice that points into the internal storage of the `StringPool`. 134 | /// always call `release` method to unlock the `StringPool`. 135 | /// 136 | /// see `stringToSliceLock` 137 | pub const LockedString = struct { 138 | slice: [:0]const u8, 139 | 140 | pub fn release(locked_string: LockedString, pool: *Pool) void { 141 | _ = locked_string; 142 | pool.mutex.unlock(); 143 | } 144 | }; 145 | 146 | /// returns the underlying slice from an interned string 147 | /// equal strings are guaranteed to share the same storage 148 | /// 149 | /// Will lock the `StringPool` until the `release` method is called on the returned locked string. 150 | pub fn stringToSliceLock(pool: *Pool, index: String) LockedString { 151 | pool.mutex.lock(); 152 | return .{ .slice = pool.stringToSliceUnsafe(index) }; 153 | } 154 | 155 | /// returns the underlying slice from an interned string 156 | /// equal strings are guaranteed to share the same storage 157 | /// 158 | /// only callable when thread safety is disabled. 159 | pub const stringToSlice = if (config.thread_safe) @"usingnamespace" else stringToSliceUnsafe; 160 | 161 | const @"usingnamespace" = {}; 162 | 163 | /// returns the underlying slice from an interned string 164 | /// equal strings are guaranteed to share the same storage 165 | pub fn stringToSliceUnsafe(pool: *Pool, index: String) [:0]const u8 { 166 | assert(@intFromEnum(index) < pool.bytes.items.len); 167 | const string_bytes: [*:0]u8 = @ptrCast(pool.bytes.items.ptr); 168 | const start = @intFromEnum(index); 169 | return std.mem.sliceTo(string_bytes + start, 0); 170 | } 171 | 172 | mutex: @TypeOf(mutex_init) = mutex_init, 173 | bytes: std.ArrayListUnmanaged(u8) = .{}, 174 | map: std.HashMapUnmanaged(u32, void, std.hash_map.StringIndexContext, std.hash_map.default_max_load_percentage) = .{}, 175 | 176 | pub fn deinit(pool: *Pool, allocator: Allocator) void { 177 | pool.bytes.deinit(allocator); 178 | pool.map.deinit(allocator); 179 | if (builtin.mode == .Debug and !builtin.single_threaded and config.thread_safe) { 180 | // detect deadlock when calling deinit while holding the lock 181 | pool.mutex.lock(); 182 | pool.mutex.unlock(); 183 | } 184 | pool.* = undefined; 185 | } 186 | 187 | const mutex_init = if (config.MutexType) |T| 188 | T{} 189 | else if (config.thread_safe) 190 | std.Thread.Mutex{} 191 | else 192 | DummyMutex{}; 193 | 194 | const DummyMutex = struct { 195 | pub fn lock(_: *@This()) void {} 196 | pub fn unlock(_: *@This()) void {} 197 | }; 198 | 199 | const FormatContext = struct { 200 | string: String, 201 | pool: *Pool, 202 | }; 203 | 204 | fn print(ctx: FormatContext, comptime fmt_str: []const u8, _: std.fmt.FormatOptions, writer: anytype) @TypeOf(writer).Error!void { 205 | if (fmt_str.len != 0) std.fmt.invalidFmtError(fmt_str, ctx.string); 206 | const locked_string = ctx.pool.stringToSliceLock(ctx.string); 207 | defer locked_string.release(ctx.pool); 208 | try writer.writeAll(locked_string.slice); 209 | } 210 | }; 211 | } 212 | 213 | /// same as `std.hash_map.StringIndexAdapter` but the hash of the adapted key is precomputed 214 | const PrecomputedStringIndexAdapter = struct { 215 | bytes: *const std.ArrayListUnmanaged(u8), 216 | adapted_key: []const u8, 217 | precomputed_key_hash: u64, 218 | 219 | pub fn eql(self: @This(), a_slice: []const u8, b: u32) bool { 220 | const b_slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(self.bytes.items.ptr)) + b, 0); 221 | return std.mem.eql(u8, a_slice, b_slice); 222 | } 223 | 224 | pub fn hash(self: @This(), adapted_key: []const u8) u64 { 225 | assert(std.mem.eql(u8, self.adapted_key, adapted_key)); 226 | return self.precomputed_key_hash; 227 | } 228 | }; 229 | 230 | test StringPool { 231 | const gpa = std.testing.allocator; 232 | var pool = StringPool(.{}){}; 233 | defer pool.deinit(gpa); 234 | 235 | const str = "All Your Codebase Are Belong To Us"; 236 | const index = try pool.getOrPutString(gpa, str); 237 | 238 | { 239 | const locked_string = pool.stringToSliceLock(index); 240 | defer locked_string.release(&pool); 241 | 242 | try std.testing.expectEqualStrings(str, locked_string.slice); 243 | } 244 | try std.testing.expectFmt(str, "{}", .{index.fmt(&pool)}); 245 | } 246 | 247 | test "StringPool - check interning" { 248 | const gpa = std.testing.allocator; 249 | var pool = StringPool(.{ .thread_safe = false }){}; 250 | defer pool.deinit(gpa); 251 | 252 | const str = "All Your Codebase Are Belong To Us"; 253 | const index1 = try pool.getOrPutString(gpa, str); 254 | const index2 = try pool.getOrPutString(gpa, str); 255 | const index3 = pool.getString(str).?; 256 | const storage1 = pool.stringToSlice(index1); 257 | const storage2 = pool.stringToSliceUnsafe(index2); 258 | 259 | try std.testing.expectEqual(index1, index2); 260 | try std.testing.expectEqual(index2, index3); 261 | try std.testing.expectEqualStrings(str, storage1); 262 | try std.testing.expectEqualStrings(str, storage2); 263 | try std.testing.expectEqual(storage1.ptr, storage2.ptr); 264 | try std.testing.expectEqual(storage1.len, storage2.len); 265 | } 266 | 267 | test "StringPool - getOrPut on existing string without allocation" { 268 | const gpa = std.testing.allocator; 269 | var failing_gpa = std.testing.FailingAllocator.init( 270 | gpa, 271 | .{ 272 | .fail_index = 0, 273 | .resize_fail_index = 0, 274 | }, 275 | ); 276 | 277 | var pool = StringPool(.{}){}; 278 | defer pool.deinit(gpa); 279 | 280 | const hello_string = try pool.getOrPutString(gpa, "hello"); 281 | const aaaaa_buffer: [std.atomic.cache_line * 2]u8 = [_]u8{0x61} ** (std.atomic.cache_line * 2); 282 | 283 | try std.testing.expectError( 284 | error.OutOfMemory, 285 | pool.getOrPutString(failing_gpa.allocator(), &aaaaa_buffer), 286 | ); 287 | try std.testing.expectEqual( 288 | hello_string, 289 | try pool.getOrPutString(failing_gpa.allocator(), "hello"), 290 | ); 291 | } 292 | -------------------------------------------------------------------------------- /src/build_runner/BuildRunnerVersion.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const build_options = @import("build_options"); 3 | 4 | /// These versions must be ordered from newest to oldest. 5 | /// There should be no need to have a build runner for minor patches (e.g. 0.10.1) 6 | /// The GitHub matrix in `.github\workflows\build_runner.yml` should be updated to check Zig master with the latest build runner file. 7 | pub const BuildRunnerVersion = enum { 8 | @"0.14.0", 9 | @"0.13.0", 10 | @"0.12.0", 11 | 12 | pub fn selectBuildRunnerVersion(runtime_zig_version: std.SemanticVersion) ?BuildRunnerVersion { 13 | const runtime_zig_version_is_tagged = runtime_zig_version.build == null and runtime_zig_version.pre == null; 14 | var target_ver: std.SemanticVersion = if (!runtime_zig_version_is_tagged) .{ 15 | .major = runtime_zig_version.major, 16 | .minor = runtime_zig_version.minor - 1, 17 | .patch = 0, 18 | } else runtime_zig_version; 19 | target_ver.patch = 0; 20 | 21 | const available_version_tags = comptime std.meta.tags(BuildRunnerVersion); 22 | for (available_version_tags) |tag| { 23 | switch (target_ver.order(std.SemanticVersion.parse(@tagName(tag)) catch unreachable)) { 24 | .eq => return tag, 25 | else => {}, 26 | } 27 | } 28 | 29 | return null; 30 | } 31 | 32 | pub fn getBuildRunnerFile(version: BuildRunnerVersion) [:0]const u8 { 33 | return switch (version) { 34 | .@"0.14.0", 35 | => @embedFile("0.14.0.zig"), 36 | .@"0.13.0", 37 | .@"0.12.0", 38 | => @embedFile("legacy.zig"), 39 | }; 40 | } 41 | }; 42 | 43 | test { 44 | @setEvalBranchQuota(6_000); 45 | const expectEqual = std.testing.expectEqual; 46 | const parse = std.SemanticVersion.parse; 47 | 48 | // 0.11.0-dev < 0.11.0 < 0.12.0-dev < 0.12.0 < 0.13.0-dev < 0.13.0 49 | 50 | { 51 | const current_zig_version = @import("builtin").zig_version; 52 | const build_runner = BuildRunnerVersion.selectBuildRunnerVersion(current_zig_version); 53 | if (build_runner == null) { 54 | std.debug.print( 55 | \\Project is being tested with Zig {}. 56 | \\No build runner could be resolved for this Zig version! 57 | \\ 58 | , .{current_zig_version}); 59 | return error.TestUnexpectedResult; 60 | } 61 | } 62 | 63 | { 64 | try expectEqual(.@"0.12.0", BuildRunnerVersion.selectBuildRunnerVersion( 65 | try parse("0.12.0"), // Zig version 66 | )); 67 | try expectEqual(.@"0.12.0", BuildRunnerVersion.selectBuildRunnerVersion( 68 | try parse("0.12.1"), // Zig version 69 | )); 70 | try expectEqual(.@"0.12.0", BuildRunnerVersion.selectBuildRunnerVersion( 71 | try parse("0.13.0-dev.1+aaaaaaaaa"), // Zig version 72 | )); 73 | try expectEqual(.@"0.14.0", BuildRunnerVersion.selectBuildRunnerVersion( 74 | try parse("0.14.0"), // Zig version 75 | )); 76 | try expectEqual(.@"0.13.0", BuildRunnerVersion.selectBuildRunnerVersion( 77 | try parse("0.14.0-dev.3445+6c3cbb0c8"), // Zig version 78 | )); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/build_runner/shared.zig: -------------------------------------------------------------------------------- 1 | //! Contains shared code between the LS and it's custom build runner 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | const native_endian = builtin.target.cpu.arch.endian(); 6 | const need_bswap = native_endian != .little; 7 | 8 | pub const BuildConfig = struct { 9 | roots_info_file: []const u8, 10 | deps_build_roots: []NamePathPair, 11 | roots: []RootEntry, 12 | packages: []NamePathPair, 13 | include_dirs: []const []const u8, 14 | top_level_steps: []const []const u8, 15 | available_options: std.json.ArrayHashMap(AvailableOption), 16 | c_macros: []const []const u8 = &.{}, 17 | 18 | pub const RootEntry = struct { 19 | name: []const u8, 20 | mods: []NamePathPair, 21 | }; 22 | pub const NamePathPair = struct { 23 | name: []const u8, 24 | path: []const u8, 25 | }; 26 | pub const AvailableOption = std.meta.FieldType(std.meta.FieldType(std.Build, .available_options_map).KV, .value); 27 | }; 28 | 29 | pub const Transport = struct { 30 | in: std.fs.File, 31 | out: std.fs.File, 32 | poller: std.io.Poller(StreamEnum), 33 | 34 | const StreamEnum = enum { in }; 35 | 36 | pub const Header = extern struct { 37 | tag: u32, 38 | /// Size of the body only; does not include this Header. 39 | bytes_len: u32, 40 | }; 41 | 42 | pub const Options = struct { 43 | gpa: std.mem.Allocator, 44 | in: std.fs.File, 45 | out: std.fs.File, 46 | }; 47 | 48 | pub fn init(options: Options) Transport { 49 | return .{ 50 | .in = options.in, 51 | .out = options.out, 52 | .poller = std.io.poll(options.gpa, StreamEnum, .{ .in = options.in }), 53 | }; 54 | } 55 | 56 | pub fn deinit(transport: *Transport) void { 57 | transport.poller.deinit(); 58 | transport.* = undefined; 59 | } 60 | 61 | pub fn receiveMessage(transport: *Transport, timeout_ns: ?u64) !Header { 62 | const fifo = transport.poller.fifo(.in); 63 | 64 | poll: while (true) { 65 | while (fifo.readableLength() < @sizeOf(Header)) { 66 | if (!(if (timeout_ns) |timeout| try transport.poller.pollTimeout(timeout) else try transport.poller.poll())) break :poll; 67 | } 68 | const header = fifo.reader().readStructEndian(Header, .little) catch unreachable; 69 | while (fifo.readableLength() < header.bytes_len) { 70 | if (!(if (timeout_ns) |timeout| try transport.poller.pollTimeout(timeout) else try transport.poller.poll())) break :poll; 71 | } 72 | return header; 73 | } 74 | return error.EndOfStream; 75 | } 76 | 77 | pub fn reader(transport: *Transport) std.io.PollFifo.Reader { 78 | return transport.poller.fifo(.in).reader(); 79 | } 80 | 81 | pub fn discard(transport: *Transport, bytes: usize) void { 82 | transport.poller.fifo(.in).discard(bytes); 83 | } 84 | 85 | pub fn receiveBytes( 86 | transport: *Transport, 87 | allocator: std.mem.Allocator, 88 | len: usize, 89 | ) (std.mem.Allocator.Error || std.fs.File.ReadError || error{EndOfStream})![]u8 { 90 | return try transport.receiveSlice(allocator, u8, len); 91 | } 92 | 93 | pub fn receiveSlice( 94 | transport: *Transport, 95 | allocator: std.mem.Allocator, 96 | comptime T: type, 97 | len: usize, 98 | ) (std.mem.Allocator.Error || std.fs.File.ReadError || error{EndOfStream})![]T { 99 | const bytes = try allocator.alignedAlloc(u8, @alignOf(T), len * @sizeOf(T)); 100 | errdefer allocator.free(bytes); 101 | const amt = try transport.reader().readAll(bytes); 102 | if (amt != len * @sizeOf(T)) return error.EndOfStream; 103 | const result = std.mem.bytesAsSlice(T, bytes); 104 | std.debug.assert(result.len == len); 105 | if (need_bswap) { 106 | for (result) |*item| { 107 | item.* = @byteSwap(item.*); 108 | } 109 | } 110 | return result; 111 | } 112 | 113 | pub fn serveMessage( 114 | client: *const Transport, 115 | header: Header, 116 | bufs: []const []const u8, 117 | ) std.fs.File.WriteError!void { 118 | std.debug.assert(bufs.len < 10); 119 | var iovecs: [10]std.posix.iovec_const = undefined; 120 | var header_le = header; 121 | if (need_bswap) std.mem.byteSwapAllFields(Header, &header_le); 122 | const header_bytes = std.mem.asBytes(&header_le); 123 | iovecs[0] = .{ .base = header_bytes.ptr, .len = header_bytes.len }; 124 | for (bufs, iovecs[1 .. bufs.len + 1]) |buf, *iovec| { 125 | iovec.* = .{ 126 | .base = buf.ptr, 127 | .len = buf.len, 128 | }; 129 | } 130 | try client.out.writevAll(iovecs[0 .. bufs.len + 1]); 131 | } 132 | }; 133 | 134 | pub const ServerToClient = struct { 135 | pub const Tag = enum(u32) { 136 | /// Body is an ErrorBundle. 137 | watch_error_bundle, 138 | 139 | _, 140 | }; 141 | 142 | /// Trailing: 143 | /// * extra: [extra_len]u32, 144 | /// * string_bytes: [string_bytes_len]u8, 145 | /// See `std.zig.ErrorBundle`. 146 | pub const ErrorBundle = extern struct { 147 | step_id: u32, 148 | cycle: u32, 149 | extra_len: u32, 150 | string_bytes_len: u32, 151 | }; 152 | }; 153 | 154 | pub const BuildOnSaveSupport = union(enum) { 155 | supported, 156 | invalid_linux_kernel_version: if (builtin.os.tag == .linux) std.meta.FieldType(std.posix.utsname, .release) else noreturn, 157 | unsupported_linux_kernel_version: if (builtin.os.tag == .linux) std.SemanticVersion else noreturn, 158 | unsupported_zig_version, 159 | unsupported_os, 160 | 161 | // const linux_support_version = std.SemanticVersion.parse("0.14.0-dev.283+1d20ff11d") catch unreachable; 162 | // const windows_support_version = std.SemanticVersion.parse("0.14.0-dev.625+2de0e2eca") catch unreachable; 163 | // const kqueue_support_version = std.SemanticVersion.parse("0.14.0-dev.2046+b8795b4d0") catch unreachable; 164 | 165 | // We can't rely on `std.Build.Watch.have_impl` because we need to 166 | // check the runtime Zig version instead of Zig version that ZLS 167 | // has been built with. 168 | pub const minimum_zig_version: std.SemanticVersion = .{ .major = 0, .minor = 14, .patch = 0 }; 169 | 170 | /// std.build.Watch requires `AT_HANDLE_FID` which is Linux 6.5+ 171 | /// https://github.com/ziglang/zig/issues/20720 172 | pub const minimum_linux_version: std.SemanticVersion = .{ .major = 6, .minor = 5, .patch = 0 }; 173 | 174 | /// Returns true if is comptime known that build on save is supported. 175 | pub inline fn isSupportedComptime() bool { 176 | if (!std.process.can_spawn) return false; 177 | if (builtin.single_threaded) return false; 178 | return true; 179 | } 180 | 181 | pub fn isSupportedRuntime(runtime_zig_version: std.SemanticVersion) BuildOnSaveSupport { 182 | comptime std.debug.assert(isSupportedComptime()); 183 | 184 | if (runtime_zig_version.order(minimum_zig_version) == .lt) { 185 | return .unsupported_zig_version; 186 | } 187 | 188 | return switch (builtin.os.tag) { 189 | .linux => { 190 | const utsname = std.posix.uname(); 191 | const unparsed_version = std.mem.sliceTo(&utsname.release, 0); 192 | const version = parseUnameKernelVersion(unparsed_version) catch 193 | return .{ .invalid_linux_kernel_version = utsname.release }; 194 | 195 | if (version.order(minimum_linux_version) != .lt) return .supported; 196 | std.debug.assert(version.build == null and version.pre == null); // Otherwise, returning the `std.SemanticVersion` would be unsafe 197 | return .{ 198 | .unsupported_linux_kernel_version = version, 199 | }; 200 | }, 201 | .windows, 202 | .dragonfly, 203 | .freebsd, 204 | .netbsd, 205 | .openbsd, 206 | .ios, 207 | .macos, 208 | .tvos, 209 | .visionos, 210 | .watchos, 211 | .haiku, 212 | => .supported, 213 | else => .unsupported_os, 214 | }; 215 | } 216 | }; 217 | 218 | /// Parses a Linux Kernel Version. The result will ignore pre-release and build metadata. 219 | fn parseUnameKernelVersion(kernel_version: []const u8) !std.SemanticVersion { 220 | const extra_index = std.mem.indexOfAny(u8, kernel_version, "-+"); 221 | const required = kernel_version[0..(extra_index orelse kernel_version.len)]; 222 | var it = std.mem.splitScalar(u8, required, '.'); 223 | return .{ 224 | .major = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), 225 | .minor = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), 226 | .patch = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), 227 | }; 228 | } 229 | 230 | test parseUnameKernelVersion { 231 | try std.testing.expectFmt("5.17.0", "{}", .{try parseUnameKernelVersion("5.17.0")}); 232 | try std.testing.expectFmt("6.12.9", "{}", .{try parseUnameKernelVersion("6.12.9-rc7")}); 233 | try std.testing.expectFmt("6.6.71", "{}", .{try parseUnameKernelVersion("6.6.71-42-generic")}); 234 | try std.testing.expectFmt("5.15.167", "{}", .{try parseUnameKernelVersion("5.15.167.4-microsoft-standard-WSL2")}); // WSL2 235 | try std.testing.expectFmt("4.4.0", "{}", .{try parseUnameKernelVersion("4.4.0-20241-Microsoft")}); // WSL1 236 | 237 | try std.testing.expectError(error.InvalidCharacter, parseUnameKernelVersion("")); 238 | try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5")); 239 | try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5.5")); 240 | } 241 | -------------------------------------------------------------------------------- /src/configuration.zig: -------------------------------------------------------------------------------- 1 | //! read and resolve configuration options. 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | 6 | const tracy = @import("tracy"); 7 | const known_folders = @import("known-folders"); 8 | 9 | const Config = @import("Config.zig"); 10 | const offsets = @import("offsets.zig"); 11 | 12 | const logger = std.log.scoped(.config); 13 | 14 | pub fn getLocalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 { 15 | const folder_path = try known_folders.getPath(allocator, .local_configuration) orelse return null; 16 | defer allocator.free(folder_path); 17 | return try std.fs.path.join(allocator, &.{ folder_path, "zls.json" }); 18 | } 19 | 20 | pub fn getGlobalConfigPath(allocator: std.mem.Allocator) known_folders.Error!?[]const u8 { 21 | const folder_path = try known_folders.getPath(allocator, .global_configuration) orelse return null; 22 | defer allocator.free(folder_path); 23 | return try std.fs.path.join(allocator, &.{ folder_path, "zls.json" }); 24 | } 25 | 26 | pub fn load(allocator: std.mem.Allocator) error{OutOfMemory}!LoadConfigResult { 27 | const local_config_path = getLocalConfigPath(allocator) catch |err| blk: { 28 | logger.warn("failed to resolve local configuration path: {}", .{err}); 29 | break :blk null; 30 | }; 31 | defer if (local_config_path) |path| allocator.free(path); 32 | 33 | const global_config_path = getGlobalConfigPath(allocator) catch |err| blk: { 34 | logger.warn("failed to resolve global configuration path: {}", .{err}); 35 | break :blk null; 36 | }; 37 | defer if (global_config_path) |path| allocator.free(path); 38 | 39 | for ([_]?[]const u8{ local_config_path, global_config_path }) |config_path| { 40 | const result = try loadFromFile(allocator, config_path orelse continue); 41 | switch (result) { 42 | .success, .failure => return result, 43 | .not_found => {}, 44 | } 45 | } 46 | 47 | return .not_found; 48 | } 49 | 50 | pub const LoadConfigResult = union(enum) { 51 | success: struct { 52 | config: std.json.Parsed(Config), 53 | /// file path of the config.json 54 | path: []const u8, 55 | }, 56 | failure: struct { 57 | /// `null` indicates that the error has already been logged 58 | error_bundle: ?std.zig.ErrorBundle, 59 | 60 | pub fn toMessage(self: @This(), allocator: std.mem.Allocator) error{OutOfMemory}!?[]u8 { 61 | const error_bundle = self.error_bundle orelse return null; 62 | var msg: std.ArrayListUnmanaged(u8) = .{}; 63 | errdefer msg.deinit(allocator); 64 | error_bundle.renderToWriter(.{ .ttyconf = .no_color }, msg.writer(allocator)) catch |err| switch (err) { 65 | error.OutOfMemory => |e| return e, 66 | else => unreachable, // why does renderToWriter return `anyerror!void`? 67 | }; 68 | return try msg.toOwnedSlice(allocator); 69 | } 70 | }, 71 | not_found, 72 | 73 | pub fn deinit(self: *LoadConfigResult, allocator: std.mem.Allocator) void { 74 | switch (self.*) { 75 | .success => |*config_with_path| { 76 | config_with_path.config.deinit(); 77 | allocator.free(config_with_path.path); 78 | }, 79 | .failure => |*payload| { 80 | if (payload.error_bundle) |*error_bundle| error_bundle.deinit(allocator); 81 | }, 82 | .not_found => {}, 83 | } 84 | } 85 | }; 86 | 87 | pub fn loadFromFile(allocator: std.mem.Allocator, file_path: []const u8) error{OutOfMemory}!LoadConfigResult { 88 | const tracy_zone = tracy.trace(@src()); 89 | defer tracy_zone.end(); 90 | 91 | const file_buf = std.fs.cwd().readFileAlloc(allocator, file_path, 16 * 1024 * 1024) catch |err| switch (err) { 92 | error.FileNotFound => return .not_found, 93 | error.OutOfMemory => |e| return e, 94 | else => { 95 | logger.warn("Error while reading configuration file: {}", .{err}); 96 | return .{ .failure = .{ .error_bundle = null } }; 97 | }, 98 | }; 99 | defer allocator.free(file_buf); 100 | 101 | const parse_options = std.json.ParseOptions{ 102 | .ignore_unknown_fields = true, 103 | .allocate = .alloc_always, 104 | }; 105 | var parse_diagnostics: std.json.Diagnostics = .{}; 106 | 107 | var scanner = std.json.Scanner.initCompleteInput(allocator, file_buf); 108 | defer scanner.deinit(); 109 | scanner.enableDiagnostics(&parse_diagnostics); 110 | 111 | @setEvalBranchQuota(10000); 112 | const config = std.json.parseFromTokenSource( 113 | Config, 114 | allocator, 115 | &scanner, 116 | parse_options, 117 | ) catch |err| { 118 | var eb: std.zig.ErrorBundle.Wip = undefined; 119 | try eb.init(allocator); 120 | errdefer eb.deinit(); 121 | 122 | const src_path = try eb.addString(file_path); 123 | const msg = try eb.addString(@errorName(err)); 124 | 125 | const src_loc = try eb.addSourceLocation(.{ 126 | .src_path = src_path, 127 | .line = @intCast(parse_diagnostics.getLine()), 128 | .column = @intCast(parse_diagnostics.getColumn()), 129 | .span_start = @intCast(parse_diagnostics.getByteOffset()), 130 | .span_main = @intCast(parse_diagnostics.getByteOffset()), 131 | .span_end = @intCast(parse_diagnostics.getByteOffset()), 132 | }); 133 | try eb.addRootErrorMessage(.{ 134 | .msg = msg, 135 | .src_loc = src_loc, 136 | }); 137 | 138 | return .{ .failure = .{ .error_bundle = try eb.toOwnedBundle("") } }; 139 | }; 140 | 141 | return .{ .success = .{ 142 | .config = config, 143 | .path = try allocator.dupe(u8, file_path), 144 | } }; 145 | } 146 | 147 | pub const Env = struct { 148 | zig_exe: []const u8, 149 | lib_dir: ?[]const u8, 150 | std_dir: []const u8, 151 | global_cache_dir: []const u8, 152 | version: []const u8, 153 | target: ?[]const u8 = null, 154 | }; 155 | 156 | pub fn getZigEnv(allocator: std.mem.Allocator, zig_exe_path: []const u8) ?std.json.Parsed(Env) { 157 | const zig_env_result = std.process.Child.run(.{ 158 | .allocator = allocator, 159 | .argv = &[_][]const u8{ zig_exe_path, "env" }, 160 | }) catch { 161 | logger.err("Failed to execute zig env", .{}); 162 | return null; 163 | }; 164 | 165 | defer { 166 | allocator.free(zig_env_result.stdout); 167 | allocator.free(zig_env_result.stderr); 168 | } 169 | 170 | switch (zig_env_result.term) { 171 | .Exited => |code| { 172 | if (code != 0) { 173 | logger.err("zig env failed with error_code: {}", .{code}); 174 | return null; 175 | } 176 | }, 177 | else => logger.err("zig env invocation failed", .{}), 178 | } 179 | 180 | return std.json.parseFromSlice( 181 | Env, 182 | allocator, 183 | zig_env_result.stdout, 184 | .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, 185 | ) catch { 186 | logger.err("Failed to parse zig env JSON result", .{}); 187 | return null; 188 | }; 189 | } 190 | 191 | /// the same struct as Config but every field is optional 192 | pub const Configuration = getConfigurationType(); 193 | 194 | // returns a Struct which is the same as `Config` except that every field is optional. 195 | fn getConfigurationType() type { 196 | var config_info: std.builtin.Type = @typeInfo(Config); 197 | var fields: [config_info.@"struct".fields.len]std.builtin.Type.StructField = undefined; 198 | for (config_info.@"struct".fields, &fields) |field, *new_field| { 199 | new_field.* = field; 200 | if (@typeInfo(field.type) != .optional) { 201 | new_field.type = @Type(std.builtin.Type{ 202 | .optional = .{ .child = field.type }, 203 | }); 204 | } 205 | new_field.default_value_ptr = &@as(new_field.type, null); 206 | } 207 | config_info.@"struct".fields = fields[0..]; 208 | config_info.@"struct".decls = &.{}; 209 | return @Type(config_info); 210 | } 211 | 212 | pub fn findZig(allocator: std.mem.Allocator) error{OutOfMemory}!?[]const u8 { 213 | const env_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { 214 | error.EnvironmentVariableNotFound => return null, 215 | error.OutOfMemory => |e| return e, 216 | error.InvalidWtf8 => |e| { 217 | logger.err("failed to load 'PATH' environment variable: {}", .{e}); 218 | return null; 219 | }, 220 | }; 221 | defer allocator.free(env_path); 222 | 223 | const zig_exe = "zig" ++ comptime builtin.target.exeFileExt(); 224 | 225 | var it = std.mem.tokenizeScalar(u8, env_path, std.fs.path.delimiter); 226 | while (it.next()) |path| { 227 | var full_path = try std.fs.path.join(allocator, &[_][]const u8{ path, zig_exe }); 228 | defer allocator.free(full_path); 229 | 230 | if (!std.fs.path.isAbsolute(full_path)) { 231 | logger.warn("ignoring entry in PATH '{s}' because it is not an absolute file path", .{full_path}); 232 | continue; 233 | } 234 | 235 | const file = std.fs.openFileAbsolute(full_path, .{}) catch |err| switch (err) { 236 | error.FileNotFound => continue, 237 | else => |e| { 238 | logger.warn("failed to open entry in PATH '{s}': {}", .{ full_path, e }); 239 | continue; 240 | }, 241 | }; 242 | defer file.close(); 243 | 244 | stat_failed: { 245 | const stat = file.stat() catch break :stat_failed; 246 | if (stat.kind == .directory) { 247 | logger.warn("ignoring entry in PATH '{s}' because it is a directory", .{full_path}); 248 | } 249 | } 250 | 251 | defer full_path = ""; 252 | return full_path; 253 | } 254 | return null; 255 | } 256 | -------------------------------------------------------------------------------- /src/debug.zig: -------------------------------------------------------------------------------- 1 | //! A set of helper functions that assist in debugging. 2 | 3 | const std = @import("std"); 4 | 5 | const analysis = @import("analysis.zig"); 6 | const offsets = @import("offsets.zig"); 7 | const DocumentScope = @import("DocumentScope.zig"); 8 | 9 | pub fn printTree(tree: std.zig.Ast) void { 10 | if (!std.debug.runtime_safety) @compileError("this function should only be used in debug mode!"); 11 | 12 | std.debug.print( 13 | \\printTree: 14 | \\nodes tag lhs rhs token 15 | \\----------------------------------------------------------- 16 | \\ 17 | , .{}); 18 | for ( 19 | tree.nodes.items(.tag), 20 | tree.nodes.items(.data), 21 | tree.nodes.items(.main_token), 22 | 0.., 23 | ) |tag, data, main_token, i| { 24 | std.debug.print( 25 | " {d:<3} {s:<20} {d:<11} {d:<11} {d:<5} {s}\n", 26 | .{ i, @tagName(tag), data.lhs, data.rhs, main_token, offsets.tokenToSlice(tree, main_token) }, 27 | ); 28 | } 29 | 30 | std.debug.print( 31 | \\ 32 | \\tokens tag start 33 | \\---------------------------------- 34 | \\ 35 | , .{}); 36 | for (tree.tokens.items(.tag), tree.tokens.items(.start), 0..) |tag, start, i| { 37 | std.debug.print( 38 | " {d:<3} {s:<20} {d:<}\n", 39 | .{ i, @tagName(tag), start }, 40 | ); 41 | } 42 | } 43 | 44 | pub fn printDocumentScope(doc_scope: DocumentScope) void { 45 | if (!std.debug.runtime_safety) @compileError("this function should only be used in debug mode!"); 46 | 47 | for (0..doc_scope.scopes.len) |index| { 48 | const scope_index: DocumentScope.Scope.Index = @enumFromInt(index); 49 | const scope = doc_scope.scopes.get(index); 50 | if (index != 0) std.debug.print("\n\n", .{}); 51 | std.debug.print( 52 | \\[{d}, {d}] 53 | \\ tag: {} 54 | \\ ast node: {?} 55 | \\ parent: {} 56 | \\ child scopes: {any} 57 | \\ usingnamespaces: {any} 58 | \\ decls: 59 | \\ 60 | , .{ 61 | scope.loc.start, 62 | scope.loc.end, 63 | scope.data.tag, 64 | doc_scope.getScopeAstNode(scope_index), 65 | doc_scope.getScopeParent(scope_index), 66 | doc_scope.getScopeChildScopesConst(scope_index), 67 | doc_scope.getScopeUsingnamespaceNodesConst(scope_index), 68 | }); 69 | 70 | for (doc_scope.getScopeDeclarationsConst(scope_index)) |decl| { 71 | std.debug.print(" - {s:<8} {}\n", .{ 72 | doc_scope.declaration_lookup_map.keys()[@intFromEnum(decl)].name, 73 | doc_scope.declarations.get(@intFromEnum(decl)), 74 | }); 75 | } 76 | } 77 | } 78 | 79 | pub const FailingAllocator = struct { 80 | internal_allocator: std.mem.Allocator, 81 | random: std.Random.DefaultPrng, 82 | likelihood: u32, 83 | 84 | /// the chance that an allocation will fail is `1/likelihood` 85 | /// `likelihood == 0` means that every allocation will fail 86 | /// `likelihood == std.math.intMax(u32)` means that no allocation will be forced to fail 87 | pub fn init(internal_allocator: std.mem.Allocator, likelihood: u32) FailingAllocator { 88 | return FailingAllocator{ 89 | .internal_allocator = internal_allocator, 90 | .random = std.Random.DefaultPrng.init(std.crypto.random.int(u64)), 91 | .likelihood = likelihood, 92 | }; 93 | } 94 | 95 | pub fn allocator(self: *FailingAllocator) std.mem.Allocator { 96 | return .{ 97 | .ptr = self, 98 | .vtable = &.{ 99 | .alloc = alloc, 100 | .resize = resize, 101 | .remap = remap, 102 | .free = free, 103 | }, 104 | }; 105 | } 106 | 107 | fn alloc( 108 | ctx: *anyopaque, 109 | len: usize, 110 | log2_ptr_align: std.mem.Alignment, 111 | return_address: usize, 112 | ) ?[*]u8 { 113 | const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); 114 | if (shouldFail(self)) return null; 115 | return self.internal_allocator.rawAlloc(len, log2_ptr_align, return_address); 116 | } 117 | 118 | fn resize( 119 | ctx: *anyopaque, 120 | old_mem: []u8, 121 | log2_old_align: std.mem.Alignment, 122 | new_len: usize, 123 | ra: usize, 124 | ) bool { 125 | const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); 126 | if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra)) 127 | return false; 128 | return true; 129 | } 130 | 131 | fn free( 132 | ctx: *anyopaque, 133 | old_mem: []u8, 134 | log2_old_align: std.mem.Alignment, 135 | ra: usize, 136 | ) void { 137 | const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); 138 | self.internal_allocator.rawFree(old_mem, log2_old_align, ra); 139 | } 140 | 141 | fn remap( 142 | ctx: *anyopaque, 143 | memory: []u8, 144 | alignment: std.mem.Alignment, 145 | new_len: usize, 146 | ret_addr: usize, 147 | ) ?[*]u8 { 148 | const self: *FailingAllocator = @ptrCast(@alignCast(ctx)); 149 | if (shouldFail(self)) return null; 150 | return self.internal_allocator.rawRemap(memory, alignment, new_len, ret_addr); 151 | } 152 | 153 | fn shouldFail(self: *FailingAllocator) bool { 154 | if (self.likelihood == std.math.maxInt(u32)) return false; 155 | return 0 == self.random.random().intRangeAtMostBiased(u32, 0, self.likelihood); 156 | } 157 | }; 158 | 159 | comptime { 160 | if (std.debug.runtime_safety) { 161 | std.testing.refAllDeclsRecursive(@This()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/diff.zig: -------------------------------------------------------------------------------- 1 | //! Text diffing between source files. 2 | 3 | const std = @import("std"); 4 | const types = @import("lsp").types; 5 | const offsets = @import("offsets.zig"); 6 | const tracy = @import("tracy"); 7 | const DiffMatchPatch = @import("diffz"); 8 | 9 | const dmp = DiffMatchPatch{ 10 | .diff_timeout = 250, 11 | }; 12 | 13 | pub const Error = error{OutOfMemory}; 14 | 15 | pub fn edits( 16 | allocator: std.mem.Allocator, 17 | before: []const u8, 18 | after: []const u8, 19 | encoding: offsets.Encoding, 20 | ) Error!std.ArrayListUnmanaged(types.TextEdit) { 21 | const tracy_zone = tracy.trace(@src()); 22 | defer tracy_zone.end(); 23 | 24 | var arena = std.heap.ArenaAllocator.init(allocator); 25 | defer arena.deinit(); 26 | const diffs = try dmp.diff(arena.allocator(), before, after, true); 27 | 28 | var edit_count: usize = 0; 29 | for (diffs.items) |diff| { 30 | switch (diff.operation) { 31 | .delete => edit_count += 1, 32 | .equal => continue, 33 | .insert => edit_count += 1, 34 | } 35 | } 36 | 37 | var eds = std.ArrayListUnmanaged(types.TextEdit){}; 38 | try eds.ensureTotalCapacity(allocator, edit_count); 39 | errdefer { 40 | for (eds.items) |edit| allocator.free(edit.newText); 41 | eds.deinit(allocator); 42 | } 43 | 44 | var offset: usize = 0; 45 | for (diffs.items) |diff| { 46 | const start = offset; 47 | switch (diff.operation) { 48 | .delete => { 49 | offset += diff.text.len; 50 | eds.appendAssumeCapacity(.{ 51 | .range = offsets.locToRange(before, .{ .start = start, .end = offset }, encoding), 52 | .newText = "", 53 | }); 54 | }, 55 | .equal => { 56 | offset += diff.text.len; 57 | }, 58 | .insert => { 59 | eds.appendAssumeCapacity(.{ 60 | .range = offsets.locToRange(before, .{ .start = start, .end = start }, encoding), 61 | .newText = try allocator.dupe(u8, diff.text), 62 | }); 63 | }, 64 | } 65 | } 66 | return eds; 67 | } 68 | 69 | pub const ContentChanges = struct { 70 | /// New contents 71 | text: [:0]const u8, 72 | /// Lowest index affected by the change(s) 73 | idx_lo: u32, 74 | /// Highest index affected by the change(s) 75 | idx_hi: u32, 76 | }; 77 | 78 | /// Caller owns returned memory. 79 | pub fn applyContentChanges( 80 | allocator: std.mem.Allocator, 81 | text: []const u8, 82 | content_changes: []const types.TextDocumentContentChangeEvent, 83 | encoding: offsets.Encoding, 84 | ) error{OutOfMemory}!ContentChanges { 85 | const tracy_zone = tracy.trace(@src()); 86 | defer tracy_zone.end(); 87 | 88 | const last_full_text_index, const last_full_text = blk: { 89 | var i: usize = content_changes.len; 90 | while (i != 0) { 91 | i -= 1; 92 | switch (content_changes[i]) { 93 | .literal_1 => |content_change| break :blk .{ i, content_change.text }, // TextDocumentContentChangeWholeDocument 94 | .literal_0 => continue, // TextDocumentContentChangePartial 95 | } 96 | } 97 | break :blk .{ null, text }; 98 | }; 99 | 100 | var text_array = std.ArrayListUnmanaged(u8){}; 101 | errdefer text_array.deinit(allocator); 102 | 103 | try text_array.appendSlice(allocator, last_full_text); 104 | 105 | // don't even bother applying changes before a full text change 106 | const changes = content_changes[if (last_full_text_index) |index| index + 1 else 0..]; 107 | 108 | // lowest and highest indexes affected by the change(s) 109 | var idx_lo: usize = last_full_text.len; 110 | var idx_hi: usize = 0; 111 | 112 | for (changes) |item| { 113 | const content_change = item.literal_0; // TextDocumentContentChangePartial 114 | 115 | const head = offsets.positionToIndex( 116 | text_array.items, 117 | content_change.range.start, 118 | encoding, 119 | ); 120 | if (head < idx_lo) idx_lo = head; 121 | 122 | const tail = offsets.positionToIndex( 123 | text_array.items, 124 | content_change.range.end, 125 | encoding, 126 | ); 127 | const upper_index = @max(tail, head + content_change.text.len); 128 | if (idx_hi < upper_index) idx_hi = upper_index; 129 | 130 | try text_array.replaceRange(allocator, head, tail - head, content_change.text); 131 | } 132 | 133 | return .{ 134 | .text = try text_array.toOwnedSliceSentinel(allocator, 0), 135 | .idx_lo = @intCast(idx_lo), 136 | .idx_hi = @intCast(idx_hi), 137 | }; 138 | } 139 | 140 | // https://cs.opensource.google/go/x/tools/+/master:internal/lsp/diff/diff.go;l=40 141 | 142 | fn textEditLessThan(_: void, lhs: types.TextEdit, rhs: types.TextEdit) bool { 143 | return offsets.rangeLessThan(lhs.range, rhs.range); 144 | } 145 | 146 | /// Caller owns returned memory. 147 | pub fn applyTextEdits( 148 | allocator: std.mem.Allocator, 149 | text: []const u8, 150 | text_edits: []const types.TextEdit, 151 | encoding: offsets.Encoding, 152 | ) ![]const u8 { 153 | const tracy_zone = tracy.trace(@src()); 154 | defer tracy_zone.end(); 155 | 156 | const text_edits_sortable = try allocator.dupe(types.TextEdit, text_edits); 157 | defer allocator.free(text_edits_sortable); 158 | 159 | std.mem.sort(types.TextEdit, text_edits_sortable, {}, textEditLessThan); 160 | 161 | var final_text = std.ArrayListUnmanaged(u8){}; 162 | errdefer final_text.deinit(allocator); 163 | 164 | var last: usize = 0; 165 | for (text_edits_sortable) |te| { 166 | const start = offsets.positionToIndex(text, te.range.start, encoding); 167 | if (start > last) { 168 | try final_text.appendSlice(allocator, text[last..start]); 169 | last = start; 170 | } 171 | try final_text.appendSlice(allocator, te.newText); 172 | last = offsets.positionToIndex(text, te.range.end, encoding); 173 | } 174 | if (last < text.len) { 175 | try final_text.appendSlice(allocator, text[last..]); 176 | } 177 | 178 | return try final_text.toOwnedSlice(allocator); 179 | } 180 | -------------------------------------------------------------------------------- /src/features/document_symbol.zig: -------------------------------------------------------------------------------- 1 | //! Implementation of [`textDocument/documentSymbol`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) 2 | 3 | const std = @import("std"); 4 | const Ast = std.zig.Ast; 5 | const log = std.log.scoped(.document_symbol); 6 | 7 | const types = @import("lsp").types; 8 | const offsets = @import("../offsets.zig"); 9 | const ast = @import("../ast.zig"); 10 | const analysis = @import("../analysis.zig"); 11 | const tracy = @import("tracy"); 12 | 13 | const Symbol = struct { 14 | name: []const u8, 15 | detail: ?[]const u8 = null, 16 | kind: types.SymbolKind, 17 | loc: offsets.Loc, 18 | selection_loc: offsets.Loc, 19 | children: std.ArrayListUnmanaged(Symbol), 20 | }; 21 | 22 | const Context = struct { 23 | arena: std.mem.Allocator, 24 | last_var_decl_name: ?[]const u8, 25 | parent_container: Ast.Node.Index, 26 | parent_node: Ast.Node.Index, 27 | parent_symbols: *std.ArrayListUnmanaged(Symbol), 28 | total_symbol_count: *usize, 29 | }; 30 | 31 | fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!void { 32 | if (node == 0) return; 33 | 34 | const node_tags = tree.nodes.items(.tag); 35 | const main_tokens = tree.nodes.items(.main_token); 36 | const token_tags = tree.tokens.items(.tag); 37 | 38 | var new_ctx = ctx.*; 39 | const maybe_symbol: ?Symbol = switch (node_tags[node]) { 40 | .global_var_decl, 41 | .local_var_decl, 42 | .simple_var_decl, 43 | .aligned_var_decl, 44 | => blk: { 45 | if (!ast.isContainer(tree, ctx.parent_node)) break :blk null; 46 | 47 | const var_decl = tree.fullVarDecl(node).?; 48 | const var_decl_name_token = var_decl.ast.mut_token + 1; 49 | const var_decl_name = offsets.identifierTokenToNameSlice(tree, var_decl_name_token); 50 | 51 | new_ctx.last_var_decl_name = var_decl_name; 52 | 53 | const kind: types.SymbolKind = switch (token_tags[main_tokens[node]]) { 54 | .keyword_var => .Variable, 55 | .keyword_const => .Constant, 56 | else => unreachable, 57 | }; 58 | 59 | break :blk .{ 60 | .name = var_decl_name, 61 | .detail = null, 62 | .kind = kind, 63 | .loc = offsets.nodeToLoc(tree, node), 64 | .selection_loc = offsets.tokenToLoc(tree, var_decl_name_token), 65 | .children = .{}, 66 | }; 67 | }, 68 | 69 | .test_decl => blk: { 70 | const test_name_token, const test_name = ast.testDeclNameAndToken(tree, node) orelse break :blk null; 71 | 72 | break :blk .{ 73 | .name = test_name, 74 | .kind = .Method, // there is no SymbolKind that represents a tests 75 | .loc = offsets.nodeToLoc(tree, node), 76 | .selection_loc = offsets.tokenToLoc(tree, test_name_token), 77 | .children = .{}, 78 | }; 79 | }, 80 | 81 | .fn_decl => blk: { 82 | var buffer: [1]Ast.Node.Index = undefined; 83 | const fn_info = tree.fullFnProto(&buffer, node).?; 84 | const name_token = fn_info.name_token orelse break :blk null; 85 | 86 | break :blk .{ 87 | .name = offsets.identifierTokenToNameSlice(tree, name_token), 88 | .detail = analysis.getFunctionSignature(tree, fn_info), 89 | .kind = .Function, 90 | .loc = offsets.nodeToLoc(tree, node), 91 | .selection_loc = offsets.tokenToLoc(tree, name_token), 92 | .children = .{}, 93 | }; 94 | }, 95 | 96 | .container_field_init, 97 | .container_field_align, 98 | .container_field, 99 | => blk: { 100 | const container_kind = token_tags[main_tokens[ctx.parent_container]]; 101 | const is_struct = container_kind == .keyword_struct; 102 | 103 | const kind: types.SymbolKind = switch (node_tags[ctx.parent_container]) { 104 | .root => .Field, 105 | .container_decl, 106 | .container_decl_trailing, 107 | .container_decl_arg, 108 | .container_decl_arg_trailing, 109 | .container_decl_two, 110 | .container_decl_two_trailing, 111 | => switch (container_kind) { 112 | .keyword_struct => .Field, 113 | .keyword_union => .Field, 114 | .keyword_enum => .EnumMember, 115 | .keyword_opaque => break :blk null, 116 | else => unreachable, 117 | }, 118 | .tagged_union, 119 | .tagged_union_trailing, 120 | .tagged_union_enum_tag, 121 | .tagged_union_enum_tag_trailing, 122 | .tagged_union_two, 123 | .tagged_union_two_trailing, 124 | => .Field, 125 | else => unreachable, 126 | }; 127 | 128 | const container_field = tree.fullContainerField(node).?; 129 | if (is_struct and container_field.ast.tuple_like) break :blk null; 130 | 131 | const decl_name_token = container_field.ast.main_token; 132 | const decl_name = offsets.tokenToSlice(tree, decl_name_token); 133 | 134 | break :blk .{ 135 | .name = decl_name, 136 | .detail = ctx.last_var_decl_name, 137 | .kind = kind, 138 | .loc = offsets.nodeToLoc(tree, node), 139 | .selection_loc = offsets.tokenToLoc(tree, decl_name_token), 140 | .children = .{}, 141 | }; 142 | }, 143 | .container_decl, 144 | .container_decl_trailing, 145 | .container_decl_arg, 146 | .container_decl_arg_trailing, 147 | .container_decl_two, 148 | .container_decl_two_trailing, 149 | .tagged_union, 150 | .tagged_union_trailing, 151 | .tagged_union_enum_tag, 152 | .tagged_union_enum_tag_trailing, 153 | .tagged_union_two, 154 | .tagged_union_two_trailing, 155 | => blk: { 156 | new_ctx.parent_container = node; 157 | break :blk null; 158 | }, 159 | else => null, 160 | }; 161 | 162 | new_ctx.parent_node = node; 163 | if (maybe_symbol) |symbol| { 164 | var symbol_ptr = try ctx.parent_symbols.addOne(ctx.arena); 165 | symbol_ptr.* = symbol; 166 | new_ctx.parent_symbols = &symbol_ptr.children; 167 | ctx.total_symbol_count.* += 1; 168 | } 169 | 170 | try ast.iterateChildren(tree, node, &new_ctx, error{OutOfMemory}, callback); 171 | } 172 | 173 | /// converts `Symbol` to `types.DocumentSymbol` 174 | fn convertSymbols( 175 | arena: std.mem.Allocator, 176 | tree: Ast, 177 | from: []const Symbol, 178 | total_symbol_count: usize, 179 | encoding: offsets.Encoding, 180 | ) error{OutOfMemory}![]types.DocumentSymbol { 181 | const tracy_zone = tracy.trace(@src()); 182 | defer tracy_zone.end(); 183 | 184 | var symbol_buffer = std.ArrayListUnmanaged(types.DocumentSymbol){}; 185 | try symbol_buffer.ensureTotalCapacityPrecise(arena, total_symbol_count); 186 | 187 | // instead of converting every `offsets.Loc` to `types.Range` by calling `offsets.locToRange` 188 | // we instead store a mapping from source indices to their desired position, sort them by their source index 189 | // and then iterate through them which avoids having to re-iterate through the source file to find out the line number 190 | var mappings = std.ArrayListUnmanaged(offsets.multiple.IndexToPositionMapping){}; 191 | try mappings.ensureTotalCapacityPrecise(arena, total_symbol_count * 4); 192 | 193 | const result = convertSymbolsInternal(from, &symbol_buffer, &mappings); 194 | 195 | offsets.multiple.indexToPositionWithMappings(tree.source, mappings.items, encoding); 196 | 197 | return result; 198 | } 199 | 200 | fn convertSymbolsInternal( 201 | from: []const Symbol, 202 | symbol_buffer: *std.ArrayListUnmanaged(types.DocumentSymbol), 203 | mappings: *std.ArrayListUnmanaged(offsets.multiple.IndexToPositionMapping), 204 | ) []types.DocumentSymbol { 205 | // acquire storage for exactly `from.len` symbols 206 | const prev_len = symbol_buffer.items.len; 207 | symbol_buffer.items.len += from.len; 208 | const to: []types.DocumentSymbol = symbol_buffer.items[prev_len..]; 209 | 210 | for (from, to) |symbol, *out| { 211 | out.* = .{ 212 | .name = symbol.name, 213 | .detail = symbol.detail, 214 | .kind = symbol.kind, 215 | // will be set later through the mapping below 216 | .range = undefined, 217 | .selectionRange = undefined, 218 | .children = convertSymbolsInternal(symbol.children.items, symbol_buffer, mappings), 219 | }; 220 | mappings.appendSliceAssumeCapacity(&[4]offsets.multiple.IndexToPositionMapping{ 221 | .{ .output = &out.range.start, .source_index = symbol.loc.start }, 222 | .{ .output = &out.selectionRange.start, .source_index = symbol.selection_loc.start }, 223 | .{ .output = &out.selectionRange.end, .source_index = symbol.selection_loc.end }, 224 | .{ .output = &out.range.end, .source_index = symbol.loc.end }, 225 | }); 226 | } 227 | 228 | return to; 229 | } 230 | 231 | pub fn getDocumentSymbols( 232 | arena: std.mem.Allocator, 233 | tree: Ast, 234 | encoding: offsets.Encoding, 235 | ) error{OutOfMemory}![]types.DocumentSymbol { 236 | var root_symbols = std.ArrayListUnmanaged(Symbol){}; 237 | var total_symbol_count: usize = 0; 238 | 239 | var ctx = Context{ 240 | .arena = arena, 241 | .last_var_decl_name = null, 242 | .parent_node = 0, // root-node 243 | .parent_container = 0, // root-node 244 | .parent_symbols = &root_symbols, 245 | .total_symbol_count = &total_symbol_count, 246 | }; 247 | try ast.iterateChildren(tree, 0, &ctx, error{OutOfMemory}, callback); 248 | 249 | return try convertSymbols(arena, tree, root_symbols.items, ctx.total_symbol_count.*, encoding); 250 | } 251 | -------------------------------------------------------------------------------- /src/features/selection_range.zig: -------------------------------------------------------------------------------- 1 | //! Implementation of [`textDocument/selectionRange`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange) 2 | 3 | const std = @import("std"); 4 | const Ast = std.zig.Ast; 5 | 6 | const DocumentStore = @import("../DocumentStore.zig"); 7 | const ast = @import("../ast.zig"); 8 | const types = @import("lsp").types; 9 | const offsets = @import("../offsets.zig"); 10 | 11 | pub fn generateSelectionRanges( 12 | arena: std.mem.Allocator, 13 | handle: *DocumentStore.Handle, 14 | positions: []const types.Position, 15 | offset_encoding: offsets.Encoding, 16 | ) error{OutOfMemory}!?[]types.SelectionRange { 17 | // For each of the input positions, we need to compute the stack of AST 18 | // nodes/ranges which contain the position. At the moment, we do this in a 19 | // super inefficient way, by iterating _all_ nodes, selecting the ones that 20 | // contain position, and then sorting. 21 | // 22 | // A faster algorithm would be to walk the tree starting from the root, 23 | // descending into the child containing the position at every step. 24 | const result = try arena.alloc(types.SelectionRange, positions.len); 25 | var locs = try std.ArrayListUnmanaged(offsets.Loc).initCapacity(arena, 32); 26 | for (positions, result) |position, *out| { 27 | const index = offsets.positionToIndex(handle.tree.source, position, offset_encoding); 28 | 29 | locs.clearRetainingCapacity(); 30 | for (0..handle.tree.nodes.len, handle.tree.nodes.items(.tag)) |i, tag| { 31 | const node = @as(Ast.Node.Index, @intCast(i)); 32 | const loc = offsets.nodeToLoc(handle.tree, node); 33 | 34 | if (!(loc.start <= index and index <= loc.end)) continue; 35 | 36 | try locs.append(arena, loc); 37 | switch (tag) { 38 | // Function parameters are not stored in the AST explicitly, iterate over them 39 | // manually. 40 | .fn_proto, .fn_proto_multi, .fn_proto_one, .fn_proto_simple => { 41 | var buffer: [1]Ast.Node.Index = undefined; 42 | const fn_proto = handle.tree.fullFnProto(&buffer, node).?; 43 | var it = fn_proto.iterate(&handle.tree); 44 | 45 | while (ast.nextFnParam(&it)) |param| { 46 | const param_loc = ast.paramLoc(handle.tree, param, true); 47 | if (param_loc.start <= index and index <= param_loc.end) { 48 | try locs.append(arena, param_loc); 49 | } 50 | } 51 | }, 52 | else => {}, 53 | } 54 | } 55 | 56 | std.mem.sort(offsets.Loc, locs.items, {}, shorterLocsFirst); 57 | { 58 | var i: usize = 0; 59 | while (i + 1 < locs.items.len) { 60 | if (std.meta.eql(locs.items[i], locs.items[i + 1])) { 61 | _ = locs.orderedRemove(i); 62 | } else { 63 | i += 1; 64 | } 65 | } 66 | } 67 | 68 | var selection_ranges = try arena.alloc(types.SelectionRange, locs.items.len); 69 | for (selection_ranges, 0..) |*range, i| { 70 | range.range = offsets.locToRange(handle.tree.source, locs.items[i], offset_encoding); 71 | range.parent = if (i + 1 < selection_ranges.len) &selection_ranges[i + 1] else null; 72 | } 73 | out.* = selection_ranges[0]; 74 | } 75 | 76 | return result; 77 | } 78 | 79 | fn shorterLocsFirst(_: void, lhs: offsets.Loc, rhs: offsets.Loc) bool { 80 | return (lhs.end - lhs.start) < (rhs.end - rhs.start); 81 | } 82 | -------------------------------------------------------------------------------- /src/snippets.zig: -------------------------------------------------------------------------------- 1 | //! Collection of all snippets and keywords. 2 | 3 | const types = @import("lsp").types; 4 | 5 | pub const Snipped = struct { 6 | label: []const u8, 7 | kind: types.CompletionItemKind, 8 | text: ?[]const u8 = null, 9 | }; 10 | 11 | pub const top_level_decl_data = [_]Snipped{ 12 | .{ .label = "std", .kind = .Snippet, .text = "const std = @import(\"std\");" }, 13 | .{ .label = "import", .kind = .Snippet, .text = "const $1 = @import(\"$2\")$0;" }, 14 | .{ .label = "fn", .kind = .Snippet, .text = "fn ${1:name}($2) ${3:!void} {$0}" }, 15 | .{ .label = "pub fn", .kind = .Snippet, .text = "pub fn ${1:name}($2) ${3:!void} {$0}" }, 16 | .{ .label = "struct", .kind = .Snippet, .text = "const $1 = struct {$0};" }, 17 | .{ .label = "error set", .kind = .Snippet, .text = "const ${1:Error} = error {$0};" }, 18 | .{ .label = "enum", .kind = .Snippet, .text = "const $1 = enum {$0};" }, 19 | .{ .label = "union", .kind = .Snippet, .text = "const $1 = union {$0};" }, 20 | .{ .label = "union tagged", .kind = .Snippet, .text = "const $1 = union(${2:enum}) {$0};" }, 21 | .{ .label = "test", .kind = .Snippet, .text = "test \"$1\" {$0}" }, 22 | .{ .label = "main", .kind = .Snippet, .text = "pub fn main() !void {$0}" }, 23 | .{ .label = "std_options", .kind = .Snippet, .text = "pub const std_options: std.Options = .{${0}};" }, 24 | }; 25 | 26 | pub const generic = [_]Snipped{ 27 | // keywords 28 | .{ .label = "align", .kind = .Keyword }, 29 | .{ .label = "allowzero", .kind = .Keyword }, 30 | .{ .label = "and", .kind = .Keyword }, 31 | .{ .label = "asm", .kind = .Keyword }, 32 | .{ .label = "async", .kind = .Keyword }, 33 | .{ .label = "await", .kind = .Keyword }, 34 | .{ .label = "break", .kind = .Keyword }, 35 | .{ .label = "callconv", .kind = .Keyword, .text = "callconv($0)" }, 36 | .{ .label = "catch", .kind = .Keyword }, 37 | .{ .label = "comptime", .kind = .Keyword }, 38 | .{ .label = "const", .kind = .Keyword }, 39 | .{ .label = "continue", .kind = .Keyword }, 40 | .{ .label = "defer", .kind = .Keyword }, 41 | .{ .label = "else", .kind = .Keyword, .text = "else {$0}" }, 42 | .{ .label = "enum", .kind = .Keyword, .text = "enum {$0}" }, 43 | .{ .label = "errdefer", .kind = .Keyword }, 44 | .{ .label = "error", .kind = .Keyword }, 45 | .{ .label = "export", .kind = .Keyword }, 46 | .{ .label = "extern", .kind = .Keyword }, 47 | .{ .label = "fn", .kind = .Keyword, .text = "fn ${1:name}($2) ${3:!void} {$0}" }, 48 | .{ .label = "for", .kind = .Keyword, .text = "for ($1) |${2:value}| {$0}" }, 49 | .{ .label = "if", .kind = .Keyword, .text = "if ($1) {$0}" }, 50 | .{ .label = "inline", .kind = .Keyword }, 51 | .{ .label = "noalias", .kind = .Keyword }, 52 | .{ .label = "nosuspend", .kind = .Keyword }, 53 | .{ .label = "noinline", .kind = .Keyword }, 54 | .{ .label = "opaque", .kind = .Keyword }, 55 | .{ .label = "or", .kind = .Keyword }, 56 | .{ .label = "orelse", .kind = .Keyword }, 57 | .{ .label = "packed", .kind = .Keyword }, 58 | .{ .label = "pub", .kind = .Keyword }, 59 | .{ .label = "resume", .kind = .Keyword }, 60 | .{ .label = "return", .kind = .Keyword }, 61 | .{ .label = "linksection", .kind = .Keyword }, 62 | .{ .label = "struct", .kind = .Keyword, .text = "struct {$0};" }, 63 | .{ .label = "suspend", .kind = .Keyword }, 64 | .{ .label = "switch", .kind = .Keyword, .text = "switch ($1) {$0}" }, 65 | .{ .label = "test", .kind = .Keyword, .text = "test \"$1\" {$0}" }, 66 | .{ .label = "threadlocal", .kind = .Keyword }, 67 | .{ .label = "try", .kind = .Keyword }, 68 | .{ .label = "union", .kind = .Keyword }, 69 | .{ .label = "unreachable", .kind = .Keyword }, 70 | .{ .label = "usingnamespace", .kind = .Keyword }, 71 | .{ .label = "var", .kind = .Keyword }, 72 | .{ .label = "volatile", .kind = .Keyword }, 73 | .{ .label = "while", .kind = .Keyword, .text = "while ($1) {$0}" }, 74 | 75 | // keyword snippets 76 | .{ .label = "asmv", .kind = .Snippet, .text = "asm volatile (${1:input}, ${0:input})" }, 77 | .{ .label = "fori", .kind = .Snippet, .text = "for ($1, 0..) |${2:value}, ${3:i}| {$0}" }, 78 | .{ .label = "if else", .kind = .Snippet, .text = "if ($1) {$2} else {$0}" }, 79 | .{ .label = "catch switch", .kind = .Snippet, .text = "catch |${1:err}| switch (${1:err}) {$0};" }, 80 | 81 | // snippets 82 | .{ .label = "print", .kind = .Snippet, .text = "std.debug.print(\"$1\", .{$0});" }, 83 | .{ .label = "log err", .kind = .Snippet, .text = "std.log.err(\"$1\", .{$0});" }, 84 | .{ .label = "log warn", .kind = .Snippet, .text = "std.log.warn(\"$1\", .{$0});" }, 85 | .{ .label = "log info", .kind = .Snippet, .text = "std.log.info(\"$1\", .{$0});" }, 86 | .{ .label = "log debug", .kind = .Snippet, .text = "std.log.debug(\"$1\", .{$0});" }, 87 | .{ .label = "assert", .kind = .Snippet, .text = "std.debug.assert($1);" }, 88 | 89 | // types 90 | .{ .label = "anyerror", .kind = .Keyword }, 91 | .{ .label = "anyframe", .kind = .Keyword }, 92 | .{ .label = "anytype", .kind = .Keyword }, 93 | .{ .label = "anyopaque", .kind = .Keyword }, 94 | .{ .label = "bool", .kind = .Keyword }, 95 | .{ .label = "c_char", .kind = .Keyword }, 96 | .{ .label = "c_int", .kind = .Keyword }, 97 | .{ .label = "c_long", .kind = .Keyword }, 98 | .{ .label = "c_longdouble", .kind = .Keyword }, 99 | .{ .label = "c_longlong", .kind = .Keyword }, 100 | .{ .label = "c_short", .kind = .Keyword }, 101 | .{ .label = "c_uint", .kind = .Keyword }, 102 | .{ .label = "c_ulong", .kind = .Keyword }, 103 | .{ .label = "c_ulonglong", .kind = .Keyword }, 104 | .{ .label = "c_ushort", .kind = .Keyword }, 105 | .{ .label = "comptime_float", .kind = .Keyword }, 106 | .{ .label = "comptime_int", .kind = .Keyword }, 107 | .{ .label = "false", .kind = .Keyword }, 108 | .{ .label = "isize", .kind = .Keyword }, 109 | .{ .label = "noreturn", .kind = .Keyword }, 110 | .{ .label = "null", .kind = .Keyword }, 111 | .{ .label = "true", .kind = .Keyword }, 112 | .{ .label = "type", .kind = .Keyword }, 113 | .{ .label = "undefined", .kind = .Keyword }, 114 | .{ .label = "usize", .kind = .Keyword }, 115 | .{ .label = "void", .kind = .Keyword }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/tools/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "name": "enable_snippets", 5 | "description": "Enables snippet completions when the client also supports them", 6 | "type": "bool", 7 | "default": true 8 | }, 9 | { 10 | "name": "enable_argument_placeholders", 11 | "description": "Whether to enable function argument placeholder completions", 12 | "type": "bool", 13 | "default": true 14 | }, 15 | { 16 | "name": "enable_build_on_save", 17 | "description": "Whether to enable build-on-save diagnostics. Will be automatically enabled if the `build.zig` has declared a 'check' step.", 18 | "type": "?bool", 19 | "default": null 20 | }, 21 | { 22 | "name": "build_on_save_args", 23 | "description": "Specify which arguments should be passed to Zig when running build-on-save.\n\nIf the `build.zig` has declared a 'check' step, it will be preferred over the default 'install' step.", 24 | "type": "[]const []const u8", 25 | "default": [] 26 | }, 27 | { 28 | "name": "enable_autofix", 29 | "description": "Whether to automatically fix errors on save. Currently supports adding and removing discards.", 30 | "type": "bool", 31 | "default": false 32 | }, 33 | { 34 | "name": "semantic_tokens", 35 | "description": "Set level of semantic tokens. `partial` only includes information that requires semantic analysis.", 36 | "type": "enum", 37 | "enum": [ 38 | "none", 39 | "partial", 40 | "full" 41 | ], 42 | "default": "full" 43 | }, 44 | { 45 | "name": "inlay_hints_show_variable_type_hints", 46 | "description": "Enable inlay hints for variable types", 47 | "type": "bool", 48 | "default": true 49 | }, 50 | { 51 | "name": "inlay_hints_show_struct_literal_field_type", 52 | "description": "Enable inlay hints for fields in struct and union literals", 53 | "type": "bool", 54 | "default": true 55 | }, 56 | { 57 | "name": "inlay_hints_show_parameter_name", 58 | "description": "Enable inlay hints for parameter names", 59 | "type": "bool", 60 | "default": true 61 | }, 62 | { 63 | "name": "inlay_hints_show_builtin", 64 | "description": "Enable inlay hints for builtin functions", 65 | "type": "bool", 66 | "default": true 67 | }, 68 | { 69 | "name": "inlay_hints_param_hint_kind", 70 | "description": "Show the parameter's name or type as the hint", 71 | "type": "enum", 72 | "enum": [ 73 | "name", 74 | "type" 75 | ], 76 | "default": "name" 77 | }, 78 | { 79 | "name": "inlay_hints_exclude_single_argument", 80 | "description": "Don't show inlay hints for single argument calls", 81 | "type": "bool", 82 | "default": true 83 | }, 84 | { 85 | "name": "inlay_hints_hide_redundant_param_names", 86 | "description": "Hides inlay hints when parameter name matches the identifier (e.g. foo: foo)", 87 | "type": "bool", 88 | "default": false 89 | }, 90 | { 91 | "name": "inlay_hints_hide_redundant_param_names_last_token", 92 | "description": "Hides inlay hints when parameter name matches the last token of a parameter node (e.g. foo: bar.foo, foo: &foo)", 93 | "type": "bool", 94 | "default": false 95 | }, 96 | { 97 | "name": "warn_style", 98 | "description": "Enables warnings for style guideline mismatches", 99 | "type": "bool", 100 | "default": false 101 | }, 102 | { 103 | "name": "highlight_global_var_declarations", 104 | "description": "Whether to highlight global var declarations", 105 | "type": "bool", 106 | "default": false 107 | }, 108 | { 109 | "name": "skip_std_references", 110 | "description": "When true, skips searching for references in std. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is", 111 | "type": "bool", 112 | "default": false 113 | }, 114 | { 115 | "name": "prefer_ast_check_as_child_process", 116 | "description": "Favor using `zig ast-check` instead of ZLS's fork", 117 | "type": "bool", 118 | "default": true 119 | }, 120 | { 121 | "name": "builtin_path", 122 | "description": "Path to 'builtin;' useful for debugging, automatically set if let null", 123 | "type": "?[]const u8", 124 | "default": null 125 | }, 126 | { 127 | "name": "zig_lib_path", 128 | "description": "Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports", 129 | "type": "?[]const u8", 130 | "default": null 131 | }, 132 | { 133 | "name": "zig_exe_path", 134 | "description": "Zig executable path, e.g. `/path/to/zig/zig`, used to run the custom build runner. If `null`, zig is looked up in `PATH`. Will be used to infer the zig standard library path if none is provided", 135 | "type": "?[]const u8", 136 | "default": null 137 | }, 138 | { 139 | "name": "build_runner_path", 140 | "description": "Path to the `build_runner.zig` file provided by ZLS. null is equivalent to `${executable_directory}/build_runner.zig`", 141 | "type": "?[]const u8", 142 | "default": null 143 | }, 144 | { 145 | "name": "global_cache_path", 146 | "description": "Path to a directory that will be used as zig's cache. null is equivalent to `${KnownFolders.Cache}/zls`", 147 | "type": "?[]const u8", 148 | "default": null 149 | }, 150 | { 151 | "name": "completion_label_details", 152 | "description": "When false, the function signature of completion results is hidden. Improves readability in some editors", 153 | "type": "bool", 154 | "default": true 155 | }, 156 | { 157 | "name": "ws_build_zig", 158 | "description": "Internal; Override/specify the build.zig file to use.", 159 | "type": "?[]const u8", 160 | "default": null 161 | } 162 | ] 163 | } 164 | -------------------------------------------------------------------------------- /src/tools/publish_http_form.zig: -------------------------------------------------------------------------------- 1 | //! Sends a `multipart/form-data` Http POST request 2 | //! 3 | //! The CLI imitates [cURL](https://curl.se/). 4 | 5 | const std = @import("std"); 6 | 7 | pub fn main() !void { 8 | var arena_allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); 9 | const arena = arena_allocator.allocator(); 10 | 11 | var arg_it = try std.process.argsWithAllocator(arena); 12 | defer arg_it.deinit(); 13 | _ = arg_it.skip(); 14 | 15 | var uri: ?std.Uri = null; 16 | var authorization: std.http.Client.Request.Headers.Value = .default; 17 | var form_fields = std.ArrayList(FormField).init(arena); 18 | 19 | while (arg_it.next()) |arg| { 20 | if (std.mem.eql(u8, arg, "--user")) { 21 | const usename_password = arg_it.next() orelse @panic("expected argument after --user"); 22 | const base64_encode_buffer = try arena.alloc(u8, std.base64.standard.Encoder.calcSize(usename_password.len)); 23 | const auth = std.base64.standard.Encoder.encode(base64_encode_buffer, usename_password); 24 | authorization = .{ .override = try std.fmt.allocPrint(arena, "Basic {s}", .{auth}) }; 25 | } else if (std.mem.eql(u8, arg, "-F") or std.mem.eql(u8, arg, "--form")) { 26 | const form = arg_it.next() orelse std.debug.panic("expected argument after {s}!", .{arg}); 27 | var it = std.mem.splitScalar(u8, form, '='); 28 | const name = it.next() orelse std.debug.panic("invalid argument '{s}' after {s}!", .{ form, arg }); 29 | const content = it.next() orelse std.debug.panic("invalid argument '{s}' after {s}!", .{ form, arg }); 30 | if (it.next() != null) std.debug.panic("invalid argument '{s}' after {s}!", .{ form, arg }); 31 | const form_field: FormField = blk: { 32 | if (std.mem.startsWith(u8, content, "@")) { 33 | const file = try std.fs.cwd().openFile(content[1..], .{}); 34 | defer file.close(); 35 | 36 | break :blk .{ 37 | .name = try arena.dupe(u8, name), 38 | .filename = std.fs.path.basename(content[1..]), 39 | .value = try file.readToEndAlloc(arena, std.math.maxInt(usize)), 40 | }; 41 | } 42 | break :blk .{ 43 | .name = try arena.dupe(u8, name), 44 | .value = try arena.dupe(u8, content), 45 | }; 46 | }; 47 | try form_fields.append(form_field); 48 | } else if (uri == null) { 49 | uri = try std.Uri.parse(arg); 50 | } else { 51 | std.debug.panic("unknown argument '{s}'!", .{arg}); 52 | } 53 | } 54 | 55 | var boundary: [64 + 3]u8 = undefined; 56 | std.debug.assert((std.fmt.bufPrint( 57 | &boundary, 58 | "{x:0>16}-{x:0>16}-{x:0>16}-{x:0>16}", 59 | .{ std.crypto.random.int(u64), std.crypto.random.int(u64), std.crypto.random.int(u64), std.crypto.random.int(u64) }, 60 | ) catch unreachable).len == boundary.len); 61 | 62 | const body = try createMultiPartFormDataBody(arena, &boundary, form_fields.items); 63 | 64 | const headers: std.http.Client.Request.Headers = .{ 65 | .content_type = .{ .override = try std.fmt.allocPrint(arena, "multipart/form-data; boundary={s}", .{boundary}) }, 66 | .authorization = authorization, 67 | }; 68 | 69 | var client: std.http.Client = .{ .allocator = arena }; 70 | defer client.deinit(); 71 | try client.initDefaultProxies(arena); 72 | 73 | var server_header_buffer: [16 * 1024]u8 = undefined; 74 | var request = try client.open(.POST, uri orelse @panic("expected URI"), .{ 75 | .keep_alive = false, 76 | .server_header_buffer = &server_header_buffer, 77 | .headers = headers, 78 | }); 79 | defer request.deinit(); 80 | request.transfer_encoding = .{ .content_length = body.len }; 81 | 82 | try request.send(); 83 | try request.writeAll(body); 84 | try request.finish(); 85 | try request.wait(); 86 | 87 | if (request.response.status.class() == .success) return; 88 | 89 | std.log.err("response {s} ({d}): {s}", .{ 90 | request.response.status.phrase() orelse "", 91 | @intFromEnum(request.response.status), 92 | try request.reader().readAllAlloc(arena, 1024 * 1024), 93 | }); 94 | std.process.exit(1); 95 | } 96 | 97 | pub const FormField = struct { 98 | name: []const u8, 99 | filename: ?[]const u8 = null, 100 | content_type: std.http.Client.Request.Headers.Value = .default, 101 | value: []const u8, 102 | }; 103 | 104 | fn createMultiPartFormDataBody( 105 | allocator: std.mem.Allocator, 106 | boundary: []const u8, 107 | fields: []const FormField, 108 | ) error{OutOfMemory}![]const u8 { 109 | var body: std.ArrayListUnmanaged(u8) = .{}; 110 | errdefer body.deinit(allocator); 111 | const writer = body.writer(allocator); 112 | 113 | for (fields) |field| { 114 | try writer.print("--{s}\r\n", .{boundary}); 115 | 116 | if (field.filename) |filename| { 117 | try writer.print("Content-Disposition: form-data; name=\"{s}\"; filename=\"{s}\"\r\n", .{ field.name, filename }); 118 | } else { 119 | try writer.print("Content-Disposition: form-data; name=\"{s}\"\r\n", .{field.name}); 120 | } 121 | 122 | switch (field.content_type) { 123 | .default => { 124 | if (field.filename != null) { 125 | try writer.writeAll("Content-Type: application/octet-stream\r\n"); 126 | } 127 | }, 128 | .omit => {}, 129 | .override => |content_type| { 130 | try writer.print("Content-Type: {s}\r\n", .{content_type}); 131 | }, 132 | } 133 | 134 | try writer.writeAll("\r\n"); 135 | try writer.writeAll(field.value); 136 | try writer.writeAll("\r\n"); 137 | } 138 | try writer.print("--{s}--\r\n", .{boundary}); 139 | 140 | return try body.toOwnedSlice(allocator); 141 | } 142 | 143 | test createMultiPartFormDataBody { 144 | const body = try createMultiPartFormDataBody(std.testing.allocator, "AAAA-BBBB-CCCC-DDDD", &.{ 145 | .{ 146 | .name = "zls-version", 147 | .value = "0.14.0", 148 | }, 149 | .{ 150 | .name = "compatibility", 151 | .content_type = .{ .override = "application/json" }, 152 | .value = "full", 153 | }, 154 | .{ 155 | .name = "zig-version", 156 | .value = "0.15.0", 157 | }, 158 | .{ 159 | .name = "zls-linux-x86_64-0.14.0-dev.77+3ec8ad16.tar.xz", 160 | .filename = "publish.zig", 161 | .value = "const std = @import(\"std\");", 162 | }, 163 | }); 164 | defer std.testing.allocator.free(body); 165 | try std.testing.expectEqualStrings( 166 | "--AAAA-BBBB-CCCC-DDDD\r\n" ++ 167 | "Content-Disposition: form-data; name=\"zls-version\"\r\n" ++ 168 | "\r\n" ++ 169 | "0.14.0\r\n" ++ 170 | "--AAAA-BBBB-CCCC-DDDD\r\n" ++ 171 | "Content-Disposition: form-data; name=\"compatibility\"\r\n" ++ 172 | "Content-Type: application/json\r\n" ++ 173 | "\r\n" ++ 174 | "full\r\n" ++ 175 | "--AAAA-BBBB-CCCC-DDDD\r\n" ++ 176 | "Content-Disposition: form-data; name=\"zig-version\"\r\n" ++ 177 | "\r\n" ++ 178 | "0.15.0\r\n" ++ 179 | "--AAAA-BBBB-CCCC-DDDD\r\n" ++ 180 | "Content-Disposition: form-data; name=\"zls-linux-x86_64-0.14.0-dev.77+3ec8ad16.tar.xz\"; filename=\"publish.zig\"\r\n" ++ 181 | "Content-Type: application/octet-stream\r\n" ++ 182 | "\r\n" ++ 183 | "const std = @import(\"std\");\r\n" ++ 184 | "--AAAA-BBBB-CCCC-DDDD--\r\n", 185 | body, 186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /src/translate_c.zig: -------------------------------------------------------------------------------- 1 | //! Implementation of the `translate-c` i.e `@cImport`. 2 | 3 | const std = @import("std"); 4 | const zig_builtin = @import("builtin"); 5 | const Config = @import("DocumentStore.zig").Config; 6 | const ast = @import("ast.zig"); 7 | const tracy = @import("tracy"); 8 | const Ast = std.zig.Ast; 9 | const URI = @import("uri.zig"); 10 | const log = std.log.scoped(.translate_c); 11 | 12 | const ZCSTransport = @import("build_runner/shared.zig").Transport; 13 | const OutMessage = std.zig.Client.Message; 14 | const InMessage = std.zig.Server.Message; 15 | 16 | /// converts a `@cInclude` node into an equivalent c header file 17 | /// which can then be handed over to `zig translate-c` 18 | /// Caller owns returned memory. 19 | /// 20 | /// **Example** 21 | /// ```zig 22 | /// const glfw = @cImport({ 23 | /// @cDefine("GLFW_INCLUDE_VULKAN", {}); 24 | /// @cInclude("GLFW/glfw3.h"); 25 | /// }); 26 | /// ``` 27 | /// gets converted into: 28 | /// ```c 29 | /// #define GLFW_INCLUDE_VULKAN 30 | /// #include "GLFW/glfw3.h" 31 | /// ``` 32 | pub fn convertCInclude(allocator: std.mem.Allocator, tree: Ast, node: Ast.Node.Index) error{ OutOfMemory, Unsupported }![]const u8 { 33 | const tracy_zone = tracy.trace(@src()); 34 | defer tracy_zone.end(); 35 | 36 | const main_tokens = tree.nodes.items(.main_token); 37 | 38 | std.debug.assert(ast.isBuiltinCall(tree, node)); 39 | std.debug.assert(std.mem.eql(u8, Ast.tokenSlice(tree, main_tokens[node]), "@cImport")); 40 | 41 | var output: std.ArrayListUnmanaged(u8) = .empty; 42 | errdefer output.deinit(allocator); 43 | 44 | var buffer: [2]Ast.Node.Index = undefined; 45 | for (ast.builtinCallParams(tree, node, &buffer).?) |child| { 46 | try convertCIncludeInternal(allocator, tree, child, &output); 47 | } 48 | 49 | return output.toOwnedSlice(allocator); 50 | } 51 | 52 | fn convertCIncludeInternal( 53 | allocator: std.mem.Allocator, 54 | tree: Ast, 55 | node: Ast.Node.Index, 56 | output: *std.ArrayListUnmanaged(u8), 57 | ) error{ OutOfMemory, Unsupported }!void { 58 | const node_tags = tree.nodes.items(.tag); 59 | const main_tokens = tree.nodes.items(.main_token); 60 | 61 | var writer = output.writer(allocator); 62 | 63 | var buffer: [2]Ast.Node.Index = undefined; 64 | if (ast.blockStatements(tree, node, &buffer)) |statements| { 65 | for (statements) |statement| { 66 | try convertCIncludeInternal(allocator, tree, statement, output); 67 | } 68 | } else if (ast.builtinCallParams(tree, node, &buffer)) |params| { 69 | if (params.len < 1) return; 70 | 71 | const call_name = Ast.tokenSlice(tree, main_tokens[node]); 72 | 73 | if (node_tags[params[0]] != .string_literal) return error.Unsupported; 74 | const first = extractString(Ast.tokenSlice(tree, main_tokens[params[0]])); 75 | 76 | if (std.mem.eql(u8, call_name, "@cInclude")) { 77 | try writer.print("#include <{s}>\n", .{first}); 78 | } else if (std.mem.eql(u8, call_name, "@cDefine")) { 79 | if (params.len < 2) return; 80 | 81 | var buffer2: [2]Ast.Node.Index = undefined; 82 | const is_void = if (ast.blockStatements(tree, params[1], &buffer2)) |block| block.len == 0 else false; 83 | 84 | if (is_void) { 85 | try writer.print("#define {s}\n", .{first}); 86 | } else { 87 | if (node_tags[params[1]] != .string_literal) return error.Unsupported; 88 | const second = extractString(Ast.tokenSlice(tree, main_tokens[params[1]])); 89 | try writer.print("#define {s} {s}\n", .{ first, second }); 90 | } 91 | } else if (std.mem.eql(u8, call_name, "@cUndef")) { 92 | try writer.print("#undef {s}\n", .{first}); 93 | } else { 94 | return error.Unsupported; 95 | } 96 | } 97 | } 98 | 99 | pub const Result = union(enum) { 100 | // uri to the generated zig file 101 | success: []const u8, 102 | // zig translate-c failed with the given error messages 103 | failure: std.zig.ErrorBundle, 104 | 105 | pub fn deinit(self: *Result, allocator: std.mem.Allocator) void { 106 | switch (self.*) { 107 | .success => |path| allocator.free(path), 108 | .failure => |*bundle| bundle.deinit(allocator), 109 | } 110 | } 111 | }; 112 | 113 | /// takes a c header file and returns the result from calling `zig translate-c` 114 | /// returns a URI to the generated zig file on success or the content of stderr on failure 115 | /// null indicates a failure which is automatically logged 116 | /// Caller owns returned memory. 117 | pub fn translate( 118 | allocator: std.mem.Allocator, 119 | config: Config, 120 | include_dirs: []const []const u8, 121 | c_macros: []const []const u8, 122 | source: []const u8, 123 | ) !?Result { 124 | const tracy_zone = tracy.trace(@src()); 125 | defer tracy_zone.end(); 126 | 127 | const zig_exe_path = config.zig_exe_path.?; 128 | const zig_lib_path = config.zig_lib_path.?; 129 | const global_cache_path = config.global_cache_path.?; 130 | 131 | var random_bytes: [16]u8 = undefined; 132 | std.crypto.random.bytes(&random_bytes); 133 | var sub_path: [std.fs.base64_encoder.calcSize(16)]u8 = undefined; 134 | _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); 135 | 136 | var global_cache_dir = try std.fs.openDirAbsolute(global_cache_path, .{}); 137 | defer global_cache_dir.close(); 138 | 139 | var sub_dir = try global_cache_dir.makeOpenPath(&sub_path, .{}); 140 | defer sub_dir.close(); 141 | 142 | sub_dir.writeFile(.{ 143 | .sub_path = "cimport.h", 144 | .data = source, 145 | }) catch |err| { 146 | log.warn("failed to write to '{s}/{s}/cimport.h': {}", .{ global_cache_path, sub_path, err }); 147 | return null; 148 | }; 149 | 150 | defer global_cache_dir.deleteTree(&sub_path) catch |err| { 151 | log.warn("failed to delete '{s}/{s}': {}", .{ global_cache_path, sub_path, err }); 152 | }; 153 | 154 | const file_path = try std.fs.path.join(allocator, &.{ global_cache_path, &sub_path, "cimport.h" }); 155 | defer allocator.free(file_path); 156 | 157 | const base_args = &[_][]const u8{ 158 | zig_exe_path, 159 | "translate-c", 160 | "--zig-lib-dir", 161 | zig_lib_path, 162 | "--cache-dir", 163 | global_cache_path, 164 | "--global-cache-dir", 165 | global_cache_path, 166 | "-lc", 167 | "--listen=-", 168 | }; 169 | 170 | const argc = base_args.len + 2 * include_dirs.len + c_macros.len + 1; 171 | var argv: std.ArrayListUnmanaged([]const u8) = try .initCapacity(allocator, argc); 172 | defer argv.deinit(allocator); 173 | 174 | argv.appendSliceAssumeCapacity(base_args); 175 | 176 | for (include_dirs) |include_dir| { 177 | argv.appendAssumeCapacity("-I"); 178 | argv.appendAssumeCapacity(include_dir); 179 | } 180 | 181 | argv.appendSliceAssumeCapacity(c_macros); 182 | 183 | argv.appendAssumeCapacity(file_path); 184 | 185 | var process: std.process.Child = .init(argv.items, allocator); 186 | process.stdin_behavior = .Pipe; 187 | process.stdout_behavior = .Pipe; 188 | process.stderr_behavior = .Ignore; 189 | 190 | errdefer |err| if (!zig_builtin.is_test) reportTranslateError(allocator, process.stderr, argv.items, @errorName(err)); 191 | 192 | process.spawn() catch |err| { 193 | log.err("failed to spawn zig translate-c process, error: {}", .{err}); 194 | return null; 195 | }; 196 | 197 | defer _ = process.wait() catch |wait_err| { 198 | log.err("zig translate-c process did not terminate, error: {}", .{wait_err}); 199 | }; 200 | 201 | var zcs: ZCSTransport = .init(.{ 202 | .gpa = allocator, 203 | .in = process.stdout.?, 204 | .out = process.stdin.?, 205 | }); 206 | defer zcs.deinit(); 207 | 208 | try zcs.serveMessage(.{ .tag = @intFromEnum(OutMessage.Tag.update), .bytes_len = 0 }, &.{}); 209 | try zcs.serveMessage(.{ .tag = @intFromEnum(OutMessage.Tag.exit), .bytes_len = 0 }, &.{}); 210 | 211 | while (true) { 212 | const header = try zcs.receiveMessage(20 * std.time.ns_per_s); 213 | // log.debug("received header: {}", .{header}); 214 | 215 | switch (@as(InMessage.Tag, @enumFromInt(header.tag))) { 216 | .zig_version => { 217 | // log.debug("zig-version: {s}", .{zcs.receive_fifo.readableSliceOfLen(header.bytes_len)}); 218 | zcs.discard(header.bytes_len); 219 | }, 220 | .emit_digest => { 221 | const expected_size: usize = @sizeOf(std.zig.Server.Message.EmitDigest) + 16; 222 | if (header.bytes_len != expected_size) return error.InvalidResponse; 223 | 224 | zcs.discard(@sizeOf(InMessage.EmitDigest)); 225 | 226 | const bin_result_path = try zcs.reader().readBytesNoEof(16); 227 | const hex_result_path = std.Build.Cache.binToHex(bin_result_path); 228 | const result_path = try std.fs.path.join(allocator, &.{ global_cache_path, "o", &hex_result_path, "cimport.zig" }); 229 | defer allocator.free(result_path); 230 | 231 | return .{ .success = try URI.fromPath(allocator, std.mem.sliceTo(result_path, '\n')) }; 232 | }, 233 | .error_bundle => { 234 | if (header.bytes_len < @sizeOf(InMessage.ErrorBundle)) return error.InvalidResponse; 235 | 236 | const error_bundle_header: InMessage.ErrorBundle = .{ 237 | .extra_len = try zcs.reader().readInt(u32, .little), 238 | .string_bytes_len = try zcs.reader().readInt(u32, .little), 239 | }; 240 | 241 | const expected_size = @sizeOf(InMessage.ErrorBundle) + error_bundle_header.extra_len * @sizeOf(u32) + error_bundle_header.string_bytes_len; 242 | if (header.bytes_len != expected_size) return error.InvalidResponse; 243 | 244 | const extra = try zcs.receiveSlice(allocator, u32, error_bundle_header.extra_len); 245 | errdefer allocator.free(extra); 246 | 247 | const string_bytes = try zcs.receiveBytes(allocator, error_bundle_header.string_bytes_len); 248 | errdefer allocator.free(string_bytes); 249 | 250 | const error_bundle: std.zig.ErrorBundle = .{ .string_bytes = string_bytes, .extra = extra }; 251 | 252 | return .{ .failure = error_bundle }; 253 | }, 254 | else => { 255 | zcs.discard(header.bytes_len); 256 | }, 257 | } 258 | } 259 | } 260 | 261 | fn reportTranslateError(allocator: std.mem.Allocator, stderr: ?std.fs.File, argv: []const []const u8, err_name: []const u8) void { 262 | const joined = std.mem.join(allocator, " ", argv) catch return; 263 | defer allocator.free(joined); 264 | if (stderr) |file| { 265 | const stderr_output = file.readToEndAlloc(allocator, 16 * 1024 * 1024) catch return; 266 | defer allocator.free(stderr_output); 267 | log.err("failed zig translate-c command:\n{s}\nstderr:{s}\nerror:{s}\n", .{ joined, stderr_output, err_name }); 268 | } else { 269 | log.err("failed zig translate-c command:\n{s}\nerror:{s}\n", .{ joined, err_name }); 270 | } 271 | } 272 | 273 | fn extractString(str: []const u8) []const u8 { 274 | if (std.mem.startsWith(u8, str, "\"") and std.mem.endsWith(u8, str, "\"")) { 275 | return str[1 .. str.len - 1]; 276 | } else { 277 | return str; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/uri.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | /// Returns a file URI from a path. 5 | /// Caller owns the returned memory 6 | pub fn fromPath(allocator: std.mem.Allocator, path: []const u8) error{OutOfMemory}![]u8 { 7 | if (path.len == 0) return try allocator.dupe(u8, "/"); 8 | const prefix = if (builtin.os.tag == .windows) "file:///" else "file://"; 9 | 10 | var buf = try std.ArrayListUnmanaged(u8).initCapacity(allocator, prefix.len + path.len); 11 | errdefer buf.deinit(allocator); 12 | 13 | buf.appendSliceAssumeCapacity(prefix); 14 | 15 | const writer = buf.writer(allocator); 16 | 17 | var start: usize = 0; 18 | for (path, 0..) |char, index| { 19 | switch (char) { 20 | // zig fmt: off 21 | 'A'...'Z', 22 | 'a'...'z', 23 | '0'...'9', 24 | '-', '.', '_', '~', '!', 25 | '$', '&', '\'','(', ')', 26 | '+', ',', ';', '=', '@', 27 | // zig fmt: on 28 | => continue, 29 | ':', '*' => if (builtin.os.tag != .windows) continue, 30 | else => {}, 31 | } 32 | 33 | try writer.writeAll(path[start..index]); 34 | if (std.fs.path.isSep(char)) { 35 | try writer.writeByte('/'); 36 | } else { 37 | try writer.print("%{X:0>2}", .{char}); 38 | } 39 | start = index + 1; 40 | } 41 | try writer.writeAll(path[start..]); 42 | 43 | // On windows, we need to lowercase the drive name. 44 | if (builtin.os.tag == .windows) { 45 | if (buf.items.len > prefix.len + 1 and 46 | std.ascii.isAlphanumeric(buf.items[prefix.len]) and 47 | std.mem.startsWith(u8, buf.items[prefix.len + 1 ..], "%3A")) 48 | { 49 | buf.items[prefix.len] = std.ascii.toLower(buf.items[prefix.len]); 50 | } 51 | } 52 | 53 | return buf.toOwnedSlice(allocator); 54 | } 55 | 56 | test fromPath { 57 | if (builtin.os.tag == .windows) { 58 | const fromPathWin = try fromPath(std.testing.allocator, "C:\\main.zig"); 59 | defer std.testing.allocator.free(fromPathWin); 60 | try std.testing.expectEqualStrings("file:///c%3A/main.zig", fromPathWin); 61 | } 62 | 63 | if (builtin.os.tag != .windows) { 64 | const fromPathUnix = try fromPath(std.testing.allocator, "/home/main.zig"); 65 | defer std.testing.allocator.free(fromPathUnix); 66 | try std.testing.expectEqualStrings("file:///home/main.zig", fromPathUnix); 67 | } 68 | } 69 | 70 | /// Parses a Uri and returns the unescaped path 71 | /// Caller owns the returned memory 72 | pub fn parse(allocator: std.mem.Allocator, str: []const u8) (std.Uri.ParseError || error{OutOfMemory})![]u8 { 73 | var uri = try std.Uri.parse(str); 74 | if (!std.mem.eql(u8, uri.scheme, "file")) return error.InvalidFormat; 75 | if (builtin.os.tag == .windows and uri.path.percent_encoded[0] == '/') { 76 | uri.path.percent_encoded = uri.path.percent_encoded[1..]; 77 | } 78 | return try std.fmt.allocPrint(allocator, "{raw}", .{uri.path}); 79 | } 80 | -------------------------------------------------------------------------------- /src/zig-components/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) Zig contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/zls.zig: -------------------------------------------------------------------------------- 1 | //! Used by tests as a package, can be used by tools such as 2 | //! zigbot9001 to take advantage of zls' tools 3 | 4 | pub const build_options = @import("build_options"); 5 | 6 | pub const ast = @import("ast.zig"); 7 | pub const Analyser = @import("analysis.zig"); 8 | pub const debug = @import("debug.zig"); 9 | pub const offsets = @import("offsets.zig"); 10 | pub const Config = @import("Config.zig"); 11 | pub const Server = @import("Server.zig"); 12 | pub const translate_c = @import("translate_c.zig"); 13 | pub const lsp = @import("lsp"); 14 | pub const types = lsp.types; 15 | pub const URI = @import("uri.zig"); 16 | pub const DocumentStore = @import("DocumentStore.zig"); 17 | pub const diff = @import("diff.zig"); 18 | pub const analyser = @import("analyser/analyser.zig"); 19 | pub const configuration = @import("configuration.zig"); 20 | pub const DocumentScope = @import("DocumentScope.zig"); 21 | pub const BuildRunnerVersion = @import("build_runner/BuildRunnerVersion.zig"); 22 | pub const DiagnosticsCollection = @import("DiagnosticsCollection.zig"); 23 | 24 | pub const signature_help = @import("features/signature_help.zig"); 25 | pub const references = @import("features/references.zig"); 26 | pub const semantic_tokens = @import("features/semantic_tokens.zig"); 27 | pub const inlay_hints = @import("features/inlay_hints.zig"); 28 | pub const code_actions = @import("features/code_actions.zig"); 29 | pub const folding_range = @import("features/folding_range.zig"); 30 | pub const document_symbol = @import("features/document_symbol.zig"); 31 | pub const completions = @import("features/completions.zig"); 32 | pub const goto = @import("features/goto.zig"); 33 | pub const hover_handler = @import("features/hover.zig"); 34 | pub const selection_range = @import("features/selection_range.zig"); 35 | pub const diagnostics = @import("features/diagnostics.zig"); 36 | 37 | comptime { 38 | const std = @import("std"); 39 | std.testing.refAllDecls(@This()); 40 | } 41 | -------------------------------------------------------------------------------- /tests/add_analysis_cases.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn addCases( 4 | b: *std.Build, 5 | test_step: *std.Build.Step, 6 | test_filters: []const []const u8, 7 | ) void { 8 | const cases_dir = b.path("tests/analysis"); 9 | const cases_path_from_root = b.pathFromRoot("tests/analysis"); 10 | 11 | const check_exe = b.addExecutable(.{ 12 | .name = "analysis_check", 13 | .root_source_file = b.path("tests/analysis_check.zig"), 14 | .target = b.graph.host, 15 | }); 16 | check_exe.root_module.addImport("zls", b.modules.get("zls").?); 17 | 18 | // https://github.com/ziglang/zig/issues/20605 19 | var dir = std.fs.openDirAbsolute(b.pathFromRoot(cases_path_from_root), .{ .iterate = true }) catch |err| 20 | std.debug.panic("failed to open '{s}': {}", .{ cases_path_from_root, err }); 21 | defer dir.close(); 22 | 23 | var it = dir.iterate(); 24 | 25 | while (true) { 26 | const entry = it.next() catch |err| 27 | std.debug.panic("failed to walk directory '{s}': {}", .{ cases_path_from_root, err }) orelse break; 28 | 29 | if (entry.kind != .file) continue; 30 | if (!std.mem.eql(u8, std.fs.path.extension(entry.name), ".zig")) continue; 31 | 32 | for (test_filters) |test_filter| { 33 | if (std.mem.indexOf(u8, entry.name, test_filter) != null) break; 34 | } else if (test_filters.len > 0) continue; 35 | 36 | const run_check = std.Build.Step.Run.create(b, b.fmt("run analysis on {s}", .{entry.name})); 37 | run_check.producer = check_exe; 38 | run_check.addArtifactArg(check_exe); 39 | run_check.addArg("--zig-exe-path"); 40 | run_check.addFileArg(.{ .cwd_relative = b.graph.zig_exe }); 41 | run_check.addArg("--zig-lib-path"); 42 | run_check.addDirectoryArg(.{ .cwd_relative = b.fmt("{}", .{b.graph.zig_lib_directory}) }); 43 | run_check.addFileArg(cases_dir.path(b, entry.name)); 44 | 45 | test_step.dependOn(&run_check.step); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/analysis/array.zig: -------------------------------------------------------------------------------- 1 | const ArrayType = [3]u8; 2 | // ^^^^^^^^^ (type)([3]u8) 3 | const ArrayTypeWithSentinel = [3:0]u8; 4 | // ^^^^^^^^^^^^^^^^^^^^^ (type)([3:0]u8) 5 | 6 | const empty_array: [0]u8 = undefined; 7 | // ^^^^^^^^^^^ ([0]u8)() 8 | const empty_array_len = empty_array.len; 9 | // ^^^^^^^^^^^^^^^ (usize)() 10 | 11 | const length = 3; 12 | const unknown_length: usize = undefined; 13 | var runtime_index: usize = 5; 14 | 15 | const some_array: [length]u8 = undefined; 16 | // ^^^^^^^^^^ ([3]u8)() 17 | 18 | const some_unsized_array: [unknown_length]u8 = undefined; 19 | // ^^^^^^^^^^^^^^^^^^ ([?]u8)() 20 | 21 | const some_array_len = some_array.len; 22 | // ^^^^^^^^^^^^^^ (usize)(3) 23 | 24 | const some_unsized_array_len = some_unsized_array.len; 25 | // ^^^^^^^^^^^^^^^^^^^^^^ (usize)() 26 | 27 | const array_indexing = some_array[0]; 28 | // ^^^^^^^^^^^^^^ (u8)() 29 | 30 | // TODO this should be `*const [2]u8` 31 | const array_slice_open_1 = some_array[1..]; 32 | // ^^^^^^^^^^^^^^^^^^ (*[2]u8)() 33 | 34 | // TODO this should be `*const [0]u8` 35 | const array_slice_open_3 = some_array[3..]; 36 | // ^^^^^^^^^^^^^^^^^^ (*[0]u8)() 37 | 38 | // TODO this should be `*const [?]u8` 39 | const array_slice_open_4 = some_array[4..]; 40 | // ^^^^^^^^^^^^^^^^^^ (*[?]u8)() 41 | 42 | const array_slice_open_runtime = some_array[runtime_index..]; 43 | // ^^^^^^^^^^^^^^^^^^^^^^^^ ([]u8)() 44 | 45 | // TODO this should be `*const [2]u8` 46 | const array_slice_0_2 = some_array[0..2]; 47 | // ^^^^^^^^^^^^^^^ (*[2]u8)() 48 | 49 | // TODO this should be `*const [2 :0]u8` 50 | const array_slice_0_2_sentinel = some_array[0..2 :0]; 51 | // TODO ^^^^^^^^^^^^^^^ ([:0]u8)() 52 | 53 | // TODO this should be `*const [?]u8` 54 | const array_slice_0_5 = some_array[0..5]; 55 | // ^^^^^^^^^^^^^^^ (*[?]u8)() 56 | 57 | // TODO this should be `*const [?]u8` 58 | const array_slice_3_2 = some_array[3..2]; 59 | // ^^^^^^^^^^^^^^^ (*[?]u8)() 60 | 61 | const array_slice_0_runtime = some_array[0..runtime_index]; 62 | // ^^^^^^^^^^^^^^^^^^^^^ ([]u8)() 63 | 64 | const array_slice_with_sentinel = some_array[0..runtime_index :0]; 65 | // TODO ^^^^^^^^^^^^^^^^^^^^^^^^^ ([:0]u8)() 66 | 67 | // 68 | // Array init 69 | // 70 | 71 | const array_init = [length]u8{}; 72 | // ^^^^^^^^^^ ([3]u8)() 73 | const array_init_inferred_len_0 = [_]u8{}; 74 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ ([0]u8)() 75 | const array_init_inferred_len_3 = [_]u8{ 1, 2, 3 }; 76 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ ([3]u8)() 77 | 78 | comptime { 79 | // Use @compileLog to verify the expected type with the compiler: 80 | // @compileLog(some_array); 81 | } 82 | -------------------------------------------------------------------------------- /tests/analysis/basic.zig: -------------------------------------------------------------------------------- 1 | const alpha: bool = true; 2 | // ^^^^^ (bool)() 3 | const beta: bool = false; 4 | // ^^^^ (bool)() 5 | const gamma: type = bool; 6 | // ^^^^^ (type)(bool) 7 | const delta: comptime_int = 4; 8 | // ^^^^^ (comptime_int)() 9 | const epsilon = null; 10 | // ^^^^^^^ (@TypeOf(null))(null) 11 | const zeta: type = u32; 12 | // ^^^^ (type)(u32) 13 | const eta: type = isize; 14 | // ^^^ (type)(isize) 15 | const theta = true; 16 | // ^^^^^ (bool)(true) 17 | const iota = false; 18 | // ^^^^ (bool)(false) 19 | const kappa = bool; 20 | // ^^^^^ (type)(bool) 21 | const lambda = 4; 22 | // ^^^^^^ (comptime_int)() 23 | const mu = undefined; 24 | // ^^ (@TypeOf(undefined))(undefined) 25 | const nu: type = i1; 26 | // ^^ (type)(i1) 27 | const xi: type = usize; 28 | // ^^ (type)(usize) 29 | const omicron = 0; 30 | // ^^^^^^^ ()() 31 | const pi = 3.14159; 32 | // ^^ (comptime_float)() 33 | const rho: type = anyopaque; 34 | // ^^^ (type)(anyopaque) 35 | const sigma = noreturn; 36 | // ^^^^^ (type)(noreturn) 37 | const tau = anyerror; 38 | // ^^^ (type)(anyerror) 39 | const upsilon = 0; 40 | // ^^^^^^^ ()() 41 | const phi = 0; 42 | // ^^^ ()() 43 | const chi = 0; 44 | // ^^^ ()() 45 | const psi = 0; 46 | // ^^^ ()() 47 | const omega = 0; 48 | // ^^^^^ ()() 49 | -------------------------------------------------------------------------------- /tests/analysis/optional.zig: -------------------------------------------------------------------------------- 1 | const alpha: ?u32 = undefined; 2 | // ^^^^^ (?u32)() 3 | 4 | const beta = alpha.?; 5 | // ^^^^ (u32)() 6 | 7 | const gamma = if (alpha) |value| value else null; 8 | // ^^^^^ (u32)() 9 | 10 | const delta = alpha orelse unreachable; 11 | // ^^^^^ (u32)() 12 | 13 | const epsilon = alpha.?; 14 | // ^^^^^^^ (u32)() 15 | 16 | const zeta = alpha orelse null; 17 | // TODO ^^^^ (?u32)() 18 | 19 | const eta = alpha orelse 5; 20 | // ^^^ (u32)() 21 | -------------------------------------------------------------------------------- /tests/analysis/peer_type_resolution.zig: -------------------------------------------------------------------------------- 1 | const S = struct { 2 | int: i64, 3 | float: f32, 4 | }; 5 | 6 | pub fn main() void { 7 | var runtime_bool: bool = undefined; 8 | 9 | const widened_int_0 = if (runtime_bool) @as(i8, undefined) else @as(i16, undefined); 10 | _ = widened_int_0; 11 | // ^^^^^^^^^^^^^ (i16)() 12 | 13 | const widened_int_1 = if (runtime_bool) @as(i16, undefined) else @as(i8, undefined); 14 | _ = widened_int_1; 15 | // ^^^^^^^^^^^^^ (i16)() 16 | 17 | const optional_0 = if (runtime_bool) @as(S, undefined) else @as(?S, undefined); 18 | _ = optional_0; 19 | // ^^^^^^^^^^ (?S)() 20 | 21 | const optional_1 = if (runtime_bool) @as(?S, undefined) else @as(S, undefined); 22 | _ = optional_1; 23 | // ^^^^^^^^^^ (?S)() 24 | 25 | const optional_2 = if (runtime_bool) null else @as(S, undefined); 26 | _ = optional_2; 27 | // ^^^^^^^^^^ (?S)() 28 | 29 | const optional_3 = if (runtime_bool) @as(S, undefined) else null; 30 | _ = optional_3; 31 | // ^^^^^^^^^^ (?S)() 32 | 33 | const optional_4 = if (runtime_bool) null else @as(?S, undefined); 34 | _ = optional_4; 35 | // ^^^^^^^^^^ (?S)() 36 | 37 | const optional_5 = if (runtime_bool) @as(?S, undefined) else null; 38 | _ = optional_5; 39 | // ^^^^^^^^^^ (?S)() 40 | 41 | const comptime_int_and_void = if (runtime_bool) 0 else {}; 42 | _ = comptime_int_and_void; 43 | // ^^^^^^^^^^^^^^^^^^^^^ (either type)() 44 | 45 | runtime_bool = undefined; 46 | } 47 | -------------------------------------------------------------------------------- /tests/analysis/pointer.zig: -------------------------------------------------------------------------------- 1 | // 2 | // single item pointer *T 3 | // 4 | 5 | const one_u32: *const u32 = &@as(u32, 5); 6 | // ^^^^^^^ (*const u32)() 7 | 8 | const one_u32_deref = one_u32.*; 9 | // ^^^^^^^^^^^^^ (u32)() 10 | 11 | const one_u32_indexing = one_u32[0]; 12 | // ^^^^^^^^^^^^^^^^ (unknown)() 13 | 14 | const one_u32_slice_len_0_5 = one_u32[0..5]; 15 | // ^^^^^^^^^^^^^^^^^^^^^ (unknown)() 16 | 17 | const one_u32_slice_len_0_0 = one_u32[0..0]; 18 | // ^^^^^^^^^^^^^^^^^^^^^ (*const [0]u32)() 19 | 20 | const one_u32_slice_len_0_1 = one_u32[0..1]; 21 | // ^^^^^^^^^^^^^^^^^^^^^ (*const [1]u32)() 22 | 23 | const one_u32_slice_len_1_1 = one_u32[1..1]; 24 | // ^^^^^^^^^^^^^^^^^^^^^ (*const [0]u32)() 25 | 26 | const one_u32_slice_open = one_u32[1..]; 27 | // ^^^^^^^^^^^^^^^^^^ (unknown)() 28 | 29 | const one_u32_orelse = one_u32 orelse unreachable; 30 | // ^^^^^^^^^^^^^^ (unknown)() 31 | 32 | const one_u32_unwrap = one_u32.?; 33 | // ^^^^^^^^^^^^^^ (unknown)() 34 | 35 | // 36 | // many item pointer [*]T 37 | // 38 | 39 | const many_u32: [*]const u32 = &[_]u32{ 1, 2 }; 40 | // ^^^^^^^^ ([*]const u32)() 41 | 42 | const many_u32_deref = many_u32.*; 43 | // ^^^^^^^^^^^^^^ (unknown)() 44 | 45 | const many_u32_indexing = many_u32[0]; 46 | // ^^^^^^^^^^^^^^^^^ (u32)() 47 | 48 | const many_u32_slice_len_comptime = many_u32[0..2]; 49 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ (*const [2]u32)() 50 | 51 | const many_u32_slice_len_runtime = many_u32[0..runtime_index]; 52 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^ ([]const u32)() 53 | 54 | const many_u32_slice_open = many_u32[1..]; 55 | // ^^^^^^^^^^^^^^^^^^^ ([*]const u32)() 56 | 57 | const many_u32_orelse = many_u32 orelse unreachable; 58 | // ^^^^^^^^^^^^^^^ (unknown)() 59 | 60 | const many_u32_unwrap = many_u32.?; 61 | // ^^^^^^^^^^^^^^^ (unknown)() 62 | 63 | // 64 | // slice []T 65 | // 66 | 67 | const slice_u32: []const u32 = &.{ 1, 2 }; 68 | // ^^^^^^^^^ ([]const u32)() 69 | 70 | const slice_u32_deref = slice_u32.*; 71 | // ^^^^^^^^^^^^^^^ (unknown)() 72 | 73 | const slice_u32_indexing = slice_u32[0]; 74 | // ^^^^^^^^^^^^^^^^^^ (u32)() 75 | 76 | const slice_u32_slice_len_comptime = slice_u32[0..2]; 77 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (*const [2]u32)() 78 | 79 | const slice_u32_slice_len_runtime = slice_u32[0..runtime_index]; 80 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ([]const u32)() 81 | 82 | // TODO this should be `*const [1]u32` 83 | const slice_u32_slice_open = slice_u32[1..]; 84 | // ^^^^^^^^^^^^^^^^^^^^ ([]const u32)() 85 | 86 | const slice_u32_orelse = slice_u32 orelse unreachable; 87 | // ^^^^^^^^^^^^^^^^ (unknown)() 88 | 89 | const slice_u32_unwrap = slice_u32.?; 90 | // ^^^^^^^^^^^^^^^^ (unknown)() 91 | 92 | // 93 | // C pointer [*c]T 94 | // 95 | 96 | const c_u32: [*c]const u32 = &[_]u32{ 1, 2 }; 97 | // ^^^^^ ([*c]const u32)() 98 | 99 | const c_u32_deref = c_u32.*; 100 | // ^^^^^^^^^^^ (u32)() 101 | 102 | const c_u32_indexing = c_u32[0]; 103 | // ^^^^^^^^^^^^^^ (u32)() 104 | 105 | const c_u32_slice_len_comptime = c_u32[0..2]; 106 | // ^^^^^^^^^^^^^^^^^^^^^^^^ (*const [2]u32)() 107 | 108 | const c_u32_slice_len_runtime = c_u32[0..runtime_index]; 109 | // ^^^^^^^^^^^^^^^^^^^^^^^ ([]const u32)() 110 | 111 | const c_u32_slice_open = c_u32[1..]; 112 | // ^^^^^^^^^^^^^^^^ ([*c]const u32)() 113 | 114 | const c_u32_orelse = c_u32 orelse unreachable; 115 | // ^^^^^^^^^^^^ ([*c]const u32)() 116 | 117 | const c_u32_unwrap = c_u32.?; 118 | // ^^^^^^^^^^^^ ([*c]const u32)() 119 | 120 | var runtime_index: usize = 5; 121 | 122 | comptime { 123 | // Use @compileLog to verify the expected type with the compiler: 124 | // @compileLog(many_u32_slice_len_comptime); 125 | } 126 | -------------------------------------------------------------------------------- /tests/analysis/tuple.zig: -------------------------------------------------------------------------------- 1 | const TupleType = struct { i64, f32 }; 2 | // ^^^^^^^^^ (type)(struct { i64, f32 }) 3 | 4 | const some_tuple: struct { i64, f32 } = undefined; 5 | // ^^^^^^^^^^ (struct { i64, f32 })() 6 | 7 | const some_tuple_array_access_0 = some_tuple[0]; 8 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ (i64)() 9 | const some_tuple_array_access_1 = some_tuple[1]; 10 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ (f32)() 11 | const some_tuple_array_access_2 = some_tuple[2]; 12 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ (unknown)() 13 | 14 | const either_tuple = if (true) .{undefined} else .{ undefined, undefined }; 15 | // ^^^^^^^^^^^^ (either type)() 16 | 17 | comptime { 18 | const some_tuple_0, const some_tuple_1 = some_tuple; 19 | // ^^^^^^^^^^^^ (i64)() 20 | // ^^^^^^^^^^^^ (f32)() 21 | _ = some_tuple_0; 22 | _ = some_tuple_1; 23 | } 24 | 25 | const int: i64 = undefined; 26 | const float: f32 = undefined; 27 | const inferred_tuple = .{ int, float }; 28 | // ^^^^^^^^^^^^^^ (struct { i64, f32 })() 29 | 30 | const inferred_tuple_array_access_0 = inferred_tuple[0]; 31 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (i64)() 32 | const inferred_tuple_array_access_1 = inferred_tuple[1]; 33 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (f32)() 34 | const inferred_tuple_array_access_2 = inferred_tuple[2]; 35 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (unknown)() 36 | 37 | comptime { 38 | const inferred_tuple_0, const inferred_tuple_1 = inferred_tuple; 39 | // ^^^^^^^^^^^^^^^^ (i64)() 40 | // ^^^^^^^^^^^^^^^^ (f32)() 41 | _ = inferred_tuple_0; 42 | _ = inferred_tuple_1; 43 | } 44 | 45 | comptime { 46 | // Use @compileLog to verify the expected type with the compiler: 47 | // @compileLog(some_tuple); 48 | } 49 | -------------------------------------------------------------------------------- /tests/analysis_check.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | 5 | const helper = @import("helper.zig"); 6 | const ErrorBuilder = @import("ErrorBuilder.zig"); 7 | 8 | const Analyser = zls.Analyser; 9 | const offsets = zls.offsets; 10 | 11 | pub const std_options: std.Options = .{ 12 | .log_level = .warn, 13 | }; 14 | 15 | const Error = error{ 16 | FailedToCreateServer, 17 | OutOfMemory, 18 | InvalidTestItem, 19 | 20 | IdentifierNotFound, 21 | ResolveTypeFailed, 22 | WrongType, 23 | WrongValue, 24 | }; 25 | 26 | pub fn main() Error!void { 27 | var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; 28 | defer _ = general_purpose_allocator.deinit(); 29 | const gpa = general_purpose_allocator.allocator(); 30 | 31 | var arg_it = std.process.argsWithAllocator(gpa) catch |err| std.debug.panic("failed to collect args: {}", .{err}); 32 | defer arg_it.deinit(); 33 | 34 | _ = arg_it.skip(); 35 | 36 | var arena_allocator: std.heap.ArenaAllocator = .init(gpa); 37 | defer arena_allocator.deinit(); 38 | 39 | const arena = arena_allocator.allocator(); 40 | 41 | var config: zls.Config = .{}; 42 | 43 | var opt_file_path: ?[]const u8 = null; 44 | 45 | while (arg_it.next()) |arg| { 46 | if (!std.mem.startsWith(u8, arg, "--")) { 47 | if (opt_file_path != null) { 48 | std.log.err("duplicate source file argument", .{}); 49 | std.process.exit(1); 50 | } else { 51 | opt_file_path = try arena.dupe(u8, arg); 52 | } 53 | } else if (std.mem.eql(u8, arg, "--zig-exe-path")) { 54 | const zig_exe_path = arg_it.next() orelse { 55 | std.log.err("expected argument after '--zig-exe-path'.", .{}); 56 | std.process.exit(1); 57 | }; 58 | config.zig_exe_path = try arena.dupe(u8, zig_exe_path); 59 | } else if (std.mem.eql(u8, arg, "--zig-lib-path")) { 60 | const zig_lib_path = arg_it.next() orelse { 61 | std.log.err("expected argument after '--zig-lib-path'.", .{}); 62 | std.process.exit(1); 63 | }; 64 | config.zig_lib_path = try arena.dupe(u8, zig_lib_path); 65 | } else { 66 | std.log.err("Unrecognized argument '{s}'.", .{arg}); 67 | std.process.exit(1); 68 | } 69 | } 70 | 71 | const server = zls.Server.create(gpa) catch return error.FailedToCreateServer; 72 | defer server.destroy(); 73 | 74 | const file_path = opt_file_path orelse { 75 | std.log.err("Missing source file path argument", .{}); 76 | std.process.exit(1); 77 | }; 78 | 79 | const file = std.fs.openFileAbsolute(file_path, .{}) catch |err| std.debug.panic("failed to open {s}: {}", .{ file_path, err }); 80 | defer file.close(); 81 | 82 | const source = file.readToEndAllocOptions(gpa, std.math.maxInt(usize), null, @alignOf(u8), 0) catch |err| 83 | std.debug.panic("failed to read from {s}: {}", .{ file_path, err }); 84 | defer gpa.free(source); 85 | 86 | const handle_uri = try zls.URI.fromPath(arena, file_path); 87 | try server.document_store.openDocument(handle_uri, source); 88 | const handle: *zls.DocumentStore.Handle = server.document_store.handles.get(handle_uri).?; 89 | 90 | var error_builder: ErrorBuilder = .init(gpa); 91 | defer error_builder.deinit(); 92 | errdefer error_builder.writeDebug(); 93 | error_builder.file_name_visibility = .always; 94 | 95 | try error_builder.addFile(file_path, handle.tree.source); 96 | 97 | const annotations = helper.collectAnnotatedSourceLocations(gpa, handle.tree.source) catch |err| switch (err) { 98 | error.InvalidSourceLoc => std.debug.panic("{s} contains invalid annotated source locations: {}", .{ file_path, err }), 99 | error.OutOfMemory => |e| return e, 100 | }; 101 | defer gpa.free(annotations); 102 | 103 | var analyser = zls.Analyser.init(gpa, &server.document_store, &server.ip, handle); 104 | defer analyser.deinit(); 105 | 106 | for (annotations) |annotation| { 107 | const identifier_loc = annotation.loc; 108 | const identifier = offsets.locToSlice(handle.tree.source, identifier_loc); 109 | 110 | const test_item = parseAnnotatedSourceLoc(annotation) catch |err| { 111 | try error_builder.msgAtLoc("invalid annotated source location '{s}'", file_path, annotation.loc, .err, .{ 112 | annotation.content, 113 | }); 114 | return err; 115 | }; 116 | 117 | const decl = try analyser.lookupSymbolGlobal(handle, identifier, identifier_loc.start) orelse { 118 | try error_builder.msgAtLoc("failed to find identifier '{s}' here", file_path, annotation.loc, .err, .{ 119 | annotation.content, 120 | }); 121 | return error.IdentifierNotFound; 122 | }; 123 | 124 | const expect_unknown = (if (test_item.expected_type) |expected_type| std.mem.eql(u8, expected_type, "unknown") else false) and 125 | (if (test_item.expected_value) |expected_value| std.mem.eql(u8, expected_value, "unknown") else true) and 126 | test_item.expected_error == null; 127 | 128 | const ty = try decl.resolveType(&analyser) orelse { 129 | if (expect_unknown) continue; 130 | try error_builder.msgAtLoc("failed to resolve type of '{s}'", file_path, annotation.loc, .err, .{ 131 | identifier, 132 | }); 133 | return error.ResolveTypeFailed; 134 | }; 135 | 136 | if (expect_unknown) { 137 | const actual_type = try std.fmt.allocPrint(gpa, "{}", .{ty.fmt(&analyser, .{ 138 | .truncate_container_decls = false, 139 | })}); 140 | defer gpa.free(actual_type); 141 | 142 | try error_builder.msgAtLoc("expected unknown but got `{s}`", file_path, identifier_loc, .err, .{ 143 | actual_type, 144 | }); 145 | return error.WrongType; 146 | } 147 | 148 | if (test_item.expected_error) |_| { 149 | @panic("unsupported"); 150 | } 151 | 152 | if (test_item.expected_type) |expected_type| { 153 | const actual_type = try std.fmt.allocPrint(gpa, "{}", .{ty.fmt(&analyser, .{ 154 | .truncate_container_decls = false, 155 | })}); 156 | defer gpa.free(actual_type); 157 | 158 | if (!std.mem.eql(u8, expected_type, actual_type)) { 159 | try error_builder.msgAtLoc("expected type `{s}` but got `{s}`", file_path, identifier_loc, .err, .{ 160 | expected_type, 161 | actual_type, 162 | }); 163 | return error.WrongType; 164 | } 165 | } 166 | 167 | if (test_item.expected_value) |expected_value| { 168 | if (ty.data != .ip_index and !ty.is_type_val) { 169 | try error_builder.msgAtLoc("unsupported value check `{s}`", file_path, identifier_loc, .err, .{ 170 | expected_value, 171 | }); 172 | return error.WrongValue; 173 | } 174 | 175 | const actual_value = try std.fmt.allocPrint(gpa, "{}", .{ty.fmtTypeVal(&analyser, .{ 176 | .truncate_container_decls = false, 177 | })}); 178 | defer gpa.free(actual_value); 179 | 180 | if (!std.mem.eql(u8, expected_value, actual_value)) { 181 | try error_builder.msgAtLoc("expected value `{s}` but got `{s}`", file_path, identifier_loc, .err, .{ 182 | expected_value, 183 | actual_value, 184 | }); 185 | return error.WrongValue; 186 | } 187 | } 188 | } 189 | } 190 | 191 | const TestItem = struct { 192 | loc: offsets.Loc, 193 | expected_type: ?[]const u8 = null, 194 | expected_value: ?[]const u8 = null, 195 | expected_error: ?[]const u8 = null, 196 | }; 197 | 198 | fn parseAnnotatedSourceLoc(annotation: helper.AnnotatedSourceLoc) error{InvalidTestItem}!TestItem { 199 | const str = annotation.content; 200 | 201 | if (std.mem.startsWith(u8, str, "error:")) { 202 | return .{ 203 | .loc = annotation.loc, 204 | .expected_error = std.mem.trim(u8, str["error:".len..], &std.ascii.whitespace), 205 | }; 206 | } 207 | 208 | if (!std.mem.startsWith(u8, str, "(")) return error.InvalidTestItem; 209 | const expected_type_start = 1; 210 | const expected_type_end = expected_type_start + (findClosingBrace(str[expected_type_start..]) orelse return error.InvalidTestItem); 211 | 212 | if (!std.mem.startsWith(u8, str[expected_type_end + 1 ..], "(")) return error.InvalidTestItem; 213 | const expected_value_start = expected_type_end + 2; 214 | const expected_value_end = expected_value_start + (findClosingBrace(str[expected_value_start..]) orelse return error.InvalidTestItem); 215 | 216 | const expected_type = std.mem.trim( 217 | u8, 218 | offsets.locToSlice(str, .{ .start = expected_type_start, .end = expected_type_end }), 219 | &std.ascii.whitespace, 220 | ); 221 | const expected_value = std.mem.trim( 222 | u8, 223 | offsets.locToSlice(str, .{ .start = expected_value_start, .end = expected_value_end }), 224 | &std.ascii.whitespace, 225 | ); 226 | 227 | return .{ 228 | .loc = annotation.loc, 229 | .expected_type = if (expected_type.len != 0) expected_type else null, 230 | .expected_value = if (expected_value.len != 0) expected_value else null, 231 | }; 232 | } 233 | 234 | fn findClosingBrace(source: []const u8) ?usize { 235 | var depth: usize = 0; 236 | for (source, 0..) |c, i| { 237 | switch (c) { 238 | '(' => depth += 1, 239 | ')' => { 240 | if (depth == 0) return i; 241 | depth -= 1; 242 | }, 243 | else => continue, 244 | } 245 | } 246 | return null; 247 | } 248 | -------------------------------------------------------------------------------- /tests/context.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | const test_options = @import("test_options"); 5 | 6 | const Config = zls.Config; 7 | const Server = zls.Server; 8 | const types = zls.types; 9 | 10 | const default_config: Config = .{ 11 | .semantic_tokens = .full, 12 | .inlay_hints_exclude_single_argument = false, 13 | .inlay_hints_show_builtin = true, 14 | 15 | .zig_exe_path = test_options.zig_exe_path, 16 | .zig_lib_path = test_options.zig_lib_path, 17 | .global_cache_path = test_options.global_cache_path, 18 | }; 19 | 20 | const allocator = std.testing.allocator; 21 | 22 | pub const Context = struct { 23 | server: *Server, 24 | arena: std.heap.ArenaAllocator, 25 | file_id: u32 = 0, 26 | 27 | var resolved_config_arena: std.heap.ArenaAllocator.State = undefined; 28 | var resolved_config: ?Config = null; 29 | 30 | pub fn init() !Context { 31 | const server = try Server.create(allocator); 32 | errdefer server.destroy(); 33 | 34 | if (resolved_config) |config| { 35 | // The configuration has previously been resolved an stored in `resolved_config` 36 | try server.updateConfiguration2(config, .{ .resolve = false }); 37 | } else { 38 | try server.updateConfiguration2(default_config, .{}); 39 | 40 | const config_string = try std.json.stringifyAlloc(allocator, server.config, .{ .whitespace = .indent_2 }); 41 | defer allocator.free(config_string); 42 | 43 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 44 | errdefer arena.deinit(); 45 | 46 | const duped_config = try std.json.parseFromSliceLeaky(Config, arena.allocator(), config_string, .{ .allocate = .alloc_always }); 47 | 48 | resolved_config_arena = arena.state; 49 | resolved_config = duped_config; 50 | } 51 | 52 | var context: Context = .{ 53 | .server = server, 54 | .arena = std.heap.ArenaAllocator.init(allocator), 55 | }; 56 | 57 | _ = try context.server.sendRequestSync(context.arena.allocator(), "initialize", .{ .capabilities = .{} }); 58 | _ = try context.server.sendNotificationSync(context.arena.allocator(), "initialized", .{}); 59 | 60 | return context; 61 | } 62 | 63 | pub fn deinit(self: *Context) void { 64 | _ = self.server.sendRequestSync(self.arena.allocator(), "shutdown", {}) catch unreachable; 65 | self.server.sendNotificationSync(self.arena.allocator(), "exit", {}) catch unreachable; 66 | std.debug.assert(self.server.status == .exiting_success); 67 | self.server.destroy(); 68 | self.arena.deinit(); 69 | } 70 | 71 | // helper 72 | pub fn addDocument(self: *Context, options: struct { 73 | uri: ?[]const u8 = null, 74 | source: []const u8, 75 | mode: std.zig.Ast.Mode = .zig, 76 | }) ![]const u8 { 77 | const fmt = switch (builtin.os.tag) { 78 | .windows => "file:///C:\\nonexistent\\test-{d}.{s}", 79 | else => "file:///nonexistent/test-{d}.{s}", 80 | }; 81 | const uri = options.uri orelse try std.fmt.allocPrint( 82 | self.arena.allocator(), 83 | fmt, 84 | .{ self.file_id, @tagName(options.mode) }, 85 | ); 86 | 87 | const params = types.DidOpenTextDocumentParams{ 88 | .textDocument = .{ 89 | .uri = uri, 90 | .languageId = "zig", 91 | .version = 420, 92 | .text = options.source, 93 | }, 94 | }; 95 | 96 | _ = try self.server.sendNotificationSync(self.arena.allocator(), "textDocument/didOpen", params); 97 | 98 | self.file_id += 1; 99 | return uri; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /tests/language_features/cimport.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | 4 | const Ast = std.zig.Ast; 5 | 6 | const Context = @import("../context.zig").Context; 7 | 8 | const offsets = zls.offsets; 9 | const translate_c = zls.translate_c; 10 | 11 | const allocator: std.mem.Allocator = std.testing.allocator; 12 | 13 | test "zig compile server - translate c" { 14 | var result1 = try testTranslate( 15 | \\void foo(int); 16 | \\void bar(float*); 17 | ); 18 | defer result1.deinit(allocator); 19 | try std.testing.expect(result1 == .success); 20 | 21 | var result2 = try testTranslate( 22 | \\#include 23 | ); 24 | defer result2.deinit(allocator); 25 | try std.testing.expect(result2 == .failure); 26 | } 27 | 28 | test "empty" { 29 | try testConvertCInclude("@cImport()", ""); 30 | try testConvertCInclude("@cImport({})", ""); 31 | try testConvertCInclude("@cImport({{}, {}})", ""); 32 | } 33 | 34 | test "cInclude" { 35 | try testConvertCInclude( 36 | \\@cImport(@cInclude("foo.zig")) 37 | , 38 | \\#include 39 | ); 40 | 41 | try testConvertCInclude( 42 | \\@cImport(@cInclude("foo.zig"), @cInclude("bar.zig")) 43 | , 44 | \\#include 45 | \\#include 46 | ); 47 | } 48 | 49 | test "cDefine" { 50 | try testConvertCInclude( 51 | \\@cImport(@cDefine("FOO", "BAR")) 52 | , 53 | \\#define FOO BAR 54 | ); 55 | try testConvertCInclude( 56 | \\@cImport(@cDefine("FOO", {})) 57 | , 58 | \\#define FOO 59 | ); 60 | } 61 | 62 | test "cUndef" { 63 | try testConvertCInclude( 64 | \\@cImport(@cUndef("FOO")) 65 | , 66 | \\#undef FOO 67 | ); 68 | } 69 | 70 | fn testConvertCInclude(cimport_source: []const u8, expected: []const u8) !void { 71 | const source: [:0]u8 = try std.fmt.allocPrintZ(allocator, "const c = {s};", .{cimport_source}); 72 | defer allocator.free(source); 73 | 74 | var tree = try Ast.parse(allocator, source, .zig); 75 | defer tree.deinit(allocator); 76 | 77 | const node_tags = tree.nodes.items(.tag); 78 | const main_tokens = tree.nodes.items(.main_token); 79 | 80 | const node: Ast.Node.Index = blk: { 81 | for (node_tags, main_tokens, 0..) |tag, token, i| { 82 | switch (tag) { 83 | .builtin_call_two, 84 | .builtin_call_two_comma, 85 | .builtin_call, 86 | .builtin_call_comma, 87 | => {}, 88 | else => continue, 89 | } 90 | 91 | if (!std.mem.eql(u8, offsets.tokenToSlice(tree, token), "@cImport")) continue; 92 | 93 | break :blk @intCast(i); 94 | } 95 | return error.TestUnexpectedResult; // source doesn't contain a cImport 96 | }; 97 | 98 | const output = try translate_c.convertCInclude(allocator, tree, node); 99 | defer allocator.free(output); 100 | 101 | const trimmed_output = std.mem.trimRight(u8, output, &.{'\n'}); 102 | 103 | try std.testing.expectEqualStrings(expected, trimmed_output); 104 | } 105 | 106 | fn testTranslate(c_source: []const u8) !translate_c.Result { 107 | if (!std.process.can_spawn) return error.SkipZigTest; 108 | 109 | var ctx = try Context.init(); 110 | defer ctx.deinit(); 111 | 112 | var result = (try translate_c.translate( 113 | allocator, 114 | zls.DocumentStore.Config.fromMainConfig(ctx.server.config), 115 | &.{}, 116 | &.{}, 117 | c_source, 118 | )).?; 119 | errdefer result.deinit(allocator); 120 | 121 | switch (result) { 122 | .success => |uri| { 123 | const path = try zls.URI.parse(allocator, uri); 124 | defer allocator.free(path); 125 | try std.testing.expect(std.fs.path.isAbsolute(path)); 126 | try std.fs.accessAbsolute(path, .{}); 127 | }, 128 | .failure => |message| { 129 | try std.testing.expect(message.errorMessageCount() != 0); 130 | }, 131 | } 132 | return result; 133 | } 134 | -------------------------------------------------------------------------------- /tests/lifecycle.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const test_options = @import("test_options"); 4 | 5 | const allocator = std.testing.allocator; 6 | 7 | test "LSP lifecycle" { 8 | var server = try zls.Server.create(allocator); 9 | defer server.destroy(); 10 | 11 | try server.updateConfiguration2(.{ 12 | .zig_exe_path = test_options.zig_exe_path, 13 | .zig_lib_path = null, 14 | .global_cache_path = test_options.global_cache_path, 15 | }, .{}); 16 | 17 | var arena_allocator = std.heap.ArenaAllocator.init(allocator); 18 | defer arena_allocator.deinit(); 19 | const arena = arena_allocator.allocator(); 20 | 21 | try std.testing.expectEqual(zls.Server.Status.uninitialized, server.status); 22 | _ = try server.sendRequestSync(arena, "initialize", .{ .capabilities = .{} }); 23 | try std.testing.expectEqual(zls.Server.Status.initializing, server.status); 24 | try server.sendNotificationSync(arena, "initialized", .{}); 25 | try std.testing.expectEqual(zls.Server.Status.initialized, server.status); 26 | _ = try server.sendRequestSync(arena, "shutdown", {}); 27 | try std.testing.expectEqual(zls.Server.Status.shutdown, server.status); 28 | try server.sendNotificationSync(arena, "exit", {}); 29 | try std.testing.expectEqual(zls.Server.Status.exiting_success, server.status); 30 | } 31 | -------------------------------------------------------------------------------- /tests/lsp_features/document_symbol.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | 5 | const Context = @import("../context.zig").Context; 6 | 7 | const types = zls.types; 8 | 9 | const allocator: std.mem.Allocator = std.testing.allocator; 10 | 11 | test "container decl" { 12 | try testDocumentSymbol( 13 | \\const S = struct { 14 | \\ fn f() void {} 15 | \\}; 16 | , 17 | \\Constant S 18 | \\ Function f 19 | ); 20 | try testDocumentSymbol( 21 | \\const S = struct { 22 | \\ alpha: u32, 23 | \\ fn f() void {} 24 | \\}; 25 | , 26 | \\Constant S 27 | \\ Field alpha 28 | \\ Function f 29 | ); 30 | } 31 | 32 | test "tuple" { 33 | try testDocumentSymbol( 34 | \\const S = struct { 35 | \\ []const u8, 36 | \\ u32, 37 | \\}; 38 | , 39 | \\Constant S 40 | ); 41 | } 42 | 43 | test "enum" { 44 | try testDocumentSymbol( 45 | \\const E = enum { 46 | \\ alpha, 47 | \\ beta, 48 | \\}; 49 | , 50 | \\Constant E 51 | \\ EnumMember alpha 52 | \\ EnumMember beta 53 | ); 54 | } 55 | 56 | test "test decl" { 57 | try testDocumentSymbol( 58 | \\test foo {} 59 | \\test "bar" {} 60 | \\test {} 61 | , 62 | \\Method foo 63 | \\Method bar 64 | ); 65 | } 66 | 67 | // https://github.com/zigtools/zls/issues/1583 68 | test "builtin" { 69 | try testDocumentSymbol( 70 | \\comptime { 71 | \\ @abs(); 72 | \\ @foo(); 73 | \\ @foo 74 | \\} 75 | \\ 76 | , 77 | \\ 78 | ); 79 | } 80 | 81 | // https://github.com/zigtools/zls/issues/986 82 | test "nested struct with self" { 83 | try testDocumentSymbol( 84 | \\const Foo = struct { 85 | \\ const Self = @This(); 86 | \\ pub fn foo() !Self {} 87 | \\ const Bar = union {}; 88 | \\}; 89 | , 90 | \\Constant Foo 91 | \\ Constant Self 92 | \\ Function foo 93 | \\ Constant Bar 94 | ); 95 | } 96 | 97 | fn testDocumentSymbol(source: []const u8, want: []const u8) !void { 98 | var ctx = try Context.init(); 99 | defer ctx.deinit(); 100 | 101 | const test_uri = try ctx.addDocument(.{ .source = source }); 102 | 103 | const params = types.DocumentSymbolParams{ 104 | .textDocument = .{ .uri = test_uri }, 105 | }; 106 | 107 | const response = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/documentSymbol", params) orelse { 108 | std.debug.print("Server returned `null` as the result\n", .{}); 109 | return error.InvalidResponse; 110 | }; 111 | 112 | var got = std.ArrayListUnmanaged(u8){}; 113 | defer got.deinit(allocator); 114 | 115 | var stack = std.BoundedArray([]const types.DocumentSymbol, 16){}; 116 | stack.appendAssumeCapacity(response.array_of_DocumentSymbol); 117 | 118 | var writer = got.writer(allocator); 119 | while (stack.len > 0) { 120 | const depth = stack.len - 1; 121 | const top = stack.get(depth); 122 | if (top.len > 0) { 123 | try writer.writeByteNTimes(' ', (depth) * 2); 124 | try writer.print("{s} {s}\n", .{ @tagName(top[0].kind), top[0].name }); 125 | if (top[0].children) |children| { 126 | try stack.append(children); 127 | } 128 | stack.set(depth, top[1..]); 129 | } else { 130 | _ = stack.pop(); 131 | } 132 | } 133 | _ = got.pop(); // Final \n 134 | 135 | try std.testing.expectEqualStrings(want, got.items); 136 | } 137 | -------------------------------------------------------------------------------- /tests/lsp_features/folding_range.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | 5 | const Context = @import("../context.zig").Context; 6 | 7 | const types = zls.types; 8 | 9 | const allocator: std.mem.Allocator = std.testing.allocator; 10 | 11 | test "empty" { 12 | try testFoldingRange("", &.{}); 13 | } 14 | 15 | test "container type without members" { 16 | try testFoldingRange( 17 | \\const S = struct { 18 | \\}; 19 | , &.{ 20 | .{ .startLine = 0, .startCharacter = 18, .endLine = 1, .endCharacter = 0 }, 21 | }); 22 | try testFoldingRange( 23 | \\const S = struct { 24 | \\ // hello there 25 | \\}; 26 | , &.{ 27 | .{ .startLine = 0, .startCharacter = 18, .endLine = 2, .endCharacter = 0 }, 28 | }); 29 | } 30 | 31 | test "doc comment" { 32 | try testFoldingRange( 33 | \\/// hello 34 | \\/// world 35 | \\var foo = 5; 36 | , &.{ 37 | .{ .startLine = 0, .startCharacter = 0, .endLine = 1, .endCharacter = 9, .kind = .comment }, 38 | }); 39 | } 40 | 41 | test "region" { 42 | try testFoldingRange( 43 | \\const foo = 0; 44 | \\//#region 45 | \\const bar = 1; 46 | \\//#endregion 47 | \\const baz = 2; 48 | , &.{ 49 | .{ .startLine = 1, .startCharacter = 0, .endLine = 3, .endCharacter = 12, .kind = .region }, 50 | }); 51 | try testFoldingRange( 52 | \\//#region 53 | \\const foo = 0; 54 | \\//#region 55 | \\const bar = 1; 56 | \\//#endregion 57 | \\const baz = 2; 58 | \\//#endregion 59 | , &.{ 60 | .{ .startLine = 2, .startCharacter = 0, .endLine = 4, .endCharacter = 12, .kind = .region }, 61 | .{ .startLine = 0, .startCharacter = 0, .endLine = 6, .endCharacter = 12, .kind = .region }, 62 | }); 63 | } 64 | 65 | test "if" { 66 | try testFoldingRange( 67 | \\const foo = if (false) { 68 | \\ 69 | \\}; 70 | , &.{ 71 | .{ .startLine = 0, .startCharacter = 24, .endLine = 1, .endCharacter = 0 }, 72 | }); 73 | try testFoldingRange( 74 | \\const foo = if (false) { 75 | \\ 76 | \\} else { 77 | \\ 78 | \\}; 79 | , &.{ 80 | .{ .startLine = 0, .startCharacter = 24, .endLine = 1, .endCharacter = 0 }, 81 | .{ .startLine = 2, .startCharacter = 8, .endLine = 3, .endCharacter = 0 }, 82 | }); 83 | } 84 | 85 | test "for/while" { 86 | try testFoldingRange( 87 | \\const foo = for ("") |_| { 88 | \\ 89 | \\}; 90 | , &.{ 91 | .{ .startLine = 0, .startCharacter = 26, .endLine = 1, .endCharacter = 0 }, 92 | }); 93 | try testFoldingRange( 94 | \\const foo = for ("") |_| { 95 | \\ return; 96 | \\} else { 97 | \\ 98 | \\}; 99 | , &.{ 100 | .{ .startLine = 0, .startCharacter = 26, .endLine = 1, .endCharacter = 11 }, 101 | .{ .startLine = 2, .startCharacter = 8, .endLine = 3, .endCharacter = 0 }, 102 | }); 103 | 104 | try testFoldingRange( 105 | \\const foo = while (true) { 106 | \\ // 107 | \\}; 108 | , &.{ 109 | .{ .startLine = 0, .startCharacter = 26, .endLine = 1, .endCharacter = 6 }, 110 | }); 111 | try testFoldingRange( 112 | \\const foo = while (true) { 113 | \\ 114 | \\} else { 115 | \\ // 116 | \\}; 117 | , &.{ 118 | .{ .startLine = 0, .startCharacter = 26, .endLine = 1, .endCharacter = 0 }, 119 | .{ .startLine = 2, .startCharacter = 8, .endLine = 3, .endCharacter = 6 }, 120 | }); 121 | } 122 | 123 | test "switch" { 124 | try testFoldingRange( 125 | \\const foo = switch (5) { 126 | \\ 0 => {}, 127 | \\ 1 => {} 128 | \\}; 129 | , &.{ 130 | .{ .startLine = 0, .startCharacter = 24, .endLine = 3, .endCharacter = 0 }, 131 | }); 132 | try testFoldingRange( 133 | \\const foo = switch (5) { 134 | \\ 0 => {}, 135 | \\ 1 => {}, 136 | \\}; 137 | , &.{ 138 | .{ .startLine = 0, .startCharacter = 24, .endLine = 3, .endCharacter = 0 }, 139 | }); 140 | try testFoldingRange( 141 | \\const foo = switch (5) { 142 | \\ 0, 143 | \\ 1, 144 | \\ 2, 145 | \\ 3, 146 | \\ 4, 147 | \\ => {}, 148 | \\ else => {}, 149 | \\}; 150 | , &.{ 151 | .{ .startLine = 1, .startCharacter = 4, .endLine = 5, .endCharacter = 6 }, 152 | .{ .startLine = 0, .startCharacter = 24, .endLine = 8, .endCharacter = 0 }, 153 | }); 154 | } 155 | 156 | test "function" { 157 | try testFoldingRange( 158 | \\fn main() u32 { 159 | \\ return 1 + 1; 160 | \\} 161 | , &.{ 162 | .{ .startLine = 0, .startCharacter = 15, .endLine = 1, .endCharacter = 17 }, 163 | }); 164 | try testFoldingRange( 165 | \\fn main( 166 | \\ a: ?u32, 167 | \\ b: anytype, 168 | \\) !u32 {} 169 | , &.{ 170 | .{ .startLine = 0, .startCharacter = 8, .endLine = 2, .endCharacter = 15 }, 171 | }); 172 | try testFoldingRange( 173 | \\fn main( 174 | \\ a: ?u32, 175 | \\) !u32 { 176 | \\ return 1 + 1; 177 | \\} 178 | , &.{ 179 | .{ .startLine = 0, .startCharacter = 8, .endLine = 1, .endCharacter = 12 }, 180 | .{ .startLine = 2, .startCharacter = 8, .endLine = 3, .endCharacter = 17 }, 181 | }); 182 | } 183 | 184 | test "function with doc comment" { 185 | try testFoldingRange( 186 | \\/// this is 187 | \\/// a function 188 | \\fn foo( 189 | \\ /// this is a parameter 190 | \\ a: u32, 191 | \\ /// 192 | \\ /// this is another parameter 193 | \\ b: u32, 194 | \\) void {} 195 | , &.{ 196 | .{ .startLine = 0, .startCharacter = 0, .endLine = 1, .endCharacter = 14, .kind = .comment }, 197 | .{ .startLine = 5, .startCharacter = 4, .endLine = 6, .endCharacter = 33, .kind = .comment }, 198 | .{ .startLine = 2, .startCharacter = 7, .endLine = 7, .endCharacter = 11 }, 199 | }); 200 | } 201 | 202 | test "container decl" { 203 | try testFoldingRange( 204 | \\const Foo = struct { 205 | \\ alpha: u32, 206 | \\ beta: []const u8, 207 | \\}; 208 | , &.{ 209 | .{ .startLine = 0, .startCharacter = 20, .endLine = 3, .endCharacter = 0 }, 210 | }); 211 | try testFoldingRange( 212 | \\const Foo = struct { 213 | \\ /// doc comment 214 | \\ alpha: u32, 215 | \\ // beta: []const u8, 216 | \\}; 217 | , &.{ 218 | .{ .startLine = 0, .startCharacter = 20, .endLine = 4, .endCharacter = 0 }, 219 | }); 220 | try testFoldingRange( 221 | \\const Foo = packed struct(u32) { 222 | \\ alpha: u16, 223 | \\ beta: u16, 224 | \\}; 225 | , &.{ 226 | .{ .startLine = 0, .startCharacter = 32, .endLine = 3, .endCharacter = 0 }, 227 | }); 228 | try testFoldingRange( 229 | \\const Foo = union { 230 | \\ alpha: u32, 231 | \\ beta: []const u8, 232 | \\}; 233 | , &.{ 234 | .{ .startLine = 0, .startCharacter = 19, .endLine = 3, .endCharacter = 0 }, 235 | }); 236 | try testFoldingRange( 237 | \\const Foo = union(enum) { 238 | \\ alpha: u32, 239 | \\ beta: []const u8, 240 | \\}; 241 | , &.{ 242 | .{ .startLine = 0, .startCharacter = 25, .endLine = 3, .endCharacter = 0 }, 243 | }); 244 | try testFoldingRange( 245 | \\const Foo = struct { 246 | \\ fn foo() void {} 247 | \\}; 248 | , &.{ 249 | .{ .startLine = 0, .startCharacter = 20, .endLine = 2, .endCharacter = 0 }, 250 | }); 251 | try testFoldingRange( 252 | \\const Foo = struct { 253 | \\ fn foo() void {} 254 | \\ fn bar() void {} 255 | \\ // some comment 256 | \\}; 257 | , &.{ 258 | .{ .startLine = 0, .startCharacter = 20, .endLine = 4, .endCharacter = 0 }, 259 | }); 260 | } 261 | 262 | test "call" { 263 | try testFoldingRange( 264 | \\extern fn foo(a: bool, b: ?usize) void; 265 | \\const result = foo( 266 | \\ false, 267 | \\ null, 268 | \\); 269 | , &.{ 270 | .{ .startLine = 1, .startCharacter = 19, .endLine = 4, .endCharacter = 0 }, 271 | }); 272 | } 273 | 274 | test "multi-line string literal" { 275 | try testFoldingRange( 276 | \\const foo = 277 | \\ \\hello 278 | \\ \\world 279 | \\; 280 | , &.{ 281 | .{ .startLine = 1, .startCharacter = 4, .endLine = 2, .endCharacter = 11 }, 282 | }); 283 | } 284 | 285 | test "invalid condition within a `switch`" { 286 | try testFoldingRange( 287 | \\switch (a.) { 288 | \\} 289 | , &.{ 290 | .{ .startLine = 0, .startCharacter = 11, .endLine = 1, .endCharacter = 0 }, 291 | }); 292 | } 293 | 294 | test "weird code" { 295 | // the expected output is irrelevant, just ensure no crash 296 | try testFoldingRange( 297 | \\if ( {fn foo()} 298 | \\ 299 | , 300 | &.{}, 301 | ); 302 | } 303 | 304 | fn testFoldingRange(source: []const u8, expect: []const types.FoldingRange) !void { 305 | var ctx = try Context.init(); 306 | defer ctx.deinit(); 307 | 308 | const test_uri = try ctx.addDocument(.{ .source = source }); 309 | 310 | const params = types.FoldingRangeParams{ .textDocument = .{ .uri = test_uri } }; 311 | 312 | const response = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/foldingRange", params) orelse { 313 | std.debug.print("Server returned `null` as the result\n", .{}); 314 | return error.InvalidResponse; 315 | }; 316 | 317 | try std.testing.expectEqualSlices(types.FoldingRange, expect, response); 318 | } 319 | -------------------------------------------------------------------------------- /tests/lsp_features/references.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | 5 | const helper = @import("../helper.zig"); 6 | const Context = @import("../context.zig").Context; 7 | const ErrorBuilder = @import("../ErrorBuilder.zig"); 8 | 9 | const types = zls.types; 10 | const offsets = zls.offsets; 11 | 12 | const allocator: std.mem.Allocator = std.testing.allocator; 13 | 14 | test "references" { 15 | try testReferences( 16 | \\const <0> = 0; 17 | \\const foo = <0>; 18 | ); 19 | try testReferences( 20 | \\var <0> = 0; 21 | \\var foo = <0>; 22 | ); 23 | try testReferences( 24 | \\const <0> = struct {}; 25 | \\var foo: <0> = <0>{}; 26 | ); 27 | try testReferences( 28 | \\const <0> = enum {}; 29 | \\var foo: <0> = undefined; 30 | ); 31 | try testReferences( 32 | \\const <0> = union {}; 33 | \\var foo: <0> = <0>{}; 34 | ); 35 | try testReferences( 36 | \\fn <0>() void {} 37 | \\var foo = <0>(); 38 | ); 39 | try testReferences( 40 | \\const <0> = error{}; 41 | \\fn bar() <0>!void {} 42 | ); 43 | } 44 | 45 | test "global scope" { 46 | try testReferences( 47 | \\const foo = <0>; 48 | \\const <0> = 0; 49 | \\const bar = <0>; 50 | ); 51 | } 52 | 53 | test "local scope" { 54 | try testReferences( 55 | \\fn foo(<0>: u32, bar: u32) void { 56 | \\ return <0> + bar; 57 | \\} 58 | ); 59 | try testReferences( 60 | \\const foo = outer: { 61 | \\ _ = inner: { 62 | \\ const <0> = 0; 63 | \\ break :inner <0>; 64 | \\ }; 65 | \\ const <1> = 0; 66 | \\ break :outer <1>; 67 | \\}; 68 | \\const bar = foo; 69 | ); 70 | } 71 | 72 | test "destructuring" { 73 | try testReferences( 74 | \\const blk = { 75 | \\ const <0>, const foo = .{ 1, 2 }; 76 | \\ const bar = <0>; 77 | \\}; 78 | ); 79 | try testReferences( 80 | \\const blk = { 81 | \\ const foo, const <0> = .{ 1, 2 }; 82 | \\ const bar = <0>; 83 | \\}; 84 | ); 85 | } 86 | 87 | test "for/while capture" { 88 | try testReferences( 89 | \\const blk = { 90 | \\ for ("") |<0>| { 91 | \\ _ = <0>; 92 | \\ } 93 | \\ while (false) |<1>| { 94 | \\ _ = <1>; 95 | \\ } 96 | \\}; 97 | ); 98 | } 99 | 100 | test "enum field access" { 101 | try testReferences( 102 | \\const E = enum { 103 | \\ <0>, 104 | \\ bar 105 | \\}; 106 | \\const e = E.<0>; 107 | ); 108 | } 109 | 110 | test "struct field access" { 111 | try testReferences( 112 | \\const S = struct {<0>: u32 = 3}; 113 | \\pub fn foo() bool { 114 | \\ const s: S = .{}; 115 | \\ return s.<0> == s.<0>; 116 | \\} 117 | ); 118 | } 119 | 120 | test "struct decl access" { 121 | try testReferences( 122 | \\const S = struct { 123 | \\ fn <0>() void {} 124 | \\}; 125 | \\pub fn foo() bool { 126 | \\ const s: S = .{}; 127 | \\ s.<0>(); 128 | \\ s.<0>(); 129 | \\ <1>(); 130 | \\} 131 | \\fn <1>() void {} 132 | ); 133 | } 134 | 135 | test "struct one field init" { 136 | try testReferences( 137 | \\const S = struct {<0>: u32}; 138 | \\const s = S{.<0> = 0 }; 139 | ); 140 | } 141 | 142 | test "struct multi-field init" { 143 | try testReferences( 144 | \\const S = struct {<0>: u32, a: bool}; 145 | \\const s = S{.<0> = 0, .a = true}; 146 | ); 147 | // Anon 148 | try testReferences( 149 | \\const S = struct {<0>: u32, a: bool}; 150 | \\const s: S = .{.<0> = 0, .a = true}; 151 | ); 152 | } 153 | 154 | test "struct_init_dot" { 155 | try testReferences( 156 | \\const S = struct {<0>: u32, a: bool, b: u8}; 157 | \\const s = S{.<0> = 0, .a = true, .b = 1}; 158 | ); 159 | // Anon 160 | try testReferences( 161 | \\const S = struct {<0>: u32, a: bool, b: u8}; 162 | \\const s: S = .{.<0> = 0, .a = true, .b = 1}; 163 | ); 164 | } 165 | 166 | test "while continue expression" { 167 | try testReferences( 168 | \\ pub fn foo() void { 169 | \\ var <0>: u32 = 0; 170 | \\ while (true) : (<0> += 1) {} 171 | \\ } 172 | ); 173 | } 174 | 175 | test "test with identifier" { 176 | try testReferences( 177 | \\pub fn <0>() bool {} 178 | \\test <0> {} 179 | \\test "placeholder" {} 180 | \\test {} 181 | ); 182 | } 183 | 184 | test "label" { 185 | try testReferences( 186 | \\const foo = <0>: { 187 | \\ break :<0> 0; 188 | \\}; 189 | ); 190 | try testReferences( 191 | \\const foo = <0>: { 192 | \\ const <1> = 0; 193 | \\ _ = <1>; 194 | \\ break :<0> 0; 195 | \\}; 196 | ); 197 | } 198 | 199 | test "asm" { 200 | try testReferences( 201 | \\fn foo(<0>: u32) void { 202 | \\ asm ("bogus" 203 | \\ : [ret] "={rax}" (-> void), 204 | \\ : [bar] "{rax}" (<0>), 205 | \\ ); 206 | \\} 207 | ); 208 | try testReferences( 209 | \\fn foo(comptime <0>: type) void { 210 | \\ asm ("bogus" 211 | \\ : [ret] "={rax}" (-> <0>), 212 | \\ ); 213 | \\} 214 | ); 215 | } 216 | 217 | test "function header" { 218 | try testReferences( 219 | \\fn foo(<0>: anytype) @TypeOf(<0>) {} 220 | ); 221 | try testReferences( 222 | \\fn foo(<0>: type, bar: <0>) <0> {} 223 | ); 224 | } 225 | 226 | test "cross-file reference" { 227 | if (true) return error.SkipZigTest; // TODO 228 | try testMFReferences(&.{ 229 | \\pub const <0> = struct {}; 230 | , 231 | \\const file = @import("file_0.zig"); 232 | \\const F = file.<0>; 233 | }); 234 | } 235 | 236 | fn testReferences(source: []const u8) !void { 237 | return testMFReferences(&.{source}); 238 | } 239 | 240 | /// source files have the following name pattern: `file_{d}.zig` 241 | fn testMFReferences(sources: []const []const u8) !void { 242 | const placeholder_name = "placeholder"; 243 | 244 | var ctx = try Context.init(); 245 | defer ctx.deinit(); 246 | 247 | const File = struct { source: []const u8, new_source: []const u8 }; 248 | const LocPair = struct { file_index: usize, old: offsets.Loc, new: offsets.Loc }; 249 | 250 | var files = std.StringArrayHashMapUnmanaged(File){}; 251 | defer { 252 | for (files.values()) |file| allocator.free(file.new_source); 253 | files.deinit(allocator); 254 | } 255 | 256 | var loc_set: std.StringArrayHashMapUnmanaged(std.MultiArrayList(LocPair)) = .{}; 257 | defer { 258 | for (loc_set.values()) |*locs| locs.deinit(allocator); 259 | loc_set.deinit(allocator); 260 | } 261 | 262 | try files.ensureTotalCapacity(allocator, sources.len); 263 | for (sources, 0..) |source, file_index| { 264 | var phr = try helper.collectReplacePlaceholders(allocator, source, placeholder_name); 265 | defer phr.deinit(allocator); 266 | 267 | const uri = try ctx.addDocument(.{ .source = phr.new_source }); 268 | files.putAssumeCapacityNoClobber(uri, .{ .source = source, .new_source = phr.new_source }); 269 | phr.new_source = ""; // `files` takes ownership of `new_source` from `phr` 270 | 271 | for (phr.locations.items(.old), phr.locations.items(.new)) |old, new| { 272 | const name = offsets.locToSlice(source, old); 273 | const gop = try loc_set.getOrPutValue(allocator, name, .{}); 274 | try gop.value_ptr.append(allocator, .{ .file_index = file_index, .old = old, .new = new }); 275 | } 276 | } 277 | 278 | var error_builder = ErrorBuilder.init(allocator); 279 | defer error_builder.deinit(); 280 | errdefer error_builder.writeDebug(); 281 | 282 | for (files.keys(), files.values()) |file_uri, file| { 283 | try error_builder.addFile(file_uri, file.new_source); 284 | } 285 | 286 | for (loc_set.values()) |locs| { 287 | error_builder.clearMessages(); 288 | 289 | for (locs.items(.file_index), locs.items(.new)) |file_index, new_loc| { 290 | const file = files.values()[file_index]; 291 | const file_uri = files.keys()[file_index]; 292 | 293 | const middle = new_loc.start + (new_loc.end - new_loc.start) / 2; 294 | const params = types.ReferenceParams{ 295 | .textDocument = .{ .uri = file_uri }, 296 | .position = offsets.indexToPosition(file.new_source, middle, ctx.server.offset_encoding), 297 | .context = .{ .includeDeclaration = true }, 298 | }; 299 | const response = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/references", params); 300 | 301 | try error_builder.msgAtLoc("asked for references here", file_uri, new_loc, .info, .{}); 302 | 303 | const actual_locations: []const types.Location = response orelse { 304 | std.debug.print("Server returned `null` as the result\n", .{}); 305 | return error.InvalidResponse; 306 | }; 307 | 308 | // keeps track of expected locations that have been given by the server 309 | // used to detect double references and missing references 310 | var visited = try std.DynamicBitSetUnmanaged.initEmpty(allocator, locs.len); 311 | defer visited.deinit(allocator); 312 | 313 | for (actual_locations) |response_location| { 314 | const actual_loc = offsets.rangeToLoc(file.new_source, response_location.range, ctx.server.offset_encoding); 315 | const actual_file_index = files.getIndex(response_location.uri) orelse { 316 | std.debug.print("received location to unknown file `{s}` as the result\n", .{response_location.uri}); 317 | return error.InvalidReference; 318 | }; 319 | 320 | const index = found_index: { 321 | for (locs.items(.new), locs.items(.file_index), 0..) |expected_loc, expected_file_index, idx| { 322 | if (expected_file_index != actual_file_index) continue; 323 | if (expected_loc.start != actual_loc.start) continue; 324 | if (expected_loc.end != actual_loc.end) continue; 325 | break :found_index idx; 326 | } 327 | try error_builder.msgAtLoc("server returned unexpected reference!", file_uri, actual_loc, .err, .{}); 328 | return error.UnexpectedReference; 329 | }; 330 | 331 | if (visited.isSet(index)) { 332 | try error_builder.msgAtLoc("server returned duplicate reference!", file_uri, actual_loc, .err, .{}); 333 | return error.DuplicateReference; 334 | } else { 335 | visited.set(index); 336 | } 337 | } 338 | 339 | var has_unvisited = false; 340 | var unvisited_it = visited.iterator(.{ .kind = .unset }); 341 | while (unvisited_it.next()) |index| { 342 | const unvisited_file_index = locs.items(.file_index)[index]; 343 | const unvisited_uri = files.keys()[unvisited_file_index]; 344 | const unvisited_loc = locs.items(.new)[index]; 345 | try error_builder.msgAtLoc("expected reference here!", unvisited_uri, unvisited_loc, .err, .{}); 346 | has_unvisited = true; 347 | } 348 | 349 | if (has_unvisited) return error.ExpectedReference; 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /tests/lsp_features/selection_range.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | const builtin = @import("builtin"); 4 | 5 | const helper = @import("../helper.zig"); 6 | const Context = @import("../context.zig").Context; 7 | const ErrorBuilder = @import("../ErrorBuilder.zig"); 8 | 9 | const types = zls.types; 10 | const offsets = zls.offsets; 11 | 12 | const allocator: std.mem.Allocator = std.testing.allocator; 13 | 14 | test "empty" { 15 | try testSelectionRange("<>", &.{}); 16 | } 17 | 18 | test "smoke" { 19 | try testSelectionRange( 20 | \\fn main() void { 21 | \\ const x = 1 <>+ 1; 22 | \\} 23 | , &.{ "1 + 1", "const x = 1 + 1", "{\n const x = 1 + 1;\n}" }); 24 | } 25 | 26 | test "function parameter" { 27 | try testSelectionRange( 28 | \\fn f(x: i32, y: <>struct {}, z: f32) void { 29 | \\ 30 | \\} 31 | , &.{ "struct {}", "y: struct {}", "fn f(x: i32, y: struct {}, z: f32) void" }); 32 | } 33 | 34 | fn testSelectionRange(source: []const u8, want: []const []const u8) !void { 35 | var phr = try helper.collectClearPlaceholders(allocator, source); 36 | defer phr.deinit(allocator); 37 | 38 | var ctx = try Context.init(); 39 | defer ctx.deinit(); 40 | 41 | const test_uri = try ctx.addDocument(.{ .source = phr.new_source }); 42 | 43 | const position = offsets.locToRange(phr.new_source, phr.locations.items(.new)[0], .@"utf-16").start; 44 | 45 | const params = types.SelectionRangeParams{ 46 | .textDocument = .{ .uri = test_uri }, 47 | .positions = &[_]types.Position{position}, 48 | }; 49 | const response = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/selectionRange", params); 50 | 51 | const selectionRanges: []const types.SelectionRange = response orelse { 52 | std.debug.print("Server returned `null` as the result\n", .{}); 53 | return error.InvalidResponse; 54 | }; 55 | 56 | var got = std.ArrayList([]const u8).init(allocator); 57 | defer got.deinit(); 58 | 59 | var it: ?*const types.SelectionRange = &selectionRanges[0]; 60 | while (it) |r| { 61 | const slice = offsets.rangeToSlice(phr.new_source, r.range, .@"utf-16"); 62 | (try got.addOne()).* = slice; 63 | it = r.parent; 64 | } 65 | const last = got.pop().?; 66 | try std.testing.expectEqualStrings(phr.new_source, last); 67 | try std.testing.expectEqual(want.len, got.items.len); 68 | for (want, got.items) |expected, actual| { 69 | try std.testing.expectEqualStrings(expected, actual); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/lsp_features/signature_help.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | 4 | const Context = @import("../context.zig").Context; 5 | 6 | const types = zls.types; 7 | const offsets = zls.offsets; 8 | 9 | const allocator: std.mem.Allocator = std.testing.allocator; 10 | 11 | test "no parameters" { 12 | try testSignatureHelp( 13 | \\fn foo() void { 14 | \\ foo() 15 | \\} 16 | , "fn foo() void", null); 17 | } 18 | 19 | test "simple" { 20 | try testSignatureHelp( 21 | \\fn foo(a: u32, b: u32) void { 22 | \\ foo() 23 | \\} 24 | , "fn foo(a: u32, b: u32) void", 0); 25 | try testSignatureHelp( 26 | \\fn foo(a: u32, b: u32) void { 27 | \\ foo(,0) 28 | \\} 29 | , "fn foo(a: u32, b: u32) void", 0); 30 | try testSignatureHelp( 31 | \\fn foo(a: u32, b: u32) void { 32 | \\ foo(0,) 33 | \\} 34 | , "fn foo(a: u32, b: u32) void", 1); 35 | try testSignatureHelp( 36 | \\fn foo(a: u32, b: u32) void { 37 | \\ foo(0,55) 38 | \\} 39 | , "fn foo(a: u32, b: u32) void", 1); 40 | try testSignatureHelp( 41 | \\fn foo(a: u32, b: u32) void { 42 | \\ foo(0,55) 43 | \\} 44 | , "fn foo(a: u32, b: u32) void", 1); 45 | try testSignatureHelp( 46 | \\fn foo(a: u32, b: u32) void { 47 | \\ foo(0,55) 48 | \\} 49 | , "fn foo(a: u32, b: u32) void", 1); 50 | try testSignatureHelp( 51 | \\fn foo(a: u32, b: u32, c: u32) void { 52 | \\ foo(0, 1, ) 53 | \\} 54 | , "fn foo(a: u32, b: u32, c: u32) void", 2); 55 | } 56 | 57 | test "no right paren" { 58 | try testSignatureHelp( 59 | \\fn foo(a: u32, b: u32) void { 60 | \\ foo( 61 | \\} 62 | , "fn foo(a: u32, b: u32) void", 0); 63 | try testSignatureHelp( 64 | \\fn foo(a: u32, b: u32) void { 65 | \\ foo(,0 66 | \\} 67 | , "fn foo(a: u32, b: u32) void", 0); 68 | try testSignatureHelp( 69 | \\fn foo(a: u32, b: u32) void { 70 | \\ foo(0, 71 | \\} 72 | , "fn foo(a: u32, b: u32) void", 1); 73 | try testSignatureHelp( 74 | \\fn foo(a: u32, b: u32) void { 75 | \\ foo(0,55 76 | \\} 77 | , "fn foo(a: u32, b: u32) void", 1); 78 | try testSignatureHelp( 79 | \\fn foo(a: u32, b: u32) void { 80 | \\ foo(0,55 81 | \\} 82 | , "fn foo(a: u32, b: u32) void", 1); 83 | try testSignatureHelp( 84 | \\fn foo(a: u32, b: u32) void { 85 | \\ foo(0,55 86 | \\} 87 | , "fn foo(a: u32, b: u32) void", 1); 88 | try testSignatureHelp( 89 | \\fn foo(a: u32, b: u32, c: u32) void { 90 | \\ foo(0, 1, 91 | \\} 92 | , "fn foo(a: u32, b: u32, c: u32) void", 2); 93 | } 94 | 95 | test "multiline" { 96 | try testSignatureHelp( 97 | \\fn foo( 98 | \\ /// a is important 99 | \\ a: u32, 100 | \\ b: u32, 101 | \\) void { 102 | \\ foo() 103 | \\} 104 | , 105 | \\fn foo( 106 | \\ /// a is important 107 | \\ a: u32, 108 | \\ b: u32, 109 | \\) void 110 | , 0); 111 | try testSignatureHelp( 112 | \\fn foo( 113 | \\ /// a is important 114 | \\ a: u32, 115 | \\ b: u32, 116 | \\) void { 117 | \\ foo(,0) 118 | \\} 119 | , 120 | \\fn foo( 121 | \\ /// a is important 122 | \\ a: u32, 123 | \\ b: u32, 124 | \\) void 125 | , 0); 126 | try testSignatureHelp( 127 | \\fn foo( 128 | \\ /// a is important 129 | \\ a: u32, 130 | \\ b: u32, 131 | \\) void { 132 | \\ foo(0,) 133 | \\} 134 | , 135 | \\fn foo( 136 | \\ /// a is important 137 | \\ a: u32, 138 | \\ b: u32, 139 | \\) void 140 | , 1); 141 | } 142 | 143 | test "syntax error resistance" { 144 | try testSignatureHelp( 145 | \\fn foo(a: u32, b: u32) void { 146 | \\ foo( 147 | \\} 148 | , "fn foo(a: u32, b: u32) void", 0); 149 | try testSignatureHelp( 150 | \\fn foo(a: u32, b: u32) void { 151 | \\ foo(5 152 | \\} 153 | , "fn foo(a: u32, b: u32) void", 0); 154 | try testSignatureHelp( 155 | \\fn foo(a: u32, b: u32) void { 156 | \\ foo(55 157 | \\} 158 | , "fn foo(a: u32, b: u32) void", 0); 159 | try testSignatureHelp( 160 | \\fn foo(a: u32, b: u32) void { 161 | \\ foo(55 162 | \\} 163 | , "fn foo(a: u32, b: u32) void", 0); 164 | try testSignatureHelp( 165 | \\fn foo(a: u32, b: u32) void { 166 | \\ foo(; 167 | \\} 168 | , "fn foo(a: u32, b: u32) void", 0); 169 | try testSignatureHelp( 170 | \\fn foo(a: u32, b: u32) void { 171 | \\ foo(, 172 | \\} 173 | , "fn foo(a: u32, b: u32) void", 0); 174 | try testSignatureHelp( 175 | \\fn foo(a: u32, b: u32) void { 176 | \\ foo(,; 177 | \\} 178 | , "fn foo(a: u32, b: u32) void", 0); 179 | } 180 | 181 | test "alias" { 182 | try testSignatureHelp( 183 | \\fn foo(a: u32, b: u32) void { 184 | \\ bar() 185 | \\} 186 | \\const bar = foo; 187 | , "fn foo(a: u32, b: u32) void", 0); 188 | try testSignatureHelp( 189 | \\fn foo(a: u32, b: u32) void { 190 | \\ bar() 191 | \\} 192 | \\const bar = &foo; 193 | , "fn foo(a: u32, b: u32) void", 0); 194 | } 195 | 196 | test "function pointer" { 197 | try testSignatureHelp( 198 | \\const foo: fn (bool, u32) void = undefined; 199 | \\comptime { 200 | \\ foo() 201 | \\} 202 | , "fn (bool, u32) void", 0); 203 | try testSignatureHelp( 204 | \\const foo: *fn (bool, u32) void = undefined; 205 | \\comptime { 206 | \\ foo() 207 | \\} 208 | , "fn (bool, u32) void", 0); 209 | try testSignatureHelp( 210 | \\const foo: *fn (bool, u32) void = undefined; 211 | \\comptime { 212 | \\ foo.*() 213 | \\} 214 | , "fn (bool, u32) void", 0); 215 | } 216 | 217 | test "function pointer container field" { 218 | try testSignatureHelp( 219 | \\const S = struct { 220 | \\ foo: fn(a: u32, b: void) bool {} 221 | \\}; 222 | \\const s: S = undefined; 223 | \\const foo = s.foo(); 224 | , "fn(a: u32, b: void) bool", 0); 225 | try testSignatureHelp( 226 | \\const S = struct { 227 | \\ foo: *const fn(a: u32, b: void) bool {} 228 | \\}; 229 | \\const s: S = undefined; 230 | \\const foo = s.foo(); 231 | , "fn(a: u32, b: void) bool", 0); 232 | try testSignatureHelp( 233 | \\const S = struct { 234 | \\ foo: *const fn(a: u32, b: void) bool {} 235 | \\}; 236 | \\const s: S = undefined; 237 | \\const foo = s.foo.*(); 238 | , "fn(a: u32, b: void) bool", 0); 239 | } 240 | 241 | test "self parameter" { 242 | // parameter: S 243 | // argument: S 244 | try testSignatureHelp( 245 | \\const S = struct { 246 | \\ alpha: u32, 247 | \\ fn foo(self: @This(), a: u32, b: void) bool {} 248 | \\}; 249 | \\const s: S = undefined; 250 | \\const foo = s.foo(3,); 251 | , "fn foo(self: @This(), a: u32, b: void) bool", 2); 252 | try testSignatureHelp( 253 | \\const S = struct { 254 | \\ alpha: u32, 255 | \\ fn foo(self: @This(), a: u32, b: void) bool {} 256 | \\}; 257 | \\const foo = S.foo(undefined,); 258 | , "fn foo(self: @This(), a: u32, b: void) bool", 1); 259 | 260 | // parameter: *S 261 | // argument: S 262 | try testSignatureHelp( 263 | \\const S = struct { 264 | \\ alpha: u32, 265 | \\ fn foo(self: *@This(), a: u32, b: void) bool {} 266 | \\}; 267 | \\const s: S = undefined; 268 | \\const foo = s.foo(3,); 269 | , "fn foo(self: *@This(), a: u32, b: void) bool", 2); 270 | try testSignatureHelp( 271 | \\const S = struct { 272 | \\ alpha: u32, 273 | \\ fn foo(self: *@This(), a: u32, b: void) bool {} 274 | \\}; 275 | \\const foo = S.foo(undefined,); 276 | , "fn foo(self: *@This(), a: u32, b: void) bool", 1); 277 | 278 | // parameter: S 279 | // argument: *S 280 | try testSignatureHelp( 281 | \\const S = struct { 282 | \\ alpha: u32, 283 | \\ fn foo(self: @This(), a: u32, b: void) bool {} 284 | \\}; 285 | \\const s: *S = undefined; 286 | \\const foo = s.foo(3,); 287 | , "fn foo(self: @This(), a: u32, b: void) bool", 2); 288 | 289 | // parameter: *S 290 | // argument: *S 291 | try testSignatureHelp( 292 | \\const S = struct { 293 | \\ alpha: u32, 294 | \\ fn foo(self: *@This(), a: u32, b: void) bool {} 295 | \\}; 296 | \\const s: *S = undefined; 297 | \\const foo = s.foo(3,); 298 | , "fn foo(self: *@This(), a: u32, b: void) bool", 2); 299 | } 300 | 301 | test "self parameter is anytype" { 302 | try testSignatureHelp( 303 | \\const S = struct { 304 | \\ alpha: u32, 305 | \\ fn foo(self: anytype, a: u32, b: void) bool {} 306 | \\}; 307 | \\const s: S = undefined; 308 | \\const foo = s.foo(3,); 309 | , "fn foo(self: anytype, a: u32, b: void) bool", 2); 310 | } 311 | 312 | test "anytype" { 313 | try testSignatureHelp( 314 | \\fn foo(a: u32, b: anytype, c: u32) void { 315 | \\ foo(1,,2) 316 | \\} 317 | , "fn foo(a: u32, b: anytype, c: u32) void", 1); 318 | } 319 | 320 | test "nested function call" { 321 | try testSignatureHelp( 322 | \\fn foo(a: u32, b: u32) i32 { 323 | \\ foo(1, bar()); 324 | \\} 325 | \\fn bar(c: bool) bool {} 326 | , "fn bar(c: bool) bool", 0); 327 | } 328 | 329 | test "builtin" { 330 | try testSignatureHelp( 331 | \\test { 332 | \\ @panic() 333 | \\} 334 | , "@panic(message: []const u8) noreturn", 0); 335 | try testSignatureHelp( 336 | \\test { 337 | \\ @as(?u32,) 338 | \\} 339 | , "@as(comptime T: type, expression) T", 1); 340 | try testSignatureHelp( 341 | \\test { 342 | \\ @as(?u32,@intCast()) 343 | \\} 344 | , "@intCast(int: anytype) anytype", 0); 345 | } 346 | 347 | fn testSignatureHelp(source: []const u8, expected_label: []const u8, expected_active_parameter: ?u32) !void { 348 | const cursor_idx = std.mem.indexOf(u8, source, "").?; 349 | const text = try std.mem.concat(allocator, u8, &.{ source[0..cursor_idx], source[cursor_idx + "".len ..] }); 350 | defer allocator.free(text); 351 | 352 | var ctx = try Context.init(); 353 | defer ctx.deinit(); 354 | 355 | const test_uri = try ctx.addDocument(.{ .source = text }); 356 | 357 | const params = types.SignatureHelpParams{ 358 | .textDocument = .{ .uri = test_uri }, 359 | .position = offsets.indexToPosition(text, cursor_idx, ctx.server.offset_encoding), 360 | }; 361 | 362 | const response: types.SignatureHelp = try ctx.server.sendRequestSync(ctx.arena.allocator(), "textDocument/signatureHelp", params) orelse { 363 | std.debug.print("Server returned `null` as the result\n", .{}); 364 | return error.InvalidResponse; 365 | }; 366 | 367 | try std.testing.expectEqual(@as(?u32, 0), response.activeSignature); 368 | try std.testing.expectEqual(@as(usize, 1), response.signatures.len); 369 | 370 | const signature = response.signatures[0]; 371 | try std.testing.expectEqual(expected_active_parameter, response.activeParameter); 372 | try std.testing.expectEqual(response.activeParameter, signature.activeParameter); 373 | 374 | try std.testing.expectEqualStrings(expected_label, signature.label); 375 | } 376 | -------------------------------------------------------------------------------- /tests/tests.zig: -------------------------------------------------------------------------------- 1 | comptime { 2 | _ = @import("helper.zig"); 3 | 4 | _ = @import("utility/ast.zig"); 5 | _ = @import("utility/position_context.zig"); 6 | _ = @import("utility/diff.zig"); 7 | 8 | _ = @import("lifecycle.zig"); 9 | 10 | // TODO Document Synchronization 11 | 12 | // LSP features 13 | _ = @import("lsp_features/code_actions.zig"); 14 | _ = @import("lsp_features/completion.zig"); 15 | _ = @import("lsp_features/definition.zig"); 16 | _ = @import("lsp_features/document_symbol.zig"); 17 | _ = @import("lsp_features/folding_range.zig"); 18 | _ = @import("lsp_features/hover.zig"); 19 | _ = @import("lsp_features/inlay_hints.zig"); 20 | _ = @import("lsp_features/references.zig"); 21 | _ = @import("lsp_features/selection_range.zig"); 22 | _ = @import("lsp_features/semantic_tokens.zig"); 23 | _ = @import("lsp_features/signature_help.zig"); 24 | 25 | // Language features 26 | _ = @import("language_features/cimport.zig"); 27 | } 28 | -------------------------------------------------------------------------------- /tests/utility/ast.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | 4 | const helper = @import("../helper.zig"); 5 | const Context = @import("../context.zig").Context; 6 | const ErrorBuilder = @import("../ErrorBuilder.zig"); 7 | 8 | const types = zls.types; 9 | const offsets = zls.offsets; 10 | const ast = zls.ast; 11 | 12 | const allocator = std.testing.allocator; 13 | 14 | test "nodesAtLoc" { 15 | try testNodesAtLoc( 16 | \\ 17 | ); 18 | try testNodesAtLoc( 19 | \\var alpha = 1; 20 | ); 21 | try testNodesAtLoc( 22 | \\const foo = 5; 23 | ); 24 | try testNodesAtLoc( 25 | \\const foo = 5; 26 | \\var bar = 2; 27 | ); 28 | try testNodesAtLoc( 29 | \\const foo = 5 + 2; 30 | ); 31 | try testNodesAtLoc( 32 | \\fn foo(alpha: u32) void {} 33 | \\const _ = foo(5); 34 | ); 35 | try testNodesAtLoc( 36 | \\var alpha = 1; 37 | \\var beta = alpha + alpha; 38 | \\var gamma = beta * alpha; 39 | \\var delta = gamma - 2; 40 | \\var epsilon = delta - beta; 41 | \\var zeta = epsilon * epsilon; 42 | ); 43 | try testNodesAtLoc( 44 | \\var alpha = 1; 45 | \\var beta = alpha + alpha; 46 | \\var gamma = beta * alpha; 47 | \\var epsilon = delta - beta; 48 | ); 49 | try testNodesAtLoc( 50 | \\fn foo() void { 51 | \\ 52 | \\} 53 | \\fn bar() void { 54 | \\ 55 | \\} 56 | \\fn baz() void { 57 | \\ 58 | \\} 59 | ); 60 | try testNodesAtLoc( 61 | \\var alpha = 1; 62 | \\var beta = alpha + alpha; 63 | \\// some comment 64 | \\// because it is 65 | \\// not a node 66 | \\var gamma = beta * alpha; 67 | \\var epsilon = delta - beta; 68 | ); 69 | } 70 | 71 | fn testNodesAtLoc(source: []const u8) !void { 72 | var ccp = try helper.collectClearPlaceholders(allocator, source); 73 | defer ccp.deinit(allocator); 74 | 75 | const old_locs = ccp.locations.items(.old); 76 | const locs = ccp.locations.items(.new); 77 | 78 | std.debug.assert(ccp.locations.len == 4); 79 | std.debug.assert(std.mem.eql(u8, offsets.locToSlice(source, old_locs[0]), "")); 80 | std.debug.assert(std.mem.eql(u8, offsets.locToSlice(source, old_locs[1]), "")); 81 | std.debug.assert(std.mem.eql(u8, offsets.locToSlice(source, old_locs[2]), "")); 82 | std.debug.assert(std.mem.eql(u8, offsets.locToSlice(source, old_locs[3]), "")); 83 | 84 | const inner_loc = offsets.Loc{ .start = locs[1].start, .end = locs[2].start }; 85 | const outer_loc = offsets.Loc{ .start = locs[0].start, .end = locs[3].end }; 86 | 87 | const new_source = try allocator.dupeZ(u8, ccp.new_source); 88 | defer allocator.free(new_source); 89 | 90 | var tree = try std.zig.Ast.parse(allocator, new_source, .zig); 91 | defer tree.deinit(allocator); 92 | 93 | const nodes = try ast.nodesAtLoc(allocator, tree, inner_loc); 94 | defer allocator.free(nodes); 95 | 96 | const actual_loc = offsets.Loc{ 97 | .start = offsets.nodeToLoc(tree, nodes[0]).start, 98 | .end = offsets.nodeToLoc(tree, nodes[nodes.len - 1]).end, 99 | }; 100 | 101 | const uri = "file.zig"; 102 | var error_builder = ErrorBuilder.init(allocator); 103 | defer error_builder.deinit(); 104 | errdefer error_builder.writeDebug(); 105 | 106 | try error_builder.addFile(uri, new_source); 107 | 108 | if (outer_loc.start != actual_loc.start) { 109 | try error_builder.msgAtIndex("actual start here", uri, actual_loc.start, .err, .{}); 110 | try error_builder.msgAtIndex("expected start here", uri, outer_loc.start, .err, .{}); 111 | return error.LocStartMismatch; 112 | } 113 | 114 | if (outer_loc.end != actual_loc.end) { 115 | try error_builder.msgAtIndex("actual end here", uri, actual_loc.end, .err, .{}); 116 | try error_builder.msgAtIndex("expected end here", uri, outer_loc.end, .err, .{}); 117 | return error.LocEndMismatch; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/utility/diff.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zls = @import("zls"); 3 | 4 | fn gen(alloc: std.mem.Allocator, rand: std.Random) ![]const u8 { 5 | const buffer = try alloc.alloc(u8, rand.intRangeAtMost(usize, 0, 256)); 6 | for (buffer) |*b| b.* = rand.intRangeAtMost(u8, ' ', '~'); 7 | return buffer; 8 | } 9 | 10 | test "diff - random" { 11 | const allocator = std.testing.allocator; 12 | try std.testing.checkAllAllocationFailures(allocator, testDiff, .{ 0, .@"utf-8" }); 13 | for (0..30) |i| { 14 | try testDiff(allocator, i, .@"utf-8"); 15 | try testDiff(allocator, i, .@"utf-16"); 16 | try testDiff(allocator, i, .@"utf-32"); 17 | } 18 | } 19 | 20 | fn testDiff(allocator: std.mem.Allocator, seed: u64, encoding: zls.offsets.Encoding) !void { 21 | var rand = std.Random.DefaultPrng.init(seed); 22 | const before = try gen(allocator, rand.random()); 23 | defer allocator.free(before); 24 | const after = try gen(allocator, rand.random()); 25 | defer allocator.free(after); 26 | 27 | var edits = try zls.diff.edits(allocator, before, after, encoding); 28 | defer { 29 | for (edits.items) |edit| allocator.free(edit.newText); 30 | edits.deinit(allocator); 31 | } 32 | 33 | const applied = try zls.diff.applyTextEdits(allocator, before, edits.items, encoding); 34 | defer allocator.free(applied); 35 | 36 | try std.testing.expectEqualStrings(after, applied); 37 | } 38 | --------------------------------------------------------------------------------