this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

window(wrap): refactor word wrap

Commit 74fb13079794 "fix: `Window.printSegments` correctly prints all
non-trailing whitespace" fixed some bugs with word wrapping, but also
introduced a bug with printing leading whitespace in a segment.

Refactor the entire word wrap logic to use a custom LineIterator and a
tokenizer which gives whitespace tokens as well.

+182 -80
+182 -80
src/Window.zig
··· 328 328 .word => { 329 329 var col: usize = opts.col_offset; 330 330 var overflow: bool = false; 331 - var soft_wrapped = false; 332 - for (segments) |segment| { 333 - var start: usize = 0; 334 - var tokenizer = std.mem.tokenizeAny(u8, segment.text, "\r\n"); 335 - while (tokenizer.peek() != null) { 336 - soft_wrapped = false; 337 - const returns = segment.text[start..tokenizer.index]; 338 - const line = tokenizer.next().?; 339 - start = tokenizer.index; 340 - var i: usize = 0; 341 - while (i < returns.len) : (i += 1) { 342 - const b = returns[i]; 343 - if (b == '\r' and i + 1 < returns.len and returns[i + 1] == '\n') { 344 - i += 1; 345 - } 346 - row += 1; 347 - col = 0; 348 - } 349 - var iter = std.mem.tokenizeScalar(u8, line, ' '); 350 - var ws_start: usize = 0; 351 - while (iter.peek() != null) { 352 - const whitespace = line[ws_start..iter.index]; 353 - const word = iter.next().?; 354 - ws_start = iter.index; 355 - var j: usize = 0; 356 - if (soft_wrapped) soft_wrapped = false else { 357 - while (j < whitespace.len) : (j += 1) { 358 - if (opts.commit) self.writeCell(col, row, .{ 359 - .char = .{ 360 - .grapheme = " ", 361 - .width = 1, 362 - }, 363 - .style = segment.style, 364 - .link = segment.link, 365 - }); 366 - col += 1; 367 - } 368 - } 369 - if (col >= self.width) { 370 - col = 0; 371 - row += 1; 372 - soft_wrapped = true; 373 - } 374 - const width = self.gwidth(word); 375 - if (width + col > self.width and width < self.width) { 331 + var soft_wrapped: bool = false; 332 + outer: for (segments) |segment| { 333 + var line_iter: LineIterator = .{ .buf = segment.text }; 334 + while (line_iter.next()) |line| { 335 + defer { 336 + // We only set soft_wrapped to false if a segment actually contains a linebreak 337 + if (line_iter.has_break) { 338 + soft_wrapped = false; 376 339 row += 1; 377 340 col = 0; 378 341 } 379 - if (row >= self.height) { 380 - overflow = true; 381 - break; 382 - } 342 + } 343 + var iter: WhitespaceTokenizer = .{ .buf = line }; 344 + while (iter.next()) |token| { 345 + switch (token) { 346 + .whitespace => |len| { 347 + if (soft_wrapped) continue; 348 + for (0..len) |_| { 349 + if (col >= self.width) { 350 + col = 0; 351 + row += 1; 352 + break; 353 + } 354 + if (opts.commit) { 355 + self.writeCell(col, row, .{ 356 + .char = .{ 357 + .grapheme = " ", 358 + .width = 1, 359 + }, 360 + .style = segment.style, 361 + .link = segment.link, 362 + }); 363 + } 364 + col += 1; 365 + } 366 + }, 367 + .word => |word| { 368 + const width = self.gwidth(word); 369 + if (width + col > self.width and width < self.width) { 370 + row += 1; 371 + col = 0; 372 + } 383 373 384 - var grapheme_iterator = self.screen.unicode.graphemeIterator(word); 385 - while (grapheme_iterator.next()) |grapheme| { 386 - soft_wrapped = false; 387 - const s = grapheme.bytes(word); 388 - const w = self.gwidth(s); 389 - if (opts.commit) self.writeCell(col, row, .{ 390 - .char = .{ 391 - .grapheme = s, 392 - .width = w, 393 - }, 394 - .style = segment.style, 395 - .link = segment.link, 396 - }); 397 - col += w; 398 - if (col >= self.width) { 399 - row += 1; 400 - col = 0; 401 - soft_wrapped = true; 402 - } 403 - } 404 - } 405 - } else { 406 - const returns = segment.text[start..tokenizer.index]; 407 - start = tokenizer.index; 408 - var i: usize = 0; 409 - while (i < returns.len) : (i += 1) { 410 - const b = returns[i]; 411 - if (b == '\r' and i + 1 < returns.len and returns[i + 1] == '\n') { 412 - i += 1; 374 + var grapheme_iterator = self.screen.unicode.graphemeIterator(word); 375 + while (grapheme_iterator.next()) |grapheme| { 376 + soft_wrapped = false; 377 + if (row >= self.height) { 378 + overflow = true; 379 + break :outer; 380 + } 381 + const s = grapheme.bytes(word); 382 + const w = self.gwidth(s); 383 + if (opts.commit) self.writeCell(col, row, .{ 384 + .char = .{ 385 + .grapheme = s, 386 + .width = w, 387 + }, 388 + .style = segment.style, 389 + .link = segment.link, 390 + }); 391 + col += w; 392 + if (col >= self.width) { 393 + row += 1; 394 + col = 0; 395 + soft_wrapped = true; 396 + } 397 + } 398 + }, 413 399 } 414 - row += 1; 415 - col = 0; 416 400 } 417 401 } 418 402 } ··· 639 623 } 640 624 { 641 625 var segments = [_]Segment{ 626 + .{ .text = " " }, 627 + }; 628 + const result = try win.print(&segments, opts); 629 + try std.testing.expectEqual(1, result.col); 630 + try std.testing.expectEqual(0, result.row); 631 + try std.testing.expectEqual(false, result.overflow); 632 + } 633 + { 634 + var segments = [_]Segment{ 635 + .{ .text = " a" }, 636 + }; 637 + const result = try win.print(&segments, opts); 638 + try std.testing.expectEqual(2, result.col); 639 + try std.testing.expectEqual(0, result.row); 640 + try std.testing.expectEqual(false, result.overflow); 641 + } 642 + { 643 + var segments = [_]Segment{ 642 644 .{ .text = "a b" }, 643 645 }; 644 646 const result = try win.print(&segments, opts); ··· 750 752 try std.testing.expectEqual(1, result.row); 751 753 try std.testing.expectEqual(false, result.overflow); 752 754 } 755 + { 756 + var segments = [_]Segment{ 757 + .{ .text = "note" }, 758 + .{ .text = " now" }, 759 + }; 760 + const result = try win.print(&segments, opts); 761 + try std.testing.expectEqual(3, result.col); 762 + try std.testing.expectEqual(1, result.row); 763 + try std.testing.expectEqual(false, result.overflow); 764 + } 765 + { 766 + var segments = [_]Segment{ 767 + .{ .text = "note " }, 768 + .{ .text = "now" }, 769 + }; 770 + const result = try win.print(&segments, opts); 771 + try std.testing.expectEqual(3, result.col); 772 + try std.testing.expectEqual(1, result.row); 773 + try std.testing.expectEqual(false, result.overflow); 774 + } 753 775 } 776 + 777 + /// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n' 778 + const LineIterator = struct { 779 + buf: []const u8, 780 + index: usize = 0, 781 + has_break: bool = true, 782 + 783 + fn next(self: *LineIterator) ?[]const u8 { 784 + if (self.index >= self.buf.len) return null; 785 + 786 + const start = self.index; 787 + const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse { 788 + if (start == 0) self.has_break = false; 789 + self.index = self.buf.len; 790 + return self.buf[start..]; 791 + }; 792 + 793 + self.index = end; 794 + self.consumeCR(); 795 + self.consumeLF(); 796 + return self.buf[start..end]; 797 + } 798 + 799 + // consumes a \n byte 800 + fn consumeLF(self: *LineIterator) void { 801 + if (self.index >= self.buf.len) return; 802 + if (self.buf[self.index] == '\n') self.index += 1; 803 + } 804 + 805 + // consumes a \r byte 806 + fn consumeCR(self: *LineIterator) void { 807 + if (self.index >= self.buf.len) return; 808 + if (self.buf[self.index] == '\r') self.index += 1; 809 + } 810 + }; 811 + 812 + /// Returns tokens of text and whitespace 813 + const WhitespaceTokenizer = struct { 814 + buf: []const u8, 815 + index: usize = 0, 816 + 817 + const Token = union(enum) { 818 + // the length of whitespace. Tab = 8 819 + whitespace: usize, 820 + word: []const u8, 821 + }; 822 + 823 + fn next(self: *WhitespaceTokenizer) ?Token { 824 + if (self.index >= self.buf.len) return null; 825 + const Mode = enum { 826 + whitespace, 827 + word, 828 + }; 829 + const first = self.buf[self.index]; 830 + const mode: Mode = if (first == ' ' or first == '\t') .whitespace else .word; 831 + switch (mode) { 832 + .whitespace => { 833 + var len: usize = 0; 834 + while (self.index < self.buf.len) : (self.index += 1) { 835 + switch (self.buf[self.index]) { 836 + ' ' => len += 1, 837 + '\t' => len += 8, 838 + else => break, 839 + } 840 + } 841 + return .{ .whitespace = len }; 842 + }, 843 + .word => { 844 + const start = self.index; 845 + while (self.index < self.buf.len) : (self.index += 1) { 846 + switch (self.buf[self.index]) { 847 + ' ', '\t' => break, 848 + else => {}, 849 + } 850 + } 851 + return .{ .word = self.buf[start..self.index] }; 852 + }, 853 + } 854 + } 855 + };