// Expect retcode = 0 // 源文件与位置映射:fname + text 借用调用方存储;行起始表由 Source 拥有。 // 提供字节偏移 <-> (行,列)(行列均为 1-based、按字节计),以及取整行文本。 // Sources 是按 fname 索引的多文件集合。 type .{ .Error, .Status, .make } = @import("error.spl"); type .{ .ByteView, .FindResult } = @import("byte_view.spl"); type .{ .Vec } = @import("vec.spl"); type .{ .Map, .hash_kind_view } = @import("map.spl"); type .{ .hash_view, .eq_view } = @import("hash.spl"); type Position = struct { line: usize, // 1-based col: usize, // 1-based,按字节 } type Source = @self; fname: ByteView, // 借用 text: ByteView, // 借用 line_starts: Vec, // usize:每行起始字节偏移(含首行 0) type SourceResult = enum { Ok: Source, Err: Error, } type AddResult = enum { Ok: usize, Err: Error, } fn blank(): Source { ret Source { .fname = ByteView.empty(), .text = ByteView.empty(), .line_starts = Vec.blank(@sizeof(usize), 8), }; } fn new(fname: ByteView, text: ByteView): SourceResult { var starts = Vec.blank(@sizeof(usize), 8); var zero: usize = 0; match Vec.push(&starts, &zero as *u8) { .Ok => {}, .Err(error) => { ret .Err(error); }, } var i: usize = 0; while i < text.len { if text.ptr[i] == '\n' { var next = i + 1; match Vec.push(&starts, &next as *u8) { .Ok => {}, .Err(error) => { Vec.drop(&starts); ret .Err(error); }, } } i = i + 1; } ret .Ok(Source { .fname = fname, .text = text, .line_starts = starts }); } fn line_count(self: Source): usize { ret self.line_starts.len; } fn line_start_offset(self: Source, line: usize): usize { if line == 0 || line > self.line_starts.len { ret ~(0 as usize); } match Vec.get(&self.line_starts, line - 1) { .Ok(ptr) => { ret (ptr as *usize).*; }, .Err(error) => { ret ~(0 as usize); }, } ret ~(0 as usize); } // offset 所在行(0-based);超出范围夹到末行。 fn line_index_at(self: Source, offset: usize): usize { var off = offset; if off > self.text.len { off = self.text.len; } var lo: usize = 0; var hi: usize = self.line_starts.len; while lo + 1 < hi { var mid = (lo + hi) / 2; var start = line_start_offset(self, mid + 1); // 1-based -> 内部 if start != ~(0 as usize) && start <= off { lo = mid; } else { hi = mid; } } ret lo; } fn line_col(self: Source, offset: usize): Position { var off = offset; if off > self.text.len { off = self.text.len; } var line0 = line_index_at(self, off); var start = line_start_offset(self, line0 + 1); // 1-based if start == ~(0 as usize) { start = 0; } ret Position { .line = line0 + 1, .col = off - start + 1 }; } // line 为 1-based;返回整行(去掉行尾 '\n' 与 '\r')。 fn line_text(self: Source, line: usize): ByteView { var n = self.line_starts.len; if line == 0 || line > n { ret ByteView.empty(); } var start = line_start_offset(self, line); if start == ~(0 as usize) { ret ByteView.empty(); } var end = self.text.len; if line < n { var next = line_start_offset(self, line + 1); if next != ~(0 as usize) { end = next; } } while end > start && (self.text.ptr[end - 1] == '\n' || self.text.ptr[end - 1] == '\r') { end = end - 1; } var r = ByteView.slice(self.text, start, end); match r { .Ok(v) => { ret v; }, .Err(error) => { ret ByteView.empty(); }, } ret ByteView.empty(); } fn drop(self: *Source) { Vec.drop(&self.line_starts); } type Sources = struct { items: Vec, // Source by_name: Map, // ByteView(fname) -> usize } fn set_new(): Sources { ret Sources { .items = Vec.blank(@sizeof(Source), 8), .by_name = Map.blank(@sizeof(ByteView), @alignof(ByteView), @sizeof(usize), @alignof(usize), hash_kind_view()), }; } fn set_add(set: *Sources, fname: ByteView, text: ByteView): AddResult { var src = new(fname, text); var s = blank(); match src { .Ok(v) => { s = v; }, .Err(error) => { ret .Err(error); }, } var index = set.items.len; match Vec.push(&set.items, &s as *u8) { .Ok => {}, .Err(error) => { drop(&s); ret .Err(error); }, } match Map.put(&set.by_name, &fname as *u8, &index as *u8) { .Ok => {}, .Err(error) => { // 回滚最后一项 set.items.len = index; drop(&s); ret .Err(error); }, } ret .Ok(index); } fn set_at(set: *Sources, index: usize): *Source { match Vec.get(&set.items, index) { .Ok(ptr) => { ret ptr as *Source; }, .Err(error) => { ret null; }, } ret null; } fn set_find(set: *Sources, fname: ByteView): FindResult { var out: usize = 0; match Map.get(&set.by_name, &fname as *u8, &out as *u8) { .Found => { ret .Found(out); }, .NotFound => {}, } ret .NotFound; } fn set_drop(set: *Sources) { var i: usize = 0; while i < set.items.len { var s = set_at(set, i); if s != null { drop(s); } i = i + 1; } Vec.drop(&set.items); Map.drop(&set.by_name); } fn view(text: []u8): ByteView { ret ByteView.from_slice(text); } fn test_single() { var text = "line1\nline2\n\nline4"; var r = new(view("a.spl"), view(text)); var src = blank(); match r { .Ok(v) => { src = v; }, .Err(error) => { @assert(false, "source new"); }, } @assert(line_count(src) == 4, "line count"); var p0 = line_col(src, 0); @assert(p0.line == 1 && p0.col == 1, "start"); var p6 = line_col(src, 6); @assert(p6.line == 2 && p6.col == 1, "line2 start"); var p8 = line_col(src, 8); @assert(p8.line == 2 && p8.col == 3, "line2 col3"); var p12 = line_col(src, 12); @assert(p12.line == 3 && p12.col == 1, "empty line3"); var p13 = line_col(src, 13); @assert(p13.line == 4 && p13.col == 1, "line4 start"); var p18 = line_col(src, 18); @assert(p18.line == 4 && p18.col == 6, "past end clamps"); var l1 = line_text(src, 1); var l3 = line_text(src, 3); var l4 = line_text(src, 4); @assert(ByteView.eq(l1, view("line1")), "line1"); @assert(ByteView.eq(l3, view("")), "line3 empty"); @assert(ByteView.eq(l4, view("line4")), "line4"); drop(&src); } fn test_sources() { var set = set_new(); var r1 = set_add(&set, view("a.spl"), view("x\ny")); var r2 = set_add(&set, view("b.spl"), view("z")); var a: usize = 0; var b: usize = 0; match r1 { .Ok(v) => { a = v; }, .Err(e) => { @assert(false, "add a"); } } match r2 { .Ok(v) => { b = v; }, .Err(e) => { @assert(false, "add b"); } } @assert(a == 0 && b == 1, "indices"); match set_find(&set, view("b.spl")) { .Found(i) => { @assert(i == 1, "find b"); }, .NotFound => { @assert(false, "b missing"); }, } match set_find(&set, view("c.spl")) { .Found(i) => { @assert(false, "c unexpected"); }, .NotFound => {}, } var sb = set_at(&set, b); @assert(sb != null && line_count(sb.*) == 1, "at b"); set_drop(&set); } fn main() { test_single(); test_sources(); }