From 4aaa6f0772e0cce0d63b95eab837b198f5ae063e Mon Sep 17 00:00:00 2001 From: Qiao Wang Date: Fri, 3 Apr 2026 10:48:57 +0800 Subject: [PATCH] Add CSharp support --- src/explore.zig | 228 +++++++++++++++++++++++++++++++++++++++++++++++- src/style.zig | 1 + src/tests.zig | 68 +++++++++++++++ 3 files changed, 296 insertions(+), 1 deletion(-) diff --git a/src/explore.zig b/src/explore.zig index a5cc186..268a936 100644 --- a/src/explore.zig +++ b/src/explore.zig @@ -74,6 +74,7 @@ pub const Language = enum(u8) { rust, go_lang, php, + csharp, markdown, json, yaml, @@ -90,6 +91,7 @@ pub fn detectLanguage(path: []const u8) Language { if (std.mem.endsWith(u8, path, ".rs")) return .rust; if (std.mem.endsWith(u8, path, ".go")) return .go_lang; if (std.mem.endsWith(u8, path, ".php")) return .php; + if (std.mem.endsWith(u8, path, ".cs")) return .csharp; if (std.mem.endsWith(u8, path, ".md")) return .markdown; if (std.mem.endsWith(u8, path, ".json")) return .json; if (std.mem.endsWith(u8, path, ".yaml") or std.mem.endsWith(u8, path, ".yml")) return .yaml; @@ -195,6 +197,8 @@ fn indexFileInner(self: *Explorer, path: []const u8, content: []const u8, full_i try self.parseRustLine(trimmed, line_num, &outline, prev_line_trimmed); } else if (outline.language == .php) { try self.parsePhpLine(trimmed, line_num, &outline, &php_state); + } else if (outline.language == .csharp) { + try self.parseCSharpLine(trimmed, line_num, &outline); } prev_line_trimmed = trimmed; @@ -1264,6 +1268,228 @@ pub fn getHotFiles(self: *Explorer, store: *Store, allocator: std.mem.Allocator, return null; } + fn parseCSharpLine(self: *Explorer, line: []const u8, line_num: u32, outline: *FileOutline) !void { + const a = self.allocator; + + if (startsWith(line, "using ")) { + const symbol_copy = try a.dupe(u8, line); + errdefer a.free(symbol_copy); + try outline.symbols.append(a, .{ + .name = symbol_copy, + .kind = .import, + .line_start = line_num, + .line_end = line_num, + }); + const import_copy = try a.dupe(u8, line); + errdefer a.free(import_copy); + try outline.imports.append(a, import_copy); + return; + } + + if (startsWith(line, "namespace ")) { + if (extractIdent(line[10..])) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .import, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + return; + } + + const stripped = stripCSharpModifiers(line); + + if (startsWith(stripped, "class ")) { + if (extractIdent(stripped[6..])) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .struct_def, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + return; + } + + if (startsWith(stripped, "interface ")) { + if (extractIdent(stripped[10..])) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .trait_def, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + return; + } + + if (startsWith(stripped, "struct ")) { + if (extractIdent(stripped[7..])) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .struct_def, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + return; + } + + if (startsWith(stripped, "enum ")) { + if (extractIdent(stripped[5..])) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .enum_def, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + return; + } + + if (startsWith(stripped, "delegate ")) { + if (std.mem.indexOf(u8, stripped, "(")) |paren| { + const before_paren = std.mem.trimRight(u8, stripped[9..paren], " \t"); + if (lastIdent(before_paren)) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .type_alias, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + } + return; + } + + if (startsWith(stripped, "const ")) { + const after = stripped[6..]; + if (std.mem.indexOf(u8, after, "=")) |eq| { + const before_eq = std.mem.trimRight(u8, after[0..eq], " \t"); + if (lastIdent(before_eq)) |name| { + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .constant, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + } + return; + } + + if (std.mem.indexOf(u8, stripped, "(")) |paren| { + if (paren > 0) { + const control = [_][]const u8{ "if ", "if(", "else ", "for ", "for(", "foreach ", "foreach(", "while ", "while(", "switch ", "switch(", "catch ", "catch(", "return ", "return(", "new ", "throw ", "lock ", "lock(" }; + for (control) |kw| { + if (startsWith(stripped, kw)) return; + } + const before_paren = std.mem.trimRight(u8, stripped[0..paren], " \t"); + if (lastIdent(before_paren)) |name| { + if (std.mem.eql(u8, name, "base") or std.mem.eql(u8, name, "this") or std.mem.eql(u8, name, "value")) return; + const name_copy = try a.dupe(u8, name); + errdefer a.free(name_copy); + const detail_copy = try a.dupe(u8, line); + errdefer a.free(detail_copy); + try outline.symbols.append(a, .{ + .name = name_copy, + .kind = .method, + .line_start = line_num, + .line_end = line_num, + .detail = detail_copy, + }); + } + } + } + } + + fn stripCSharpModifiers(line: []const u8) []const u8 { + const modifiers = [_][]const u8{ + "public ", "private ", "protected ", "internal ", + "static ", "abstract ", "virtual ", "override ", + "sealed ", "async ", "partial ", "readonly ", + "new ", "extern ", "unsafe ", "volatile ", + }; + var result = line; + var changed = true; + while (changed) { + changed = false; + for (modifiers) |mod| { + if (std.mem.startsWith(u8, result, mod)) { + result = result[mod.len..]; + changed = true; + } + } + } + return result; + } + + fn lastIdent(s: []const u8) ?[]const u8 { + if (s.len == 0) return null; + var end = s.len; + if (s[end - 1] == '>') { + var depth: u32 = 0; + var i = end; + while (i > 0) : (i -= 1) { + if (s[i - 1] == '>') depth += 1; + if (s[i - 1] == '<') { + depth -= 1; + if (depth == 0) { + end = i - 1; + break; + } + } + } + } + while (end > 0 and s[end - 1] == ']') { + if (end >= 2 and s[end - 2] == '[') { + end -= 2; + } else break; + } + var ident_end = end; + while (ident_end > 0 and (std.ascii.isAlphanumeric(s[ident_end - 1]) or s[ident_end - 1] == '_')) { + ident_end -= 1; + } + if (ident_end == end) return null; + return s[ident_end..end]; + } + fn rebuildDepsFor(self: *Explorer, path: []const u8, outline: *FileOutline) !void { var deps: std.ArrayList([]const u8) = .{}; errdefer deps.deinit(self.allocator); @@ -1499,7 +1725,7 @@ pub fn isCommentOrBlank(line: []const u8, language: Language) bool { const trimmed = std.mem.trim(u8, line, " \t"); if (trimmed.len == 0) return true; return switch (language) { - .zig, .rust, .go_lang => std.mem.startsWith(u8, trimmed, "//"), + .zig, .rust, .go_lang, .csharp => std.mem.startsWith(u8, trimmed, "//"), .python => std.mem.startsWith(u8, trimmed, "#"), .javascript, .typescript, .c, .cpp => std.mem.startsWith(u8, trimmed, "//") or std.mem.startsWith(u8, trimmed, "/*") or std.mem.startsWith(u8, trimmed, "*"), else => false, diff --git a/src/style.zig b/src/style.zig index 35b9637..9c2bd7e 100644 --- a/src/style.zig +++ b/src/style.zig @@ -23,6 +23,7 @@ pub const Style = struct { if (std.mem.eql(u8, lang, "rust")) return self.orange; if (std.mem.eql(u8, lang, "python")) return self.blue; if (std.mem.eql(u8, lang, "c") or std.mem.eql(u8, lang, "cpp")) return self.blue; + if (std.mem.eql(u8, lang, "csharp")) return self.magenta; if (std.mem.eql(u8, lang, "markdown")) return self.dim; if (std.mem.eql(u8, lang, "json") or std.mem.eql(u8, lang, "yaml")) return self.dim; return self.dim; // unknown diff --git a/src/tests.zig b/src/tests.zig index 163bc25..acb00f9 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -879,6 +879,73 @@ test "explorer: typescript parser" { try testing.expect(outline.symbols.items.len >= 3); } +test "explorer: csharp parser" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + var explorer = Explorer.init(arena.allocator()); + + try explorer.indexFile("Program.cs", + \\using System; + \\using System.Collections.Generic; + \\namespace MyApp + \\{ + \\ public class Program + \\ { + \\ public static void Main(string[] args) + \\ { + \\ } + \\ private int Calculate(int x) + \\ { + \\ return x * 2; + \\ } + \\ } + \\ public interface IService + \\ { + \\ void Execute(); + \\ } + \\ public enum Status + \\ { + \\ Active, + \\ Inactive, + \\ } + \\ public struct Point + \\ { + \\ public int X; + \\ public int Y; + \\ } + \\} + ); + + var outline = (try explorer.getOutline("Program.cs", testing.allocator)) orelse return error.TestUnexpectedResult; + defer outline.deinit(); + try testing.expect(outline.language == .csharp); + // using System, using System.Collections.Generic, namespace MyApp, class Program, + // Main, Calculate, interface IService, Execute, enum Status, struct Point + try testing.expect(outline.symbols.items.len >= 10); + + // Verify specific symbol kinds + var has_class = false; + var has_method = false; + var has_interface = false; + var has_enum = false; + var has_struct = false; + var has_using = false; + for (outline.symbols.items) |sym| { + if (sym.kind == .struct_def and std.mem.eql(u8, sym.name, "Program")) has_class = true; + if (sym.kind == .method and std.mem.eql(u8, sym.name, "Main")) has_method = true; + if (sym.kind == .trait_def and std.mem.eql(u8, sym.name, "IService")) has_interface = true; + if (sym.kind == .enum_def and std.mem.eql(u8, sym.name, "Status")) has_enum = true; + if (sym.kind == .struct_def and std.mem.eql(u8, sym.name, "Point")) has_struct = true; + if (sym.kind == .import) has_using = true; + } + try testing.expect(has_class); + try testing.expect(has_method); + try testing.expect(has_interface); + try testing.expect(has_enum); + try testing.expect(has_struct); + try testing.expect(has_using); +} + // ── Version tests ─────────────────────────────────────────── test "file versions: append and latest" { @@ -2020,6 +2087,7 @@ test "detectLanguage: all supported extensions" { try testing.expect(explore.detectLanguage("comp.tsx") == .typescript); try testing.expect(explore.detectLanguage("main.rs") == .rust); try testing.expect(explore.detectLanguage("main.go") == .go_lang); + try testing.expect(explore.detectLanguage("Program.cs") == .csharp); try testing.expect(explore.detectLanguage("README.md") == .markdown); try testing.expect(explore.detectLanguage("pkg.json") == .json); try testing.expect(explore.detectLanguage("config.yaml") == .yaml);