1085 lines
42 KiB
Zig
1085 lines
42 KiB
Zig
const std = @import("std");
|
|
const zig_builtin = @import("builtin");
|
|
|
|
const tres = @import("tres");
|
|
|
|
const ConfigOption = struct {
|
|
/// Name of config option
|
|
name: []const u8,
|
|
/// (used in doc comments & schema.json)
|
|
description: []const u8,
|
|
/// zig type in string form. e.g "u32", "[]const u8", "?usize"
|
|
type: []const u8,
|
|
/// if the zig type should be an enum, this should contain
|
|
/// a list of enum values and `type`
|
|
/// If this is set, the value `type` should to be `enum`
|
|
@"enum": ?[]const []const u8 = null,
|
|
/// used in Config.zig as the default initializer
|
|
default: []const u8,
|
|
|
|
fn getTypescriptType(self: ConfigOption) error{UnsupportedType}![]const u8 {
|
|
return if (std.mem.eql(u8, self.type, "?[]const u8"))
|
|
"string"
|
|
else if (std.mem.eql(u8, self.type, "bool"))
|
|
"boolean"
|
|
else if (std.mem.eql(u8, self.type, "usize"))
|
|
"integer"
|
|
else if (std.mem.eql(u8, self.type, "enum"))
|
|
"string"
|
|
else
|
|
error.UnsupportedType;
|
|
}
|
|
|
|
fn formatZigType(
|
|
config: ConfigOption,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = options;
|
|
if (fmt.len != 0) return std.fmt.invalidFmtError(fmt, ConfigOption);
|
|
if (config.@"enum") |enum_members| {
|
|
try writer.writeAll("enum {\n ");
|
|
for (enum_members) |member_name| {
|
|
try writer.writeAll(member_name);
|
|
try writer.writeAll(",\n ");
|
|
}
|
|
std.debug.assert(enum_members.len > 1);
|
|
try writer.writeAll(
|
|
\\
|
|
\\ pub const tres_string_enum = true;
|
|
\\}
|
|
);
|
|
return;
|
|
}
|
|
try writer.writeAll(config.type);
|
|
}
|
|
|
|
fn fmtZigType(self: ConfigOption) std.fmt.Formatter(formatZigType) {
|
|
return .{ .data = self };
|
|
}
|
|
|
|
fn formatDefaultValue(
|
|
config: ConfigOption,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = options;
|
|
if (fmt.len != 0) return std.fmt.invalidFmtError(fmt, ConfigOption);
|
|
if (config.@"enum" != null) {
|
|
try writer.writeByte('.');
|
|
}
|
|
const default = std.mem.trim(u8, config.default, &std.ascii.whitespace);
|
|
try writer.writeAll(default);
|
|
}
|
|
|
|
fn fmtDefaultValue(self: ConfigOption) std.fmt.Formatter(formatDefaultValue) {
|
|
return .{ .data = self };
|
|
}
|
|
};
|
|
|
|
const Config = struct {
|
|
options: []ConfigOption,
|
|
};
|
|
|
|
const Schema = struct {
|
|
@"$schema": []const u8 = "http://json-schema.org/schema",
|
|
title: []const u8 = "ZLS Config",
|
|
description: []const u8 = "Configuration file for the zig language server (ZLS)",
|
|
type: []const u8 = "object",
|
|
properties: std.StringArrayHashMap(SchemaEntry),
|
|
};
|
|
|
|
const SchemaEntry = struct {
|
|
description: []const u8,
|
|
type: []const u8,
|
|
@"enum": ?[]const []const u8 = null,
|
|
default: []const u8,
|
|
};
|
|
|
|
fn generateConfigFile(allocator: std.mem.Allocator, config: Config, path: []const u8) !void {
|
|
_ = allocator;
|
|
|
|
const config_file = try std.fs.createFileAbsolute(path, .{});
|
|
defer config_file.close();
|
|
|
|
var buff_out = std.io.bufferedWriter(config_file.writer());
|
|
|
|
_ = try buff_out.write(
|
|
\\//! DO NOT EDIT
|
|
\\//! Configuration options for zls.
|
|
\\//! If you want to add a config option edit
|
|
\\//! src/config_gen/config.json and run `zig build gen`
|
|
\\//! GENERATED BY src/config_gen/config_gen.zig
|
|
\\
|
|
);
|
|
|
|
for (config.options) |option| {
|
|
try buff_out.writer().print(
|
|
\\
|
|
\\/// {s}
|
|
\\{}: {} = {},
|
|
\\
|
|
, .{
|
|
std.mem.trim(u8, option.description, &std.ascii.whitespace),
|
|
std.zig.fmtId(std.mem.trim(u8, option.name, &std.ascii.whitespace)),
|
|
option.fmtZigType(),
|
|
option.fmtDefaultValue(),
|
|
});
|
|
}
|
|
|
|
_ = try buff_out.write(
|
|
\\
|
|
\\// DO NOT EDIT
|
|
\\
|
|
);
|
|
|
|
try buff_out.flush();
|
|
}
|
|
|
|
fn generateSchemaFile(allocator: std.mem.Allocator, config: Config, path: []const u8) !void {
|
|
const schema_file = try std.fs.openFileAbsolute(path, .{
|
|
.mode = .write_only,
|
|
});
|
|
defer schema_file.close();
|
|
|
|
var buff_out = std.io.bufferedWriter(schema_file.writer());
|
|
|
|
var properties = std.StringArrayHashMapUnmanaged(SchemaEntry){};
|
|
defer properties.deinit(allocator);
|
|
try properties.ensureTotalCapacity(allocator, config.options.len);
|
|
|
|
for (config.options) |option| {
|
|
properties.putAssumeCapacityNoClobber(option.name, .{
|
|
.description = option.description,
|
|
.type = try option.getTypescriptType(),
|
|
.@"enum" = option.@"enum",
|
|
.default = option.default,
|
|
});
|
|
}
|
|
|
|
_ = try buff_out.write(
|
|
\\{
|
|
\\ "$schema": "http://json-schema.org/schema",
|
|
\\ "title": "ZLS Config",
|
|
\\ "description": "Configuration file for the zig language server (ZLS)",
|
|
\\ "type": "object",
|
|
\\ "properties":
|
|
);
|
|
|
|
try tres.stringify(properties, .{
|
|
.whitespace = .{
|
|
.indent_level = 1,
|
|
},
|
|
.emit_null_optional_fields = false,
|
|
}, buff_out.writer());
|
|
|
|
_ = 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 = 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, start_indicator) orelse return error.SectionNotFound);
|
|
const end = std.mem.indexOfPos(u8, readme, start, end_indicator) orelse return error.SectionNotFound;
|
|
|
|
try readme_file.seekTo(0);
|
|
var writer = readme_file.writer();
|
|
|
|
try writer.writeAll(readme[0..start]);
|
|
|
|
try writer.writeAll(
|
|
\\
|
|
\\| Option | Type | Default value | What it Does |
|
|
\\| --- | --- | --- | --- |
|
|
\\
|
|
);
|
|
|
|
for (config.options) |option| {
|
|
try writer.print(
|
|
\\| `{s}` | `{s}` | `{}` | {s} |
|
|
\\
|
|
, .{
|
|
std.mem.trim(u8, option.name, &std.ascii.whitespace),
|
|
std.mem.trim(u8, option.type, &std.ascii.whitespace),
|
|
option.fmtDefaultValue(),
|
|
std.mem.trim(u8, option.description, &std.ascii.whitespace),
|
|
});
|
|
}
|
|
|
|
try writer.writeAll(readme[end..]);
|
|
|
|
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("zig.trace.server", .{
|
|
.scope = "window",
|
|
.type = "string",
|
|
.@"enum" = &.{ "off", "messages", "verbose" },
|
|
.description = "Traces the communication between VS Code and the language server.",
|
|
.default = .{ .string = "off" },
|
|
});
|
|
configuration.putAssumeCapacityNoClobber("zig.zls.checkForUpdate", .{
|
|
.type = "boolean",
|
|
.description = "Whether to automatically check for new updates",
|
|
.default = .{ .bool = true },
|
|
});
|
|
configuration.putAssumeCapacityNoClobber("zig.zls.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 snake_case_name = try std.fmt.allocPrint(allocator, "zig.zls.{s}", .{option.name});
|
|
defer allocator.free(snake_case_name);
|
|
const name = try snakeCaseToCamelCase(allocator, snake_case_name);
|
|
errdefer allocator.free(name);
|
|
|
|
const default: ?std.json.Value = blk: {
|
|
if (option.@"enum" != null) break :blk .{ .string = option.default };
|
|
|
|
var parser = std.json.Parser.init(allocator, .alloc_always);
|
|
defer parser.deinit();
|
|
|
|
var value = try parser.parse(option.default);
|
|
defer value.deinit();
|
|
|
|
break :blk if (value.root != .null) value.root else null;
|
|
};
|
|
|
|
configuration.putAssumeCapacityNoClobber(name, .{
|
|
.type = try option.getTypescriptType(),
|
|
.description = option.description,
|
|
.@"enum" = option.@"enum",
|
|
.format = if (std.mem.indexOf(u8, option.name, "path") != null) "path" else null,
|
|
.default = 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();
|
|
}
|
|
|
|
fn snakeCaseToCamelCase(allocator: std.mem.Allocator, str: []const u8) error{OutOfMemory}![]u8 {
|
|
const underscore_count = std.mem.count(u8, str, "_");
|
|
var result = try allocator.alloc(u8, str.len - underscore_count);
|
|
var i: usize = 0;
|
|
var j: usize = 0;
|
|
while (i < str.len) : (i += 1) {
|
|
if (str[i] != '_') {
|
|
result[j] = str[i];
|
|
j += 1;
|
|
continue;
|
|
}
|
|
if (i + 1 < str.len and 'a' <= str[i + 1] and str[i + 1] <= 'z') {
|
|
result[j] = std.ascii.toUpper(str[i + 1]);
|
|
i += 1;
|
|
j += 1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Tokenizer for a langref.html.in file
|
|
/// example file: https://raw.githubusercontent.com/ziglang/zig/master/doc/langref.html.in
|
|
/// this is a modified version from https://github.com/ziglang/zig/blob/master/doc/docgen.zig
|
|
const Tokenizer = struct {
|
|
buffer: []const u8,
|
|
index: usize = 0,
|
|
state: State = .Start,
|
|
|
|
const State = enum {
|
|
Start,
|
|
LBracket,
|
|
Hash,
|
|
TagName,
|
|
Eof,
|
|
};
|
|
|
|
const Token = struct {
|
|
id: Id,
|
|
start: usize,
|
|
end: usize,
|
|
|
|
const Id = enum {
|
|
Invalid,
|
|
Content,
|
|
BracketOpen,
|
|
TagContent,
|
|
Separator,
|
|
BracketClose,
|
|
Eof,
|
|
};
|
|
};
|
|
|
|
fn next(self: *Tokenizer) Token {
|
|
var result = Token{
|
|
.id = .Eof,
|
|
.start = self.index,
|
|
.end = undefined,
|
|
};
|
|
while (self.index < self.buffer.len) : (self.index += 1) {
|
|
const c = self.buffer[self.index];
|
|
switch (self.state) {
|
|
.Start => switch (c) {
|
|
'{' => {
|
|
self.state = .LBracket;
|
|
},
|
|
else => {
|
|
result.id = .Content;
|
|
},
|
|
},
|
|
.LBracket => switch (c) {
|
|
'#' => {
|
|
if (result.id != .Eof) {
|
|
self.index -= 1;
|
|
self.state = .Start;
|
|
break;
|
|
} else {
|
|
result.id = .BracketOpen;
|
|
self.index += 1;
|
|
self.state = .TagName;
|
|
break;
|
|
}
|
|
},
|
|
else => {
|
|
result.id = .Content;
|
|
self.state = .Start;
|
|
},
|
|
},
|
|
.TagName => switch (c) {
|
|
'|' => {
|
|
if (result.id != .Eof) {
|
|
break;
|
|
} else {
|
|
result.id = .Separator;
|
|
self.index += 1;
|
|
break;
|
|
}
|
|
},
|
|
'#' => {
|
|
self.state = .Hash;
|
|
},
|
|
else => {
|
|
result.id = .TagContent;
|
|
},
|
|
},
|
|
.Hash => switch (c) {
|
|
'}' => {
|
|
if (result.id != .Eof) {
|
|
self.index -= 1;
|
|
self.state = .TagName;
|
|
break;
|
|
} else {
|
|
result.id = .BracketClose;
|
|
self.index += 1;
|
|
self.state = .Start;
|
|
break;
|
|
}
|
|
},
|
|
else => {
|
|
result.id = .TagContent;
|
|
self.state = .TagName;
|
|
},
|
|
},
|
|
.Eof => unreachable,
|
|
}
|
|
} else {
|
|
switch (self.state) {
|
|
.Start,
|
|
.LBracket,
|
|
.Eof,
|
|
=> {},
|
|
else => {
|
|
result.id = .Invalid;
|
|
},
|
|
}
|
|
self.state = .Eof;
|
|
}
|
|
result.end = self.index;
|
|
return result;
|
|
}
|
|
};
|
|
|
|
const Builtin = struct {
|
|
name: []const u8,
|
|
signature: []const u8,
|
|
documentation: std.ArrayListUnmanaged(u8),
|
|
};
|
|
|
|
/// parses a `langref.html.in` file and extracts builtins from this section: `https://ziglang.org/documentation/master/#Builtin-Functions`
|
|
/// the documentation field contains poorly formated html
|
|
fn collectBuiltinData(allocator: std.mem.Allocator, version: []const u8, langref_file: []const u8) error{OutOfMemory}![]Builtin {
|
|
var tokenizer = Tokenizer{ .buffer = langref_file };
|
|
|
|
const State = enum {
|
|
/// searching for this line:
|
|
/// {#header_open|Builtin Functions|2col#}
|
|
searching,
|
|
/// skippig builtin functions description:
|
|
/// Builtin functions are provided by the compiler and are prefixed ...
|
|
prefix,
|
|
/// every entry begins with this:
|
|
/// {#syntax#}@addrSpaceCast(comptime addrspace: std.builtin.AddressSpace, ptr: anytype) anytype{#endsyntax#}
|
|
builtin_begin,
|
|
/// iterate over documentation
|
|
builtin_content,
|
|
};
|
|
var state: State = .searching;
|
|
|
|
var builtins = std.ArrayListUnmanaged(Builtin){};
|
|
errdefer {
|
|
for (builtins.items) |*builtin| {
|
|
builtin.documentation.deinit(allocator);
|
|
}
|
|
builtins.deinit(allocator);
|
|
}
|
|
|
|
var depth: u32 = undefined;
|
|
while (true) {
|
|
const token = tokenizer.next();
|
|
switch (token.id) {
|
|
.Content => {
|
|
switch (state) {
|
|
.builtin_content => {
|
|
try builtins.items[builtins.items.len - 1].documentation.appendSlice(allocator, tokenizer.buffer[token.start..token.end]);
|
|
},
|
|
else => continue,
|
|
}
|
|
},
|
|
.BracketOpen => {
|
|
const tag_token = tokenizer.next();
|
|
std.debug.assert(tag_token.id == .TagContent);
|
|
const tag_name = tokenizer.buffer[tag_token.start..tag_token.end];
|
|
|
|
if (std.mem.eql(u8, tag_name, "header_open")) {
|
|
std.debug.assert(tokenizer.next().id == .Separator);
|
|
const content_token = tokenizer.next();
|
|
std.debug.assert(tag_token.id == .TagContent);
|
|
const content_name = tokenizer.buffer[content_token.start..content_token.end];
|
|
|
|
switch (state) {
|
|
.searching => {
|
|
if (std.mem.eql(u8, content_name, "Builtin Functions")) {
|
|
state = .prefix;
|
|
depth = 0;
|
|
}
|
|
},
|
|
.prefix, .builtin_begin => {
|
|
state = .builtin_begin;
|
|
try builtins.append(allocator, .{
|
|
.name = content_name,
|
|
.signature = "",
|
|
.documentation = .{},
|
|
});
|
|
},
|
|
.builtin_content => unreachable,
|
|
}
|
|
if (state != .searching) {
|
|
depth += 1;
|
|
}
|
|
|
|
while (true) {
|
|
const bracket_tok = tokenizer.next();
|
|
switch (bracket_tok.id) {
|
|
.BracketClose => break,
|
|
.Separator, .TagContent => continue,
|
|
else => unreachable,
|
|
}
|
|
}
|
|
} else if (std.mem.eql(u8, tag_name, "header_close")) {
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
|
|
if (state == .builtin_content) {
|
|
state = .builtin_begin;
|
|
}
|
|
if (state != .searching) {
|
|
depth -= 1;
|
|
if (depth == 0) break;
|
|
}
|
|
} else if (state != .searching and std.mem.eql(u8, tag_name, "syntax")) {
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
const content_tag = tokenizer.next();
|
|
std.debug.assert(content_tag.id == .Content);
|
|
const content_name = tokenizer.buffer[content_tag.start..content_tag.end];
|
|
std.debug.assert(tokenizer.next().id == .BracketOpen);
|
|
const end_syntax_tag = tokenizer.next();
|
|
std.debug.assert(end_syntax_tag.id == .TagContent);
|
|
const end_tag_name = tokenizer.buffer[end_syntax_tag.start..end_syntax_tag.end];
|
|
std.debug.assert(std.mem.eql(u8, end_tag_name, "endsyntax"));
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
|
|
switch (state) {
|
|
.builtin_begin => {
|
|
builtins.items[builtins.items.len - 1].signature = content_name;
|
|
state = .builtin_content;
|
|
},
|
|
.builtin_content => {
|
|
const writer = builtins.items[builtins.items.len - 1].documentation.writer(allocator);
|
|
|
|
try writeMarkdownCode(content_name, "zig", writer);
|
|
},
|
|
else => {},
|
|
}
|
|
} else if (state != .searching and std.mem.eql(u8, tag_name, "syntax_block")) {
|
|
std.debug.assert(tokenizer.next().id == .Separator);
|
|
|
|
const source_type_tag = tokenizer.next();
|
|
std.debug.assert(tag_token.id == .TagContent);
|
|
const source_type = tokenizer.buffer[source_type_tag.start..source_type_tag.end];
|
|
switch (tokenizer.next().id) {
|
|
.Separator => {
|
|
std.debug.assert(tokenizer.next().id == .TagContent);
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
},
|
|
.BracketClose => {},
|
|
else => unreachable,
|
|
}
|
|
|
|
var content_token = tokenizer.next();
|
|
std.debug.assert(content_token.id == .Content);
|
|
const content = tokenizer.buffer[content_token.start..content_token.end];
|
|
const writer = builtins.items[builtins.items.len - 1].documentation.writer(allocator);
|
|
try writeMarkdownCode(content, source_type, writer);
|
|
|
|
std.debug.assert(tokenizer.next().id == .BracketOpen);
|
|
const end_code_token = tokenizer.next();
|
|
std.debug.assert(tag_token.id == .TagContent);
|
|
const end_code_name = tokenizer.buffer[end_code_token.start..end_code_token.end];
|
|
std.debug.assert(std.mem.eql(u8, end_code_name, "end_syntax_block"));
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
} else if (state != .searching and std.mem.eql(u8, tag_name, "link")) {
|
|
std.debug.assert(tokenizer.next().id == .Separator);
|
|
const name_token = tokenizer.next();
|
|
std.debug.assert(name_token.id == .TagContent);
|
|
const name = tokenizer.buffer[name_token.start..name_token.end];
|
|
|
|
const url_name = switch (tokenizer.next().id) {
|
|
.Separator => blk: {
|
|
const url_name_token = tokenizer.next();
|
|
std.debug.assert(url_name_token.id == .TagContent);
|
|
const url_name = tokenizer.buffer[url_name_token.start..url_name_token.end];
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
break :blk url_name;
|
|
},
|
|
.BracketClose => name,
|
|
else => unreachable,
|
|
};
|
|
|
|
const spaceless_url_name = try std.mem.replaceOwned(u8, allocator, url_name, " ", "-");
|
|
defer allocator.free(spaceless_url_name);
|
|
|
|
const writer = builtins.items[builtins.items.len - 1].documentation.writer(allocator);
|
|
try writer.print("[{s}](https://ziglang.org/documentation/{s}/#{s})", .{
|
|
name,
|
|
version,
|
|
std.mem.trimLeft(u8, spaceless_url_name, "@"),
|
|
});
|
|
} else if (state != .searching and std.mem.eql(u8, tag_name, "code_begin")) {
|
|
std.debug.assert(tokenizer.next().id == .Separator);
|
|
std.debug.assert(tokenizer.next().id == .TagContent);
|
|
switch (tokenizer.next().id) {
|
|
.Separator => {
|
|
std.debug.assert(tokenizer.next().id == .TagContent);
|
|
switch (tokenizer.next().id) {
|
|
.Separator => {
|
|
std.debug.assert(tokenizer.next().id == .TagContent);
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
},
|
|
.BracketClose => {},
|
|
else => unreachable,
|
|
}
|
|
},
|
|
.BracketClose => {},
|
|
else => unreachable,
|
|
}
|
|
|
|
while (true) {
|
|
const content_token = tokenizer.next();
|
|
std.debug.assert(content_token.id == .Content);
|
|
const content = tokenizer.buffer[content_token.start..content_token.end];
|
|
std.debug.assert(tokenizer.next().id == .BracketOpen);
|
|
const end_code_token = tokenizer.next();
|
|
std.debug.assert(end_code_token.id == .TagContent);
|
|
const end_tag_name = tokenizer.buffer[end_code_token.start..end_code_token.end];
|
|
|
|
if (std.mem.eql(u8, end_tag_name, "code_end")) {
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
|
|
const writer = builtins.items[builtins.items.len - 1].documentation.writer(allocator);
|
|
try writeMarkdownCode(content, "zig", writer);
|
|
break;
|
|
}
|
|
std.debug.assert(tokenizer.next().id == .BracketClose);
|
|
}
|
|
} else {
|
|
while (true) {
|
|
switch (tokenizer.next().id) {
|
|
.Eof => unreachable,
|
|
.BracketClose => break,
|
|
else => continue,
|
|
}
|
|
}
|
|
}
|
|
},
|
|
else => unreachable,
|
|
}
|
|
}
|
|
|
|
return try builtins.toOwnedSlice(allocator);
|
|
}
|
|
|
|
/// single line: \`{content}\`
|
|
/// multi line:
|
|
/// \`\`\`{source_type}
|
|
/// {content}
|
|
/// \`\`\`
|
|
fn writeMarkdownCode(content: []const u8, source_type: []const u8, writer: anytype) @TypeOf(writer).Error!void {
|
|
const trimmed_content = std.mem.trim(u8, content, " \n");
|
|
const is_multiline = std.mem.indexOfScalar(u8, trimmed_content, '\n') != null;
|
|
if (is_multiline) {
|
|
var line_it = std.mem.tokenize(u8, trimmed_content, "\n");
|
|
try writer.print("\n```{s}", .{source_type});
|
|
while (line_it.next()) |line| {
|
|
try writer.print("\n{s}", .{line});
|
|
}
|
|
try writer.writeAll("\n```");
|
|
} else {
|
|
try writer.print("`{s}`", .{trimmed_content});
|
|
}
|
|
}
|
|
|
|
fn writeLine(str: []const u8, single_line: bool, writer: anytype) @TypeOf(writer).Error!void {
|
|
const trimmed_content = std.mem.trim(u8, str, &std.ascii.whitespace);
|
|
if (trimmed_content.len == 0) return;
|
|
|
|
if (single_line) {
|
|
var line_it = std.mem.split(u8, trimmed_content, "\n");
|
|
while (line_it.next()) |line| {
|
|
try writer.print("{s} ", .{std.mem.trim(u8, line, &std.ascii.whitespace)});
|
|
}
|
|
} else {
|
|
try writer.writeAll(trimmed_content);
|
|
}
|
|
|
|
try writer.writeByte('\n');
|
|
}
|
|
|
|
/// converts text with various html tags into markdown
|
|
/// supported tags:
|
|
/// - `<p>`
|
|
/// - `<pre>`
|
|
/// - `<em>`
|
|
/// - `<ul>` and `<li>`
|
|
/// - `<a>`
|
|
/// - `<code>`
|
|
fn writeMarkdownFromHtml(html: []const u8, writer: anytype) !void {
|
|
return writeMarkdownFromHtmlInternal(html, false, 0, writer);
|
|
}
|
|
|
|
/// this is kind of a hacky solution. A cleaner solution would be to implement using a xml/html parser.
|
|
fn writeMarkdownFromHtmlInternal(html: []const u8, single_line: bool, depth: u32, writer: anytype) !void {
|
|
var index: usize = 0;
|
|
while (std.mem.indexOfScalarPos(u8, html, index, '<')) |tag_start_index| {
|
|
const tags: []const []const u8 = &.{ "pre", "p", "em", "ul", "li", "a", "code" };
|
|
const opening_tags: []const []const u8 = &.{ "<pre>", "<p>", "<em>", "<ul>", "<li>", "<a>", "<code>" };
|
|
const closing_tags: []const []const u8 = &.{ "</pre>", "</p>", "</em>", "</ul>", "</li>", "</a>", "</code>" };
|
|
const tag_index = for (tags, 0..) |tag_name, i| {
|
|
if (std.mem.startsWith(u8, html[tag_start_index + 1 ..], tag_name)) break i;
|
|
} else {
|
|
index += 1;
|
|
continue;
|
|
};
|
|
|
|
try writeLine(html[index..tag_start_index], single_line, writer);
|
|
|
|
const tag_name = tags[tag_index];
|
|
const opening_tag_name = opening_tags[tag_index];
|
|
const closing_tag_name = closing_tags[tag_index];
|
|
|
|
// std.debug.print("tag: '{s}'\n", .{tag_name});
|
|
|
|
const content_start = 1 + (std.mem.indexOfScalarPos(u8, html, tag_start_index + 1 + tag_name.len, '>') orelse return error.InvalidTag);
|
|
|
|
index = content_start;
|
|
const content_end = while (std.mem.indexOfScalarPos(u8, html, index, '<')) |end| {
|
|
if (std.mem.startsWith(u8, html[end..], closing_tag_name)) break end;
|
|
if (std.mem.startsWith(u8, html[end..], opening_tag_name)) {
|
|
index = std.mem.indexOfPos(u8, html, end + opening_tag_name.len, closing_tag_name) orelse return error.MissingEndTag;
|
|
index += closing_tag_name.len;
|
|
continue;
|
|
}
|
|
index += 1;
|
|
} else html.len;
|
|
|
|
const content = html[content_start..content_end];
|
|
index = @min(html.len, content_end + closing_tag_name.len);
|
|
// std.debug.print("content: {s}\n", .{content});
|
|
|
|
if (std.mem.eql(u8, tag_name, "p")) {
|
|
try writeMarkdownFromHtmlInternal(content, true, depth, writer);
|
|
try writer.writeByte('\n');
|
|
} else if (std.mem.eql(u8, tag_name, "pre")) {
|
|
try writeMarkdownFromHtmlInternal(content, false, depth, writer);
|
|
} else if (std.mem.eql(u8, tag_name, "em")) {
|
|
try writer.print("**{s}** ", .{content});
|
|
} else if (std.mem.eql(u8, tag_name, "ul")) {
|
|
try writeMarkdownFromHtmlInternal(content, false, depth + 1, writer);
|
|
} else if (std.mem.eql(u8, tag_name, "li")) {
|
|
try writer.writeByteNTimes(' ', 1 + (depth -| 1) * 2);
|
|
try writer.writeAll("- ");
|
|
try writeMarkdownFromHtmlInternal(content, true, depth, writer);
|
|
} else if (std.mem.eql(u8, tag_name, "a")) {
|
|
const href_part = std.mem.trimLeft(u8, html[tag_start_index + 2 .. content_start - 1], " ");
|
|
std.debug.assert(std.mem.startsWith(u8, href_part, "href=\""));
|
|
std.debug.assert(href_part[href_part.len - 1] == '\"');
|
|
const url = href_part["href=\"".len .. href_part.len - 1];
|
|
try writer.print("[{s}]({s})", .{ content, std.mem.trimLeft(u8, url, "@") });
|
|
} else if (std.mem.eql(u8, tag_name, "code")) {
|
|
try writeMarkdownCode(content, "zig", writer);
|
|
} else return error.UnsupportedTag;
|
|
}
|
|
|
|
try writeLine(html[index..], single_line, writer);
|
|
}
|
|
|
|
/// takes in a signature like this: `@intToEnum(comptime DestType: type, integer: anytype) DestType`
|
|
/// and outputs its arguments: `comptime DestType: type`, `integer: anytype`
|
|
fn extractArgumentsFromSignature(allocator: std.mem.Allocator, signature: []const u8) error{OutOfMemory}![][]const u8 {
|
|
var arguments = std.ArrayListUnmanaged([]const u8){};
|
|
defer arguments.deinit(allocator);
|
|
|
|
var argument_start: usize = 0;
|
|
var index: usize = 0;
|
|
while (std.mem.indexOfAnyPos(u8, signature, index, ",()")) |token_index| {
|
|
if (signature[token_index] == '(') {
|
|
argument_start = index;
|
|
index = 1 + std.mem.indexOfScalarPos(u8, signature, token_index + 1, ')').?;
|
|
continue;
|
|
}
|
|
const argument = std.mem.trim(u8, signature[argument_start..token_index], &std.ascii.whitespace);
|
|
if (argument.len != 0) try arguments.append(allocator, argument);
|
|
if (signature[token_index] == ')') break;
|
|
argument_start = token_index + 1;
|
|
index = token_index + 1;
|
|
}
|
|
|
|
return arguments.toOwnedSlice(allocator);
|
|
}
|
|
|
|
/// takes in a signature like this: `@intToEnum(comptime DestType: type, integer: anytype) DestType`
|
|
/// and outputs a snippet: `@intToEnum(${1:comptime DestType: type}, ${2:integer: anytype})`
|
|
fn extractSnippetFromSignature(allocator: std.mem.Allocator, signature: []const u8) error{OutOfMemory}![]const u8 {
|
|
var snippet = std.ArrayListUnmanaged(u8){};
|
|
defer snippet.deinit(allocator);
|
|
var writer = snippet.writer(allocator);
|
|
|
|
const start_index = 1 + std.mem.indexOfScalar(u8, signature, '(').?;
|
|
try writer.writeAll(signature[0..start_index]);
|
|
|
|
var argument_start: usize = start_index;
|
|
var index: usize = start_index;
|
|
var i: u32 = 1;
|
|
while (std.mem.indexOfAnyPos(u8, signature, index, ",()")) |token_index| {
|
|
if (signature[token_index] == '(') {
|
|
argument_start = index;
|
|
index = 1 + std.mem.indexOfScalarPos(u8, signature, token_index + 1, ')').?;
|
|
continue;
|
|
}
|
|
const argument = std.mem.trim(u8, signature[argument_start..token_index], &std.ascii.whitespace);
|
|
if (argument.len != 0) {
|
|
if (i != 1) try writer.writeAll(", ");
|
|
try writer.print("${{{d}:{s}}}", .{ i, argument });
|
|
}
|
|
if (signature[token_index] == ')') break;
|
|
argument_start = token_index + 1;
|
|
index = token_index + 1;
|
|
i += 1;
|
|
}
|
|
try writer.writeByte(')');
|
|
|
|
return snippet.toOwnedSlice(allocator);
|
|
}
|
|
|
|
/// Generates data files from the Zig language Reference (https://ziglang.org/documentation/master/)
|
|
/// An output example would `zls/src/master.zig`
|
|
fn generateVersionDataFile(allocator: std.mem.Allocator, version: []const u8, path: []const u8) !void {
|
|
const url = try std.fmt.allocPrint(allocator, "https://raw.githubusercontent.com/ziglang/zig/{s}/doc/langref.html.in", .{version});
|
|
defer allocator.free(url);
|
|
|
|
const response = try httpGET(allocator, try std.Uri.parse(url));
|
|
const response_bytes = switch (response) {
|
|
.success => |response_bytes| response_bytes,
|
|
.other => |status| {
|
|
const error_name = status.phrase() orelse @tagName(status.class());
|
|
std.log.err("failed to download {s}: {s}", .{ url, error_name });
|
|
return error.DownloadFailed;
|
|
},
|
|
};
|
|
defer allocator.free(response_bytes);
|
|
|
|
// const response_bytes: []const u8 = @embedFile("langref.html.in");
|
|
|
|
var builtins = try collectBuiltinData(allocator, version, response_bytes);
|
|
defer {
|
|
for (builtins) |*builtin| {
|
|
builtin.documentation.deinit(allocator);
|
|
}
|
|
allocator.free(builtins);
|
|
}
|
|
|
|
var builtin_file = try std.fs.createFileAbsolute(path, .{});
|
|
defer builtin_file.close();
|
|
|
|
var buffered_writer = std.io.bufferedWriter(builtin_file.writer());
|
|
var writer = buffered_writer.writer();
|
|
|
|
try writer.print(
|
|
\\//! DO NOT EDIT
|
|
\\//! If you want to update this file run:
|
|
\\//! `zig build gen -- --generate-version-data {s}` (requires an internet connection)
|
|
\\//! GENERATED BY src/config_gen/config_gen.zig
|
|
\\
|
|
\\const Builtin = struct {{
|
|
\\ name: []const u8,
|
|
\\ signature: []const u8,
|
|
\\ snippet: []const u8,
|
|
\\ documentation: []const u8,
|
|
\\ arguments: []const []const u8,
|
|
\\}};
|
|
\\
|
|
\\pub const builtins = [_]Builtin{{
|
|
\\
|
|
, .{version});
|
|
|
|
for (builtins) |builtin| {
|
|
const signature = try std.mem.replaceOwned(u8, allocator, builtin.signature, "\n", "");
|
|
defer allocator.free(signature);
|
|
|
|
const snippet = try extractSnippetFromSignature(allocator, signature);
|
|
defer allocator.free(snippet);
|
|
|
|
var arguments = try extractArgumentsFromSignature(allocator, signature[builtin.name.len + 1 ..]);
|
|
defer allocator.free(arguments);
|
|
|
|
try writer.print(
|
|
\\ .{{
|
|
\\ .name = "{}",
|
|
\\ .signature = "{}",
|
|
\\ .snippet = "{}",
|
|
\\
|
|
, .{
|
|
std.zig.fmtEscapes(builtin.name),
|
|
std.zig.fmtEscapes(signature),
|
|
std.zig.fmtEscapes(snippet),
|
|
});
|
|
|
|
const html = builtin.documentation.items["</pre>".len..];
|
|
var markdown = std.ArrayListUnmanaged(u8){};
|
|
defer markdown.deinit(allocator);
|
|
try writeMarkdownFromHtml(html, markdown.writer(allocator));
|
|
|
|
try writer.writeAll(" .documentation =\n");
|
|
var line_it = std.mem.split(u8, std.mem.trim(u8, markdown.items, "\n"), "\n");
|
|
while (line_it.next()) |line| {
|
|
try writer.print(" \\\\{s}\n", .{std.mem.trimRight(u8, line, " ")});
|
|
}
|
|
|
|
try writer.writeAll(
|
|
\\ ,
|
|
\\ .arguments = &.{
|
|
);
|
|
|
|
if (arguments.len != 0) {
|
|
try writer.writeByte('\n');
|
|
for (arguments) |arg| {
|
|
try writer.print(" \"{}\",\n", .{std.zig.fmtEscapes(arg)});
|
|
}
|
|
try writer.writeByteNTimes(' ', 8);
|
|
}
|
|
|
|
try writer.writeAll(
|
|
\\},
|
|
\\ },
|
|
\\
|
|
);
|
|
}
|
|
|
|
try writer.writeAll(
|
|
\\};
|
|
\\
|
|
\\// DO NOT EDIT
|
|
\\
|
|
);
|
|
try buffered_writer.flush();
|
|
}
|
|
|
|
const Response = union(enum) {
|
|
success: []const u8,
|
|
other: std.http.Status,
|
|
};
|
|
|
|
fn httpGET(allocator: std.mem.Allocator, uri: std.Uri) !Response {
|
|
var client = std.http.Client{ .allocator = allocator };
|
|
defer client.deinit();
|
|
try client.ca_bundle.rescan(allocator);
|
|
|
|
var request = try client.request(.GET, uri, .{ .allocator = allocator }, .{});
|
|
defer request.deinit();
|
|
|
|
try request.start();
|
|
// try request.finish();
|
|
try request.wait();
|
|
|
|
if (request.response.status.class() != .success) {
|
|
return .{
|
|
.other = request.response.status,
|
|
};
|
|
}
|
|
|
|
return .{
|
|
.success = try request.reader().readAllAlloc(allocator, std.math.maxInt(usize)),
|
|
};
|
|
}
|
|
|
|
pub fn main() !void {
|
|
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer std.debug.assert(general_purpose_allocator.deinit() == .ok);
|
|
var gpa = general_purpose_allocator.allocator();
|
|
|
|
var stderr = std.io.getStdErr().writer();
|
|
|
|
var args_it = try std.process.argsWithAllocator(gpa);
|
|
defer args_it.deinit();
|
|
|
|
_ = args_it.next() orelse @panic("");
|
|
const config_path = args_it.next() orelse @panic("first argument must be path to Config.zig");
|
|
const schema_path = args_it.next() orelse @panic("second argument must be path to schema.json");
|
|
const readme_path = args_it.next() orelse @panic("third argument must be path to README.md");
|
|
const data_path = args_it.next() orelse @panic("fourth argument must be path to data directory");
|
|
|
|
var maybe_vscode_config_path: ?[]const u8 = null;
|
|
var maybe_data_file_version: ?[]const u8 = null;
|
|
var maybe_data_file_path: ?[]const u8 = null;
|
|
|
|
while (args_it.next()) |argname| {
|
|
if (std.mem.eql(u8, argname, "--help")) {
|
|
try stderr.writeAll(
|
|
\\ Usage: zig build gen -- [command]
|
|
\\
|
|
\\ Commands:
|
|
\\
|
|
\\ --help Prints this message
|
|
\\ --vscode-config-path [path] Output zls-vscode configurations
|
|
\\ --generate-version-data [version] Output version data file (see src/data/master.zig)
|
|
\\ --generate-version-data-path [path] Override default data file path (default: src/data/*.zig)
|
|
\\
|
|
);
|
|
} else if (std.mem.eql(u8, argname, "--vscode-config-path")) {
|
|
const vscode_config_path = args_it.next() orelse {
|
|
try stderr.print("Expected output path after --vscode-config-path argument.\n", .{});
|
|
return;
|
|
};
|
|
if (!std.fs.path.isAbsolute(vscode_config_path)) {
|
|
try stderr.print("Expected absolute path after --vscode-config-path but got `{s}`", .{vscode_config_path});
|
|
return;
|
|
}
|
|
maybe_vscode_config_path = vscode_config_path;
|
|
} else if (std.mem.eql(u8, argname, "--generate-version-data")) {
|
|
maybe_data_file_version = args_it.next() orelse {
|
|
try stderr.print("Expected version after --generate-version-data argument.\n", .{});
|
|
return;
|
|
};
|
|
const is_valid_version = blk: {
|
|
if (std.mem.eql(u8, maybe_data_file_version.?, "master")) break :blk true;
|
|
_ = std.SemanticVersion.parse(maybe_data_file_version.?) catch break :blk false;
|
|
break :blk true;
|
|
};
|
|
if (!is_valid_version) {
|
|
try stderr.print("'{s}' is not a valid argument after --generate-version-data.\n", .{maybe_data_file_version.?});
|
|
return;
|
|
}
|
|
} else if (std.mem.eql(u8, argname, "--generate-version-data-path")) {
|
|
maybe_data_file_path = args_it.next() orelse {
|
|
try stderr.print("Expected output path after --generate-version-data-path argument.\n", .{});
|
|
return;
|
|
};
|
|
} else {
|
|
try stderr.print("Unrecognized argument '{s}'.\n", .{argname});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const config = try std.json.parseFromSlice(Config, gpa, @embedFile("config.json"), .{});
|
|
defer std.json.parseFree(Config, gpa, config);
|
|
|
|
try generateConfigFile(gpa, config, config_path);
|
|
try generateSchemaFile(gpa, config, schema_path);
|
|
try updateREADMEFile(gpa, config, readme_path);
|
|
|
|
if (maybe_vscode_config_path) |vscode_config_path| {
|
|
try generateVSCodeConfigFile(gpa, config, vscode_config_path);
|
|
}
|
|
|
|
if (maybe_data_file_version) |data_version| {
|
|
const path = if (maybe_data_file_path) |path| path else blk: {
|
|
const file_name = try std.fmt.allocPrint(gpa, "{s}.zig", .{data_version});
|
|
defer gpa.free(file_name);
|
|
break :blk try std.fs.path.join(gpa, &.{ data_path, file_name });
|
|
};
|
|
defer if (maybe_data_file_path == null) gpa.free(path);
|
|
|
|
try generateVersionDataFile(gpa, data_version, path);
|
|
}
|
|
|
|
if (zig_builtin.os.tag == .windows) {
|
|
std.log.warn("Running on windows may result in CRLF and LF mismatch", .{});
|
|
}
|
|
|
|
try stderr.writeAll(
|
|
\\Changing configuration options may also require editing the `package.json` from zls-vscode at https://github.com/zigtools/zls-vscode/blob/master/package.json
|
|
\\You can use `zig build gen -- --vscode-config-path /path/to/output/file.json` to generate the new configuration properties which you can then copy into `package.json`
|
|
\\
|
|
);
|
|
}
|