Add source-patched syntax profiles
This commit is contained in:
@@ -11,6 +11,7 @@ const repo = @import("repo.zig");
|
|||||||
const session = @import("session.zig");
|
const session = @import("session.zig");
|
||||||
const socket = @import("socket.zig");
|
const socket = @import("socket.zig");
|
||||||
const symbol = @import("symbol.zig");
|
const symbol = @import("symbol.zig");
|
||||||
|
const syntax = @import("syntax.zig");
|
||||||
const tui = @import("tui.zig");
|
const tui = @import("tui.zig");
|
||||||
|
|
||||||
pub const version = "0.1.0-dev";
|
pub const version = "0.1.0-dev";
|
||||||
@@ -136,6 +137,7 @@ test {
|
|||||||
_ = session;
|
_ = session;
|
||||||
_ = socket;
|
_ = socket;
|
||||||
_ = symbol;
|
_ = symbol;
|
||||||
|
_ = syntax;
|
||||||
_ = tui;
|
_ = tui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Source-patched syntax profile registry for v1 coding feedback.
|
||||||
|
// req: coding/002, coding/003, ui/002, governance/002
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = spansAlloc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
InvalidLanguage,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Class = enum {
|
||||||
|
keyword,
|
||||||
|
builtin,
|
||||||
|
string,
|
||||||
|
comment,
|
||||||
|
number,
|
||||||
|
|
||||||
|
pub fn text(self: Class) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.keyword => "keyword",
|
||||||
|
.builtin => "builtin",
|
||||||
|
.string => "string",
|
||||||
|
.comment => "comment",
|
||||||
|
.number => "number",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Span = struct {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
class: Class,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Profile = struct {
|
||||||
|
language: []const u8,
|
||||||
|
source_patched: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn profile(language: []const u8) ?Profile {
|
||||||
|
if (std.mem.eql(u8, language, "zig")) return .{ .language = "zig", .source_patched = true };
|
||||||
|
if (std.mem.eql(u8, language, "text")) return .{ .language = "text", .source_patched = false };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spansAlloc(allocator: std.mem.Allocator, language: []const u8, source: []const u8) ![]Span {
|
||||||
|
const resolved = profile(language) orelse return Error.InvalidLanguage;
|
||||||
|
if (!resolved.source_patched) return allocator.alloc(Span, 0);
|
||||||
|
var spans = std.ArrayList(Span).empty;
|
||||||
|
errdefer spans.deinit(allocator);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < source.len) {
|
||||||
|
if (source[i] == '/' and i + 1 < source.len and source[i + 1] == '/') {
|
||||||
|
const start = i;
|
||||||
|
i += 2;
|
||||||
|
while (i < source.len and source[i] != '\n') : (i += 1) {}
|
||||||
|
try spans.append(allocator, .{ .start = start, .end = i, .class = .comment });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (source[i] == '"') {
|
||||||
|
const start = i;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.len) : (i += 1) {
|
||||||
|
if (source[i] == '\\') {
|
||||||
|
i += @intFromBool(i + 1 < source.len);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (source[i] == '"') {
|
||||||
|
i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try spans.append(allocator, .{ .start = start, .end = i, .class = .string });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isDigit(source[i])) {
|
||||||
|
const start = i;
|
||||||
|
while (i < source.len and isDigit(source[i])) : (i += 1) {}
|
||||||
|
try spans.append(allocator, .{ .start = start, .end = i, .class = .number });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isIdentStart(source[i])) {
|
||||||
|
const start = i;
|
||||||
|
i += 1;
|
||||||
|
while (i < source.len and isIdentContinue(source[i])) : (i += 1) {}
|
||||||
|
const token = source[start..i];
|
||||||
|
if (keywordClass(token)) |class| try spans.append(allocator, .{ .start = start, .end = i, .class = class });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return spans.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rowsAlloc(allocator: std.mem.Allocator, language: []const u8, source: []const u8) ![][]const u8 {
|
||||||
|
const spans = try spansAlloc(allocator, language, source);
|
||||||
|
defer allocator.free(spans);
|
||||||
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
const resolved = profile(language) orelse return Error.InvalidLanguage;
|
||||||
|
if (!resolved.source_patched) {
|
||||||
|
try rows.append(allocator, try allocator.dupe(u8, "syntax:text:fallback:no_spans"));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
if (spans.len == 0) {
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "syntax:{s}:no_spans", .{resolved.language}));
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
for (spans) |span| {
|
||||||
|
const preview = try previewAlloc(allocator, source[span.start..span.end]);
|
||||||
|
defer allocator.free(preview);
|
||||||
|
try rows.append(allocator, try std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"syntax:{s}:{s}:{d}:{d}:{s}",
|
||||||
|
.{ resolved.language, span.class.text(), span.start, span.end, preview },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keywordClass(token: []const u8) ?Class {
|
||||||
|
const keywords = [_][]const u8{ "pub", "const", "var", "fn", "return", "if", "else", "while", "for", "switch", "try", "catch", "defer", "test", "struct", "enum", "error", "union", "comptime", "inline", "break", "continue" };
|
||||||
|
for (keywords) |keyword| if (std.mem.eql(u8, token, keyword)) return .keyword;
|
||||||
|
const builtins = [_][]const u8{ "true", "false", "null", "undefined", "usize", "u8", "i32", "void", "bool", "anyerror" };
|
||||||
|
for (builtins) |builtin| if (std.mem.eql(u8, token, builtin)) return .builtin;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previewAlloc(allocator: std.mem.Allocator, token: []const u8) ![]u8 {
|
||||||
|
var out = std.ArrayList(u8).empty;
|
||||||
|
errdefer out.deinit(allocator);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < token.len and out.items.len < 48) {
|
||||||
|
const len = std.unicode.utf8ByteSequenceLength(token[i]) catch 1;
|
||||||
|
const end = @min(token.len, i + len);
|
||||||
|
const slice = token[i..end];
|
||||||
|
if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == ':' or slice[0] == '|')) {
|
||||||
|
try out.append(allocator, '_');
|
||||||
|
} else {
|
||||||
|
try out.appendSlice(allocator, slice);
|
||||||
|
}
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
if (out.items.len == 0) try out.append(allocator, '_');
|
||||||
|
return out.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isDigit(byte: u8) bool {
|
||||||
|
return byte >= '0' and byte <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isIdentStart(byte: u8) bool {
|
||||||
|
return (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or byte == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isIdentContinue(byte: u8) bool {
|
||||||
|
return isIdentStart(byte) or isDigit(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freeRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||||
|
for (rows) |row| allocator.free(row);
|
||||||
|
allocator.free(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: zig source profile produces deterministic highlight spans" {
|
||||||
|
const source = "pub const answer = 42; // ok\n";
|
||||||
|
const spans = try spansAlloc(std.testing.allocator, "zig", source);
|
||||||
|
defer std.testing.allocator.free(spans);
|
||||||
|
try std.testing.expectEqual(@as(usize, 4), spans.len);
|
||||||
|
try std.testing.expectEqualDeep(Span{ .start = 0, .end = 3, .class = .keyword }, spans[0]);
|
||||||
|
try std.testing.expectEqualDeep(Span{ .start = 4, .end = 9, .class = .keyword }, spans[1]);
|
||||||
|
try std.testing.expectEqualDeep(Span{ .start = 19, .end = 21, .class = .number }, spans[2]);
|
||||||
|
try std.testing.expectEqualDeep(Span{ .start = 23, .end = 28, .class = .comment }, spans[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: zig syntax rows include class byte ranges and previews" {
|
||||||
|
const rows = try rowsAlloc(std.testing.allocator, "zig", "fn main() void { return; }");
|
||||||
|
defer freeRows(std.testing.allocator, rows);
|
||||||
|
try std.testing.expectEqualStrings("syntax:zig:keyword:0:2:fn", rows[0]);
|
||||||
|
try std.testing.expectEqualStrings("syntax:zig:builtin:10:14:void", rows[1]);
|
||||||
|
try std.testing.expectEqualStrings("syntax:zig:keyword:17:23:return", rows[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: plain text falls back and unknown languages fail" {
|
||||||
|
const rows = try rowsAlloc(std.testing.allocator, "text", "pub const not highlighted");
|
||||||
|
defer freeRows(std.testing.allocator, rows);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), rows.len);
|
||||||
|
try std.testing.expectEqualStrings("syntax:text:fallback:no_spans", rows[0]);
|
||||||
|
try std.testing.expectError(Error.InvalidLanguage, rowsAlloc(std.testing.allocator, "python", "def x(): pass"));
|
||||||
|
}
|
||||||
+46
@@ -7,6 +7,7 @@ const replay = @import("replay.zig");
|
|||||||
const repo_mod = @import("repo.zig");
|
const repo_mod = @import("repo.zig");
|
||||||
const session_mod = @import("session.zig");
|
const session_mod = @import("session.zig");
|
||||||
const symbol_mod = @import("symbol.zig");
|
const symbol_mod = @import("symbol.zig");
|
||||||
|
const syntax_mod = @import("syntax.zig");
|
||||||
|
|
||||||
// First terminal thin client surface, scriptable for E2E-style tests.
|
// First terminal thin client surface, scriptable for E2E-style tests.
|
||||||
// req: session/001, session/003, ui/001, coding/001, testing/001, testing/002
|
// req: session/001, session/003, ui/001, coding/001, testing/001, testing/002
|
||||||
@@ -146,6 +147,7 @@ pub const Client = struct {
|
|||||||
if (std.mem.eql(u8, line, "terminal_status")) return self.openStaticJobRow("terminal-status", "terminal_status:idle");
|
if (std.mem.eql(u8, line, "terminal_status")) return self.openStaticJobRow("terminal-status", "terminal_status:idle");
|
||||||
if (std.mem.eql(u8, line, "terminal_cancel")) return self.openStaticJobRow("terminal-status", "terminal_cancel:no_running_terminal");
|
if (std.mem.eql(u8, line, "terminal_cancel")) return self.openStaticJobRow("terminal-status", "terminal_cancel:no_running_terminal");
|
||||||
if (std.mem.eql(u8, line, "terminal_exit")) return self.closeTerminalPanel();
|
if (std.mem.eql(u8, line, "terminal_exit")) return self.closeTerminalPanel();
|
||||||
|
if (std.mem.startsWith(u8, line, "syntax_spans ")) return self.openSyntaxSpans(line[13..]);
|
||||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||||
@@ -406,6 +408,14 @@ pub const Client = struct {
|
|||||||
self.message = null;
|
self.message = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn openSyntaxSpans(self: *Client, language: []const u8) !void {
|
||||||
|
const snap = try self.session.snapshot();
|
||||||
|
const rows = syntax_mod.rowsAlloc(self.allocator, language, snap.bytes) catch return Error.ProtocolRejected;
|
||||||
|
defer freeOwnedRows(self.allocator, rows);
|
||||||
|
try self.session.openListPanel("syntax-spans", rows);
|
||||||
|
self.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
fn openStaticJobRow(self: *Client, title: []const u8, row: []const u8) !void {
|
fn openStaticJobRow(self: *Client, title: []const u8, row: []const u8) !void {
|
||||||
try self.session.openListPanel(title, &.{row});
|
try self.session.openListPanel(title, &.{row});
|
||||||
self.message = null;
|
self.message = null;
|
||||||
@@ -1459,3 +1469,39 @@ test "adversarial: terminal command rejection does not corrupt editor" {
|
|||||||
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal:status:exit_127") != null);
|
try std.testing.expect(std.mem.indexOf(u8, frame, "terminal:status:exit_127") != null);
|
||||||
try std.testing.expect(std.mem.indexOf(u8, frame, "stderr:") != null);
|
try std.testing.expect(std.mem.indexOf(u8, frame, "stderr:") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: syntax spans panel renders zig highlight classes" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 7 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open pub const answer = 42; // ok");
|
||||||
|
try client.handleTraceLine("syntax_spans zig");
|
||||||
|
const frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax-spans") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax:zig:keyword:0:3:pub") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax:zig:keyword:4:9:const") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax:zig:number:") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax:zig:comment:") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: syntax text fallback renders no spans row" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 5 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open pub_const_plain_text");
|
||||||
|
try client.handleTraceLine("syntax_spans text");
|
||||||
|
const frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "syntax:text:fallback:no_spans") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: unsupported syntax language is rejected without changing buffer" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 5 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open safe");
|
||||||
|
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("syntax_spans python"));
|
||||||
|
const snap = try client.session.snapshot();
|
||||||
|
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user