Skip to content

Commit 1f8c095

Browse files
sanderdewijsunliftedqclaude
committed
feat(explore): add C# language support
Cherry-picked from justrach/codedb PR justrach#106 (unliftedq). Adds parseCSharpLine with support for using directives, namespaces, classes, interfaces, structs, enums, delegates, constants, and methods. Includes stripCSharpModifiers and lastIdent helpers. Co-Authored-By: unliftedq <unliftedq@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 679c3df commit 1f8c095

3 files changed

Lines changed: 293 additions & 1 deletion

File tree

src/explore.zig

Lines changed: 227 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pub const Language = enum(u8) {
7474
rust,
7575
go_lang,
7676
php,
77+
csharp,
7778
markdown,
7879
json,
7980
yaml,
@@ -90,6 +91,7 @@ pub fn detectLanguage(path: []const u8) Language {
9091
if (std.mem.endsWith(u8, path, ".rs")) return .rust;
9192
if (std.mem.endsWith(u8, path, ".go")) return .go_lang;
9293
if (std.mem.endsWith(u8, path, ".php")) return .php;
94+
if (std.mem.endsWith(u8, path, ".cs")) return .csharp;
9395
if (std.mem.endsWith(u8, path, ".md")) return .markdown;
9496
if (std.mem.endsWith(u8, path, ".json")) return .json;
9597
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
195197
try self.parseRustLine(trimmed, line_num, &outline, prev_line_trimmed);
196198
} else if (outline.language == .php) {
197199
try self.parsePhpLine(trimmed, line_num, &outline, &php_state);
200+
} else if (outline.language == .csharp) {
201+
try self.parseCSharpLine(trimmed, line_num, &outline);
198202
}
199203

200204
prev_line_trimmed = trimmed;
@@ -1200,6 +1204,228 @@ pub fn getHotFiles(self: *Explorer, store: *Store, allocator: std.mem.Allocator,
12001204
return null;
12011205
}
12021206

1207+
fn parseCSharpLine(self: *Explorer, line: []const u8, line_num: u32, outline: *FileOutline) !void {
1208+
const a = self.allocator;
1209+
1210+
if (startsWith(line, "using ")) {
1211+
const symbol_copy = try a.dupe(u8, line);
1212+
errdefer a.free(symbol_copy);
1213+
try outline.symbols.append(a, .{
1214+
.name = symbol_copy,
1215+
.kind = .import,
1216+
.line_start = line_num,
1217+
.line_end = line_num,
1218+
});
1219+
const import_copy = try a.dupe(u8, line);
1220+
errdefer a.free(import_copy);
1221+
try outline.imports.append(a, import_copy);
1222+
return;
1223+
}
1224+
1225+
if (startsWith(line, "namespace ")) {
1226+
if (extractIdent(line[10..])) |name| {
1227+
const name_copy = try a.dupe(u8, name);
1228+
errdefer a.free(name_copy);
1229+
const detail_copy = try a.dupe(u8, line);
1230+
errdefer a.free(detail_copy);
1231+
try outline.symbols.append(a, .{
1232+
.name = name_copy,
1233+
.kind = .import,
1234+
.line_start = line_num,
1235+
.line_end = line_num,
1236+
.detail = detail_copy,
1237+
});
1238+
}
1239+
return;
1240+
}
1241+
1242+
const stripped = stripCSharpModifiers(line);
1243+
1244+
if (startsWith(stripped, "class ")) {
1245+
if (extractIdent(stripped[6..])) |name| {
1246+
const name_copy = try a.dupe(u8, name);
1247+
errdefer a.free(name_copy);
1248+
const detail_copy = try a.dupe(u8, line);
1249+
errdefer a.free(detail_copy);
1250+
try outline.symbols.append(a, .{
1251+
.name = name_copy,
1252+
.kind = .struct_def,
1253+
.line_start = line_num,
1254+
.line_end = line_num,
1255+
.detail = detail_copy,
1256+
});
1257+
}
1258+
return;
1259+
}
1260+
1261+
if (startsWith(stripped, "interface ")) {
1262+
if (extractIdent(stripped[10..])) |name| {
1263+
const name_copy = try a.dupe(u8, name);
1264+
errdefer a.free(name_copy);
1265+
const detail_copy = try a.dupe(u8, line);
1266+
errdefer a.free(detail_copy);
1267+
try outline.symbols.append(a, .{
1268+
.name = name_copy,
1269+
.kind = .trait_def,
1270+
.line_start = line_num,
1271+
.line_end = line_num,
1272+
.detail = detail_copy,
1273+
});
1274+
}
1275+
return;
1276+
}
1277+
1278+
if (startsWith(stripped, "struct ")) {
1279+
if (extractIdent(stripped[7..])) |name| {
1280+
const name_copy = try a.dupe(u8, name);
1281+
errdefer a.free(name_copy);
1282+
const detail_copy = try a.dupe(u8, line);
1283+
errdefer a.free(detail_copy);
1284+
try outline.symbols.append(a, .{
1285+
.name = name_copy,
1286+
.kind = .struct_def,
1287+
.line_start = line_num,
1288+
.line_end = line_num,
1289+
.detail = detail_copy,
1290+
});
1291+
}
1292+
return;
1293+
}
1294+
1295+
if (startsWith(stripped, "enum ")) {
1296+
if (extractIdent(stripped[5..])) |name| {
1297+
const name_copy = try a.dupe(u8, name);
1298+
errdefer a.free(name_copy);
1299+
const detail_copy = try a.dupe(u8, line);
1300+
errdefer a.free(detail_copy);
1301+
try outline.symbols.append(a, .{
1302+
.name = name_copy,
1303+
.kind = .enum_def,
1304+
.line_start = line_num,
1305+
.line_end = line_num,
1306+
.detail = detail_copy,
1307+
});
1308+
}
1309+
return;
1310+
}
1311+
1312+
if (startsWith(stripped, "delegate ")) {
1313+
if (std.mem.indexOf(u8, stripped, "(")) |paren| {
1314+
const before_paren = std.mem.trimRight(u8, stripped[9..paren], " \t");
1315+
if (lastIdent(before_paren)) |name| {
1316+
const name_copy = try a.dupe(u8, name);
1317+
errdefer a.free(name_copy);
1318+
const detail_copy = try a.dupe(u8, line);
1319+
errdefer a.free(detail_copy);
1320+
try outline.symbols.append(a, .{
1321+
.name = name_copy,
1322+
.kind = .type_alias,
1323+
.line_start = line_num,
1324+
.line_end = line_num,
1325+
.detail = detail_copy,
1326+
});
1327+
}
1328+
}
1329+
return;
1330+
}
1331+
1332+
if (startsWith(stripped, "const ")) {
1333+
const after = stripped[6..];
1334+
if (std.mem.indexOf(u8, after, "=")) |eq| {
1335+
const before_eq = std.mem.trimRight(u8, after[0..eq], " \t");
1336+
if (lastIdent(before_eq)) |name| {
1337+
const name_copy = try a.dupe(u8, name);
1338+
errdefer a.free(name_copy);
1339+
const detail_copy = try a.dupe(u8, line);
1340+
errdefer a.free(detail_copy);
1341+
try outline.symbols.append(a, .{
1342+
.name = name_copy,
1343+
.kind = .constant,
1344+
.line_start = line_num,
1345+
.line_end = line_num,
1346+
.detail = detail_copy,
1347+
});
1348+
}
1349+
}
1350+
return;
1351+
}
1352+
1353+
if (std.mem.indexOf(u8, stripped, "(")) |paren| {
1354+
if (paren > 0) {
1355+
const control = [_][]const u8{ "if ", "if(", "else ", "for ", "for(", "foreach ", "foreach(", "while ", "while(", "switch ", "switch(", "catch ", "catch(", "return ", "return(", "new ", "throw ", "lock ", "lock(" };
1356+
for (control) |kw| {
1357+
if (startsWith(stripped, kw)) return;
1358+
}
1359+
const before_paren = std.mem.trimRight(u8, stripped[0..paren], " \t");
1360+
if (lastIdent(before_paren)) |name| {
1361+
if (std.mem.eql(u8, name, "base") or std.mem.eql(u8, name, "this") or std.mem.eql(u8, name, "value")) return;
1362+
const name_copy = try a.dupe(u8, name);
1363+
errdefer a.free(name_copy);
1364+
const detail_copy = try a.dupe(u8, line);
1365+
errdefer a.free(detail_copy);
1366+
try outline.symbols.append(a, .{
1367+
.name = name_copy,
1368+
.kind = .method,
1369+
.line_start = line_num,
1370+
.line_end = line_num,
1371+
.detail = detail_copy,
1372+
});
1373+
}
1374+
}
1375+
}
1376+
}
1377+
1378+
fn stripCSharpModifiers(line: []const u8) []const u8 {
1379+
const modifiers = [_][]const u8{
1380+
"public ", "private ", "protected ", "internal ",
1381+
"static ", "abstract ", "virtual ", "override ",
1382+
"sealed ", "async ", "partial ", "readonly ",
1383+
"new ", "extern ", "unsafe ", "volatile ",
1384+
};
1385+
var result = line;
1386+
var changed = true;
1387+
while (changed) {
1388+
changed = false;
1389+
for (modifiers) |mod| {
1390+
if (std.mem.startsWith(u8, result, mod)) {
1391+
result = result[mod.len..];
1392+
changed = true;
1393+
}
1394+
}
1395+
}
1396+
return result;
1397+
}
1398+
1399+
fn lastIdent(s: []const u8) ?[]const u8 {
1400+
if (s.len == 0) return null;
1401+
var end = s.len;
1402+
if (s[end - 1] == '>') {
1403+
var depth: u32 = 0;
1404+
var i = end;
1405+
while (i > 0) : (i -= 1) {
1406+
if (s[i - 1] == '>') depth += 1;
1407+
if (s[i - 1] == '<') {
1408+
depth -= 1;
1409+
if (depth == 0) {
1410+
end = i - 1;
1411+
break;
1412+
}
1413+
}
1414+
}
1415+
}
1416+
while (end > 0 and s[end - 1] == ']') {
1417+
if (end >= 2 and s[end - 2] == '[') {
1418+
end -= 2;
1419+
} else break;
1420+
}
1421+
var ident_end = end;
1422+
while (ident_end > 0 and (std.ascii.isAlphanumeric(s[ident_end - 1]) or s[ident_end - 1] == '_')) {
1423+
ident_end -= 1;
1424+
}
1425+
if (ident_end == end) return null;
1426+
return s[ident_end..end];
1427+
}
1428+
12031429
fn rebuildDepsFor(self: *Explorer, path: []const u8, outline: *FileOutline) !void {
12041430
var deps: std.ArrayList([]const u8) = .{};
12051431
errdefer deps.deinit(self.allocator);
@@ -1418,7 +1644,7 @@ pub fn isCommentOrBlank(line: []const u8, language: Language) bool {
14181644
const trimmed = std.mem.trim(u8, line, " \t");
14191645
if (trimmed.len == 0) return true;
14201646
return switch (language) {
1421-
.zig, .rust, .go_lang => std.mem.startsWith(u8, trimmed, "//"),
1647+
.zig, .rust, .go_lang, .csharp => std.mem.startsWith(u8, trimmed, "//"),
14221648
.python => std.mem.startsWith(u8, trimmed, "#"),
14231649
.javascript, .typescript, .c, .cpp => std.mem.startsWith(u8, trimmed, "//") or std.mem.startsWith(u8, trimmed, "/*") or std.mem.startsWith(u8, trimmed, "*"),
14241650
else => false,

src/style.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub const Style = struct {
2323
if (std.mem.eql(u8, lang, "rust")) return self.orange;
2424
if (std.mem.eql(u8, lang, "python")) return self.blue;
2525
if (std.mem.eql(u8, lang, "c") or std.mem.eql(u8, lang, "cpp")) return self.blue;
26+
if (std.mem.eql(u8, lang, "csharp")) return self.magenta;
2627
if (std.mem.eql(u8, lang, "markdown")) return self.dim;
2728
if (std.mem.eql(u8, lang, "json") or std.mem.eql(u8, lang, "yaml")) return self.dim;
2829
return self.dim; // unknown

src/tests.zig

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,70 @@ test "explorer: typescript parser" {
879879
try testing.expect(outline.symbols.items.len >= 3);
880880
}
881881

882+
test "explorer: csharp parser" {
883+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
884+
defer arena.deinit();
885+
var explorer = Explorer.init(arena.allocator());
886+
887+
try explorer.indexFile("Program.cs",
888+
\\using System;
889+
\\using System.Collections.Generic;
890+
\\namespace MyApp
891+
\\{
892+
\\ public class Program
893+
\\ {
894+
\\ public static void Main(string[] args)
895+
\\ {
896+
\\ }
897+
\\ private int Calculate(int x)
898+
\\ {
899+
\\ return x * 2;
900+
\\ }
901+
\\ }
902+
\\ public interface IService
903+
\\ {
904+
\\ void Execute();
905+
\\ }
906+
\\ public enum Status
907+
\\ {
908+
\\ Active,
909+
\\ Inactive
910+
\\ }
911+
\\ public struct Point
912+
\\ {
913+
\\ public int X;
914+
\\ public int Y;
915+
\\ }
916+
\\}
917+
);
918+
919+
var outline = (try explorer.getOutline("Program.cs", testing.allocator)) orelse return error.TestUnexpectedResult;
920+
defer outline.deinit();
921+
922+
try testing.expectEqual(Language.csharp, outline.language);
923+
924+
var has_class = false;
925+
var has_method = false;
926+
var has_interface = false;
927+
var has_enum = false;
928+
var has_struct = false;
929+
var has_using = false;
930+
for (outline.symbols.items) |sym| {
931+
if (sym.kind == .struct_def and std.mem.eql(u8, sym.name, "Program")) has_class = true;
932+
if (sym.kind == .method and std.mem.eql(u8, sym.name, "Main")) has_method = true;
933+
if (sym.kind == .trait_def and std.mem.eql(u8, sym.name, "IService")) has_interface = true;
934+
if (sym.kind == .enum_def and std.mem.eql(u8, sym.name, "Status")) has_enum = true;
935+
if (sym.kind == .struct_def and std.mem.eql(u8, sym.name, "Point")) has_struct = true;
936+
if (sym.kind == .import) has_using = true;
937+
}
938+
try testing.expect(has_class);
939+
try testing.expect(has_method);
940+
try testing.expect(has_interface);
941+
try testing.expect(has_enum);
942+
try testing.expect(has_struct);
943+
try testing.expect(has_using);
944+
}
945+
882946
// ── Version tests ───────────────────────────────────────────
883947

884948
test "file versions: append and latest" {
@@ -2020,6 +2084,7 @@ test "detectLanguage: all supported extensions" {
20202084
try testing.expect(explore.detectLanguage("comp.tsx") == .typescript);
20212085
try testing.expect(explore.detectLanguage("main.rs") == .rust);
20222086
try testing.expect(explore.detectLanguage("main.go") == .go_lang);
2087+
try testing.expect(explore.detectLanguage("Program.cs") == .csharp);
20232088
try testing.expect(explore.detectLanguage("README.md") == .markdown);
20242089
try testing.expect(explore.detectLanguage("pkg.json") == .json);
20252090
try testing.expect(explore.detectLanguage("config.yaml") == .yaml);

0 commit comments

Comments
 (0)