Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Sergeeeek 2020-05-18 10:34:26 +03:00
commit 2c5cc2b48f
9 changed files with 495 additions and 286 deletions

View File

@ -1,4 +1,4 @@
![Zig Language Server](https://raw.githubusercontent.com/SuperAuguste/zls/master/.assets/zls.svg)
![Zig Language Server](https://raw.githubusercontent.com/zigtools/zls/master/.assets/zls.svg)
![CI](https://github.com/zigtools/zls/workflows/CI/badge.svg)
![Zig Tools](https://img.shields.io/static/v1?label=zigtools&message=for%20all%20of%20ziguanity&color=F7A41D&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAEDklEQVR4nOzYbUwbZRwA8Oe53vXuSltaa2lxc+KKBGcyBhLDgH3SiSMZ++TQRT8uJH4x8S0myL6YmUjUbIkfZvZtX3TJlAwjiYImxuBwa1hEtxAcQ8GFQrtBWXvXu17vTH1ux1lv99IeLcn6//Rw9/D0l+ft/28xsC2jyrISVZaV2KYsfCsGxSDYs5OIhPCAB0tlpFt3hF//yqYyUsVYrQ3Eaz2ew0/Tta7/rENOlCZnuTMTqZHLrJlxoF2ggAf7+FVff2eNfrf+U/HRaMZwNHtmqzGMf/NucNfDxqNFQqY+0QZWYxifGKoL1TrQnzlRGrvKXphio/M8ANLEUKjeL7+aW86e+5EpB4vEwRevBxTTtSX++Gd3rv6ZBQCEfdi3g3VqU8/J1dspsRysd454n3rUidq//MH1Dcc3WEkxNdUTalNsXTYFPNgr3TULcWE0qn0CStryXhoufPqIi8wfusWE0DEYW0sbm9Rvj52Oj1zROAElXacvd7mQCQAwdH4dmdwUNGkCAAwc9GiOXBKrp4VGjcWEcGFKXo6B59wmTQCA7mbSTWmsWEmstsflXfXdTEa8d4e375YfMpx46AM9EwDAgcGWXYSdLAyCkE8+Zdf/5pXnqxs51HCR2Pv9PgxqmJbXckr/HQGHnSx1cNnN9tnvU5msPHXHumvODjy0w194AvqGV5X+bkrDUDxLlPI3J2rXujb3x+9LwoufxNWymY/qC3Ybw22m7cTdnJ0sAMD8ioAaHU+Q6ucTv3FqmXJalRPQHnEqnW/GBJtZk7Mcajy/l/bSUEdWcCqP7pczejItXr+lwSr+lg/7sK5meZIoJ2x5jPhpli+QHTixcvxZd73fcfkGd2Y8hUqu1gbihX0U6vP1NCNqlWFF3vL/v8c7BmMsb/yPXhr+cKJOyVed78VQAi2IYhZRM7eYMflr4MjbQcV0/ue0pqkYln6+o53wwJNkwT5Dl9zR/fTUyXBnk7zuiwnhzXPr9/sUa3vLZA7OZKXxGfbSHJ9kRIqAe3YSB/dS6iIxsZHrG47rFDkW9pb5ukA/ri3xL52+fUPrXlDC7GzZYmI48dTY3eGLG5weyTTLkmluOTs5y3U1k5EQ7vg3I64kc9F5fnwm8/lkGhWJhmHMsmpSvy06DE5iRUwGrEqZ9FgYBF++EayISY91pJ1qu1dnltmkx+ptlev0JCOW2aTH8rvlWvbKPFdmkx5rNSkXjZ1NZGMYL6dJL/kc2kd99VYQtRlOvDTHt0ecys9DW2rKfyO634ubK0J3M9kQzM8TgcPdIZwiYHlMeiwJgNEo+0yjE8mUmF7gD38Y31KTcQWBQdDbSvW20XVex1paHJtmL0ZZzTL3gYht+ktzlWUlqiwrUWVZiX8CAAD//7jyYLmjqPd4AAAAAElFTkSuQmCC)
@ -20,7 +20,7 @@ Zig Language Server, or `zls`, is a language server for Zig. The Zig wiki states
Installing `zls` is pretty simple;
```bash
git clone https://github.com/SuperAuguste/zls
git clone https://github.com/zigtools/zls
cd zls
zig build
```
@ -33,6 +33,8 @@ zig build
Then, you can use the `zls` executable in an editor of your choice that has a Zig language server client!
*Note:`zls` itself must be built using the master branch of zig currently due to a bug in `std.json` which was [fixed](https://github.com/ziglang/zig/pull/5167) after 0.6.0 was released.*
### Configuration Options
You can configure zls by providing a zls.json file in the same directory as the executable.
@ -42,6 +44,7 @@ The following options are currently available.
| --- | --- | --- | --- |
| `enable_snippets` | `bool` | `true` | Enables snippet completion, set to false for compatibility with language clients that do not support snippets (such as ale). |
| `zig_lib_path` | `?[]const u8` | `null` | zig library path, used to analyze std library imports. |
| `warn_style` | `bool` | `false` | Enables warnings for style *guideline* mismatches |
## Usage

View File

@ -79,6 +79,7 @@ pub fn getFunctionSignature(tree: *ast.Tree, func: *ast.Node.FnProto) []const u8
const start = tree.tokens.at(func.firstToken()).start;
const end = tree.tokens.at(switch (func.return_type) {
.Explicit, .InferErrorSet => |node| node.lastToken(),
.Invalid => |r_paren| r_paren,
}).end;
return tree.source[start..end];
}
@ -118,21 +119,22 @@ pub fn getFunctionSnippet(allocator: *std.mem.Allocator, tree: *ast.Tree, func:
try buffer.appendSlice(": ");
}
if (param_decl.var_args_token) |_| {
try buffer.appendSlice("...");
continue;
}
switch (param_decl.param_type) {
.var_args => try buffer.appendSlice("..."),
.var_type => try buffer.appendSlice("var"),
.type_expr => |type_expr| {
var curr_tok = type_expr.firstToken();
var end_tok = type_expr.lastToken();
while (curr_tok <= end_tok) : (curr_tok += 1) {
const id = tree.tokens.at(curr_tok).id;
const is_comma = tree.tokens.at(curr_tok).id == .Comma;
var curr_tok = param_decl.type_node.firstToken();
var end_tok = param_decl.type_node.lastToken();
while (curr_tok <= end_tok) : (curr_tok += 1) {
const id = tree.tokens.at(curr_tok).id;
const is_comma = tree.tokens.at(curr_tok).id == .Comma;
if (curr_tok == end_tok and is_comma) continue;
if (curr_tok == end_tok and is_comma) continue;
try buffer.appendSlice(tree.tokenSlice(curr_tok));
if (is_comma or id == .Keyword_const) try buffer.append(' ');
try buffer.appendSlice(tree.tokenSlice(curr_tok));
if (is_comma or id == .Keyword_const) try buffer.append(' ');
}
},
}
try buffer.append('}');
@ -146,12 +148,26 @@ pub fn getFunctionSnippet(allocator: *std.mem.Allocator, tree: *ast.Tree, func:
pub fn getVariableSignature(tree: *ast.Tree, var_decl: *ast.Node.VarDecl) []const u8 {
const start = tree.tokens.at(var_decl.firstToken()).start;
const end = tree.tokens.at(var_decl.semicolon_token).start;
// var end =
// if (var_decl.init_n) |body| tree.tokens.at(body.firstToken()).start
// else tree.tokens.at(var_decl.name_token).end;
return tree.source[start..end];
}
/// Gets a param signature
pub fn getParamSignature(tree: *ast.Tree, param: *ast.Node.ParamDecl) []const u8 {
const start = tree.tokens.at(param.firstToken()).start;
const end = tree.tokens.at(param.lastToken()).end;
return tree.source[start..end];
}
pub fn isTypeFunction(tree: *ast.Tree, func: *ast.Node.FnProto) bool {
switch (func.return_type) {
.Explicit => |node| return if (node.cast(std.zig.ast.Node.Identifier)) |ident|
std.mem.eql(u8, tree.tokenSlice(ident.token), "type")
else
false,
.InferErrorSet, .Invalid => return false,
}
}
// STYLE
pub fn isCamelCase(name: []const u8) bool {
@ -188,20 +204,59 @@ pub fn getChild(tree: *ast.Tree, node: *ast.Node, name: []const u8) ?*ast.Node {
return null;
}
/// Gets the child of slice
pub fn getChildOfSlice(tree: *ast.Tree, nodes: []*ast.Node, name: []const u8) ?*ast.Node {
// var index: usize = 0;
for (nodes) |child| {
switch (child.id) {
.VarDecl => {
const vari = child.cast(ast.Node.VarDecl).?;
if (std.mem.eql(u8, tree.tokenSlice(vari.name_token), name)) return child;
},
.ParamDecl => {
const decl = child.cast(ast.Node.ParamDecl).?;
if (decl.name_token != null and std.mem.eql(u8, tree.tokenSlice(decl.name_token.?), name)) return child;
},
.FnProto => {
const func = child.cast(ast.Node.FnProto).?;
if (func.name_token != null and std.mem.eql(u8, tree.tokenSlice(func.name_token.?), name)) return child;
},
.ContainerField => {
const field = child.cast(ast.Node.ContainerField).?;
if (std.mem.eql(u8, tree.tokenSlice(field.name_token), name)) return child;
},
else => {},
}
// index += 1;
}
return null;
}
/// Resolves the type of a node
pub fn resolveTypeOfNode(analysis_ctx: *AnalysisContext, node: *ast.Node) ?*ast.Node {
std.debug.warn("Resolving node of type {}\n", .{node.id});
std.debug.warn("NODE {}\n", .{node});
switch (node.id) {
.VarDecl => {
const vari = node.cast(ast.Node.VarDecl).?;
return resolveTypeOfNode(analysis_ctx, vari.type_node orelse vari.init_node.?) orelse null;
},
.ParamDecl => {
const decl = node.cast(ast.Node.ParamDecl).?;
switch (decl.param_type) {
.var_type, .type_expr => |var_type| {
return resolveTypeOfNode(analysis_ctx, var_type) orelse null;
},
else => {},
}
},
.FnProto => {
return node;
},
.Identifier => {
if (getChild(analysis_ctx.tree, &analysis_ctx.tree.root_node.base, analysis_ctx.tree.getNodeSource(node))) |child| {
// std.debug.warn("IDENTIFIER {}\n", .{analysis_ctx.tree.getNodeSource(node)});
if (getChildOfSlice(analysis_ctx.tree, analysis_ctx.scope_nodes, analysis_ctx.tree.getNodeSource(node))) |child| {
// std.debug.warn("CHILD {}\n", .{child});
return resolveTypeOfNode(analysis_ctx, child);
} else return null;
},
@ -218,13 +273,14 @@ pub fn resolveTypeOfNode(analysis_ctx: *AnalysisContext, node: *ast.Node) ?*ast.
.SuffixOp => {
const suffix_op = node.cast(ast.Node.SuffixOp).?;
switch (suffix_op.op) {
.Call => {
const func_decl = resolveTypeOfNode(analysis_ctx, suffix_op.lhs.node) orelse return null;
.Call, .StructInitializer => {
const func_decl = resolveTypeOfNode(analysis_ctx, suffix_op.lhs.node) orelse return null;
if (func_decl.id == .FnProto) {
const func = node.cast(ast.Node.FnProto).?;
switch (func.return_type) {
.Explicit, .InferErrorSet => |return_type| return resolveTypeOfNode(analysis_ctx, return_type),
.Invalid => {},
}
}
return null;
@ -246,15 +302,7 @@ pub fn resolveTypeOfNode(analysis_ctx: *AnalysisContext, node: *ast.Node) ?*ast.
// Use the analysis context temporary arena to store the rhs string.
rhs_str = std.mem.dupe(&analysis_ctx.arena.allocator, u8, rhs_str) catch return null;
const left = resolveTypeOfNode(analysis_ctx, infix_op.lhs) orelse return null;
std.debug.warn("InfixOp left = {}\n", .{left});
const child = getChild(analysis_ctx.tree, left, rhs_str) orelse return null;
std.debug.warn("InfixOp child = {}\n", .{child});
const right_type = resolveTypeOfNode(analysis_ctx, child);
std.debug.warn("InfixOp rightType = {}\n", .{right_type});
return right_type;
return resolveTypeOfNode(analysis_ctx, getChild(analysis_ctx.tree, left, rhs_str) orelse return null);
},
else => {},
}
@ -262,8 +310,14 @@ pub fn resolveTypeOfNode(analysis_ctx: *AnalysisContext, node: *ast.Node) ?*ast.
.PrefixOp => {
const prefix_op = node.cast(ast.Node.PrefixOp).?;
switch (prefix_op.op) {
.SliceType, .ArrayType => return node,
.PtrType => {
return resolveTypeOfNode(analysis_ctx, prefix_op.rhs);
const op_token = analysis_ctx.tree.tokens.at(prefix_op.op_token);
switch (op_token.id) {
.Asterisk => return resolveTypeOfNode(analysis_ctx, prefix_op.rhs),
.LBracket, .AsteriskAsterisk => return null,
else => unreachable,
}
},
else => {},
}
@ -279,10 +333,13 @@ pub fn resolveTypeOfNode(analysis_ctx: *AnalysisContext, node: *ast.Node) ?*ast.
const import_str = analysis_ctx.tree.tokenSlice(import_param.cast(ast.Node.StringLiteral).?.token);
return analysis_ctx.onImport(import_str[1 .. import_str.len - 1]) catch |err| block: {
std.debug.warn("Error {} while proessing import {}\n", .{ err, import_str });
std.debug.warn("Error {} while processing import {}\n", .{ err, import_str });
break :block null;
};
},
.MultilineStringLiteral, .StringLiteral => {
return node;
},
else => {
std.debug.warn("Type resolution case not implemented; {}\n", .{node.id});
},
@ -347,7 +404,7 @@ pub fn getFieldAccessTypeNode(analysis_ctx: *AnalysisContext, tokenizer: *std.zi
.Identifier => {
// var root = current_node.cast(ast.Node.Root).?;
// current_node.
if (getChild(analysis_ctx.tree, current_node, tokenizer.buffer[next.start..next.end])) |child| {
if (getChildOfSlice(analysis_ctx.tree, analysis_ctx.scope_nodes, tokenizer.buffer[next.start..next.end])) |child| {
if (resolveTypeOfNode(analysis_ctx, child)) |node_type| {
current_node = node_type;
} else return null;
@ -378,25 +435,14 @@ pub fn isNodePublic(tree: *ast.Tree, node: *ast.Node) bool {
switch (node.id) {
.VarDecl => {
const var_decl = node.cast(ast.Node.VarDecl).?;
if (var_decl.visib_token) |visib_token| {
return std.mem.eql(u8, tree.tokenSlice(visib_token), "pub");
} else return false;
return var_decl.visib_token != null;
},
.FnProto => {
const func = node.cast(ast.Node.FnProto).?;
if (func.visib_token) |visib_token| {
return std.mem.eql(u8, tree.tokenSlice(visib_token), "pub");
} else return false;
},
.ContainerField => {
return true;
},
else => {
return false;
return func.visib_token != null;
},
else => return true,
}
return false;
}
pub fn nodeToString(tree: *ast.Tree, node: *ast.Node) ?[]const u8 {
@ -426,3 +472,71 @@ pub fn nodeToString(tree: *ast.Tree, node: *ast.Node) ?[]const u8 {
return null;
}
pub fn declsFromIndexInternal(allocator: *std.mem.Allocator, tree: *ast.Tree, node: *ast.Node, nodes: *std.ArrayList(*ast.Node)) anyerror!void {
switch (node.id) {
.FnProto => {
const func = node.cast(ast.Node.FnProto).?;
var param_index: usize = 0;
while (param_index < func.params.len) : (param_index += 1)
try declsFromIndexInternal(allocator, tree, func.params.at(param_index).*, nodes);
if (func.body_node) |body_node|
try declsFromIndexInternal(allocator, tree, body_node, nodes);
},
.Block => {
var index: usize = 0;
while (node.iterate(index)) |inode| {
try declsFromIndexInternal(allocator, tree, inode, nodes);
index += 1;
}
},
.VarDecl => {
try nodes.append(node);
},
.ParamDecl => {
try nodes.append(node);
},
else => {
try nodes.appendSlice(try getCompletionsFromNode(allocator, tree, node));
},
}
}
pub fn getCompletionsFromNode(allocator: *std.mem.Allocator, tree: *ast.Tree, node: *ast.Node) ![]*ast.Node {
var nodes = std.ArrayList(*ast.Node).init(allocator);
var index: usize = 0;
while (node.iterate(index)) |child_node| {
try nodes.append(child_node);
index += 1;
}
return nodes.items;
}
pub fn declsFromIndex(allocator: *std.mem.Allocator, tree: *ast.Tree, index: usize) ![]*ast.Node {
var iindex: usize = 0;
var node = &tree.root_node.base;
var nodes = std.ArrayList(*ast.Node).init(allocator);
try nodes.appendSlice(try getCompletionsFromNode(allocator, tree, node));
while (node.iterate(iindex)) |inode| {
if (tree.tokens.at(inode.firstToken()).start < index and index < tree.tokens.at(inode.lastToken()).start) {
try declsFromIndexInternal(allocator, tree, inode, &nodes);
}
iindex += 1;
}
if (tree.tokens.at(node.firstToken()).start < index and index < tree.tokens.at(node.lastToken()).start) {
return nodes.items;
}
return nodes.items;
}

View File

@ -5,3 +5,7 @@ enable_snippets: bool = true,
/// zig library path
zig_lib_path: ?[]const u8 = null,
/// Whether to pay attention to style issues. This is opt-in since the style
/// guide explicitly states that the style info provided is a guideline only.
warn_style: bool = false,

124
src/debug_allocator.zig Normal file
View File

@ -0,0 +1,124 @@
//! This allocator collects information about allocation sizes
const std = @import("std");
const DebugAllocator = @This();
fn toMB(value: var) f64 {
return switch (@TypeOf(value)) {
f64 => value / (1024 * 1024),
else => @intToFloat(f64, value) / (1024 * 1024),
};
}
const Stats = struct {
mean: f64 = 0,
mean_of_squares: f64 = 0,
total: usize = 0,
count: usize = 0,
fn addSample(self: *Stats, value: usize) void {
const count_f64 = @intToFloat(f64, self.count);
self.mean = (self.mean * count_f64 + @intToFloat(f64, value)) / (count_f64 + 1);
self.mean_of_squares = (self.mean_of_squares * count_f64 + @intToFloat(f64, value * value)) / (count_f64 + 1);
self.total += value;
self.count += 1;
}
fn stdDev(self: Stats) f64 {
return std.math.sqrt(self.mean_of_squares - self.mean * self.mean);
}
};
pub const AllocationInfo = struct {
allocation_stats: Stats = Stats{},
deallocation_count: usize = 0,
deallocation_total: usize = 0,
reallocation_stats: Stats = Stats{},
shrink_stats: Stats = Stats{},
pub fn format(
self: AllocationInfo,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
out_stream: var,
) !void {
@setEvalBranchQuota(2000);
// TODO: Make these behave like {Bi}, which doesnt work on floating point numbers.
return std.fmt.format(
out_stream,
\\------------------------------------------ Allocation info ------------------------------------------
\\{} total allocations (total: {d:.2} MB, mean: {d:.2} MB, std. dev: {d:.2} MB), {} deallocations
\\{} current allocations ({d:.2} MB)
\\{} reallocations (total: {d:.2} MB, mean: {d:.2} MB, std. dev: {d:.2} MB)
\\{} shrinks (total: {d:.2} MB, mean: {d:.2} MB, std. dev: {d:.2} MB)
\\-----------------------------------------------------------------------------------------------------
,
.{
self.allocation_stats.count,
toMB(self.allocation_stats.total),
toMB(self.allocation_stats.mean),
toMB(self.allocation_stats.stdDev()),
self.deallocation_count,
self.allocation_stats.count - self.deallocation_count,
toMB(self.allocation_stats.total + self.reallocation_stats.total - self.deallocation_total - self.shrink_stats.total),
self.reallocation_stats.count,
toMB(self.reallocation_stats.total),
toMB(self.reallocation_stats.mean),
toMB(self.reallocation_stats.stdDev()),
self.shrink_stats.count,
toMB(self.shrink_stats.total),
toMB(self.shrink_stats.mean),
toMB(self.shrink_stats.stdDev()),
},
);
}
};
base_allocator: *std.mem.Allocator,
info: AllocationInfo,
// Interface implementation
allocator: std.mem.Allocator,
pub fn init(base_allocator: *std.mem.Allocator) DebugAllocator {
return .{
.base_allocator = base_allocator,
.info = .{},
.allocator = .{
.reallocFn = realloc,
.shrinkFn = shrink,
},
};
}
fn realloc(allocator: *std.mem.Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) ![]u8 {
const self = @fieldParentPtr(DebugAllocator, "allocator", allocator);
var data = try self.base_allocator.reallocFn(self.base_allocator, old_mem, old_align, new_size, new_align);
if (old_mem.len == 0) {
self.info.allocation_stats.addSample(new_size);
} else if (new_size > old_mem.len) {
self.info.reallocation_stats.addSample(new_size - old_mem.len);
} else if (new_size < old_mem.len) {
self.info.shrink_stats.addSample(old_mem.len - new_size);
}
return data;
}
fn shrink(allocator: *std.mem.Allocator, old_mem: []u8, old_align: u29, new_size: usize, new_align: u29) []u8 {
const self = @fieldParentPtr(DebugAllocator, "allocator", allocator);
if (new_size == 0) {
if (self.info.allocation_stats.count == self.info.deallocation_count) {
@panic("error - too many calls to free, most likely double free");
}
self.info.deallocation_total += old_mem.len;
self.info.deallocation_count += 1;
} else if (new_size < old_mem.len) {
self.info.shrink_stats.addSample(old_mem.len - new_size);
} else if (new_size > old_mem.len) {
@panic("error - trying to shrink to a bigger size");
}
return self.base_allocator.shrinkFn(self.base_allocator, old_mem, old_align, new_size, new_align);
}

View File

@ -14,24 +14,10 @@ pub const Handle = struct {
return handle.document.uri;
}
/// Returns the zig AST resulting from parsing the document's text, even
/// if it contains errors.
pub fn dirtyTree(handle: Handle, allocator: *std.mem.Allocator) !*std.zig.ast.Tree {
/// Returns a zig AST, with all its errors.
pub fn tree(handle: Handle, allocator: *std.mem.Allocator) !*std.zig.ast.Tree {
return try std.zig.parse(allocator, handle.document.text);
}
/// Returns a zig AST with no errors, either from the current text or
/// the stored sane text, null if no such ast exists.
pub fn saneTree(handle: Handle, allocator: *std.mem.Allocator) !?*std.zig.ast.Tree {
var tree = try std.zig.parse(allocator, handle.document.text);
if (tree.errors.len == 0) return tree;
tree.deinit();
if (handle.document.sane_text) |sane| {
return try std.zig.parse(allocator, sane);
}
return null;
}
};
allocator: *std.mem.Allocator,
@ -61,32 +47,23 @@ pub fn init(self: *DocumentStore, allocator: *std.mem.Allocator, zig_lib_path: ?
}
}
/// This function assersts the document is not open yet and takes owneship
/// This function asserts the document is not open yet and takes ownership
/// of the uri and text passed in.
fn newDocument(self: *DocumentStore, uri: []const u8, text: []u8) !*Handle {
std.debug.warn("Opened document: {}\n", .{uri});
errdefer {
self.allocator.free(uri);
self.allocator.free(text);
}
var handle = try self.allocator.create(Handle);
errdefer self.allocator.destroy(handle);
handle.* = Handle{
var handle = Handle{
.count = 1,
.import_uris = std.ArrayList([]const u8).init(self.allocator),
.document = .{
.uri = uri,
.text = text,
.mem = text,
.sane_text = null,
},
};
try self.checkSanity(handle);
try self.handles.putNoClobber(uri, handle);
return (self.handles.get(uri) orelse unreachable).value;
try self.checkSanity(&handle);
const kv = try self.handles.getOrPutValue(uri, handle);
return &kv.value;
}
pub fn openDocument(self: *DocumentStore, uri: []const u8, text: []const u8) !*Handle {
@ -102,7 +79,7 @@ pub fn openDocument(self: *DocumentStore, uri: []const u8, text: []const u8) !*H
const duped_uri = try std.mem.dupe(self.allocator, u8, uri);
errdefer self.allocator.free(duped_uri);
return self.newDocument(duped_uri, duped_text);
return try self.newDocument(duped_uri, duped_text);
}
fn decrementCount(self: *DocumentStore, uri: []const u8) void {
@ -113,9 +90,6 @@ fn decrementCount(self: *DocumentStore, uri: []const u8) void {
std.debug.warn("Freeing document: {}\n", .{uri});
self.allocator.free(entry.value.document.mem);
if (entry.value.document.sane_text) |sane| {
self.allocator.free(sane);
}
for (entry.value.import_uris.items) |import_uri| {
self.decrementCount(import_uri);
@ -145,18 +119,10 @@ pub fn getHandle(self: *DocumentStore, uri: []const u8) ?*Handle {
// Check if the document text is now sane, move it to sane_text if so.
fn checkSanity(self: *DocumentStore, handle: *Handle) !void {
const dirty_tree = try handle.dirtyTree(self.allocator);
defer dirty_tree.deinit();
if (dirty_tree.errors.len > 0) return;
std.debug.warn("New sane text for document {}\n", .{handle.uri()});
if (handle.document.sane_text) |sane| {
self.allocator.free(sane);
}
handle.document.sane_text = try std.mem.dupe(self.allocator, u8, handle.document.text);
const tree = try handle.tree(self.allocator);
defer tree.deinit();
std.debug.warn("New text for document {}\n", .{handle.uri()});
// TODO: Better algorithm or data structure?
// Removing the imports is costly since they live in an array list
// Perhaps we should use an AutoHashMap([]const u8, {}) ?
@ -164,7 +130,7 @@ fn checkSanity(self: *DocumentStore, handle: *Handle) !void {
// Try to detect removed imports and decrement their counts.
if (handle.import_uris.items.len == 0) return;
const import_strs = try analysis.collectImports(self.allocator, dirty_tree);
const import_strs = try analysis.collectImports(self.allocator, tree);
defer self.allocator.free(import_strs);
const still_exist = try self.allocator.alloc(bool, handle.import_uris.items.len);
@ -175,7 +141,7 @@ fn checkSanity(self: *DocumentStore, handle: *Handle) !void {
}
for (import_strs) |str| {
const uri = (try uriFromImportStr(self, handle, str)) orelse continue;
const uri = (try uriFromImportStr(self, handle.*, str)) orelse continue;
defer self.allocator.free(uri);
var idx: usize = 0;
@ -205,7 +171,7 @@ fn checkSanity(self: *DocumentStore, handle: *Handle) !void {
}
pub fn applyChanges(self: *DocumentStore, handle: *Handle, content_changes: std.json.Array) !void {
var document = &handle.document;
const document = &handle.document;
for (content_changes.items) |change| {
if (change.Object.getValue("range")) |range| {
@ -260,7 +226,7 @@ pub fn applyChanges(self: *DocumentStore, handle: *Handle, content_changes: std.
try self.checkSanity(handle);
}
fn uriFromImportStr(store: *DocumentStore, handle: *Handle, import_str: []const u8) !?[]const u8 {
fn uriFromImportStr(store: *DocumentStore, handle: Handle, import_str: []const u8) !?[]const u8 {
return if (std.mem.eql(u8, import_str, "std"))
if (store.std_uri) |std_root_uri| try std.mem.dupe(store.allocator, u8, std_root_uri) else {
std.debug.warn("Cannot resolve std library import, path is null.\n", .{});
@ -289,10 +255,11 @@ pub const AnalysisContext = struct {
// not for the tree allocations.
arena: *std.heap.ArenaAllocator,
tree: *std.zig.ast.Tree,
scope_nodes: []*std.zig.ast.Node,
pub fn onImport(self: *AnalysisContext, import_str: []const u8) !?*std.zig.ast.Node {
const allocator = self.store.allocator;
const final_uri = (try uriFromImportStr(self.store, self.handle, import_str)) orelse return null;
const final_uri = (try uriFromImportStr(self.store, self.handle.*, import_str)) orelse return null;
std.debug.warn("Import final URI: {}\n", .{final_uri});
var consumed_final_uri = false;
@ -305,11 +272,8 @@ pub const AnalysisContext = struct {
self.handle = self.store.getHandle(final_uri) orelse return null;
self.tree.deinit();
if (try self.handle.saneTree(allocator)) |tree| {
self.tree = tree;
return &self.tree.root_node.base;
}
return null;
self.tree = try self.handle.tree(allocator);
return &self.tree.root_node.base;
}
}
@ -321,11 +285,8 @@ pub const AnalysisContext = struct {
self.handle = new_handle;
self.tree.deinit();
if (try self.handle.saneTree(allocator)) |tree| {
self.tree = tree;
return &self.tree.root_node.base;
}
return null;
self.tree = try self.handle.tree(allocator);
return &self.tree.root_node.base;
}
// New document, read the file then call into openDocument.
@ -355,17 +316,16 @@ pub const AnalysisContext = struct {
// Swap handles and get new tree.
// This takes ownership of the passed uri and text.
self.handle = try newDocument(self.store, try std.mem.dupe(allocator, u8, final_uri), file_contents);
const duped_final_uri = try std.mem.dupe(allocator, u8, final_uri);
errdefer allocator.free(duped_final_uri);
self.handle = try newDocument(self.store, duped_final_uri, file_contents);
}
// Free old tree, add new one if it exists.
// If we return null, no one should access the tree.
self.tree.deinit();
if (try self.handle.saneTree(allocator)) |tree| {
self.tree = tree;
return &self.tree.root_node.base;
}
return null;
self.tree = try self.handle.tree(allocator);
return &self.tree.root_node.base;
}
pub fn deinit(self: *AnalysisContext) void {
@ -373,14 +333,14 @@ pub const AnalysisContext = struct {
}
};
pub fn analysisContext(self: *DocumentStore, handle: *Handle, arena: *std.heap.ArenaAllocator) !?AnalysisContext {
const tree = (try handle.saneTree(self.allocator)) orelse return null;
pub fn analysisContext(self: *DocumentStore, handle: *Handle, arena: *std.heap.ArenaAllocator, position: types.Position) !AnalysisContext {
const tree = try handle.tree(self.allocator);
return AnalysisContext{
.store = self,
.handle = handle,
.arena = arena,
.tree = tree,
.scope_nodes = try analysis.declsFromIndex(&arena.allocator, tree, try handle.document.positionToIndex(position))
};
}
@ -388,9 +348,6 @@ pub fn deinit(self: *DocumentStore) void {
var entry_iterator = self.handles.iterator();
while (entry_iterator.next()) |entry| {
self.allocator.free(entry.value.document.mem);
if (entry.value.document.sane_text) |sane| {
self.allocator.free(sane);
}
for (entry.value.import_uris.items) |uri| {
self.allocator.free(uri);

44
src/header.zig Normal file
View File

@ -0,0 +1,44 @@
const std = @import("std");
const mem = std.mem;
const RequestHeader = struct {
content_length: usize,
/// null implies "application/vscode-jsonrpc; charset=utf-8"
content_type: ?[]const u8,
pub fn deinit(self: @This(), allocator: *mem.Allocator) void {
if (self.content_type) |ct| allocator.free(ct);
}
};
pub fn readRequestHeader(allocator: *mem.Allocator, instream: var) !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..mem.indexOf(u8, header, ": ") orelse return error.MissingColon];
const header_value = header[header_name.len + 2..header.len-1];
if (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 (mem.eql(u8, header_name, "Content-Type")) {
r.content_type = try mem.dupe(allocator, u8, header_value);
} else {
return error.UnknownHeader;
}
}
if (!has_content_length) return error.MissingContentLength;
return r;
}

View File

@ -3,6 +3,8 @@ const build_options = @import("build_options");
const Config = @import("config.zig");
const DocumentStore = @import("document_store.zig");
const DebugAllocator = @import("debug_allocator.zig");
const readRequestHeader = @import("header.zig").readRequestHeader;
const data = @import("data/" ++ build_options.data_version ++ ".zig");
const types = @import("types.zig");
const analysis = @import("analysis.zig");
@ -99,7 +101,7 @@ fn astLocationToRange(loc: std.zig.ast.Tree.Location) types.Range {
}
fn publishDiagnostics(handle: DocumentStore.Handle, config: Config) !void {
const tree = try handle.dirtyTree(allocator);
const tree = try handle.tree(allocator);
defer tree.deinit();
// Use an arena for our local memory allocations.
@ -137,34 +139,30 @@ fn publishDiagnostics(handle: DocumentStore.Handle, config: Config) !void {
if (is_extern)
break :blk;
if (func.name_token) |name_token| {
const loc = tree.tokenLocation(0, name_token);
if (config.warn_style) {
if (func.name_token) |name_token| {
const loc = tree.tokenLocation(0, name_token);
const is_type_function = switch (func.return_type) {
.Explicit => |node| if (node.cast(std.zig.ast.Node.Identifier)) |ident|
std.mem.eql(u8, tree.tokenSlice(ident.token), "type")
else
false,
.InferErrorSet => false,
};
const is_type_function = analysis.isTypeFunction(tree, func);
const func_name = tree.tokenSlice(name_token);
if (!is_type_function and !analysis.isCamelCase(func_name)) {
try diagnostics.append(.{
.range = astLocationToRange(loc),
.severity = .Information,
.code = "BadStyle",
.source = "zls",
.message = "Functions should be camelCase",
});
} else if (is_type_function and !analysis.isPascalCase(func_name)) {
try diagnostics.append(.{
.range = astLocationToRange(loc),
.severity = .Information,
.code = "BadStyle",
.source = "zls",
.message = "Type functions should be PascalCase",
});
const func_name = tree.tokenSlice(name_token);
if (!is_type_function and !analysis.isCamelCase(func_name)) {
try diagnostics.append(.{
.range = astLocationToRange(loc),
.severity = .Information,
.code = "BadStyle",
.source = "zls",
.message = "Functions should be camelCase",
});
} else if (is_type_function and !analysis.isPascalCase(func_name)) {
try diagnostics.append(.{
.range = astLocationToRange(loc),
.severity = .Information,
.code = "BadStyle",
.source = "zls",
.message = "Type functions should be PascalCase",
});
}
}
}
},
@ -184,8 +182,17 @@ fn publishDiagnostics(handle: DocumentStore.Handle, config: Config) !void {
});
}
fn nodeToCompletion(alloc: *std.mem.Allocator, tree: *std.zig.ast.Tree, decl: *std.zig.ast.Node, config: Config) !?types.CompletionItem {
var doc = if (try analysis.getDocComments(alloc, tree, decl)) |doc_comments|
fn containerToCompletion(list: *std.ArrayList(types.CompletionItem), tree: *std.zig.ast.Tree, container: *std.zig.ast.Node, config: Config) !void {
var index: usize = 0;
while (container.iterate(index)) |child_node| : (index+=1) {
if (analysis.isNodePublic(tree, child_node)) {
try nodeToCompletion(list, tree, child_node, config);
}
}
}
fn nodeToCompletion(list: *std.ArrayList(types.CompletionItem), tree: *std.zig.ast.Tree, node: *std.zig.ast.Node, config: Config) error{OutOfMemory}!void {
var doc = if (try analysis.getDocComments(list.allocator, tree, node)) |doc_comments|
types.MarkupContent{
.kind = .Markdown,
.value = doc_comments,
@ -193,48 +200,78 @@ fn nodeToCompletion(alloc: *std.mem.Allocator, tree: *std.zig.ast.Tree, decl: *s
else
null;
switch (decl.id) {
switch (node.id) {
.ErrorSetDecl, .Root, .ContainerDecl => {
try containerToCompletion(list, tree, node, config);
},
.FnProto => {
const func = decl.cast(std.zig.ast.Node.FnProto).?;
const func = node.cast(std.zig.ast.Node.FnProto).?;
if (func.name_token) |name_token| {
const insert_text = if (config.enable_snippets)
try analysis.getFunctionSnippet(alloc, tree, func)
try analysis.getFunctionSnippet(list.allocator, tree, func)
else
null;
return types.CompletionItem{
const is_type_function = analysis.isTypeFunction(tree, func);
try list.append(.{
.label = tree.tokenSlice(name_token),
.kind = .Function,
.kind = if (is_type_function) .Struct else .Function,
.documentation = doc,
.detail = analysis.getFunctionSignature(tree, func),
.insertText = insert_text,
.insertTextFormat = if (config.enable_snippets) .Snippet else .PlainText,
};
});
}
},
.VarDecl => {
const var_decl = decl.cast(std.zig.ast.Node.VarDecl).?;
return types.CompletionItem{
const var_decl = node.cast(std.zig.ast.Node.VarDecl).?;
const is_const = tree.tokens.at(var_decl.mut_token).id == .Keyword_const;
try list.append(.{
.label = tree.tokenSlice(var_decl.name_token),
.kind = .Variable,
.kind = if (is_const) .Constant else .Variable,
.documentation = doc,
.detail = analysis.getVariableSignature(tree, var_decl),
};
});
},
else => if (analysis.nodeToString(tree, decl)) |string| {
return types.CompletionItem{
.ParamDecl => {
const param = node.cast(std.zig.ast.Node.ParamDecl).?;
if (param.name_token) |name_token|
try list.append(.{
.label = tree.tokenSlice(name_token),
.kind = .Constant,
.documentation = doc,
.detail = analysis.getParamSignature(tree, param),
});
},
.PrefixOp => {
try list.append(.{
.label = "len",
.kind = .Field,
});
try list.append(.{
.label = "ptr",
.kind = .Field,
});
},
.StringLiteral => {
try list.append(.{
.label = "len",
.kind = .Field,
});
},
else => if (analysis.nodeToString(tree, node)) |string| {
try list.append(.{
.label = string,
.kind = .Field,
.documentation = doc,
};
});
},
}
return null;
}
fn completeGlobal(id: i64, handle: DocumentStore.Handle, config: Config) !void {
var tree = (try handle.saneTree(allocator)) orelse return respondGeneric(id, no_completions_response);
fn completeGlobal(id: i64, pos_index: usize, handle: DocumentStore.Handle, config: Config) !void {
var tree = try handle.tree(allocator);
defer tree.deinit();
// We use a local arena allocator to deallocate all temporary data without iterating
@ -243,12 +280,11 @@ fn completeGlobal(id: i64, handle: DocumentStore.Handle, config: Config) !void {
// Deallocate all temporary data.
defer arena.deinit();
var decls = tree.root_node.decls.iterator(0);
while (decls.next()) |decl_ptr| {
// var decls = tree.root_node.decls.iterator(0);
var decls = try analysis.declsFromIndex(&arena.allocator, tree, pos_index);
for (decls) |decl_ptr| {
var decl = decl_ptr.*;
if (try nodeToCompletion(&arena.allocator, tree, decl, config)) |completion| {
try completions.append(completion);
}
try nodeToCompletion(&completions, tree, decl_ptr, config);
}
try send(types.Response{
@ -266,26 +302,15 @@ fn completeFieldAccess(id: i64, handle: *DocumentStore.Handle, position: types.P
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
var analysis_ctx = (try document_store.analysisContext(handle, &arena)) orelse {
return send(types.Response{
.id = .{ .Integer = id },
.result = .{
.CompletionList = .{
.isIncomplete = false,
.items = &[_]types.CompletionItem{},
},
},
});
};
var analysis_ctx = try document_store.analysisContext(handle, &arena, position);
defer analysis_ctx.deinit();
var completions = std.ArrayList(types.CompletionItem).init(&arena.allocator);
var line = try handle.document.getLine(@intCast(usize, position.line));
// handle pointer could change from underneath us, so let's copy the line
var line_copy = try std.mem.dupe(&arena.allocator, u8, line[line_start_idx..]);
var tokenizer = std.zig.Tokenizer.init(line_copy);
const line = try handle.document.getLine(@intCast(usize, position.line));
var tokenizer = std.zig.Tokenizer.init(line[line_start_idx..]);
// var decls = try analysis.declsFromIndex(&arena.allocator, analysis_ctx.tree, try handle.document.positionToIndex(position));
if (analysis.getFieldAccessTypeNode(&analysis_ctx, &tokenizer)) |node| {
var index: usize = 0;
while (node.iterate(index)) |child_node| {
@ -579,7 +604,7 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
},
},
}),
.var_access, .empty => try completeGlobal(id, handle.*, config),
.var_access, .empty => try completeGlobal(id, pos_index, handle.*, config),
.field_access => |start_idx| try completeFieldAccess(id, handle, pos, start_idx, config),
else => try respondGeneric(id, no_completions_response),
}
@ -607,20 +632,20 @@ fn processJsonRpc(parser: *std.json.Parser, json: []const u8, config: Config) !v
}
}
var debug_alloc_state: std.testing.LeakCountAllocator = undefined;
var debug_alloc_state: DebugAllocator = undefined;
// We can now use if(leak_count_alloc) |alloc| { ... } as a comptime check.
const debug_alloc: ?*std.testing.LeakCountAllocator = if (build_options.allocation_info) &debug_alloc_state else null;
const debug_alloc: ?*DebugAllocator = if (build_options.allocation_info) &debug_alloc_state else null;
pub fn main() anyerror!void {
// TODO: Use a better purpose general allocator once std has one.
// Probably after the generic composable allocators PR?
// This is not too bad for now since most allocations happen in local areans.
// This is not too bad for now since most allocations happen in local arenas.
allocator = std.heap.page_allocator;
if (build_options.allocation_info) {
// TODO: Use a better debugging allocator, track size in bytes, memory reserved etc..
// Initialize the leak counting allocator.
debug_alloc_state = std.testing.LeakCountAllocator.init(allocator);
debug_alloc_state = DebugAllocator.init(allocator);
allocator = &debug_alloc_state.allocator;
}
@ -636,9 +661,10 @@ pub fn main() anyerror!void {
const stdin = std.io.getStdIn().inStream();
stdout = std.io.getStdOut().outStream();
// Read he configuration, if any.
var config = Config{};
// Read the configuration, if any.
const config_parse_options = std.json.ParseOptions{ .allocator = allocator };
var config = Config{};
defer std.json.parseFree(Config, config, config_parse_options);
// TODO: Investigate using std.fs.Watch to detect writes to the config and reload it.
config_read: {
@ -648,30 +674,26 @@ pub fn main() anyerror!void {
var exec_dir = std.fs.cwd().openDir(exec_dir_path, .{}) catch break :config_read;
defer exec_dir.close();
var conf_file = exec_dir.openFile("zls.json", .{}) catch break :config_read;
const conf_file = exec_dir.openFile("zls.json", .{}) catch break :config_read;
defer conf_file.close();
const conf_file_stat = conf_file.stat() catch break :config_read;
// Allocate enough memory for the whole file.
var file_buf = try allocator.alloc(u8, conf_file_stat.size);
// Max 1MB
const file_buf = conf_file.inStream().readAllAlloc(allocator, 0x1000000) catch break :config_read;
defer allocator.free(file_buf);
const bytes_read = conf_file.readAll(file_buf) catch break :config_read;
if (bytes_read != conf_file_stat.size) break :config_read;
// TODO: Better errors? Doesnt seem like std.json can provide us positions or context.
// TODO: Better errors? Doesn't seem like std.json can provide us positions or context.
config = std.json.parse(Config, &std.json.TokenStream.init(file_buf), config_parse_options) catch |err| {
std.debug.warn("Error while parsing configuration file: {}\nUsing default config.\n", .{err});
break :config_read;
};
}
defer std.json.parseFree(Config, config, config_parse_options);
if (config.zig_lib_path != null and !std.fs.path.isAbsolute(config.zig_lib_path.?)) {
std.debug.warn("zig library path is not absolute, defaulting to null.\n", .{});
allocator.free(config.zig_lib_path.?);
config.zig_lib_path = null;
if (config.zig_lib_path) |zig_lib_path| {
if (!std.fs.path.isAbsolute(zig_lib_path)) {
std.debug.warn("zig library path is not absolute, defaulting to null.\n", .{});
allocator.free(zig_lib_path);
config.zig_lib_path = null;
}
}
try document_store.init(allocator, config.zig_lib_path);
@ -681,77 +703,20 @@ pub fn main() anyerror!void {
var json_parser = std.json.Parser.init(allocator, false);
defer json_parser.deinit();
var offset: usize = 0;
var bytes_read: usize = 0;
var index: usize = 0;
var content_len: usize = 0;
stdin_poll: while (true) {
if (offset >= 16 and std.mem.startsWith(u8, buffer.items, "Content-Length: ")) {
index = 16;
while (index <= offset + 10) : (index += 1) {
const c = buffer.items[index];
if (c >= '0' and c <= '9') {
content_len = content_len * 10 + (c - '0');
}
if (c == '\r' and buffer.items[index + 1] == '\n') {
index += 2;
break;
}
}
if (buffer.items[index] == '\r') {
index += 2;
if (buffer.items.len < index + content_len) {
try buffer.resize(index + content_len);
}
body_poll: while (offset < content_len + index) {
bytes_read = try stdin.readAll(buffer.items[offset .. index + content_len]);
if (bytes_read == 0) {
try log("0 bytes read; exiting!", .{});
return;
}
offset += bytes_read;
}
try processJsonRpc(&json_parser, buffer.items[index .. index + content_len], config);
json_parser.reset();
offset = 0;
content_len = 0;
} else {
try log("\\r not found", .{});
}
} else if (offset >= 16) {
try log("Offset is greater than 16!", .{});
while (true) {
const headers = readRequestHeader(allocator, stdin) catch |err| {
try log("{}; exiting!", .{@errorName(err)});
return;
}
if (offset < 16) {
bytes_read = try stdin.readAll(buffer.items[offset..25]);
} else {
if (offset == buffer.items.len) {
try buffer.resize(buffer.items.len * 2);
}
if (index + content_len > buffer.items.len) {
bytes_read = try stdin.readAll(buffer.items[offset..buffer.items.len]);
} else {
bytes_read = try stdin.readAll(buffer.items[offset .. index + content_len]);
}
}
if (bytes_read == 0) {
try log("0 bytes read; exiting!", .{});
return;
}
offset += bytes_read;
};
defer headers.deinit(allocator);
const buf = try allocator.alloc(u8, headers.content_length);
defer allocator.free(buf);
try stdin.readNoEof(buf);
try processJsonRpc(&json_parser, buf, config);
json_parser.reset();
if (debug_alloc) |dbg| {
try log("Allocations alive: {}", .{dbg.count});
try log("{}", .{dbg.info});
}
}
}

View File

@ -140,7 +140,6 @@ pub const TextDocument = struct {
text: String,
// This holds the memory that we have actually allocated.
mem: []u8,
sane_text: ?String = null,
pub fn positionToIndex(self: TextDocument, position: Position) !usize {
var split_iterator = std.mem.split(self.text, "\n");
@ -186,11 +185,11 @@ pub const MarkupKind = enum(u1) {
options: json.StringifyOptions,
out_stream: var,
) !void {
if (@enumToInt(value) == 0) {
try json.stringify("plaintext", options, out_stream);
} else {
try json.stringify("markdown", options, out_stream);
}
const str = switch (value) {
.PlainText => "plaintext",
.Markdown => "markdown",
};
try json.stringify(str, options, out_stream);
}
};

View File

@ -14,7 +14,7 @@ pub fn fromPath(allocator: *std.mem.Allocator, path: []const u8) ![]const u8 {
var buf = std.ArrayList(u8).init(allocator);
try buf.appendSlice(prefix);
var out_stream = buf.outStream();
const out_stream = buf.outStream();
for (path) |char| {
if (char == std.fs.path.sep) {
@ -55,17 +55,16 @@ fn parseHex(c: u8) !u8 {
pub fn parse(allocator: *std.mem.Allocator, str: []const u8) ![]u8 {
if (str.len < 7 or !std.mem.eql(u8, "file://", str[0..7])) return error.UriBadScheme;
var uri = try allocator.alloc(u8, str.len - (if (std.fs.path.sep == '\\') 8 else 7));
const uri = try allocator.alloc(u8, str.len - (if (std.fs.path.sep == '\\') 8 else 7));
errdefer allocator.free(uri);
const path = if (std.fs.path.sep == '\\') str[8..] else str[7..];
var i: usize = 0;
var j: usize = 0;
var e: usize = path.len;
while (j < e) : (i += 1) {
while (j < path.len) : (i += 1) {
if (path[j] == '%') {
if (j + 2 >= e) return error.UriBadEscape;
if (j + 2 >= path.len) return error.UriBadEscape;
const upper = try parseHex(path[j + 1]);
const lower = try parseHex(path[j + 2]);
uri[i] = (upper << 4) + lower;