diff --git a/src/Document.zig b/src/Document.zig index da6bccb..4864316 100644 --- a/src/Document.zig +++ b/src/Document.zig @@ -24,7 +24,7 @@ pub fn invalidate(self: *@This()) void { self.parse_tree = null; } -pub fn source(self: *@This()) []const u8 { +pub fn source(self: @This()) []const u8 { return self.contents.items; } @@ -64,20 +64,20 @@ pub fn utf8FromPosition(self: @This(), position: lsp.Position) u32 { return @intCast(i + codepoints.i); } -pub fn positionFromUtf8(self: @This(), offset: u32) lsp.Position { +pub fn positionFromUtf8(text: []const u8, offset: u32) lsp.Position { var line_breaks: u32 = 0; var line_start: usize = 0; - const text = self.contents.items[0..offset]; + const before = text[0..offset]; - for (text, 0..) |ch, index| { + for (before, 0..) |ch, index| { if (ch == '\n') { line_breaks += 1; line_start = index + 1; } } - const last_line = text[line_start..]; + const last_line = before[line_start..]; const character = std.unicode.calcUtf16LeLen(last_line) catch last_line.len; return .{ .line = line_breaks, .character = @intCast(character) }; @@ -86,7 +86,7 @@ pub fn positionFromUtf8(self: @This(), offset: u32) lsp.Position { pub fn wholeRange(self: @This()) lsp.Range { return .{ .start = .{ .line = 0, .character = 0 }, - .end = self.positionFromUtf8(@intCast(self.contents.items.len)), + .end = positionFromUtf8(self.source(), @intCast(self.contents.items.len)), }; } @@ -94,8 +94,8 @@ pub fn nodeRange(self: *@This(), node: u32) !lsp.Range { const parsed = try self.parseTree(); const span = parsed.tree.nodeSpan(node); return .{ - .start = self.positionFromUtf8(span.start), - .end = self.positionFromUtf8(span.end), + .start = positionFromUtf8(self.source(), span.start), + .end = positionFromUtf8(self.source(), span.end), }; } diff --git a/src/cli.zig b/src/cli.zig index 50521b9..1e14a83 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -3,10 +3,12 @@ const std = @import("std"); pub const NAME = "glsl_analyzer"; pub const Arguments = struct { + version: bool = false, channel: ChannelKind = .stdio, client_pid: ?c_int = null, dev_mode: ?[]const u8 = null, - version: bool = false, + parse_file: ?[]const u8 = null, + print_ast: bool = false, pub const ChannelKind = union(enum) { stdio: void, @@ -17,11 +19,16 @@ pub const Arguments = struct { "Usage: " ++ NAME ++ \\ [OPTIONS] \\ + \\LSP: + \\ --clientProcessId PID of the client process (used by LSP client). + \\ \\Options: - \\ --stdio Communicate over stdio [default] - \\ -p, --port Communicate over socket - \\ --clientProcessId PID of the client process - \\ --dev-mode Enable development mode + \\ -h, --help Print this message. + \\ --stdio Communicate over stdio. [default] + \\ -p, --port Communicate over socket. + \\ --dev-mode Enable development mode: redirects stderr to the given path. + \\ --parse-file Parses the given file, prints diagnostics, then exits. + \\ --print-ast Prints the parse tree. Only valid with --parse-file. \\ \\ ; @@ -42,6 +49,18 @@ pub const Arguments = struct { std.process.exit(1); } + const ValueParser = struct { + args: *std.process.ArgIterator, + option: []const u8, + value: ?[]const u8, + + pub fn get(self: *@This(), name: []const u8) []const u8 { + if (self.value) |value| return value; + if (self.args.next()) |value| return value; + fail("'{s}' expects an argument '{s}'", .{ self.option, name }); + } + }; + pub fn parse(allocator: std.mem.Allocator) !Arguments { var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); @@ -50,43 +69,56 @@ pub const Arguments = struct { var parsed = Arguments{}; while (args.next()) |arg| { - const name_end = std.mem.indexOfScalar(u8, arg, '=') orelse arg.len; - const name = arg[0..name_end]; - const extra_value = if (name_end == arg.len) null else arg[name_end + 1 ..]; + const option_end = std.mem.indexOfScalar(u8, arg, '=') orelse arg.len; + const option = arg[0..option_end]; - if (isAny(name, &.{ "--help", "-h" })) { + var value_parser = ValueParser{ + .args = &args, + .option = option, + .value = if (option_end == arg.len) null else arg[option_end + 1 ..], + }; + + if (isAny(option, &.{ "--help", "-h" })) { printHelp(); } - if (isAny(name, &.{ "--version", "-v" })) { + if (isAny(option, &.{ "--version", "-v" })) { printVersion(); } - if (isAny(name, &.{"--stdio"})) { + if (isAny(option, &.{"--stdio"})) { parsed.channel = .stdio; continue; } - if (isAny(name, &.{"--dev-mode"})) { - const path = extra_value orelse args.next() orelse - fail("{s}: expected a path", .{name}); + if (isAny(option, &.{"--dev-mode"})) { + const path = value_parser.get("PATH"); parsed.dev_mode = path; continue; } - if (isAny(name, &.{ "--port", "-p" })) { - const value = extra_value orelse args.next() orelse - fail("{s}: expected port number", .{name}); + if (isAny(option, &.{ "--port", "-p" })) { + const value = value_parser.get("PORT"); const port = std.fmt.parseInt(u16, value, 10) catch - fail("{s}: not a valid port number: {s}", .{ name, value }); + fail("{s}: not a valid port number: {s}", .{ option, value }); parsed.channel = .{ .socket = port }; continue; } - if (isAny(name, &.{"--clientProcessId"})) { - const value = extra_value orelse args.next() orelse fail("expected PID", .{}); + if (isAny(option, &.{"--clientProcessId"})) { + const value = value_parser.get("PID"); parsed.client_pid = std.fmt.parseInt(c_int, value, 10) catch - fail("{s}: not a valid PID: {s}", .{ name, value }); + fail("{s}: not a valid PID: {s}", .{ option, value }); + continue; + } + + if (isAny(option, &.{"--parse-file"})) { + parsed.parse_file = value_parser.get("PATH"); + continue; + } + + if (isAny(option, &.{"--print-ast"})) { + parsed.print_ast = true; continue; } diff --git a/src/main.zig b/src/main.zig index 0b9bd36..b6bc9cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -35,9 +35,7 @@ fn enableDevelopmentMode(stderr_target: []const u8) !void { } } -pub fn main() !void { - defer std.debug.print("exited\n", .{}); - +pub fn main() !u8 { var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 8 }){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); @@ -55,6 +53,39 @@ pub fn main() !void { } } + if (args.parse_file) |path| { + const source = std.fs.cwd().readFileAlloc(allocator, path, 1 << 30) catch |err| { + std.log.err("could not open '{s}': {s}", .{ path, @errorName(err) }); + return err; + }; + defer allocator.free(source); + + var diagnostics = std.ArrayList(parse.Diagnostic).init(allocator); + defer diagnostics.deinit(); + + var tree = try parse.parse(allocator, source, .{ .diagnostics = &diagnostics }); + defer tree.deinit(allocator); + + if (args.print_ast) { + var buffered_stdout = std.io.bufferedWriter(std.io.getStdOut().writer()); + try buffered_stdout.writer().print("{}", .{tree.format(source)}); + try buffered_stdout.flush(); + } + + if (diagnostics.items.len != 0) { + for (diagnostics.items) |diagnostic| { + const position = Workspace.Document.positionFromUtf8(source, diagnostic.span.start); + try std.io.getStdErr().writer().print( + "{s}:{}:{}: {s}\n", + .{ path, position.line + 1, position.character + 1, diagnostic.message }, + ); + } + return 1; + } + + return 0; + } + var channel: Channel = switch (args.channel) { .stdio => .{ .stdio = .{ .stdout = std.io.getStdOut(), @@ -148,6 +179,8 @@ pub fn main() !void { else => return err, }; } + + return 0; } fn logJsonError(err: []const u8, diagnostics: std.json.Diagnostics, bytes: []const u8) void {