Merge branch 'master' into intern-pool

This commit is contained in:
Techatrix 2023-01-04 04:42:06 +01:00
commit d56a274c16
44 changed files with 9838 additions and 2585 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
zig-*
debug
release
*.zlsreplay

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "src/tracy"]
path = src/tracy
url = https://github.com/wolfpld/tracy
[submodule "src/tres"]
path = src/tres
url = https://github.com/ziglibs/tres.git

View File

@ -34,7 +34,6 @@ Building `zls` is very easy. You will need [a build of Zig master](https://zigla
git clone --recurse-submodules https://github.com/zigtools/zls
cd zls
zig build -Drelease-safe
./zig-out/bin/zls --config # Configure ZLS
```
#### Build Options
@ -48,12 +47,21 @@ zig build -Drelease-safe
There is a `generate-data.py` in the `src/data` folder, run this file to update data files.
It writes to stdout and you can redirect output to a zig file like `master.zig`. By default it generates data file for `master`, but can be configured to generate for a different version by modifying the `zig_version` variable. Files generated by this tool **contains** formatting information.
On Powershell 5.1 (the default Powershell on Windows 10 & 11), the following will update `master.zig`.
```pwsh
New-Item -Force .\src\data\master.zig -Value ((python .\src\data\generate-data.py) -split "`r?`n" -join "`n")
```
This unweidly command is necesary in order to guarantee Unix-style line endings and UTF-8 text encoding.
There is also a `generate-data.js` in the `src/data` folder, you'll need to run this inside a Chrome DevTools console and copy the output. Files generated by this tool **does not contain** formatting information.
### Configuration Options
You can configure zls by running `zls --config` or manually creating your own `zls.json` configuration file.
zls will look for a zls.json configuration file in multiple locations with the following priority:
You can configure zls by editing your `zls.json` configuration file.
Running `zls --show-config-path` will a path to an already existing `zls.json` or a path to the local configuration folder instead.
zls will look for a `zls.json` configuration file in multiple locations with the following priority:
- In the local configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders/blob/master/RESOURCES.md#folder-list))
- In the global configuration folder of your OS (as provided by [known-folders](https://github.com/ziglibs/known-folders/blob/master/RESOURCES.md#folder-list))
@ -62,12 +70,12 @@ The following options are currently available.
<!-- DO NOT EDIT | THIS SECTION IS AUTO-GENERATED | DO NOT EDIT -->
| Option | Type | Default value | What it Does |
| --- | --- | --- | --- |
| `enable_snippets` | `bool` | `false` | Enables snippet completions when the client also supports them |
| `enable_snippets` | `bool` | `true` | Enables snippet completions when the client also supports them |
| `enable_ast_check_diagnostics` | `bool` | `true` | Whether to enable ast-check diagnostics |
| `enable_autofix` | `bool` | `false` | Whether to automatically fix errors on save. Currently supports adding and removing discards. |
| `enable_import_embedfile_argument_completions` | `bool` | `false` | Whether to enable import/embedFile argument completions |
| `enable_autofix` | `bool` | `true` | Whether to automatically fix errors on save. Currently supports adding and removing discards. |
| `enable_import_embedfile_argument_completions` | `bool` | `true` | Whether to enable import/embedFile argument completions |
| `enable_semantic_tokens` | `bool` | `true` | Enables semantic token support when the client also supports it |
| `enable_inlay_hints` | `bool` | `false` | Enables inlay hint support when the client also supports it |
| `enable_inlay_hints` | `bool` | `true` | Enables inlay hint support when the client also supports it |
| `inlay_hints_show_builtin` | `bool` | `true` | Enable inlay hints for builtin functions |
| `inlay_hints_exclude_single_argument` | `bool` | `true` | Don't show inlay hints for single argument calls |
| `inlay_hints_hide_redundant_param_names` | `bool` | `false` | Hides inlay hints when parameter name matches the identifier (e.g. foo: foo) |
@ -79,6 +87,9 @@ The following options are currently available.
| `include_at_in_builtins` | `bool` | `false` | Whether the @ sign should be part of the completion of builtins |
| `skip_std_references` | `bool` | `false` | 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 |
| `max_detail_length` | `usize` | `1048576` | The detail field of completions is truncated to be no longer than this (in bytes) |
| `record_session` | `bool` | `false` | When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay` |
| `record_session_path` | `?[]const u8` | `null` | Output file path when `record_session` is set. The recommended file extension *.zlsreplay |
| `replay_session_path` | `?[]const u8` | `null` | Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path. |
| `builtin_path` | `?[]const u8` | `null` | Path to 'builtin;' useful for debugging, automatically set if let null |
| `zig_lib_path` | `?[]const u8` | `null` | Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports |
| `zig_exe_path` | `?[]const u8` | `null` | 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 |

View File

@ -50,6 +50,18 @@ pub fn build(b: *std.build.Builder) !void {
b.option(bool, "enable_tracy_callstack", "Enable callstack graphs.") orelse false,
);
exe_options.addOption(
bool,
"enable_failing_allocator",
b.option(bool, "enable_failing_allocator", "Whether to use a randomly failing allocator.") orelse false,
);
exe_options.addOption(
u32,
"enable_failing_allocator_likelihood",
b.option(u32, "enable_failing_allocator_likelihood", "The chance that an allocation will fail is `1/likelihood`") orelse 256,
);
const version = v: {
const version_string = b.fmt("{d}.{d}.{d}", .{ zls_version.major, zls_version.minor, zls_version.patch });
@ -92,6 +104,10 @@ pub fn build(b: *std.build.Builder) !void {
const known_folders_path = b.option([]const u8, "known-folders", "Path to known-folders package (default: " ++ KNOWN_FOLDERS_DEFAULT_PATH ++ ")") orelse KNOWN_FOLDERS_DEFAULT_PATH;
exe.addPackage(.{ .name = "known-folders", .source = .{ .path = known_folders_path } });
const TRES_DEFAULT_PATH = "src/tres/tres.zig";
const tres_path = b.option([]const u8, "tres", "Path to tres package (default: " ++ TRES_DEFAULT_PATH ++ ")") orelse TRES_DEFAULT_PATH;
exe.addPackage(.{ .name = "tres", .source = .{ .path = tres_path } });
if (enable_tracy) {
const client_cpp = "src/tracy/TracyClient.cpp";
@ -117,6 +133,7 @@ pub fn build(b: *std.build.Builder) !void {
exe.install();
const gen_exe = b.addExecutable("zls_gen", "src/config_gen/config_gen.zig");
gen_exe.addPackage(.{ .name = "tres", .source = .{ .path = "src/tres/tres.zig" } });
const gen_cmd = gen_exe.run();
gen_cmd.addArgs(&.{
@ -125,6 +142,10 @@ pub fn build(b: *std.build.Builder) !void {
b.fmt("{s}/README.md", .{b.build_root}),
});
if (b.option([]const u8, "vscode-config-path", "Output path to vscode-config")) |path| {
gen_cmd.addArg(b.pathFromRoot(path));
}
const gen_step = b.step("gen", "Regenerate config files");
gen_step.dependOn(&gen_cmd.step);
@ -146,6 +167,7 @@ pub fn build(b: *std.build.Builder) !void {
}
tests.addPackage(.{ .name = "zls", .source = .{ .path = "src/zls.zig" }, .dependencies = exe.packages.items });
tests.addPackage(.{ .name = "tres", .source = .{ .path = tres_path } });
tests.setBuildMode(.Debug);
tests.setTarget(target);
test_step.dependOn(&tests.step);

View File

@ -68,11 +68,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1670086663,
"narHash": "sha256-hT8C8AQB74tdoCPwz4nlJypLMD7GI2F5q+vn+VE/qQk=",
"lastModified": 1672057183,
"narHash": "sha256-GN7/10DNNvs1FPj9tlZA2qgNdFuYKKuS3qlHTqAxasQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "813836d64fa57285d108f0dbf2356457ccd304e3",
"rev": "b139e44d78c36c69bcbb825b20dbfa51e7738347",
"type": "github"
},
"original": {
@ -88,9 +88,26 @@
"gitignore": "gitignore",
"known-folders": "known-folders",
"nixpkgs": "nixpkgs",
"tres": "tres",
"zig-overlay": "zig-overlay"
}
},
"tres": {
"flake": false,
"locked": {
"lastModified": 1672008284,
"narHash": "sha256-AtM9SV56PEud1MfbKDZMU2FlsNrI46PkcFQh3yMcDX0=",
"owner": "ziglibs",
"repo": "tres",
"rev": "16774b94efa61757a5302a690837dfb8cf750a11",
"type": "github"
},
"original": {
"owner": "ziglibs",
"repo": "tres",
"type": "github"
}
},
"zig-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
@ -99,11 +116,11 @@
]
},
"locked": {
"lastModified": 1670113356,
"narHash": "sha256-43aMRMU0OuBin6M2LM+nxVG+whazyHuHnUvu92xoth0=",
"lastModified": 1672142864,
"narHash": "sha256-uXljuSZK8DP5c4o9u+gcF+Yc3dKYH1wsHmDpWcFBVRQ=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "17352071583eda4be43fa2a312f6e061326374f7",
"rev": "16e9191142d2a13d7870c03e500842321a466a74",
"type": "github"
},
"original": {

View File

@ -12,9 +12,12 @@
known-folders.url = "github:ziglibs/known-folders";
known-folders.flake = false;
tres.url = "github:ziglibs/tres";
tres.flake = false;
};
outputs = { self, nixpkgs, zig-overlay, gitignore, flake-utils, known-folders }:
outputs = { self, nixpkgs, zig-overlay, gitignore, flake-utils, known-folders, tres }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
inherit (gitignore.lib) gitignoreSource;
@ -35,7 +38,7 @@
dontInstall = true;
buildPhase = ''
mkdir -p $out
zig build install -Dcpu=baseline -Drelease-safe=true -Ddata_version=master -Dknown-folders=${known-folders}/known-folders.zig --prefix $out
zig build install -Dcpu=baseline -Drelease-safe=true -Ddata_version=master -Dtres=${tres}/tres.zig -Dknown-folders=${known-folders}/known-folders.zig --prefix $out
'';
XDG_CACHE_HOME = ".cache";
};

View File

@ -7,7 +7,7 @@
"enable_snippets": {
"description": "Enables snippet completions when the client also supports them",
"type": "boolean",
"default": "false"
"default": "true"
},
"enable_ast_check_diagnostics": {
"description": "Whether to enable ast-check diagnostics",
@ -17,12 +17,12 @@
"enable_autofix": {
"description": "Whether to automatically fix errors on save. Currently supports adding and removing discards.",
"type": "boolean",
"default": "false"
"default": "true"
},
"enable_import_embedfile_argument_completions": {
"description": "Whether to enable import/embedFile argument completions",
"type": "boolean",
"default": "false"
"default": "true"
},
"enable_semantic_tokens": {
"description": "Enables semantic token support when the client also supports it",
@ -32,7 +32,7 @@
"enable_inlay_hints": {
"description": "Enables inlay hint support when the client also supports it",
"type": "boolean",
"default": "false"
"default": "true"
},
"inlay_hints_show_builtin": {
"description": "Enable inlay hints for builtin functions",
@ -89,6 +89,21 @@
"type": "integer",
"default": "1048576"
},
"record_session": {
"description": "When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay`",
"type": "boolean",
"default": "false"
},
"record_session_path": {
"description": "Output file path when `record_session` is set. The recommended file extension *.zlsreplay",
"type": "string",
"default": "null"
},
"replay_session_path": {
"description": "Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path.",
"type": "string",
"default": "null"
},
"builtin_path": {
"description": "Path to 'builtin;' useful for debugging, automatically set if let null",
"type": "string",

View File

@ -5,22 +5,22 @@
//! GENERATED BY src/config_gen/config_gen.zig
/// Enables snippet completions when the client also supports them
enable_snippets: bool = false,
enable_snippets: bool = true,
/// Whether to enable ast-check diagnostics
enable_ast_check_diagnostics: bool = true,
/// Whether to automatically fix errors on save. Currently supports adding and removing discards.
enable_autofix: bool = false,
enable_autofix: bool = true,
/// Whether to enable import/embedFile argument completions
enable_import_embedfile_argument_completions: bool = false,
enable_import_embedfile_argument_completions: bool = true,
/// Enables semantic token support when the client also supports it
enable_semantic_tokens: bool = true,
/// Enables inlay hint support when the client also supports it
enable_inlay_hints: bool = false,
enable_inlay_hints: bool = true,
/// Enable inlay hints for builtin functions
inlay_hints_show_builtin: bool = true,
@ -55,6 +55,15 @@ skip_std_references: bool = false,
/// The detail field of completions is truncated to be no longer than this (in bytes)
max_detail_length: usize = 1048576,
/// When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay`
record_session: bool = false,
/// Output file path when `record_session` is set. The recommended file extension *.zlsreplay
record_session_path: ?[]const u8 = null,
/// Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path.
replay_session_path: ?[]const u8 = null,
/// Path to 'builtin;' useful for debugging, automatically set if let null
builtin_path: ?[]const u8 = null,

View File

@ -1,7 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const types = @import("types.zig");
const requests = @import("requests.zig");
const types = @import("lsp.zig");
const URI = @import("uri.zig");
const analysis = @import("analysis.zig");
const offsets = @import("offsets.zig");
@ -67,7 +66,6 @@ pub const Handle = struct {
/// `DocumentStore.build_files` is guaranteed to contain this uri
/// uri memory managed by its build_file
associated_build_file: ?Uri = null,
is_build_file: bool = false,
pub fn deinit(self: *Handle, allocator: std.mem.Allocator) void {
self.document_scope.deinit(allocator);
@ -125,11 +123,11 @@ fn getOrLoadHandleInternal(self: *DocumentStore, uri: Uri) !?*const Handle {
var handle = try self.allocator.create(Handle);
errdefer self.allocator.destroy(handle);
const dependency_uri = try self.allocator.dupe(u8, uri);
handle.* = (try self.createDocumentFromURI(dependency_uri, false)) orelse return error.Unknown; // error name doesn't matter
handle.* = (try self.createDocumentFromURI(uri, false)) orelse return error.Unknown; // error name doesn't matter
errdefer handle.deinit(self.allocator);
const gop = try self.handles.getOrPutValue(self.allocator, handle.uri, handle);
std.debug.assert(!gop.found_existing);
if (gop.found_existing) return error.Unknown;
return gop.value_ptr.*;
}
@ -147,18 +145,15 @@ pub fn openDocument(self: *DocumentStore, uri: Uri, text: []const u8) error{OutO
return handle.*;
}
const duped_text = try self.allocator.dupeZ(u8, text);
errdefer self.allocator.free(duped_text);
const duped_uri = try self.allocator.dupeZ(u8, uri);
errdefer self.allocator.free(duped_uri);
var handle = try self.allocator.create(Handle);
errdefer self.allocator.destroy(handle);
handle.* = try self.createDocument(duped_uri, duped_text, true);
const duped_text = try self.allocator.dupeZ(u8, text);
handle.* = try self.createDocument(uri, duped_text, true);
errdefer handle.deinit(self.allocator);
try self.handles.putNoClobber(self.allocator, duped_uri, handle);
try self.handles.putNoClobber(self.allocator, handle.uri, handle);
return handle.*;
}
@ -229,7 +224,7 @@ pub fn applySave(self: *DocumentStore, handle: *const Handle) !void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
if (handle.is_build_file) {
if (isBuildFile(handle.uri)) {
const build_file = self.build_files.getPtr(handle.uri).?;
const build_config = loadBuildConfiguration(self.allocator, build_file.*, self.config.*) catch |err| {
@ -251,33 +246,29 @@ fn garbageCollectionImports(self: *DocumentStore) error{OutOfMemory}!void {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
var reachable_handles = std.StringHashMapUnmanaged(void){};
defer reachable_handles.deinit(self.allocator);
defer reachable_handles.deinit(arena.allocator());
var queue = std.ArrayListUnmanaged(Uri){};
defer {
for (queue.items) |uri| {
self.allocator.free(uri);
}
queue.deinit(self.allocator);
}
for (self.handles.values()) |handle| {
if (!handle.open) continue;
try reachable_handles.put(self.allocator, handle.uri, {});
try reachable_handles.put(arena.allocator(), handle.uri, {});
try self.collectDependencies(self.allocator, handle.*, &queue);
try self.collectDependencies(arena.allocator(), handle.*, &queue);
}
while (queue.popOrNull()) |uri| {
if (reachable_handles.contains(uri)) continue;
try reachable_handles.putNoClobber(self.allocator, uri, {});
const gop = try reachable_handles.getOrPut(arena.allocator(), uri);
if (gop.found_existing) continue;
const handle = self.handles.get(uri) orelse continue;
try self.collectDependencies(self.allocator, handle.*, &queue);
try self.collectDependencies(arena.allocator(), handle.*, &queue);
}
var i: usize = 0;
@ -287,7 +278,7 @@ fn garbageCollectionImports(self: *DocumentStore) error{OutOfMemory}!void {
i += 1;
continue;
}
std.log.debug("Closing document {s}", .{handle.uri});
log.debug("Closing document {s}", .{handle.uri});
var kv = self.handles.fetchSwapRemove(handle.uri).?;
kv.value.deinit(self.allocator);
self.allocator.destroy(kv.value);
@ -321,7 +312,7 @@ fn garbageCollectionCImports(self: *DocumentStore) error{OutOfMemory}!void {
.failure => "",
.success => |uri| uri,
};
std.log.debug("Destroying cimport {s}", .{message});
log.debug("Destroying cimport {s}", .{message});
kv.value.deinit(self.allocator);
}
}
@ -347,11 +338,24 @@ fn garbageCollectionBuildFiles(self: *DocumentStore) error{OutOfMemory}!void {
continue;
}
var kv = self.build_files.fetchSwapRemove(hash).?;
std.log.debug("Destroying build file {s}", .{kv.value.uri});
log.debug("Destroying build file {s}", .{kv.value.uri});
kv.value.deinit(self.allocator);
}
}
pub fn isBuildFile(uri: Uri) bool {
return std.mem.endsWith(u8, uri, "/build.zig");
}
pub fn isBuiltinFile(uri: Uri) bool {
return std.mem.endsWith(u8, uri, "/builtin.zig");
}
pub fn isInStd(uri: Uri) bool {
// TODO: Better logic for detecting std or subdirectories?
return std.mem.indexOf(u8, uri, "/std/") != null;
}
/// looks for a `zls.build.json` file in the build file directory
/// has to be freed with `std.json.parseFree`
fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: BuildFile) !BuildAssociatedConfig {
@ -451,6 +455,7 @@ fn loadBuildConfiguration(
const parse_options = std.json.ParseOptions{ .allocator = allocator };
var token_stream = std.json.TokenStream.init(zig_run_result.stdout);
var build_config = std.json.parse(BuildConfig, &token_stream, parse_options) catch return error.RunFailed;
errdefer std.json.parseFree(BuildConfig, build_config, parse_options);
for (build_config.packages) |*pkg| {
const pkg_abs_path = try std.fs.path.resolve(allocator, &[_][]const u8{ directory_path, pkg.path });
@ -552,14 +557,8 @@ fn uriAssociatedWithBuild(
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
var checked_uris = std.StringHashMap(void).init(self.allocator);
defer {
var it = checked_uris.iterator();
while (it.next()) |entry|
self.allocator.free(entry.key_ptr.*);
checked_uris.deinit();
}
var checked_uris = std.StringHashMapUnmanaged(void){};
defer checked_uris.deinit(self.allocator);
for (build_file.config.packages) |package| {
const package_uri = try URI.fromPath(self.allocator, package.path);
@ -569,7 +568,7 @@ fn uriAssociatedWithBuild(
return true;
}
if (try self.uriInImports(&checked_uris, package_uri, uri))
if (try self.uriInImports(&checked_uris, build_file, package_uri, uri))
return true;
}
@ -578,38 +577,47 @@ fn uriAssociatedWithBuild(
fn uriInImports(
self: *DocumentStore,
checked_uris: *std.StringHashMap(void),
checked_uris: *std.StringHashMapUnmanaged(void),
build_file: BuildFile,
source_uri: Uri,
uri: Uri,
) error{OutOfMemory}!bool {
if (checked_uris.contains(source_uri))
return false;
if (isInStd(source_uri)) return false;
// consider it checked even if a failure happens
try checked_uris.put(try self.allocator.dupe(u8, source_uri), {});
try checked_uris.put(self.allocator, source_uri, {});
const handle = self.getOrLoadHandle(source_uri) orelse return false;
if (handle.associated_build_file) |associated_build_file_uri| {
return std.mem.eql(u8, associated_build_file_uri, build_file.uri);
}
for (handle.import_uris.items) |import_uri| {
if (std.mem.eql(u8, uri, import_uri))
return true;
if (self.uriInImports(checked_uris, import_uri, uri) catch false)
if (try self.uriInImports(checked_uris, build_file, import_uri, uri))
return true;
}
return false;
}
/// takes ownership of the uri and text passed in.
/// takes ownership of the text passed in.
fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) error{OutOfMemory}!Handle {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
var handle: Handle = blk: {
errdefer self.allocator.free(uri);
errdefer self.allocator.free(text);
var duped_uri = try self.allocator.dupe(u8, uri);
errdefer self.allocator.free(duped_uri);
var tree = try std.zig.parse(self.allocator, text);
errdefer tree.deinit(self.allocator);
@ -618,7 +626,7 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) erro
break :blk Handle{
.open = open,
.uri = uri,
.uri = duped_uri,
.text = text,
.tree = tree,
.document_scope = document_scope,
@ -629,7 +637,7 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) erro
defer {
if (handle.associated_build_file) |build_file_uri| {
log.debug("Opened document `{s}` with build file `{s}`", .{ handle.uri, build_file_uri });
} else if (handle.is_build_file) {
} else if (isBuildFile(handle.uri)) {
log.debug("Opened document `{s}` (build file)", .{handle.uri});
} else {
log.debug("Opened document `{s}`", .{handle.uri});
@ -639,18 +647,20 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) erro
handle.import_uris = try self.collectImportUris(handle);
handle.cimports = try self.collectCIncludes(handle);
// TODO: Better logic for detecting std or subdirectories?
const in_std = std.mem.indexOf(u8, uri, "/std/") != null;
if (self.config.zig_exe_path != null and std.mem.endsWith(u8, uri, "/build.zig") and !in_std) {
const dupe_uri = try self.allocator.dupe(u8, uri);
if (self.createBuildFile(dupe_uri)) |build_file| {
try self.build_files.put(self.allocator, dupe_uri, build_file);
handle.is_build_file = true;
} else |err| {
if (self.config.zig_exe_path != null and isBuildFile(handle.uri) and !isInStd(handle.uri)) {
const gop = try self.build_files.getOrPut(self.allocator, uri);
errdefer |err| {
self.build_files.swapRemoveAt(gop.index);
log.debug("Failed to load build file {s}: (error: {})", .{ uri, err });
}
} else if (self.config.zig_exe_path != null and !std.mem.endsWith(u8, uri, "/builtin.zig") and !in_std) blk: {
log.debug("Going to walk down the tree towards: {s}", .{uri});
if (!gop.found_existing) {
const duped_uri = try self.allocator.dupe(u8, uri);
gop.value_ptr.* = try self.createBuildFile(duped_uri);
gop.key_ptr.* = gop.value_ptr.uri;
}
} else if (self.config.zig_exe_path != null and !isBuiltinFile(handle.uri) and !isInStd(handle.uri)) blk: {
// log.debug("Going to walk down the tree towards: {s}", .{uri});
// walk down the tree towards the uri. When we hit build.zig files
// determine if the uri we're interested in is involved with the build.
// This ensures that _relevant_ build.zig files higher in the
@ -658,30 +668,29 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) erro
const path = URI.parse(self.allocator, uri) catch break :blk;
defer self.allocator.free(path);
var prev_build_file: ?Uri = null;
var build_it = try BuildDotZigIterator.init(self.allocator, path);
while (try build_it.next()) |build_path| {
defer self.allocator.free(build_path);
log.debug("found build path: {s}", .{build_path});
// log.debug("found build path: {s}", .{build_path});
const build_file_uri = try URI.fromPath(self.allocator, build_path);
const gop = self.build_files.getOrPut(self.allocator, build_file_uri) catch |err| {
self.allocator.free(build_file_uri);
return err;
};
const build_file_uri = URI.fromPath(self.allocator, build_path) catch unreachable;
const gop = try self.build_files.getOrPut(self.allocator, build_file_uri);
if (!gop.found_existing) {
errdefer self.build_files.swapRemoveAt(gop.index);
gop.value_ptr.* = try self.createBuildFile(build_file_uri);
} else {
self.allocator.free(build_file_uri);
}
if (try self.uriAssociatedWithBuild(gop.value_ptr.*, uri)) {
handle.associated_build_file = build_file_uri;
handle.associated_build_file = gop.key_ptr.*;
break;
} else {
prev_build_file = build_file_uri;
}
}
// if there was no direct imports found, use the closest build file if possible
if (handle.associated_build_file == null) {
if (prev_build_file) |build_file_uri| {
} else if (handle.associated_build_file == null) {
handle.associated_build_file = build_file_uri;
}
}
@ -690,7 +699,6 @@ fn createDocument(self: *DocumentStore, uri: Uri, text: [:0]u8, open: bool) erro
return handle;
}
/// takes ownership of the uri passed in.
fn createDocumentFromURI(self: *DocumentStore, uri: Uri, open: bool) error{OutOfMemory}!?Handle {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
@ -706,15 +714,21 @@ fn createDocumentFromURI(self: *DocumentStore, uri: Uri, open: bool) error{OutOf
return try self.createDocument(uri, file_contents, open);
}
/// Caller owns returned memory.
fn collectImportUris(self: *const DocumentStore, handle: Handle) error{OutOfMemory}!std.ArrayListUnmanaged(Uri) {
const tracy_zone = tracy.trace(@src());
defer tracy_zone.end();
var imports = try analysis.collectImports(self.allocator, handle.tree);
errdefer imports.deinit(self.allocator);
var i: usize = 0;
errdefer {
// only free the uris
for (imports.items[0..i]) |uri| self.allocator.free(uri);
imports.deinit(self.allocator);
}
// Convert to URIs
var i: usize = 0;
while (i < imports.items.len) {
const maybe_uri = try self.uriFromImportStr(self.allocator, handle, imports.items[i]);

57
src/Header.zig Normal file
View File

@ -0,0 +1,57 @@
const std = @import("std");
const Header = @This();
content_length: usize,
/// null implies "application/vscode-jsonrpc; charset=utf-8"
content_type: ?[]const u8 = null,
pub fn deinit(self: @This(), allocator: std.mem.Allocator) void {
if (self.content_type) |ct| allocator.free(ct);
}
// Caller owns returned memory.
pub fn parse(allocator: std.mem.Allocator, include_carriage_return: bool, reader: anytype) !Header {
var r = Header{
.content_length = undefined,
.content_type = null,
};
errdefer r.deinit(allocator);
var has_content_length = false;
while (true) {
const header = try reader.readUntilDelimiterAlloc(allocator, '\n', 0x100);
defer allocator.free(header);
if (include_carriage_return) {
if (header.len == 0 or header[header.len - 1] != '\r') return error.MissingCarriageReturn;
if (header.len == 1) break;
} else {
if (header.len == 0) break;
}
const header_name = header[0 .. std.mem.indexOf(u8, header, ": ") orelse return error.MissingColon];
const header_value = header[header_name.len + 2 .. header.len - @boolToInt(include_carriage_return)];
if (std.mem.eql(u8, header_name, "Content-Length")) {
if (header_value.len == 0) return error.MissingHeaderValue;
r.content_length = std.fmt.parseInt(usize, header_value, 10) catch return error.InvalidContentLength;
has_content_length = true;
} else if (std.mem.eql(u8, header_name, "Content-Type")) {
r.content_type = try allocator.dupe(u8, header_value);
} else {
return error.UnknownHeader;
}
}
if (!has_content_length) return error.MissingContentLength;
return r;
}
pub fn write(header: Header, include_carriage_return: bool, writer: anytype) @TypeOf(writer).Error!void {
const seperator: []const u8 = if (include_carriage_return) "\r\n" else "\n";
try writer.print("Content-Length: {}{s}", .{ header.content_length, seperator });
if (header.content_type) |content_type| {
try writer.print("Content-Type: {s}{s}", .{ content_type, seperator });
}
try writer.writeAll(seperator);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
const std = @import("std");
const DocumentStore = @import("DocumentStore.zig");
const Ast = std.zig.Ast;
const types = @import("types.zig");
const types = @import("lsp.zig");
const offsets = @import("offsets.zig");
const log = std.log.scoped(.analysis);
const ast = @import("ast.zig");
@ -19,7 +19,7 @@ pub fn deinit() void {
}
/// Gets a declaration's doc comments. Caller owns returned memory.
pub fn getDocComments(allocator: std.mem.Allocator, tree: Ast, node: Ast.Node.Index, format: types.MarkupContent.Kind) !?[]const u8 {
pub fn getDocComments(allocator: std.mem.Allocator, tree: Ast, node: Ast.Node.Index, format: types.MarkupKind) !?[]const u8 {
const base = tree.nodes.items(.main_token)[node];
const base_kind = tree.nodes.items(.tag)[node];
const tokens = tree.tokens.items(.tag);
@ -68,7 +68,7 @@ pub fn getDocCommentTokenIndex(tokens: []const std.zig.Token.Tag, base_token: As
} else idx + 1;
}
pub fn collectDocComments(allocator: std.mem.Allocator, tree: Ast, doc_comments: Ast.TokenIndex, format: types.MarkupContent.Kind, container_doc: bool) ![]const u8 {
pub fn collectDocComments(allocator: std.mem.Allocator, tree: Ast, doc_comments: Ast.TokenIndex, format: types.MarkupKind, container_doc: bool) ![]const u8 {
var lines = std.ArrayList([]const u8).init(allocator);
defer lines.deinit();
const tokens = tree.tokens.items(.tag);
@ -81,7 +81,7 @@ pub fn collectDocComments(allocator: std.mem.Allocator, tree: Ast, doc_comments:
} else break;
}
return try std.mem.join(allocator, if (format == .Markdown) " \n" else "\n", lines.items);
return try std.mem.join(allocator, if (format == .markdown) " \n" else "\n", lines.items);
}
/// Gets a function's keyword, name, arguments and return value.
@ -1162,12 +1162,7 @@ pub fn resolveTypeOfNode(store: *DocumentStore, arena: *std.heap.ArenaAllocator,
/// Collects all `@import`'s we can find into a slice of import paths (without quotes).
pub fn collectImports(allocator: std.mem.Allocator, tree: Ast) error{OutOfMemory}!std.ArrayListUnmanaged([]const u8) {
var imports = std.ArrayListUnmanaged([]const u8){};
errdefer {
for (imports.items) |imp| {
allocator.free(imp);
}
imports.deinit(allocator);
}
errdefer imports.deinit(allocator);
const tags = tree.tokens.items(.tag);
@ -1485,6 +1480,8 @@ pub fn getImportStr(tree: Ast, node: Ast.Node.Index, source_index: usize) ?[]con
if (params.len != 1) return null;
if (node_tags[params[0]] != .string_literal) return null;
const import_str = tree.tokenSlice(tree.nodes.items(.main_token)[params[0]]);
return import_str[1 .. import_str.len - 1];
}
@ -2406,41 +2403,23 @@ pub const DocumentScope = struct {
error_completions: CompletionSet,
enum_completions: CompletionSet,
pub fn debugPrint(self: DocumentScope) void {
for (self.scopes.items) |scope| {
log.debug(
\\--------------------------
\\Scope {}, loc: [{d}, {d})
\\ {d} usingnamespaces
\\Decls:
, .{
scope.data,
scope.loc.start,
scope.loc.end,
scope.uses.len,
});
var decl_it = scope.decls.iterator();
var idx: usize = 0;
while (decl_it.next()) |_| : (idx += 1) {
if (idx != 0) log.debug(", ", .{});
}
// log.debug("{s}", .{name_decl.key});
log.debug("\n--------------------------\n", .{});
}
}
pub fn deinit(self: *DocumentScope, allocator: std.mem.Allocator) void {
for (self.scopes.items) |*scope| {
scope.deinit(allocator);
}
self.scopes.deinit(allocator);
for (self.error_completions.entries.items(.key)) |item| {
if (item.documentation) |doc| allocator.free(doc.value);
switch (item.documentation orelse continue) {
.string => |str| allocator.free(str),
.MarkupContent => |content| allocator.free(content.value),
}
}
self.error_completions.deinit(allocator);
for (self.enum_completions.entries.items(.key)) |item| {
if (item.documentation) |doc| allocator.free(doc.value);
switch (item.documentation orelse continue) {
.string => |str| allocator.free(str),
.MarkupContent => |content| allocator.free(content.value),
}
}
self.enum_completions.deinit(allocator);
}
@ -2575,13 +2554,18 @@ fn makeInnerScope(allocator: std.mem.Allocator, context: ScopeContext, node_idx:
if (container_field) |_| {
if (!std.mem.eql(u8, name, "_")) {
var doc = if (try getDocComments(allocator, tree, decl, .Markdown)) |docs|
types.MarkupContent{ .kind = .Markdown, .value = docs }
else
null;
var gop_res = try context.enums.getOrPut(allocator, .{ .label = name, .kind = .Constant, .insertText = name, .insertTextFormat = .PlainText, .documentation = doc });
const Documentation = @TypeOf(@as(types.CompletionItem, undefined).documentation);
var doc: Documentation = if (try getDocComments(allocator, tree, decl, .markdown)) |docs| .{ .MarkupContent = types.MarkupContent{ .kind = .markdown, .value = docs } } else null;
var gop_res = try context.enums.getOrPut(allocator, .{
.label = name,
.kind = .Constant,
.insertText = name,
.insertTextFormat = .PlainText,
.documentation = doc,
});
if (gop_res.found_existing) {
if (doc) |d| allocator.free(d.value);
if (doc) |d| allocator.free(d.MarkupContent.value);
}
}
}

View File

@ -167,15 +167,15 @@ fn fullWhile(tree: Ast, info: full.While.Components) full.While {
.else_token = undefined,
.error_token = null,
};
var tok_i = info.while_token - 1;
var tok_i = info.while_token -| 1;
if (token_tags[tok_i] == .keyword_inline) {
result.inline_token = tok_i;
tok_i -= 1;
}
if (token_tags[tok_i] == .colon and
token_tags[tok_i - 1] == .identifier)
token_tags[tok_i -| 1] == .identifier)
{
result.label_token = tok_i - 1;
result.label_token = tok_i -| 1;
}
const last_cond_token = lastToken(tree, info.cond_expr);
if (token_tags[last_cond_token + 2] == .pipe) {
@ -539,10 +539,15 @@ pub fn lastToken(tree: Ast, node: Ast.Node.Index) Ast.TokenIndex {
.container_decl_arg_trailing,
.switch_comma,
=> {
if (datas[n].rhs != 0) {
const members = tree.extraData(datas[n].rhs, Node.SubRange);
std.debug.assert(members.end - members.start > 0);
end_offset += 2; // for the comma + rbrace
n = tree.extra_data[members.end - 1]; // last parameter
} else {
end_offset += 1;
n = datas[n].lhs;
}
},
.array_init_dot,
.struct_init_dot,

View File

@ -5,8 +5,7 @@ const DocumentStore = @import("DocumentStore.zig");
const analysis = @import("analysis.zig");
const ast = @import("ast.zig");
const types = @import("types.zig");
const requests = @import("requests.zig");
const types = @import("lsp.zig");
const offsets = @import("offsets.zig");
pub const Builder = struct {
@ -55,11 +54,9 @@ pub const Builder = struct {
}
pub fn createWorkspaceEdit(self: *Builder, edits: []const types.TextEdit) error{OutOfMemory}!types.WorkspaceEdit {
var text_edits = std.ArrayListUnmanaged(types.TextEdit){};
try text_edits.appendSlice(self.arena.allocator(), edits);
const allocator = self.arena.allocator();
var workspace_edit = types.WorkspaceEdit{ .changes = .{} };
try workspace_edit.changes.putNoClobber(self.arena.allocator(), self.handle.uri, text_edits);
try workspace_edit.changes.?.putNoClobber(allocator, self.handle.uri, try allocator.dupe(types.TextEdit, edits));
return workspace_edit;
}
@ -74,7 +71,7 @@ fn handleNonCamelcaseFunction(builder: *Builder, actions: *std.ArrayListUnmanage
const action1 = types.CodeAction{
.title = "make function name camelCase",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(loc, new_text)}),
};
@ -115,7 +112,7 @@ fn handleUnusedFunctionParameter(builder: *Builder, actions: *std.ArrayListUnman
const action1 = types.CodeAction{
.title = "discard function parameter",
.kind = .SourceFixAll,
.kind = .@"source.fixAll",
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(index, new_text)}),
};
@ -123,7 +120,7 @@ fn handleUnusedFunctionParameter(builder: *Builder, actions: *std.ArrayListUnman
// TODO fix formatting
const action2 = types.CodeAction{
.title = "remove function parameter",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = false,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(getParamRemovalRange(tree, payload.param), "")}),
};
@ -162,7 +159,7 @@ fn handleUnusedVariableOrConstant(builder: *Builder, actions: *std.ArrayListUnma
try actions.append(builder.arena.allocator(), .{
.title = "discard value",
.kind = .SourceFixAll,
.kind = .@"source.fixAll",
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditPos(index, new_text)}),
});
@ -179,7 +176,7 @@ fn handleUnusedIndexCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(
// TODO fix formatting
try actions.append(builder.arena.allocator(), .{
.title = "remove capture",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.loc, "")}),
});
@ -188,7 +185,7 @@ fn handleUnusedIndexCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(
// |v, _| -> |v|
try actions.append(builder.arena.allocator(), .{
.title = "remove index capture",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(
.{ .start = capture_locs.value.end, .end = capture_locs.loc.end - 1 },
@ -207,7 +204,7 @@ fn handleUnusedCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(types
// |v, i| -> |_, i|
try actions.append(builder.arena.allocator(), .{
.title = "discard capture",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.value, "_")}),
});
@ -216,7 +213,7 @@ fn handleUnusedCapture(builder: *Builder, actions: *std.ArrayListUnmanaged(types
// TODO fix formatting
try actions.append(builder.arena.allocator(), .{
.title = "remove capture",
.kind = .QuickFix,
.kind = .quickfix,
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{builder.createTextEditLoc(capture_locs.loc, "")}),
});
@ -228,7 +225,7 @@ fn handlePointlessDiscard(builder: *Builder, actions: *std.ArrayListUnmanaged(ty
try actions.append(builder.arena.allocator(), .{
.title = "remove pointless discard",
.kind = .SourceFixAll,
.kind = .@"source.fixAll",
.isPreferred = true,
.edit = try builder.createWorkspaceEdit(&.{
builder.createTextEditLoc(edit_loc, ""),

View File

@ -4,155 +4,151 @@
"name": "enable_snippets",
"description": "Enables snippet completions when the client also supports them",
"type": "bool",
"default": "false",
"setup_question": "Do you want to enable snippets?"
"default": "true",
},
{
"name": "enable_ast_check_diagnostics",
"description": "Whether to enable ast-check diagnostics",
"type": "bool",
"default": "true",
"setup_question": "Do you want to enable ast-check diagnostics?"
"default": "true"
},
{
"name": "enable_autofix",
"description": "Whether to automatically fix errors on save. Currently supports adding and removing discards.",
"type": "bool",
"default": "false",
"setup_question": "Do you want to zls to automatically try to fix errors on save? (supports adding & removing discards)"
"default": "true",
},
{
"name": "enable_import_embedfile_argument_completions",
"description": "Whether to enable import/embedFile argument completions",
"type": "bool",
"default": "false",
"setup_question": "Do you want to enable @import/@embedFile argument path completion?"
"default": "true",
},
{
"name": "enable_semantic_tokens",
"description": "Enables semantic token support when the client also supports it",
"type": "bool",
"default": "true",
"setup_question": "Do you want to enable semantic highlighting?"
"default": "true"
},
{
"name": "enable_inlay_hints",
"description": "Enables inlay hint support when the client also supports it",
"type": "bool",
"default": "false",
"setup_question": "Do you want to enable inlay hints?"
"default": "true",
},
{
"name": "inlay_hints_show_builtin",
"description": "Enable inlay hints for builtin functions",
"type": "bool",
"default": "true",
"setup_question": null
"default": "true"
},
{
"name": "inlay_hints_exclude_single_argument",
"description": "Don't show inlay hints for single argument calls",
"type": "bool",
"default": "true",
"setup_question": null
"default": "true"
},
{
"name": "inlay_hints_hide_redundant_param_names",
"description": "Hides inlay hints when parameter name matches the identifier (e.g. foo: foo)",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "inlay_hints_hide_redundant_param_names_last_token",
"description": "Hides inlay hints when parameter name matches the last token of a parameter node (e.g. foo: bar.foo, foo: &foo)",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "operator_completions",
"description": "Enables `*` and `?` operators in completion lists",
"type": "bool",
"default": "true",
"setup_question": "Do you want to enable .* and .? completions?"
"default": "true"
},
{
"name": "warn_style",
"description": "Enables warnings for style guideline mismatches",
"type": "bool",
"default": "false",
"setup_question": "Do you want to enable style warnings?"
"default": "false"
},
{
"name": "highlight_global_var_declarations",
"description": "Whether to highlight global var declarations",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "use_comptime_interpreter",
"description": "Whether to use the comptime interpreter",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "include_at_in_builtins",
"description": "Whether the @ sign should be part of the completion of builtins",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "skip_std_references",
"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",
"type": "bool",
"default": "false",
"setup_question": null
"default": "false"
},
{
"name": "max_detail_length",
"description": "The detail field of completions is truncated to be no longer than this (in bytes)",
"type": "usize",
"default": "1048576",
"setup_question": null
"default": "1048576"
},
{
"name": "record_session",
"description": "When true, zls will record all request is receives and write in into `record_session_path`, so that they can replayed with `zls replay`",
"type": "bool",
"default": "false"
},
{
"name": "record_session_path",
"description": "Output file path when `record_session` is set. The recommended file extension *.zlsreplay",
"type": "?[]const u8",
"default": "null"
},
{
"name": "replay_session_path",
"description": "Used when calling `zls replay` for specifying the replay file. If no extra argument is given `record_session_path` is used as the default path.",
"type": "?[]const u8",
"default": "null"
},
{
"name": "builtin_path",
"description": "Path to 'builtin;' useful for debugging, automatically set if let null",
"type": "?[]const u8",
"default": "null",
"setup_question": null
"default": "null"
},
{
"name": "zig_lib_path",
"description": "Zig library path, e.g. `/path/to/zig/lib/zig`, used to analyze std library imports",
"type": "?[]const u8",
"default": "null",
"setup_question": null
"default": "null"
},
{
"name": "zig_exe_path",
"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",
"type": "?[]const u8",
"default": "null",
"setup_question": null
"default": "null"
},
{
"name": "build_runner_path",
"description": "Path to the `build_runner.zig` file provided by zls. null is equivalent to `${executable_directory}/build_runner.zig`",
"type": "?[]const u8",
"default": "null",
"setup_question": null
"default": "null"
},
{
"name": "global_cache_path",
"description": "Path to a directroy that will be used as zig's cache. null is equivalent to `${KnownFloders.Cache}/zls`",
"type": "?[]const u8",
"default": "null",
"setup_question": null
"default": "null"
}
]
}

View File

@ -1,4 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const tres = @import("tres");
const ConfigOption = struct {
/// Name of config option
@ -9,9 +11,6 @@ const ConfigOption = struct {
type: []const u8,
/// used in Config.zig as the default initializer
default: []const u8,
/// If set, this option can be configured through `zls --config`
/// currently unused but could laer be used to automatically generate queries for setup.zig
setup_question: ?[]const u8,
};
const Config = struct {
@ -46,9 +45,7 @@ fn zigTypeToTypescript(ty: []const u8) ![]const u8 {
fn generateConfigFile(allocator: std.mem.Allocator, config: Config, path: []const u8) !void {
_ = allocator;
const config_file = try std.fs.openFileAbsolute(path, .{
.mode = .write_only,
});
const config_file = try std.fs.createFileAbsolute(path, .{});
defer config_file.close();
var buff_out = std.io.bufferedWriter(config_file.writer());
@ -69,10 +66,10 @@ fn generateConfigFile(allocator: std.mem.Allocator, config: Config, path: []cons
\\{s}: {s} = {s},
\\
, .{
std.mem.trim(u8, option.description, " \t\n\r"),
std.mem.trim(u8, option.name, " \t\n\r"),
std.mem.trim(u8, option.type, " \t\n\r"),
std.mem.trim(u8, option.default, " \t\n\r"),
std.mem.trim(u8, option.description, &std.ascii.whitespace),
std.mem.trim(u8, option.name, &std.ascii.whitespace),
std.mem.trim(u8, option.type, &std.ascii.whitespace),
std.mem.trim(u8, option.default, &std.ascii.whitespace),
});
}
@ -114,7 +111,7 @@ fn generateSchemaFile(allocator: std.mem.Allocator, config: Config, path: []cons
\\ "properties":
);
try serializeObjectMap(properties, .{
try tres.stringify(properties, .{
.whitespace = .{
.indent_level = 1,
},
@ -122,26 +119,26 @@ fn generateSchemaFile(allocator: std.mem.Allocator, config: Config, path: []cons
_ = try buff_out.write("\n}\n");
try buff_out.flush();
try schema_file.setEndPos(try schema_file.getPos());
}
fn updateREADMEFile(allocator: std.mem.Allocator, config: Config, path: []const u8) !void {
var readme_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_write });
defer readme_file.close();
var readme = std.ArrayListUnmanaged(u8){
.items = try readme_file.readToEndAlloc(allocator, std.math.maxInt(usize)),
};
defer readme.deinit(allocator);
var readme = try readme_file.readToEndAlloc(allocator, std.math.maxInt(usize));
defer allocator.free(readme);
const start_indicator = "<!-- DO NOT EDIT | THIS SECTION IS AUTO-GENERATED | DO NOT EDIT -->";
const end_indicator = "<!-- DO NOT EDIT -->";
const start = start_indicator.len + (std.mem.indexOf(u8, readme.items, start_indicator) orelse return error.SectionNotFound);
const end = std.mem.indexOfPos(u8, readme.items, start, end_indicator) orelse return error.SectionNotFound;
const start = start_indicator.len + (std.mem.indexOf(u8, readme, start_indicator) orelse return error.SectionNotFound);
const end = std.mem.indexOfPos(u8, readme, start, end_indicator) orelse return error.SectionNotFound;
var new_readme = std.ArrayListUnmanaged(u8){};
defer new_readme.deinit(allocator);
var writer = new_readme.writer(allocator);
try readme_file.seekTo(0);
var writer = readme_file.writer();
try writer.writeAll(readme[0..start]);
try writer.writeAll(
\\
@ -155,29 +152,95 @@ fn updateREADMEFile(allocator: std.mem.Allocator, config: Config, path: []const
\\| `{s}` | `{s}` | `{s}` | {s} |
\\
, .{
std.mem.trim(u8, option.name, " \t\n\r"),
std.mem.trim(u8, option.type, " \t\n\r"),
std.mem.trim(u8, option.default, " \t\n\r"),
std.mem.trim(u8, option.description, " \t\n\r"),
std.mem.trim(u8, option.name, &std.ascii.whitespace),
std.mem.trim(u8, option.type, &std.ascii.whitespace),
std.mem.trim(u8, option.default, &std.ascii.whitespace),
std.mem.trim(u8, option.description, &std.ascii.whitespace),
});
}
try readme.replaceRange(allocator, start, end - start, new_readme.items);
try writer.writeAll(readme[end..]);
try readme_file.seekTo(0);
try readme_file.writeAll(readme.items);
try readme_file.setEndPos(try readme_file.getPos());
}
const ConfigurationProperty = struct {
scope: []const u8 = "resource",
type: []const u8,
description: []const u8,
@"enum": ?[]const []const u8 = null,
format: ?[]const u8 = null,
default: ?std.json.Value = null,
};
fn generateVSCodeConfigFile(allocator: std.mem.Allocator, config: Config, path: []const u8) !void {
var config_file = try std.fs.createFileAbsolute(path, .{});
defer config_file.close();
const predefined_configurations: usize = 3;
var configuration: std.StringArrayHashMapUnmanaged(ConfigurationProperty) = .{};
try configuration.ensureTotalCapacity(allocator, predefined_configurations + @intCast(u32, config.options.len));
defer {
for (configuration.keys()[predefined_configurations..]) |name| allocator.free(name);
configuration.deinit(allocator);
}
configuration.putAssumeCapacityNoClobber("trace.server", .{
.scope = "window",
.type = "string",
.@"enum" = &.{ "off", "message", "verbose" },
.description = "Traces the communication between VS Code and the language server.",
.default = .{ .String = "off" },
});
configuration.putAssumeCapacityNoClobber("check_for_update", .{
.type = "boolean",
.description = "Whether to automatically check for new updates",
.default = .{ .Bool = true },
});
configuration.putAssumeCapacityNoClobber("path", .{
.type = "string",
.description = "Path to `zls` executable. Example: `C:/zls/zig-cache/bin/zls.exe`.",
.format = "path",
.default = null,
});
for (config.options) |option| {
const name = try std.fmt.allocPrint(allocator, "zls.{s}", .{option.name});
var parser = std.json.Parser.init(allocator, false);
const default = (try parser.parse(option.default)).root;
configuration.putAssumeCapacityNoClobber(name, .{
.type = try zigTypeToTypescript(option.type),
.description = option.description,
.format = if (std.mem.indexOf(u8, option.name, "path") != null) "path" else null,
.default = if (default == .Null) null else default,
});
}
var buffered_writer = std.io.bufferedWriter(config_file.writer());
var writer = buffered_writer.writer();
try tres.stringify(configuration, .{
.whitespace = .{},
.emit_null_optional_fields = false,
}, writer);
try buffered_writer.flush();
}
pub fn main() !void {
var arg_it = std.process.args();
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
var gpa = general_purpose_allocator.allocator();
var arg_it = try std.process.argsWithAllocator(gpa);
defer arg_it.deinit();
_ = arg_it.next() orelse @panic("");
const config_path = arg_it.next() orelse @panic("first argument must be path to Config.zig");
const schema_path = arg_it.next() orelse @panic("second argument must be path to schema.json");
const readme_path = arg_it.next() orelse @panic("third argument must be path to README.md");
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
var gpa = general_purpose_allocator.allocator();
const maybe_vscode_config_path = arg_it.next();
const parse_options = std.json.ParseOptions{
.allocator = gpa,
@ -190,50 +253,19 @@ pub fn main() !void {
try generateSchemaFile(gpa, config, schema_path);
try updateREADMEFile(gpa, config, readme_path);
std.log.warn(
\\ If you have added a new configuration option and it should be configuration through the config wizard, then edit src/setup.zig
, .{});
if (maybe_vscode_config_path) |vscode_config_path| {
try generateVSCodeConfigFile(gpa, config, vscode_config_path);
}
std.log.info(
if (builtin.os.tag == .windows) {
std.log.warn("Running on windows may result in CRLF and LF mismatch", .{});
}
try std.io.getStdOut().writeAll(
\\If you have added a new configuration option and it should be configuration through the config wizard, then edit `src/setup.zig`
\\
\\Changing configuration options may also require editing the `package.json` from zls-vscode at https://github.com/zigtools/zls-vscode/blob/master/package.json
, .{});
}
fn serializeObjectMap(
value: anytype,
options: std.json.StringifyOptions,
out_stream: anytype,
) @TypeOf(out_stream).Error!void {
try out_stream.writeByte('{');
var field_output = false;
var child_options = options;
if (child_options.whitespace) |*child_whitespace| {
child_whitespace.indent_level += 1;
}
var it = value.iterator();
while (it.next()) |entry| {
if (!field_output) {
field_output = true;
} else {
try out_stream.writeByte(',');
}
if (child_options.whitespace) |child_whitespace| {
try child_whitespace.outputIndent(out_stream);
}
try std.json.stringify(entry.key_ptr.*, options, out_stream);
try out_stream.writeByte(':');
if (child_options.whitespace) |child_whitespace| {
if (child_whitespace.separator) {
try out_stream.writeByte(' ');
}
}
try std.json.stringify(entry.value_ptr.*, child_options, out_stream);
}
if (field_output) {
if (options.whitespace) |whitespace| {
try whitespace.outputIndent(out_stream);
}
}
try out_stream.writeByte('}');
\\You can use `zig build gen -Dvscode-config-path=/path/to/output/file.json` to generate the new configuration properties which you can then copy into `package.json`
\\
);
}

View File

@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const setup = @import("setup.zig");
const tracy = @import("tracy.zig");
const known_folders = @import("known-folders");
@ -65,7 +65,7 @@ pub fn configChanged(config: *Config, allocator: std.mem.Allocator, builtin_crea
logger.debug("zig path `{s}` is not absolute, will look in path", .{exe_path});
allocator.free(exe_path);
}
config.zig_exe_path = try setup.findZig(allocator);
config.zig_exe_path = try findZig(allocator);
}
if (config.zig_exe_path) |exe_path| blk: {
@ -208,3 +208,36 @@ fn getConfigurationType() type {
config_info.Struct.decls = &.{};
return @Type(config_info);
}
pub fn findZig(allocator: std.mem.Allocator) !?[]const u8 {
const env_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) {
error.EnvironmentVariableNotFound => {
return null;
},
else => return err,
};
defer allocator.free(env_path);
const exe_extension = builtin.target.exeFileExt();
const zig_exe = try std.fmt.allocPrint(allocator, "zig{s}", .{exe_extension});
defer allocator.free(zig_exe);
var it = std.mem.tokenize(u8, env_path, &[_]u8{std.fs.path.delimiter});
while (it.next()) |path| {
if (builtin.os.tag == .windows) {
if (std.mem.indexOfScalar(u8, path, '/') != null) continue;
}
const full_path = try std.fs.path.join(allocator, &[_][]const u8{ path, zig_exe });
defer allocator.free(full_path);
if (!std.fs.path.isAbsolute(full_path)) continue;
const file = std.fs.openFileAbsolute(full_path, .{}) catch continue;
defer file.close();
const stat = file.stat() catch continue;
if (stat.kind == .Directory) continue;
return try allocator.dupe(u8, full_path);
}
return null;
}

View File

@ -1,45 +0,0 @@
// Run with node
const fs = require("fs");
const path = require("path");
const sourceOfTruth = fs.readFileSync(path.join(__dirname, "..", "Config.zig"));
const lines = sourceOfTruth.toString().split("\n");
function mapType(type) {
switch (type) {
case "?[]const u8":
return "string";
case "bool":
return "boolean";
case "usize":
return "integer";
default:
throw new Error("unknown type!");
}
}
let comment = null;
for (const line of lines) {
if (line.startsWith("///")) {
if (comment === null) comment = line.slice(3).trim();
else comment += line.slice(3);
} else if (comment) {
const name = line.split(":")[0].trim();
const type = line.split(":")[1].split("=")[0].trim();
const defaultValue = line.split(":")[1].split("=")[1].trim().replace(",","");
console.log(`"zls.${name}": ${JSON.stringify({
"scope": "resource",
"type": mapType(type),
"description": comment,
"default": JSON.parse(defaultValue)
})},`);
comment = null;
}
}

View File

@ -21,16 +21,14 @@ pub const builtins = [_]Builtin{
},
.{
.name = "@addWithOverflow",
.signature = "@addWithOverflow(comptime T: type, a: T, b: T, result: *T) bool",
.snippet = "@addWithOverflow(${1:comptime T: type}, ${2:a: T}, ${3:b: T}, ${4:result: *T})",
.signature = "@addWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }",
.snippet = "@addWithOverflow(${1:a: anytype}, ${2:b: anytype})",
.documentation =
\\Performs `result.* = a + b`. If overflow or underflow occurs, stores the overflowed bits in `result` and returns `true`. If no overflow or underflow occurs, returns `false`.
\\Performs `a + b` and returns a tuple with the result and a possible overflow bit.
,
.arguments = &.{
"comptime T: type",
"a: T",
"b: T",
"result: *T",
"a: anytype",
"b: anytype",
},
},
.{
@ -1066,16 +1064,14 @@ pub const builtins = [_]Builtin{
},
.{
.name = "@mulWithOverflow",
.signature = "@mulWithOverflow(comptime T: type, a: T, b: T, result: *T) bool",
.snippet = "@mulWithOverflow(${1:comptime T: type}, ${2:a: T}, ${3:b: T}, ${4:result: *T})",
.signature = "@mulWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }",
.snippet = "@mulWithOverflow(${1:a: anytype}, ${2:b: anytype})",
.documentation =
\\Performs `result.* = a * b`. If overflow or underflow occurs, stores the overflowed bits in `result` and returns `true`. If no overflow or underflow occurs, returns `false`.
\\Performs `a * b` and returns a tuple with the result and a possible overflow bit.
,
.arguments = &.{
"comptime T: type",
"a: T",
"b: T",
"result: *T",
"a: anytype",
"b: anytype",
},
},
.{
@ -1326,18 +1322,16 @@ pub const builtins = [_]Builtin{
},
.{
.name = "@shlWithOverflow",
.signature = "@shlWithOverflow(comptime T: type, a: T, shift_amt: Log2T, result: *T) bool",
.snippet = "@shlWithOverflow(${1:comptime T: type}, ${2:a: T}, ${3:shift_amt: Log2T}, ${4:result: *T})",
.signature = "@shlWithOverflow(a: anytype, shift_amt: Log2T) struct { @TypeOf(a), u1 }",
.snippet = "@shlWithOverflow(${1:a: anytype}, ${2:shift_amt: Log2T})",
.documentation =
\\Performs `result.* = a << b`. If overflow or underflow occurs, stores the overflowed bits in `result` and returns `true`. If no overflow or underflow occurs, returns `false`.
\\Performs `a << b` and returns a tuple with the result and a possible overflow bit.
\\
\\The type of `shift_amt` is an unsigned integer with `log2(@typeInfo(T).Int.bits)` bits. This is because `shift_amt >= @typeInfo(T).Int.bits` is undefined behavior.
\\The type of `shift_amt` is an unsigned integer with `log2(@typeInfo(@TypeOf(a)).Int.bits)` bits. This is because `shift_amt >= @typeInfo(@TypeOf(a)).Int.bits` is undefined behavior.
,
.arguments = &.{
"comptime T: type",
"a: T",
"a: anytype",
"shift_amt: Log2T",
"result: *T",
},
},
.{
@ -1619,16 +1613,14 @@ pub const builtins = [_]Builtin{
},
.{
.name = "@subWithOverflow",
.signature = "@subWithOverflow(comptime T: type, a: T, b: T, result: *T) bool",
.snippet = "@subWithOverflow(${1:comptime T: type}, ${2:a: T}, ${3:b: T}, ${4:result: *T})",
.signature = "@subWithOverflow(a: anytype, b: anytype) struct { @TypeOf(a, b), u1 }",
.snippet = "@subWithOverflow(${1:a: anytype}, ${2:b: anytype})",
.documentation =
\\Performs `result.* = a - b`. If overflow or underflow occurs, stores the overflowed bits in `result` and returns `true`. If no overflow or underflow occurs, returns `false`.
\\Performs `a - b` and returns a tuple with the result and a possible overflow bit.
,
.arguments = &.{
"comptime T: type",
"a: T",
"b: T",
"result: *T",
"a: anytype",
"b: anytype",
},
},
.{

View File

@ -1,8 +1,8 @@
const types = @import("../types.zig");
const types = @import("../lsp.zig");
pub const Snipped = struct {
label: []const u8,
kind: types.CompletionItem.Kind,
kind: types.CompletionItemKind,
text: ?[]const u8 = null,
};

136
src/debug.zig Normal file
View File

@ -0,0 +1,136 @@
const std = @import("std");
const analysis = @import("analysis.zig");
const offsets = @import("offsets.zig");
pub fn printTree(tree: std.zig.Ast) void {
if (!std.debug.runtime_safety) @compileError("this function should only be used in debug mode!");
std.debug.print(
\\
\\nodes tag lhs rhs token
\\-----------------------------------------------
\\
, .{});
var i: usize = 0;
while (i < tree.nodes.len) : (i += 1) {
std.debug.print(" {d:<3} {s:<20} {d:<3} {d:<3} {d:<3} {s}\n", .{
i,
@tagName(tree.nodes.items(.tag)[i]),
tree.nodes.items(.data)[i].lhs,
tree.nodes.items(.data)[i].rhs,
tree.nodes.items(.main_token)[i],
offsets.tokenToSlice(tree, tree.nodes.items(.main_token)[i]),
});
}
std.debug.print(
\\
\\tokens tag start
\\----------------------------------
\\
, .{});
i = 0;
while (i < tree.tokens.len) : (i += 1) {
std.debug.print(" {d:<3} {s:<20} {d:<}\n", .{
i,
@tagName(tree.tokens.items(.tag)[i]),
tree.tokens.items(.start)[i],
});
}
}
pub fn printDocumentScope(doc_scope: analysis.DocumentScope) void {
if (!std.debug.runtime_safety) @compileError("this function should only be used in debug mode!");
for (doc_scope.scopes.items) |scope, i| {
if (i != 0) std.debug.print("\n\n", .{});
std.debug.print(
\\[{d}, {d}] {}
\\usingnamespaces: {d}
\\Decls:
\\
, .{
scope.loc.start,
scope.loc.end,
scope.data,
scope.uses.items.len,
});
var decl_it = scope.decls.iterator();
var idx: usize = 0;
while (decl_it.next()) |entry| : (idx += 1) {
std.debug.print(" {s:<8} {}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}
}
}
pub const FailingAllocator = struct {
internal_allocator: std.mem.Allocator,
random: std.rand.DefaultPrng,
likelihood: u32,
/// the chance that an allocation will fail is `1/likelihood`
/// `likelihood == 0` means that every allocation will fail
/// `likelihood == std.math.intMax(u32)` means that no allocation will be forced to fail
pub fn init(internal_allocator: std.mem.Allocator, likelihood: u32) FailingAllocator {
var seed = std.mem.zeroes([8]u8);
std.os.getrandom(&seed) catch {};
return FailingAllocator{
.internal_allocator = internal_allocator,
.random = std.rand.DefaultPrng.init(@bitCast(u64, seed)),
.likelihood = likelihood,
};
}
pub fn allocator(self: *FailingAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(
ctx: *anyopaque,
len: usize,
log2_ptr_align: u8,
return_address: usize,
) ?[*]u8 {
const self = @ptrCast(*FailingAllocator, @alignCast(@alignOf(FailingAllocator), ctx));
if (shouldFail(self)) return null;
return self.internal_allocator.rawAlloc(len, log2_ptr_align, return_address);
}
fn resize(
ctx: *anyopaque,
old_mem: []u8,
log2_old_align: u8,
new_len: usize,
ra: usize,
) bool {
const self = @ptrCast(*FailingAllocator, @alignCast(@alignOf(FailingAllocator), ctx));
if (!self.internal_allocator.rawResize(old_mem, log2_old_align, new_len, ra))
return false;
return true;
}
fn free(
ctx: *anyopaque,
old_mem: []u8,
log2_old_align: u8,
ra: usize,
) void {
const self = @ptrCast(*FailingAllocator, @alignCast(@alignOf(FailingAllocator), ctx));
self.internal_allocator.rawFree(old_mem, log2_old_align, ra);
}
fn shouldFail(self: *FailingAllocator) bool {
if (self.likelihood == std.math.maxInt(u32)) return false;
return 0 == self.random.random().intRangeAtMostBiased(u32, 0, self.likelihood);
}
};

View File

@ -1,6 +1,5 @@
const std = @import("std");
const types = @import("types.zig");
const requests = @import("requests.zig");
const types = @import("lsp.zig");
const offsets = @import("offsets.zig");
pub const Error = error{ OutOfMemory, InvalidRange };
@ -357,14 +356,14 @@ fn char_pos_to_range(
pub fn applyTextEdits(
allocator: std.mem.Allocator,
text: []const u8,
content_changes: []const requests.TextDocumentContentChangeEvent,
content_changes: []const types.TextDocumentContentChangeEvent,
encoding: offsets.Encoding,
) ![:0]const u8 {
var last_full_text_change: ?usize = null;
var i: usize = content_changes.len;
while (i > 0) {
i -= 1;
if (content_changes[i].range == null) {
if (content_changes[i] == .literal_1) {
last_full_text_change = i;
continue;
}
@ -373,16 +372,16 @@ pub fn applyTextEdits(
var text_array = std.ArrayListUnmanaged(u8){};
errdefer text_array.deinit(allocator);
try text_array.appendSlice(allocator, if (last_full_text_change) |index| content_changes[index].text else text);
try text_array.appendSlice(allocator, if (last_full_text_change) |index| content_changes[index].literal_1.text else text);
// don't even bother applying changes before a full text change
const changes = content_changes[if (last_full_text_change) |index| index + 1 else 0..];
for (changes) |item| {
const range = item.range.?; // every element is guaranteed to have `range` set
const range = item.literal_0.range;
const loc = offsets.rangeToLoc(text_array.items, range, encoding);
try text_array.replaceRange(allocator, loc.start, loc.end - loc.start, item.text);
try text_array.replaceRange(allocator, loc.start, loc.end - loc.start, item.literal_0.text);
}
return try text_array.toOwnedSliceSentinel(allocator, 0);

View File

@ -1,43 +0,0 @@
const std = @import("std");
const RequestHeader = struct {
content_length: usize,
/// null implies "application/vscode-jsonrpc; charset=utf-8"
content_type: ?[]const u8,
pub fn deinit(self: @This(), allocator: std.mem.Allocator) void {
if (self.content_type) |ct| allocator.free(ct);
}
};
pub fn readRequestHeader(allocator: std.mem.Allocator, instream: anytype) !RequestHeader {
var r = RequestHeader{
.content_length = undefined,
.content_type = null,
};
errdefer r.deinit(allocator);
var has_content_length = false;
while (true) {
const header = try instream.readUntilDelimiterAlloc(allocator, '\n', 0x100);
defer allocator.free(header);
if (header.len == 0 or header[header.len - 1] != '\r') return error.MissingCarriageReturn;
if (header.len == 1) break;
const header_name = header[0 .. std.mem.indexOf(u8, header, ": ") orelse return error.MissingColon];
const header_value = header[header_name.len + 2 .. header.len - 1];
if (std.mem.eql(u8, header_name, "Content-Length")) {
if (header_value.len == 0) return error.MissingHeaderValue;
r.content_length = std.fmt.parseInt(usize, header_value, 10) catch return error.InvalidContentLength;
has_content_length = true;
} else if (std.mem.eql(u8, header_name, "Content-Type")) {
r.content_type = try allocator.dupe(u8, header_value);
} else {
return error.UnknownHeader;
}
}
if (!has_content_length) return error.MissingContentLength;
return r;
}

View File

@ -2,7 +2,7 @@ const std = @import("std");
const zig_builtin = @import("builtin");
const DocumentStore = @import("DocumentStore.zig");
const analysis = @import("analysis.zig");
const types = @import("types.zig");
const types = @import("lsp.zig");
const offsets = @import("offsets.zig");
const Ast = std.zig.Ast;
const log = std.log.scoped(.inlay_hint);
@ -22,7 +22,7 @@ pub const inlay_hints_max_inline_children = 12;
/// checks whether node is inside the range
fn isNodeInRange(tree: Ast, node: Ast.Node.Index, range: types.Range) bool {
const endLocation = tree.tokenLocation(0, tree.lastToken(node));
const endLocation = tree.tokenLocation(0, ast.lastToken(tree, node));
if (endLocation.line < range.start.line) return false;
const beginLocation = tree.tokenLocation(0, tree.firstToken(node));
@ -32,20 +32,13 @@ fn isNodeInRange(tree: Ast, node: Ast.Node.Index, range: types.Range) bool {
}
const Builder = struct {
allocator: std.mem.Allocator,
arena: std.mem.Allocator,
config: *const Config,
handle: *const DocumentStore.Handle,
hints: std.ArrayListUnmanaged(types.InlayHint),
hover_kind: types.MarkupContent.Kind,
hover_kind: types.MarkupKind,
encoding: offsets.Encoding,
fn deinit(self: *Builder) void {
for (self.hints.items) |hint| {
self.allocator.free(hint.tooltip.value);
}
self.hints.deinit(self.allocator);
}
fn appendParameterHint(self: *Builder, position: types.Position, label: []const u8, tooltip: []const u8, tooltip_noalias: bool, tooltip_comptime: bool) !void {
// TODO allocation could be avoided by extending InlayHint.jsonStringify
// adding tooltip_noalias & tooltip_comptime to InlayHint should be enough
@ -53,28 +46,28 @@ const Builder = struct {
if (tooltip.len == 0) break :blk "";
const prefix = if (tooltip_noalias) if (tooltip_comptime) "noalias comptime " else "noalias " else if (tooltip_comptime) "comptime " else "";
if (self.hover_kind == .Markdown) {
break :blk try std.fmt.allocPrint(self.allocator, "```zig\n{s}{s}\n```", .{ prefix, tooltip });
if (self.hover_kind == .markdown) {
break :blk try std.fmt.allocPrint(self.arena, "```zig\n{s}{s}\n```", .{ prefix, tooltip });
}
break :blk try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ prefix, tooltip });
break :blk try std.fmt.allocPrint(self.arena, "{s}{s}", .{ prefix, tooltip });
};
try self.hints.append(self.allocator, .{
try self.hints.append(self.arena, .{
.position = position,
.label = label,
.label = .{ .string = label },
.kind = types.InlayHintKind.Parameter,
.tooltip = .{
.tooltip = .{ .MarkupContent = .{
.kind = self.hover_kind,
.value = tooltip_text,
},
} },
.paddingLeft = false,
.paddingRight = true,
});
}
fn toOwnedSlice(self: *Builder) error{OutOfMemory}![]types.InlayHint {
return self.hints.toOwnedSlice(self.allocator);
return self.hints.toOwnedSlice(self.arena);
}
};
@ -689,26 +682,23 @@ fn writeNodeInlayHint(builder: *Builder, arena: *std.heap.ArenaAllocator, store:
/// creates a list of `InlayHint`'s from the given document
/// only parameter hints are created
/// only hints in the given range are created
/// Caller owns returned memory.
/// `InlayHint.tooltip.value` has to deallocated separately
pub fn writeRangeInlayHint(
arena: *std.heap.ArenaAllocator,
config: Config,
store: *DocumentStore,
handle: *const DocumentStore.Handle,
range: types.Range,
hover_kind: types.MarkupContent.Kind,
hover_kind: types.MarkupKind,
encoding: offsets.Encoding,
) error{OutOfMemory}![]types.InlayHint {
var builder: Builder = .{
.allocator = arena.child_allocator,
.arena = arena.allocator(),
.config = &config,
.handle = handle,
.hints = .{},
.hover_kind = hover_kind,
.encoding = encoding,
};
errdefer builder.deinit();
var buf: [2]Ast.Node.Index = undefined;
for (ast.declMembers(handle.tree, 0, &buf)) |child| {

7852
src/lsp.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@ const known_folders = @import("known-folders");
const Config = @import("Config.zig");
const configuration = @import("configuration.zig");
const Server = @import("Server.zig");
const setup = @import("setup.zig");
const readRequestHeader = @import("header.zig").readRequestHeader;
const Header = @import("Header.zig");
const debug = @import("debug.zig");
const logger = std.log.scoped(.main);
@ -34,21 +34,114 @@ pub fn log(
std.debug.print(format ++ "\n", args);
}
fn loop(server: *Server) !void {
var reader = std.io.getStdIn().reader();
fn loop(
server: *Server,
record_file: ?std.fs.File,
replay_file: ?std.fs.File,
) !void {
const std_in = std.io.getStdIn().reader();
const std_out = std.io.getStdOut().writer();
var buffered_reader = std.io.bufferedReader(if (replay_file) |file| file.reader() else std_in);
const reader = buffered_reader.reader();
var buffered_writer = std.io.bufferedWriter(std_out);
const writer = buffered_writer.writer();
while (true) {
const headers = readRequestHeader(server.allocator, reader) catch |err| {
logger.err("{s}; exiting!", .{@errorName(err)});
return;
};
const buffer = try server.allocator.alloc(u8, headers.content_length);
defer server.allocator.free(buffer);
var arena = std.heap.ArenaAllocator.init(server.allocator);
defer arena.deinit();
try reader.readNoEof(buffer);
// write server -> client messages
for (server.outgoing_messages.items) |outgoing_message| {
const header = Header{ .content_length = outgoing_message.len };
try header.write(true, writer);
try writer.writeAll(outgoing_message);
}
try buffered_writer.flush();
for (server.outgoing_messages.items) |outgoing_message| {
server.allocator.free(outgoing_message);
}
server.outgoing_messages.clearRetainingCapacity();
const writer = std.io.getStdOut().writer();
try server.processJsonRpc(writer, buffer);
// read and handle client -> server message
const header = try Header.parse(arena.allocator(), replay_file == null, reader);
const json_message = try arena.allocator().alloc(u8, header.content_length);
try reader.readNoEof(json_message);
if (record_file) |file| {
try header.write(false, file.writer());
try file.writeAll(json_message);
}
server.processJsonRpc(&arena, json_message);
}
}
fn getRecordFile(config: Config) ?std.fs.File {
if (!config.record_session) return null;
if (config.record_session_path) |record_path| {
if (std.fs.createFileAbsolute(record_path, .{})) |file| {
std.debug.print("recording to {s}\n", .{record_path});
return file;
} else |err| {
std.log.err("failed to create record file at {s}: {}", .{ record_path, err });
return null;
}
} else {
std.log.err("`record_session` is set but `record_session_path` is unspecified", .{});
return null;
}
}
fn getReplayFile(config: Config) ?std.fs.File {
const replay_path = config.replay_session_path orelse config.record_session_path orelse return null;
if (std.fs.openFileAbsolute(replay_path, .{})) |file| {
std.debug.print("replaying from {s}\n", .{replay_path});
return file;
} else |err| {
std.log.err("failed to open replay file at {s}: {}", .{ replay_path, err });
return null;
}
}
/// when recording we add a message that saves the current configuration in the replay
/// when replaying we read this message and replace the current config
fn updateConfig(
allocator: std.mem.Allocator,
config: *Config,
record_file: ?std.fs.File,
replay_file: ?std.fs.File,
) !void {
if (record_file) |file| {
var cfg = config.*;
cfg.record_session = false;
cfg.record_session_path = null;
cfg.replay_session_path = null;
var buffer = std.ArrayListUnmanaged(u8){};
defer buffer.deinit(allocator);
try std.json.stringify(cfg, .{}, buffer.writer(allocator));
const header = Header{ .content_length = buffer.items.len };
try header.write(false, file.writer());
try file.writeAll(buffer.items);
}
if (replay_file) |file| {
const header = try Header.parse(allocator, false, file.reader());
defer header.deinit(allocator);
const json_message = try allocator.alloc(u8, header.content_length);
defer allocator.free(json_message);
try file.reader().readNoEof(json_message);
var token_stream = std.json.TokenStream.init(json_message);
const new_config = try std.json.parse(Config, &token_stream, .{ .allocator = allocator });
std.json.parseFree(Config, config.*, .{ .allocator = allocator });
config.* = new_config;
}
}
@ -60,44 +153,28 @@ const ConfigWithPath = struct {
fn getConfig(
allocator: std.mem.Allocator,
config_path: ?[]const u8,
/// If true, and the provided config_path is non-null, frees
/// the aforementioned path, in the case that it is
/// not returned.
free_old_config_path: bool,
) !ConfigWithPath {
if (config_path) |path| {
if (configuration.loadFromFile(allocator, path)) |conf| {
return ConfigWithPath{
.config = conf,
.config_path = path,
};
if (configuration.loadFromFile(allocator, path)) |config| {
return ConfigWithPath{ .config = config, .config_path = path };
}
std.debug.print(
\\Could not open configuration file '{s}'
\\Falling back to a lookup in the local and global configuration folders
\\
, .{path});
if (free_old_config_path) {
allocator.free(path);
}
}
if (try known_folders.getPath(allocator, .local_configuration)) |path| {
if (configuration.loadFromFolder(allocator, path)) |conf| {
return ConfigWithPath{
.config = conf,
.config_path = path,
};
if (configuration.loadFromFolder(allocator, path)) |config| {
return ConfigWithPath{ .config = config, .config_path = path };
}
allocator.free(path);
}
if (try known_folders.getPath(allocator, .global_configuration)) |path| {
if (configuration.loadFromFolder(allocator, path)) |conf| {
return ConfigWithPath{
.config = conf,
.config_path = path,
};
if (configuration.loadFromFolder(allocator, path)) |config| {
return ConfigWithPath{ .config = config, .config_path = path };
}
allocator.free(path);
}
@ -108,22 +185,32 @@ fn getConfig(
};
}
const ParseArgsResult = enum { proceed, exit };
fn parseArgs(
allocator: std.mem.Allocator,
config: *ConfigWithPath,
) !ParseArgsResult {
const ParseArgsResult = struct {
action: enum { proceed, exit },
config_path: ?[]const u8,
replay_enabled: bool,
replay_session_path: ?[]const u8,
};
fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult {
var result = ParseArgsResult{
.action = .exit,
.config_path = null,
.replay_enabled = false,
.replay_session_path = null,
};
const ArgId = enum {
help,
version,
config,
replay,
@"enable-debug-log",
@"show-config-path",
@"config-path",
};
const arg_id_map = std.ComptimeStringMap(ArgId, comptime blk: {
const fields = @typeInfo(ArgId).Enum.fields;
const KV = std.meta.Tuple(&.{ []const u8, ArgId });
const KV = struct { []const u8, ArgId };
var pairs: [fields.len]KV = undefined;
for (pairs) |*pair, i| pair.* = .{ fields[i].name, @intToEnum(ArgId, fields[i].value) };
break :blk pairs[0..];
@ -140,10 +227,10 @@ fn parseArgs(
var cmd_infos: InfoMap = InfoMap.init(.{
.help = "Prints this message.",
.version = "Prints the compiler version with which the server was compiled.",
.replay = "Replay a previous recorded zls session",
.@"enable-debug-log" = "Enables debug logs.",
.@"config-path" = "Specify the path to a configuration file specifying LSP behaviour.",
.@"show-config-path" = "Prints the path to the configuration file to stdout",
.config = "Run the ZLS configuration wizard.",
});
var info_it = cmd_infos.iterator();
while (info_it.next()) |entry| {
@ -159,9 +246,6 @@ fn parseArgs(
// Makes behavior of enabling debug more logging consistent regardless of argument order.
var specified = std.enums.EnumArray(ArgId, bool).initFill(false);
var config_path: ?[]const u8 = null;
errdefer if (config_path) |path| allocator.free(path);
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
@ -169,76 +253,82 @@ fn parseArgs(
if (!std.mem.startsWith(u8, tok, "--") or tok.len == 2) {
try stderr.print("{s}\n", .{help_message});
try stderr.print("Unexpected positional argument '{s}'.\n", .{tok});
return .exit;
return result;
}
const argname = tok["--".len..];
const id = arg_id_map.get(argname) orelse {
try stderr.print("{s}\n", .{help_message});
try stderr.print("Unrecognized argument '{s}'.\n", .{argname});
return .exit;
return result;
};
if (specified.get(id)) {
try stderr.print("{s}\n", .{help_message});
try stderr.print("Duplicate argument '{s}'.\n", .{argname});
return .exit;
return result;
}
specified.set(id, true);
switch (id) {
.help => {},
.version => {},
.@"enable-debug-log" => {},
.config => {},
.@"show-config-path" => {},
.help,
.version,
.@"enable-debug-log",
.@"show-config-path",
=> {},
.@"config-path" => {
const path = args_it.next() orelse {
try stderr.print("Expected configuration file path after --config-path argument.\n", .{});
return .exit;
return result;
};
config.config_path = try allocator.dupe(u8, path);
result.config_path = try allocator.dupe(u8, path);
},
.replay => {
result.replay_enabled = true;
const path = args_it.next() orelse break;
result.replay_session_path = try allocator.dupe(u8, path);
},
}
}
if (specified.get(.help)) {
try stderr.print("{s}\n", .{help_message});
return .exit;
return result;
}
if (specified.get(.version)) {
try std.io.getStdOut().writeAll(build_options.version ++ "\n");
return .exit;
}
if (specified.get(.config)) {
try setup.wizard(allocator);
return .exit;
try stdout.writeAll(build_options.version ++ "\n");
return result;
}
if (specified.get(.@"enable-debug-log")) {
actual_log_level = .debug;
logger.info("Enabled debug logging.\n", .{});
}
if (specified.get(.@"config-path")) {
std.debug.assert(config.config_path != null);
std.debug.assert(result.config_path != null);
}
if (specified.get(.@"show-config-path")) {
const new_config = try getConfig(allocator, config.config_path, true);
const new_config = try getConfig(allocator, result.config_path);
defer if (new_config.config_path) |path| allocator.free(path);
defer std.json.parseFree(Config, new_config.config, .{ .allocator = allocator });
if (new_config.config_path) |path| {
const full_path = try std.fs.path.resolve(allocator, &.{ path, "zls.json" });
const full_path = if (new_config.config_path) |path| blk: {
break :blk try std.fs.path.resolve(allocator, &.{ path, "zls.json" });
} else blk: {
const local_config_path = try known_folders.getPath(allocator, .local_configuration) orelse {
logger.err("failed to find local configuration folder", .{});
return result;
};
defer allocator.free(local_config_path);
break :blk try std.fs.path.resolve(allocator, &.{ local_config_path, "zls.json" });
};
defer allocator.free(full_path);
try stdout.writeAll(full_path);
try stdout.writeByte('\n');
} else {
logger.err("Failed to find zls.json!\n", .{});
}
return .exit;
return result;
}
return .proceed;
result.action = .proceed;
return result;
}
const stack_frames = switch (zig_builtin.mode) {
@ -249,34 +339,52 @@ const stack_frames = switch (zig_builtin.mode) {
pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = stack_frames }){};
defer _ = gpa_state.deinit();
var tracy_state = if (tracy.enable_allocation) tracy.tracyAllocator(gpa_state.allocator()) else void{};
const inner_allocator: std.mem.Allocator = if (tracy.enable_allocation) tracy_state.allocator() else gpa_state.allocator();
const allocator: std.mem.Allocator = if (tracy.enable_allocation) tracy_state.allocator() else gpa_state.allocator();
var failing_allocator_state = if (build_options.enable_failing_allocator) debug.FailingAllocator.init(inner_allocator, build_options.enable_failing_allocator_likelihood) else void{};
const allocator: std.mem.Allocator = if (build_options.enable_failing_allocator) failing_allocator_state.allocator() else inner_allocator;
var config = ConfigWithPath{
.config = undefined,
.config_path = null,
};
defer if (config.config_path) |path| allocator.free(path);
switch (try parseArgs(allocator, &config)) {
const result = try parseArgs(allocator);
defer if (result.config_path) |path| allocator.free(path);
defer if (result.replay_session_path) |path| allocator.free(path);
switch (result.action) {
.proceed => {},
.exit => return,
}
config = try getConfig(allocator, config.config_path, true);
var config = try getConfig(allocator, result.config_path);
defer std.json.parseFree(Config, config.config, .{ .allocator = allocator });
defer if (config.config_path) |path| allocator.free(path);
if (result.replay_enabled and config.config.replay_session_path == null and config.config.record_session_path == null) {
logger.err("No replay file specified", .{});
return;
}
if (config.config_path == null) {
logger.info("No config file zls.json found.", .{});
}
const record_file = if (!result.replay_enabled) getRecordFile(config.config) else null;
defer if (record_file) |file| file.close();
const replay_file = if (result.replay_enabled) getReplayFile(config.config) else null;
defer if (replay_file) |file| file.close();
std.debug.assert(record_file == null or replay_file == null);
try updateConfig(allocator, &config.config, record_file, replay_file);
var server = try Server.init(
allocator,
&config.config,
config.config_path,
record_file != null,
replay_file != null,
);
defer server.deinit();
try loop(&server);
try loop(&server, record_file, replay_file);
}

View File

@ -1,5 +1,5 @@
const std = @import("std");
const types = @import("types.zig");
const types = @import("lsp.zig");
const ast = @import("ast.zig");
const Ast = std.zig.Ast;
@ -263,8 +263,8 @@ pub fn advancePosition(text: []const u8, position: types.Position, from_index: u
/// returns the number of code units in `text`
pub fn countCodeUnits(text: []const u8, encoding: Encoding) usize {
switch (encoding) {
.utf8 => return text.len,
.utf16 => {
.@"utf-8" => return text.len,
.@"utf-16" => {
var iter: std.unicode.Utf8Iterator = .{ .bytes = text, .i = 0 };
var utf16_len: usize = 0;
@ -277,15 +277,15 @@ pub fn countCodeUnits(text: []const u8, encoding: Encoding) usize {
}
return utf16_len;
},
.utf32 => return std.unicode.utf8CountCodepoints(text) catch unreachable,
.@"utf-32" => return std.unicode.utf8CountCodepoints(text) catch unreachable,
}
}
/// returns the number of (utf-8 code units / bytes) that represent `n` code units in `text`
pub fn getNCodeUnitByteCount(text: []const u8, n: usize, encoding: Encoding) usize {
switch (encoding) {
.utf8 => return n,
.utf16 => {
.@"utf-8" => return n,
.@"utf-16" => {
if (n == 0) return 0;
var iter: std.unicode.Utf8Iterator = .{ .bytes = text, .i = 0 };
@ -300,7 +300,7 @@ pub fn getNCodeUnitByteCount(text: []const u8, n: usize, encoding: Encoding) usi
}
return iter.i;
},
.utf32 => {
.@"utf-32" => {
var i: usize = 0;
var count: usize = 0;
while (count != n) : (count += 1) {

View File

@ -2,7 +2,7 @@ const std = @import("std");
const Ast = std.zig.Ast;
const DocumentStore = @import("DocumentStore.zig");
const analysis = @import("analysis.zig");
const types = @import("types.zig");
const types = @import("lsp.zig");
const offsets = @import("offsets.zig");
const log = std.log.scoped(.references);
const ast = @import("ast.zig");
@ -488,7 +488,7 @@ pub fn symbolReferences(
}
var handle_dependencies = std.ArrayListUnmanaged([]const u8){};
try store.collectDependencies(store.allocator, handle.*, &handle_dependencies);
try store.collectDependencies(arena.allocator(), handle.*, &handle_dependencies);
for (handle_dependencies.items) |uri| {
try dependencies.put(arena.allocator(), uri, {});

View File

@ -1,324 +0,0 @@
//! This file contains request types zls handles.
//! Note that the parameter types may be incomplete.
//! We only define what we actually use.
const std = @import("std");
const types = @import("types.zig");
/// Only check for the field's existence.
const Exists = struct {
exists: bool,
};
fn Default(comptime T: type, comptime default_value: T) type {
return struct {
pub const value_type = T;
pub const default = default_value;
value: T,
};
}
pub fn ErrorUnwrappedReturnOf(comptime func: anytype) type {
return switch (@typeInfo(@TypeOf(func))) {
.Fn, .BoundFn => |fn_info| switch (@typeInfo(fn_info.return_type.?)) {
.ErrorUnion => |err_union| err_union.payload,
else => |T| return T,
},
else => unreachable,
};
}
fn Transform(comptime Original: type, comptime transform_fn: anytype) type {
return struct {
pub const original_type = Original;
pub const transform = transform_fn;
value: ErrorUnwrappedReturnOf(transform_fn),
};
}
fn fromDynamicTreeInternal(arena: *std.heap.ArenaAllocator, value: std.json.Value, out: anytype) error{ MalformedJson, OutOfMemory }!void {
const T = comptime std.meta.Child(@TypeOf(out));
if (comptime std.meta.trait.is(.Struct)(T)) {
if (value != .Object) return error.MalformedJson;
var err = false;
inline for (std.meta.fields(T)) |field| {
const is_exists = field.type == Exists;
const is_optional = comptime std.meta.trait.is(.Optional)(field.type);
const actual_type = if (is_optional) std.meta.Child(field.type) else field.type;
const is_struct = comptime std.meta.trait.is(.Struct)(actual_type);
const is_default = comptime if (is_struct) std.meta.trait.hasDecls(actual_type, .{ "default", "value_type" }) else false;
const is_transform = comptime if (is_struct) std.meta.trait.hasDecls(actual_type, .{ "original_type", "transform" }) else false;
if (value.Object.get(field.name)) |json_field| {
if (is_exists) {
@field(out, field.name) = Exists{ .exists = true };
} else if (is_transform) {
var original_value: actual_type.original_type = undefined;
try fromDynamicTreeInternal(arena, json_field, &original_value);
@field(out, field.name) = actual_type{
.value = actual_type.transform(original_value) catch
return error.MalformedJson,
};
} else if (is_default) {
try fromDynamicTreeInternal(arena, json_field, &@field(out, field.name).value);
} else if (is_optional) {
if (json_field == .Null) {
@field(out, field.name) = null;
} else {
var actual_value: actual_type = undefined;
try fromDynamicTreeInternal(arena, json_field, &actual_value);
@field(out, field.name) = actual_value;
}
} else {
try fromDynamicTreeInternal(arena, json_field, &@field(out, field.name));
}
} else {
if (is_exists) {
@field(out, field.name) = Exists{ .exists = false };
} else if (is_optional) {
@field(out, field.name) = null;
} else if (is_default) {
@field(out, field.name) = actual_type{ .value = actual_type.default };
} else {
err = true;
}
}
}
if (err) return error.MalformedJson;
} else if (comptime (std.meta.trait.isSlice(T) and T != []const u8)) {
if (value != .Array) return error.MalformedJson;
const Child = std.meta.Child(T);
if (value.Array.items.len == 0) {
out.* = &[0]Child{};
} else {
var slice = try arena.allocator().alloc(Child, value.Array.items.len);
for (value.Array.items) |arr_item, idx| {
try fromDynamicTreeInternal(arena, arr_item, &slice[idx]);
}
out.* = slice;
}
} else if (T == std.json.Value) {
out.* = value;
} else if (comptime std.meta.trait.is(.Enum)(T)) {
const info = @typeInfo(T).Enum;
const TagType = info.tag_type;
if (value != .Integer) return error.MalformedJson;
out.* = std.meta.intToEnum(
T,
std.math.cast(TagType, value.Integer) orelse return error.MalformedJson,
) catch return error.MalformedJson;
} else if (comptime std.meta.trait.is(.Int)(T)) {
if (value != .Integer) return error.MalformedJson;
out.* = std.math.cast(T, value.Integer) orelse return error.MalformedJson;
} else switch (T) {
bool => {
if (value != .Bool) return error.MalformedJson;
out.* = value.Bool;
},
f64 => {
if (value != .Float) return error.MalformedJson;
out.* = value.Float;
},
[]const u8 => {
if (value != .String) return error.MalformedJson;
out.* = value.String;
},
else => @compileError("Invalid type " ++ @typeName(T)),
}
}
pub fn fromDynamicTree(arena: *std.heap.ArenaAllocator, comptime T: type, value: std.json.Value) error{ MalformedJson, OutOfMemory }!T {
var out: T = undefined;
try fromDynamicTreeInternal(arena, value, &out);
return out;
}
const MaybeStringArray = Default([]const []const u8, &.{});
pub const Initialize = struct {
pub const ClientCapabilities = struct {
workspace: ?struct {
configuration: Default(bool, false),
didChangeConfiguration: ?struct {
dynamicRegistration: Default(bool, false), // NOTE: Should this be true? Seems like this critical feature should be nearly universal
},
workspaceFolders: Default(bool, false),
},
textDocument: ?struct {
synchronization: ?struct {
willSave: Default(bool, false),
willSaveWaitUntil: Default(bool, false),
didSave: Default(bool, false),
},
semanticTokens: Exists,
inlayHint: Exists,
hover: ?struct {
contentFormat: MaybeStringArray,
},
completion: ?struct {
completionItem: ?struct {
snippetSupport: Default(bool, false),
labelDetailsSupport: Default(bool, false),
documentationFormat: MaybeStringArray,
},
},
documentHighlight: Exists,
},
general: ?struct {
positionEncodings: MaybeStringArray,
},
};
pub const ClientInfo = struct {
name: []const u8,
version: ?[]const u8,
};
params: struct {
clientInfo: ?ClientInfo,
capabilities: ClientCapabilities,
workspaceFolders: ?[]const types.WorkspaceFolder,
},
};
pub const WorkspaceFoldersChange = struct {
params: struct {
event: struct {
added: []const types.WorkspaceFolder,
removed: []const types.WorkspaceFolder,
},
},
};
pub const OpenDocument = struct {
params: struct {
textDocument: struct {
uri: []const u8,
text: []const u8,
},
},
};
const TextDocumentIdentifier = struct {
uri: []const u8,
};
pub const ChangeDocument = struct {
params: struct {
textDocument: TextDocumentIdentifier,
contentChanges: []TextDocumentContentChangeEvent,
},
};
pub const TextDocumentContentChangeEvent = struct {
range: ?types.Range,
text: []const u8,
};
const TextDocumentIdentifierRequest = struct {
params: struct {
textDocument: TextDocumentIdentifier,
},
};
pub const SaveDocument = TextDocumentIdentifierRequest;
pub const CloseDocument = TextDocumentIdentifierRequest;
pub const SemanticTokensFull = TextDocumentIdentifierRequest;
const TextDocumentIdentifierPositionRequest = struct {
params: struct {
textDocument: TextDocumentIdentifier,
position: types.Position,
},
};
pub const SaveReason = enum(u32) {
Manual = 1,
AfterDelay = 2,
FocusOut = 3,
};
pub const WillSave = struct {
params: struct {
textDocument: TextDocumentIdentifier,
reason: SaveReason,
},
};
pub const SignatureHelp = struct {
params: struct {
textDocument: TextDocumentIdentifier,
position: types.Position,
context: ?struct {
triggerKind: enum(u32) {
invoked = 1,
trigger_character = 2,
content_change = 3,
},
triggerCharacter: ?[]const u8,
isRetrigger: bool,
activeSignatureHelp: ?types.SignatureHelp,
},
},
};
pub const Completion = TextDocumentIdentifierPositionRequest;
pub const GotoDefinition = TextDocumentIdentifierPositionRequest;
pub const GotoDeclaration = TextDocumentIdentifierPositionRequest;
pub const Hover = TextDocumentIdentifierPositionRequest;
pub const DocumentSymbols = TextDocumentIdentifierRequest;
pub const Formatting = TextDocumentIdentifierRequest;
pub const DocumentHighlight = TextDocumentIdentifierPositionRequest;
pub const Rename = struct {
params: struct {
textDocument: TextDocumentIdentifier,
position: types.Position,
newName: []const u8,
},
};
pub const References = struct {
params: struct {
textDocument: TextDocumentIdentifier,
position: types.Position,
context: struct {
includeDeclaration: bool,
},
},
};
pub const InlayHint = struct {
params: struct {
textDocument: TextDocumentIdentifier,
range: types.Range,
},
};
pub const CodeAction = struct {
params: struct {
textDocument: TextDocumentIdentifier,
range: types.Range,
context: struct {
diagnostics: []types.Diagnostic,
},
},
};
pub const FoldingRange = struct {
params: struct {
textDocument: TextDocumentIdentifier,
},
};
pub const SelectionRange = struct {
params: struct {
textDocument: TextDocumentIdentifier,
positions: []types.Position,
},
};

View File

@ -289,7 +289,7 @@ fn writeNodeTokens(builder: *Builder, maybe_node: ?Ast.Node.Index) WriteTokensEr
const token_tags = tree.tokens.items(.tag);
const node_data = tree.nodes.items(.data);
const main_tokens = tree.nodes.items(.main_token);
if (node == 0 or node > node_data.len) return;
if (node == 0 or node >= node_data.len) return;
var allocator = builder.arena.allocator();

View File

@ -1,230 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const known_folders = @import("known-folders");
/// Caller must free memory.
pub fn askString(allocator: std.mem.Allocator, prompt: []const u8, max_size: usize) ![]u8 {
const in = std.io.getStdIn().reader();
const out = std.io.getStdOut().writer();
try out.print("? {s}", .{prompt});
const result = try in.readUntilDelimiterAlloc(allocator, '\n', max_size);
return if (std.mem.endsWith(u8, result, "\r")) result[0..(result.len - 1)] else result;
}
/// Caller must free memory. Max size is recommended to be a high value, like 512.
pub fn askDirPath(allocator: std.mem.Allocator, prompt: []const u8, max_size: usize) ![]u8 {
const out = std.io.getStdOut().writer();
while (true) {
const path = try askString(allocator, prompt, max_size);
if (!std.fs.path.isAbsolute(path)) {
try out.writeAll("Error: Invalid directory, please try again.\n\n");
allocator.free(path);
continue;
}
var dir = std.fs.cwd().openDir(path, std.fs.Dir.OpenDirOptions{}) catch {
try out.writeAll("Error: Invalid directory, please try again.\n\n");
allocator.free(path);
continue;
};
dir.close();
return path;
}
}
pub fn askBool(prompt: []const u8) !bool {
const in = std.io.getStdIn().reader();
const out = std.io.getStdOut().writer();
var buffer: [1]u8 = undefined;
while (true) {
try out.print("? {s} (y/n) > ", .{prompt});
const read = in.read(&buffer) catch continue;
try in.skipUntilDelimiterOrEof('\n');
if (read == 0) return error.EndOfStream;
switch (buffer[0]) {
'y' => return true,
'n' => return false,
else => continue,
}
}
}
pub fn askSelectOne(prompt: []const u8, comptime Options: type) !Options {
const in = std.io.getStdIn().reader();
const out = std.io.getStdOut().writer();
try out.print("? {s} (select one)\n\n", .{prompt});
comptime var max_size: usize = 0;
inline for (@typeInfo(Options).Enum.fields) |option| {
try out.print(" - {s}\n", .{option.name});
if (option.name.len > max_size) max_size = option.name.len;
}
while (true) {
var buffer: [max_size + 1]u8 = undefined;
try out.writeAll("\n> ");
var result = (in.readUntilDelimiterOrEof(&buffer, '\n') catch {
try in.skipUntilDelimiterOrEof('\n');
try out.writeAll("Error: Invalid option, please try again.\n");
continue;
}) orelse return error.EndOfStream;
result = if (std.mem.endsWith(u8, result, "\r")) result[0..(result.len - 1)] else result;
inline for (@typeInfo(Options).Enum.fields) |option|
if (std.ascii.eqlIgnoreCase(option.name, result))
return @intToEnum(Options, option.value);
try out.writeAll("Error: Invalid option, please try again.\n");
}
}
pub fn wizard(allocator: std.mem.Allocator) !void {
@setEvalBranchQuota(2500);
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(
\\Welcome to the ZLS configuration wizard!
\\ *
\\ |\
\\ /* \
\\ | *\
\\ _/_*___|_ x
\\ | @ @ /
\\ @ \ /
\\ \__-/ /
\\
\\
);
var local_path = known_folders.getPath(allocator, .local_configuration) catch null;
var global_path = known_folders.getPath(allocator, .global_configuration) catch null;
defer if (local_path) |d| allocator.free(d);
defer if (global_path) |d| allocator.free(d);
const can_access_global = blk: {
std.fs.accessAbsolute(global_path orelse break :blk false, .{}) catch break :blk false;
break :blk true;
};
if (global_path == null and local_path == null) {
try stdout.writeAll("Could not open a global or local config directory.\n");
return;
}
var config_path: []const u8 = undefined;
if (can_access_global and try askBool("Should this configuration be system-wide?")) {
config_path = global_path.?;
} else {
if (local_path) |p| {
config_path = p;
} else {
try stdout.writeAll("Could not find a local config directory.\n");
return;
}
}
var dir = std.fs.cwd().openDir(config_path, .{}) catch |err| {
try stdout.print("Could not open {s}: {}.\n", .{ config_path, err });
return;
};
defer dir.close();
var file = dir.createFile("zls.json", .{}) catch |err| {
try stdout.print("Could not create {s}/zls.json: {}.\n", .{ config_path, err });
return;
};
defer file.close();
const out = file.writer();
var zig_exe_path = try findZig(allocator);
defer if (zig_exe_path) |p| allocator.free(p);
if (zig_exe_path) |path| {
try stdout.print("Found zig executable '{s}' in PATH.\n", .{path});
} else {
try stdout.writeAll("Could not find 'zig' in PATH\n");
zig_exe_path = try askString(allocator, if (builtin.os.tag == .windows)
\\What is the path to the 'zig' executable you would like to use?
\\Note that due to a bug in zig (https://github.com/ziglang/zig/issues/6044),
\\your zig directory cannot contain the '/' character.
else
"What is the path to the 'zig' executable you would like to use?", std.fs.MAX_PATH_BYTES);
}
const snippets = try askBool("Do you want to enable snippets?");
const ast_check = try askBool("Do you want to enable ast-check diagnostics?");
const autofix = try askBool("Do you want to zls to automatically try to fix errors on save? (supports adding & removing discards)");
const ief_apc = try askBool("Do you want to enable @import/@embedFile argument path completion?");
const style = try askBool("Do you want to enable style warnings?");
const semantic_tokens = try askBool("Do you want to enable semantic highlighting?");
const inlay_hints = try askBool("Do you want to enable inlay hints?");
const operator_completions = try askBool("Do you want to enable .* and .? completions?");
std.debug.print("Writing config to {s}/zls.json ... ", .{config_path});
try std.json.stringify(.{
.@"$schema" = "https://raw.githubusercontent.com/zigtools/zls/master/schema.json",
.zig_exe_path = zig_exe_path,
.enable_snippets = snippets,
.enable_ast_check_diagnostics = ast_check,
.enable_autofix = autofix,
.enable_import_embedfile_argument_completions = ief_apc,
.warn_style = style,
.enable_semantic_tokens = semantic_tokens,
.enable_inlay_hints = inlay_hints,
.operator_completions = operator_completions,
}, .{
.whitespace = .{},
}, out);
try stdout.writeAll(
\\successful.
\\
\\You can find information on how to setup zls for your editor on zigtools.github.io/install-zls/
\\
\\Thank you for choosing ZLS!
\\
);
}
pub fn findZig(allocator: std.mem.Allocator) !?[]const u8 {
const env_path = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) {
error.EnvironmentVariableNotFound => {
return null;
},
else => return err,
};
defer allocator.free(env_path);
const exe_extension = builtin.target.exeFileExt();
const zig_exe = try std.fmt.allocPrint(allocator, "zig{s}", .{exe_extension});
defer allocator.free(zig_exe);
var it = std.mem.tokenize(u8, env_path, &[_]u8{std.fs.path.delimiter});
while (it.next()) |path| {
if (builtin.os.tag == .windows) {
if (std.mem.indexOfScalar(u8, path, '/') != null) continue;
}
const full_path = try std.fs.path.join(allocator, &[_][]const u8{ path, zig_exe });
defer allocator.free(full_path);
if (!std.fs.path.isAbsolute(full_path)) continue;
const file = std.fs.openFileAbsolute(full_path, .{}) catch continue;
defer file.close();
const stat = file.stat() catch continue;
if (stat.kind == .Directory) continue;
return try allocator.dupe(u8, full_path);
}
return null;
}

View File

@ -2,31 +2,29 @@ const std = @import("std");
const analysis = @import("analysis.zig");
const offsets = @import("offsets.zig");
const DocumentStore = @import("DocumentStore.zig");
const types = @import("types.zig");
const types = @import("lsp.zig");
const Ast = std.zig.Ast;
const Token = std.zig.Token;
const identifierFromPosition = @import("Server.zig").identifierFromPosition;
const ast = @import("ast.zig");
fn fnProtoToSignatureInfo(document_store: *DocumentStore, arena: *std.heap.ArenaAllocator, commas: u32, skip_self_param: bool, handle: *const DocumentStore.Handle, fn_node: Ast.Node.Index, proto: Ast.full.FnProto) !types.SignatureInformation {
const ParameterInformation = types.SignatureInformation.ParameterInformation;
const tree = handle.tree;
const token_starts = tree.tokens.items(.start);
const alloc = arena.allocator();
const label = analysis.getFunctionSignature(tree, proto);
const proto_comments = (try analysis.getDocComments(alloc, tree, fn_node, .Markdown)) orelse "";
const proto_comments = (try analysis.getDocComments(alloc, tree, fn_node, .markdown)) orelse "";
const arg_idx = if (skip_self_param) blk: {
const has_self_param = try analysis.hasSelfParam(arena, document_store, handle, proto);
break :blk commas + @boolToInt(has_self_param);
} else commas;
var params = std.ArrayListUnmanaged(ParameterInformation){};
var params = std.ArrayListUnmanaged(types.ParameterInformation){};
var param_it = proto.iterate(&tree);
while (ast.nextFnParam(&param_it)) |param| {
const param_comments = if (param.first_doc_comment) |dc|
try analysis.collectDocComments(alloc, tree, dc, .Markdown, false)
try analysis.collectDocComments(alloc, tree, dc, .markdown, false)
else
"";
@ -55,13 +53,19 @@ fn fnProtoToSignatureInfo(document_store: *DocumentStore, arena: *std.heap.Arena
}
const param_label = tree.source[param_label_start..param_label_end];
try params.append(alloc, .{
.label = param_label,
.documentation = types.MarkupContent{ .value = param_comments },
.label = .{ .string = param_label },
.documentation = .{ .MarkupContent = .{
.kind = .markdown,
.value = param_comments,
} },
});
}
return types.SignatureInformation{
.label = label,
.documentation = types.MarkupContent{ .value = proto_comments },
.documentation = .{ .MarkupContent = .{
.kind = .markdown,
.value = proto_comments,
} },
.parameters = params.items,
.activeParameter = arg_idx,
};
@ -188,20 +192,18 @@ pub fn getSignatureInfo(document_store: *DocumentStore, arena: *std.heap.ArenaAl
for (data.builtins) |builtin| {
if (std.mem.eql(u8, builtin.name, tree.tokenSlice(expr_last_token))) {
const param_infos = try alloc.alloc(
types.SignatureInformation.ParameterInformation,
types.ParameterInformation,
builtin.arguments.len,
);
for (param_infos) |*info, i| {
info.* = .{
.label = builtin.arguments[i],
.label = .{ .string = builtin.arguments[i] },
.documentation = null,
};
}
return types.SignatureInformation{
.label = builtin.signature,
.documentation = .{
.value = builtin.documentation,
},
.documentation = .{ .string = builtin.documentation },
.parameters = param_infos,
.activeParameter = paren_commas,
};

1
src/tres Submodule

@ -0,0 +1 @@
Subproject commit fb23d644500ae5b93dd71b5a8406d0c83e8e4fbe

View File

@ -1,536 +0,0 @@
const std = @import("std");
const string = []const u8;
// LSP types
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
pub const Position = struct {
line: u32,
character: u32,
};
pub const Range = struct {
start: Position,
end: Position,
};
pub const Location = struct {
uri: string,
range: Range,
};
/// Id of a request
pub const RequestId = union(enum) {
String: string,
Integer: i32,
};
/// Params of a response (result)
pub const ResponseParams = union(enum) {
SignatureHelp: SignatureHelp,
CompletionList: CompletionList,
Location: Location,
Hover: Hover,
DocumentSymbols: []DocumentSymbol,
SemanticTokensFull: SemanticTokens,
InlayHint: []InlayHint,
TextEdits: []TextEdit,
Locations: []Location,
WorkspaceEdit: WorkspaceEdit,
InitializeResult: InitializeResult,
ConfigurationParams: ConfigurationParams,
RegistrationParams: RegistrationParams,
DocumentHighlight: []DocumentHighlight,
CodeAction: []CodeAction,
ApplyEdit: ApplyWorkspaceEditParams,
FoldingRange: []FoldingRange,
SelectionRange: []*SelectionRange,
};
pub const Response = struct {
jsonrpc: string = "2.0",
id: RequestId,
result: ResponseParams,
};
pub const Request = struct {
jsonrpc: string = "2.0",
id: RequestId,
method: []const u8,
params: ?ResponseParams,
};
pub const ResponseError = struct {
code: i32,
message: string,
data: std.json.Value,
};
pub const ErrorCodes = enum(i32) {
// Defined by JSON-RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
// JSON-RPC reserved error codes
ServerNotInitialized = -32002,
UnknownErrorCode = -3200,
// LSP reserved error codes
RequestFailed = -32803,
ServerCancelled = -32802,
ContentModified = -32801,
RequestCancelled = -32800,
};
pub const Notification = struct {
jsonrpc: string = "2.0",
method: string,
params: NotificationParams,
};
pub const NotificationParams = union(enum) {
LogMessage: struct {
type: MessageType,
message: string,
},
PublishDiagnostics: struct {
uri: string,
diagnostics: []Diagnostic,
},
ShowMessage: struct {
type: MessageType,
message: string,
},
};
/// Type of a debug message
pub const MessageType = enum(i64) {
Error = 1,
Warning = 2,
Info = 3,
Log = 4,
pub fn jsonStringify(value: MessageType, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
pub const DiagnosticSeverity = enum(i64) {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
pub fn jsonStringify(value: DiagnosticSeverity, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
pub const DiagnosticRelatedInformation = struct {
location: Location,
message: string,
};
pub const Diagnostic = struct {
range: Range,
severity: ?DiagnosticSeverity,
code: ?string,
source: ?string,
message: string,
relatedInformation: ?[]DiagnosticRelatedInformation = null,
};
pub const WorkspaceEdit = struct {
changes: std.StringHashMapUnmanaged(std.ArrayListUnmanaged(TextEdit)),
pub fn jsonStringify(self: WorkspaceEdit, options: std.json.StringifyOptions, writer: anytype) @TypeOf(writer).Error!void {
try writer.writeAll("{\"changes\": {");
var it = self.changes.iterator();
var idx: usize = 0;
while (it.next()) |entry| : (idx += 1) {
if (idx != 0) try writer.writeAll(", ");
try writer.writeByte('"');
try writer.writeAll(entry.key_ptr.*);
try writer.writeAll("\":");
try std.json.stringify(entry.value_ptr.items, options, writer);
}
try writer.writeAll("}}");
}
};
pub const TextEdit = struct {
range: Range,
newText: string,
};
pub const MarkupContent = struct {
pub const Kind = enum(u1) {
PlainText = 0,
Markdown = 1,
pub fn jsonStringify(value: Kind, options: std.json.StringifyOptions, out_stream: anytype) !void {
const str = switch (value) {
.PlainText => "plaintext",
.Markdown => "markdown",
};
try std.json.stringify(str, options, out_stream);
}
};
kind: Kind = .Markdown,
value: string,
};
pub const CompletionList = struct {
isIncomplete: bool,
items: []const CompletionItem,
};
pub const InsertTextFormat = enum(i64) {
PlainText = 1,
Snippet = 2,
pub fn jsonStringify(value: InsertTextFormat, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
pub const Hover = struct {
contents: MarkupContent,
};
pub const SemanticTokens = struct {
data: []const u32,
};
pub const CompletionItem = struct {
pub const Kind = enum(i64) {
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18,
Folder = 19,
EnumMember = 20,
Constant = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25,
pub fn jsonStringify(value: Kind, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
label: string,
labelDetails: ?CompletionItemLabelDetails = null,
kind: Kind,
detail: ?string = null,
sortText: ?string = null,
filterText: ?string = null,
insertText: ?string = null,
insertTextFormat: ?InsertTextFormat = .PlainText,
documentation: ?MarkupContent = null,
// FIXME: i commented this out, because otherwise the vscode client complains about *ranges*
// and breaks code completion entirely
// see: https://github.com/zigtools/zls-vscode/pull/33
// textEdit: ?TextEdit = null,
};
pub const CompletionItemLabelDetails = struct {
detail: ?string,
description: ?string,
sortText: ?string = null,
};
pub const DocumentSymbol = struct {
const Kind = enum(u32) {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
pub fn jsonStringify(value: Kind, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
name: string,
detail: ?string = null,
kind: Kind,
deprecated: bool = false,
range: Range,
selectionRange: Range,
children: []const DocumentSymbol = &[_]DocumentSymbol{},
};
pub const WorkspaceFolder = struct {
uri: string,
name: string,
};
pub const SignatureInformation = struct {
pub const ParameterInformation = struct {
// TODO Can also send a pair of encoded offsets
label: string,
documentation: ?MarkupContent,
};
label: string,
documentation: ?MarkupContent,
parameters: ?[]const ParameterInformation,
activeParameter: ?u32,
};
pub const SignatureHelp = struct {
signatures: ?[]const SignatureInformation,
activeSignature: ?u32,
activeParameter: ?u32,
};
pub const InlayHint = struct {
position: Position,
label: string,
kind: InlayHintKind,
tooltip: MarkupContent,
paddingLeft: bool,
paddingRight: bool,
// appends a colon to the label and reduces the output size
pub fn jsonStringify(value: InlayHint, options: std.json.StringifyOptions, writer: anytype) @TypeOf(writer).Error!void {
try writer.writeAll("{\"position\":");
try std.json.stringify(value.position, options, writer);
try writer.writeAll(",\"label\":\"");
try writer.writeAll(value.label);
try writer.writeAll(":\",\"kind\":");
try std.json.stringify(value.kind, options, writer);
if (value.tooltip.value.len != 0) {
try writer.writeAll(",\"tooltip\":");
try std.json.stringify(value.tooltip, options, writer);
}
if (value.paddingLeft) try writer.writeAll(",\"paddingLeft\":true");
if (value.paddingRight) try writer.writeAll(",\"paddingRight\":true");
try writer.writeByte('}');
}
};
pub const InlayHintKind = enum(i64) {
Type = 1,
Parameter = 2,
pub fn jsonStringify(value: InlayHintKind, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
pub const CodeActionKind = enum {
Empty,
QuickFix,
Refactor,
RefactorExtract,
RefactorInline,
RefactorRewrite,
Source,
SourceOrganizeImports,
SourceFixAll,
pub fn jsonStringify(value: CodeActionKind, options: std.json.StringifyOptions, out_stream: anytype) !void {
const name = switch (value) {
.Empty => "",
.QuickFix => "quickfix",
.Refactor => "refactor",
.RefactorExtract => "refactor.extract",
.RefactorInline => "refactor.inline",
.RefactorRewrite => "refactor.rewrite",
.Source => "source",
.SourceOrganizeImports => "source.organizeImports",
.SourceFixAll => "source.fixAll",
};
try std.json.stringify(name, options, out_stream);
}
};
pub const CodeAction = struct {
title: string,
kind: CodeActionKind,
// diagnostics: []Diagnostic,
isPreferred: bool,
edit: WorkspaceEdit,
};
pub const ApplyWorkspaceEditParams = struct {
label: string,
edit: WorkspaceEdit,
};
pub const PositionEncodingKind = enum {
utf8,
utf16,
utf32,
pub fn jsonStringify(value: PositionEncodingKind, options: std.json.StringifyOptions, out_stream: anytype) !void {
const str = switch (value) {
.utf8 => "utf-8",
.utf16 => "utf-16",
.utf32 => "utf-32",
};
try std.json.stringify(str, options, out_stream);
}
};
const TextDocumentSyncKind = enum(u32) {
None = 0,
Full = 1,
Incremental = 2,
pub fn jsonStringify(value: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
// Only includes options we set in our initialize result.
const InitializeResult = struct {
capabilities: struct {
positionEncoding: PositionEncodingKind,
signatureHelpProvider: struct {
triggerCharacters: []const string,
retriggerCharacters: []const string,
},
textDocumentSync: struct {
openClose: bool,
change: TextDocumentSyncKind,
willSave: bool,
willSaveWaitUntil: bool,
save: bool,
},
renameProvider: bool,
completionProvider: struct {
resolveProvider: bool,
triggerCharacters: []const string,
completionItem: struct { labelDetailsSupport: bool },
},
documentHighlightProvider: bool,
hoverProvider: bool,
codeActionProvider: bool,
declarationProvider: bool,
definitionProvider: bool,
typeDefinitionProvider: bool,
implementationProvider: bool,
referencesProvider: bool,
documentSymbolProvider: bool,
colorProvider: bool,
documentFormattingProvider: bool,
documentRangeFormattingProvider: bool,
foldingRangeProvider: bool,
selectionRangeProvider: bool,
workspaceSymbolProvider: bool,
rangeProvider: bool,
documentProvider: bool,
workspace: ?struct {
workspaceFolders: ?struct {
supported: bool,
changeNotifications: bool,
},
},
semanticTokensProvider: struct {
full: bool,
range: bool,
legend: struct {
tokenTypes: []const string,
tokenModifiers: []const string,
},
},
inlayHintProvider: bool,
},
serverInfo: struct {
name: string,
version: ?string = null,
},
};
pub const ConfigurationParams = struct {
items: []const ConfigurationItem,
pub const ConfigurationItem = struct {
section: ?[]const u8,
};
};
pub const RegistrationParams = struct {
registrations: []const Registration,
pub const Registration = struct {
id: string,
method: string,
// registerOptions?: LSPAny;
};
};
pub const DocumentHighlightKind = enum(u8) {
Text = 1,
Read = 2,
Write = 3,
pub fn jsonStringify(value: DocumentHighlightKind, options: std.json.StringifyOptions, out_stream: anytype) !void {
try std.json.stringify(@enumToInt(value), options, out_stream);
}
};
pub const DocumentHighlight = struct {
range: Range,
kind: ?DocumentHighlightKind,
};
pub const FoldingRange = struct {
startLine: usize,
endLine: usize,
};
pub const SelectionRange = struct {
range: Range,
parent: ?*SelectionRange,
};

View File

@ -2,13 +2,13 @@
// zigbot9001 to take advantage of zls' tools
pub const analysis = @import("analysis.zig");
pub const header = @import("header.zig");
pub const Header = @import("Header.zig");
pub const debug = @import("debug.zig");
pub const offsets = @import("offsets.zig");
pub const requests = @import("requests.zig");
pub const Config = @import("Config.zig");
pub const Server = @import("Server.zig");
pub const translate_c = @import("translate_c.zig");
pub const types = @import("types.zig");
pub const types = @import("lsp.zig");
pub const URI = @import("uri.zig");
pub const DocumentStore = @import("DocumentStore.zig");
pub const ComptimeInterpreter = @import("ComptimeInterpreter.zig");

File diff suppressed because one or more lines are too long

View File

@ -8,13 +8,12 @@ const ErrorBuilder = @import("../ErrorBuilder.zig");
const types = zls.types;
const offsets = zls.offsets;
const requests = zls.requests;
const allocator: std.mem.Allocator = std.testing.allocator;
const Completion = struct {
label: []const u8,
kind: types.CompletionItem.Kind,
kind: types.CompletionItemKind,
detail: ?[]const u8 = null,
};
@ -412,16 +411,13 @@ fn testCompletion(source: []const u8, expected_completions: []const Completion)
try ctx.requestDidOpen(test_uri, text);
const request = requests.Completion{
.params = .{
const params = types.CompletionParams{
.textDocument = .{ .uri = test_uri },
.position = offsets.indexToPosition(source, cursor_idx, ctx.server.offset_encoding),
},
};
@setEvalBranchQuota(2000);
const response = try ctx.requestGetResponse(?types.CompletionList, "textDocument/completion", request);
defer response.deinit();
@setEvalBranchQuota(5000);
const response = try ctx.requestGetResponse(?types.CompletionList, "textDocument/completion", params);
const completion_list: types.CompletionList = response.result orelse {
std.debug.print("Server returned `null` as the result\n", .{});
@ -462,11 +458,11 @@ fn testCompletion(source: []const u8, expected_completions: []const Completion)
unreachable;
};
if (expected_completion.kind != actual_completion.kind) {
try error_builder.msgAtIndex("label '{s}' should be of kind '{s}' but was '{s}'!", cursor_idx, .err, .{
if (actual_completion.kind == null or expected_completion.kind != actual_completion.kind.?) {
try error_builder.msgAtIndex("label '{s}' should be of kind '{s}' but was '{?s}'!", cursor_idx, .err, .{
label,
@tagName(expected_completion.kind),
@tagName(actual_completion.kind),
if (actual_completion.kind) |kind| @tagName(kind) else null,
});
return error.InvalidCompletionKind;
}
@ -500,10 +496,16 @@ fn extractCompletionLabels(items: anytype) error{ DuplicateCompletionLabel, OutO
errdefer set.deinit(allocator);
try set.ensureTotalCapacity(allocator, items.len);
for (items) |item| {
switch (item.kind) {
const maybe_kind = switch (@typeInfo(@TypeOf(item.kind))) {
.Optional => item.kind,
else => @as(?@TypeOf(item.kind), item.kind),
};
if (maybe_kind) |kind| {
switch (kind) {
.Keyword, .Snippet => continue,
else => {},
}
}
if (set.fetchPutAssumeCapacity(item.label, {}) != null) return error.DuplicateCompletionLabel;
}
return set;

View File

@ -2,10 +2,11 @@ const std = @import("std");
const zls = @import("zls");
const builtin = @import("builtin");
const tres = @import("tres");
const Context = @import("../context.zig").Context;
const types = zls.types;
const requests = zls.requests;
const allocator: std.mem.Allocator = std.testing.allocator;
@ -33,7 +34,7 @@ test "foldingRange - #801" {
\\ };
\\}
,
\\[]
\\[{"startLine":1,"endLine":4},{"startLine":0,"endLine":5}]
);
}
@ -48,15 +49,16 @@ fn testFoldingRange(source: []const u8, expect: []const u8) !void {
try ctx.requestDidOpen(test_uri, source);
const request = requests.FoldingRange{ .params = .{ .textDocument = .{ .uri = test_uri } } };
const params = types.FoldingRangeParams{ .textDocument = .{ .uri = test_uri } };
const response = try ctx.requestGetResponse(?[]types.FoldingRange, "textDocument/foldingRange", request);
defer response.deinit();
const response = try ctx.requestGetResponse(?[]types.FoldingRange, "textDocument/foldingRange", params);
var actual = std.ArrayList(u8).init(allocator);
defer actual.deinit();
try std.json.stringify(response.result, .{}, actual.writer());
try tres.stringify(response.result, .{
.emit_null_optional_fields = false,
}, actual.writer());
try expectEqualJson(expect, actual.items);
}

View File

@ -8,7 +8,6 @@ const ErrorBuilder = @import("../ErrorBuilder.zig");
const types = zls.types;
const offsets = zls.offsets;
const requests = zls.requests;
const allocator: std.mem.Allocator = std.testing.allocator;
@ -83,7 +82,7 @@ fn testInlayHints(source: []const u8) !void {
const range = types.Range{
.start = types.Position{ .line = 0, .character = 0 },
.end = offsets.indexToPosition(phr.new_source, phr.new_source.len, .utf16),
.end = offsets.indexToPosition(phr.new_source, phr.new_source.len, .@"utf-16"),
};
const InlayHint = struct {
@ -92,15 +91,12 @@ fn testInlayHints(source: []const u8) !void {
kind: types.InlayHintKind,
};
const request = requests.InlayHint{
.params = .{
const params = types.InlayHintParams{
.textDocument = .{ .uri = test_uri },
.range = range,
},
};
const response = try ctx.requestGetResponse(?[]InlayHint, "textDocument/inlayHint", request);
defer response.deinit();
const response = try ctx.requestGetResponse(?[]InlayHint, "textDocument/inlayHint", params);
const hints: []InlayHint = response.result orelse {
std.debug.print("Server returned `null` as the result\n", .{});
@ -124,7 +120,7 @@ fn testInlayHints(source: []const u8) !void {
for (hints) |hint| {
if (position.line != hint.position.line or position.character != hint.position.character) continue;
const actual_label = hint.label[0 .. hint.label.len - 1]; // exclude :
const actual_label = hint.label[0..hint.label.len];
if (!std.mem.eql(u8, expected_label, actual_label)) {
try error_builder.msgAtLoc("expected label `{s}` here but got `{s}`!", new_loc, .err, .{ expected_label, actual_label });

View File

@ -7,7 +7,6 @@ const Context = @import("../context.zig").Context;
const ErrorBuilder = @import("../ErrorBuilder.zig");
const types = zls.types;
const requests = zls.requests;
const offsets = zls.offsets;
const allocator: std.mem.Allocator = std.testing.allocator;
@ -113,16 +112,13 @@ fn testReferences(source: []const u8) !void {
const var_name = offsets.locToSlice(source, var_loc);
const var_loc_middle = var_loc.start + (var_loc.end - var_loc.start) / 2;
const request = requests.References{
.params = .{
const params = types.ReferenceParams{
.textDocument = .{ .uri = file_uri },
.position = offsets.indexToPosition(source, var_loc_middle, ctx.server.offset_encoding),
.context = .{ .includeDeclaration = true },
},
};
const response = try ctx.requestGetResponse(?[]types.Location, "textDocument/references", request);
defer response.deinit();
const response = try ctx.requestGetResponse(?[]types.Location, "textDocument/references", params);
const locations: []types.Location = response.result orelse {
std.debug.print("Server returned `null` as the result\n", .{});

View File

@ -38,20 +38,19 @@ fn testSelectionRange(source: []const u8, want: []const []const u8) !void {
try ctx.requestDidOpen(test_uri, phr.new_source);
const position = offsets.locToRange(phr.new_source, phr.locations.items(.new)[0], .utf16).start;
const position = offsets.locToRange(phr.new_source, phr.locations.items(.new)[0], .@"utf-16").start;
const SelectionRange = struct {
range: types.Range,
parent: ?*@This(),
parent: ?*@This() = null,
};
const request = requests.SelectionRange{ .params = .{
const params = types.SelectionRangeParams{
.textDocument = .{ .uri = test_uri },
.positions = &[_]types.Position{position},
} };
};
const response = try ctx.requestGetResponse(?[]SelectionRange, "textDocument/selectionRange", request);
defer response.deinit();
const response = try ctx.requestGetResponse(?[]SelectionRange, "textDocument/selectionRange", params);
const selectionRanges: []SelectionRange = response.result orelse {
std.debug.print("Server returned `null` as the result\n", .{});
@ -63,7 +62,7 @@ fn testSelectionRange(source: []const u8, want: []const []const u8) !void {
var it: ?*SelectionRange = &selectionRanges[0];
while (it) |r| {
const slice = offsets.rangeToSlice(phr.new_source, r.range, .utf16);
const slice = offsets.rangeToSlice(phr.new_source, r.range, .@"utf-16");
(try got.addOne()).* = slice;
it = r.parent;
}

View File

@ -4,7 +4,7 @@ const builtin = @import("builtin");
const Context = @import("../context.zig").Context;
const requests = zls.requests;
const types = zls.types;
const allocator: std.mem.Allocator = std.testing.allocator;
@ -41,21 +41,7 @@ fn testSemanticTokens(source: []const u8, expected: []const u32) !void {
var ctx = try Context.init();
defer ctx.deinit();
const open_document = requests.OpenDocument{
.params = .{
.textDocument = .{
.uri = file_uri,
// .languageId = "zig",
// .version = 420,
.text = source,
},
},
};
const did_open_method = try std.json.stringifyAlloc(allocator, open_document.params, .{});
defer allocator.free(did_open_method);
try ctx.request("textDocument/didOpen", did_open_method, null);
try ctx.requestDidOpen(file_uri, source);
const Response = struct {
data: []const u32,

View File

@ -107,13 +107,13 @@ fn testIndexPosition(text: []const u8, index: usize, line: u32, characters: [3]u
const position16: types.Position = .{ .line = line, .character = characters[1] };
const position32: types.Position = .{ .line = line, .character = characters[2] };
try std.testing.expectEqual(position8, offsets.indexToPosition(text, index, .utf8));
try std.testing.expectEqual(position16, offsets.indexToPosition(text, index, .utf16));
try std.testing.expectEqual(position32, offsets.indexToPosition(text, index, .utf32));
try std.testing.expectEqual(position8, offsets.indexToPosition(text, index, .@"utf-8"));
try std.testing.expectEqual(position16, offsets.indexToPosition(text, index, .@"utf-16"));
try std.testing.expectEqual(position32, offsets.indexToPosition(text, index, .@"utf-32"));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position8, .utf8));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position16, .utf16));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position32, .utf32));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position8, .@"utf-8"));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position16, .@"utf-16"));
try std.testing.expectEqual(index, offsets.positionToIndex(text, position32, .@"utf-32"));
}
fn testTokenToLoc(text: [:0]const u8, token_index: std.zig.Ast.TokenIndex, start: usize, end: usize) !void {
@ -135,7 +135,7 @@ fn testTokenIndexToLoc(text: [:0]const u8, index: usize, start: usize, end: usiz
fn testAdvancePosition(text: [:0]const u8, expected_line: u32, expected_character: u32, line: u32, character: u32, from: usize, to: usize) !void {
const expected: types.Position = .{ .line = expected_line, .character = expected_character };
const actual = offsets.advancePosition(text, .{ .line = line, .character = character }, from, to, .utf16);
const actual = offsets.advancePosition(text, .{ .line = line, .character = character }, from, to, .@"utf-16");
try std.testing.expectEqual(expected, actual);
}
@ -143,9 +143,9 @@ fn testAdvancePosition(text: [:0]const u8, expected_line: u32, expected_characte
fn testConvertPositionEncoding(text: [:0]const u8, line: u32, character: u32, new_characters: [3]u32) !void {
const position: types.Position = .{ .line = line, .character = character };
const position8 = offsets.convertPositionEncoding(text, position, .utf8, .utf8);
const position16 = offsets.convertPositionEncoding(text, position, .utf8, .utf16);
const position32 = offsets.convertPositionEncoding(text, position, .utf8, .utf32);
const position8 = offsets.convertPositionEncoding(text, position, .@"utf-8", .@"utf-8");
const position16 = offsets.convertPositionEncoding(text, position, .@"utf-8", .@"utf-16");
const position32 = offsets.convertPositionEncoding(text, position, .@"utf-8", .@"utf-32");
try std.testing.expectEqual(line, position8.line);
try std.testing.expectEqual(line, position16.line);
@ -157,13 +157,13 @@ fn testConvertPositionEncoding(text: [:0]const u8, line: u32, character: u32, ne
}
fn testCountCodeUnits(text: []const u8, counts: [3]usize) !void {
try std.testing.expectEqual(counts[0], offsets.countCodeUnits(text, .utf8));
try std.testing.expectEqual(counts[1], offsets.countCodeUnits(text, .utf16));
try std.testing.expectEqual(counts[2], offsets.countCodeUnits(text, .utf32));
try std.testing.expectEqual(counts[0], offsets.countCodeUnits(text, .@"utf-8"));
try std.testing.expectEqual(counts[1], offsets.countCodeUnits(text, .@"utf-16"));
try std.testing.expectEqual(counts[2], offsets.countCodeUnits(text, .@"utf-32"));
}
fn testGetNCodeUnitByteCount(text: []const u8, n: [3]usize) !void {
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[0], .utf8));
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[1], .utf16));
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[2], .utf32));
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[0], .@"utf-8"));
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[1], .@"utf-16"));
try std.testing.expectEqual(n[0], offsets.getNCodeUnitByteCount(text, n[2], .@"utf-32"));
}