╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │13 const Focus = xitui.focus.Focus; │ │14 const inp = @import("../input.zig"); │ │15 const fnd = @import("../../find.zig"); │ │16 +const md = @import("../../markdown.zig"); │ │17 +const Markdown = @import("Markdown.zig"); │ │18 │ │19 // how many lines of a file's content one window shows before a "next" link.│ │20 const file_page = 5000; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │21 + │ │22 +// a longer markdown file shows as numbered source rather than rendered. │ │23 +const max_markdown_lines = 2000; │ │24 │ │25 // budget for the total bytes of file content preloaded (windowed) per page,│ │26 // so selecting a preloaded file shows its contents without a reload. │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │599 // the content scroll's border is single normally and double when the │ │600 // content is focused (the content box itself is borderless). │ │601 const has_content = self.detailInner().children.count() > 0; │ │597 - const content_focused = if (root_focus.grandchild_id) |g| self.detailInner().children.contains(g) else false; │ │598 - self.detailScrollFrame().options.border_style = if (!has_content) null else if (content_focused) .double else .single; │ │602 + self.detailScrollFrame().options.border_style = if (!has_content) null else if (self.focusOnContent(root_focus)) .double else .single;│ │603 │ │604 // cap the list at list_max_width only while the detail pane fits beside │ │605 // it. the box drops the detail pane when the width can't hold both │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │658 │ │659 // only files have contents to show; directories and the ".." row leave│ │660 // the pane empty. │ │661 + var markdown = false; │ │662 if (self.selectedEntry()) |entry| { │ │663 if (!entry.is_dir) { │ │664 // the window links sit above the scroll, so they stay put │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │673 try self.addContentBox(allocator, inner, "(press enter to load this file)", link); │ │674 } else if (entry.is_binary) { │ │675 try self.addContentBox(allocator, inner, "(binary file)", ""); │ │676 + } else if (isMarkdown(entry.name) and entry.window_start == 0 and !entry.has_more and entry.lines.len <= max_markdown_lines) { │ │677 + const page_arena = self.session.page_arena; │ │678 + const doc = try md.parse(page_arena.allocator(), entry.lines); │ │679 + const path = try childDir(page_arena.allocator(), self.data.dir, entry.name); │ │680 + var view = try Markdown.View.init(allocator, doc, self.data, path, page_arena); │ │681 + errdefer view.deinit(allocator); │ │682 + try inner.children.put(allocator, view.getFocus().id, .{ .widget = .{ .markdown = view }, .rect = null, .min_size = null });│ │683 + markdown = true; │ │684 } else { │ │685 const text = try numberedContent(self.session.page_arena.allocator(), entry.lines, entry.window_start); │ │686 try self.addContentBox(allocator, inner, text, ""); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │694 // reserve the nav row's height (the shrink scroll frame yields it). │ │695 self.detailOuter().children.values()[detail_nav_index].min_size = │ │696 if (nav_box.children.count() > 0) .{ .width = null, .height = row_height } else null;│ │697 + │ │698 + // rendered markdown wraps to the pane's width, so it only scrolls down │ │699 + const sc = self.detailScroll(); │ │700 + sc.options.direction = if (markdown) .vert else .both; │ │701 │ │702 // reset the scroll to the top for the newly-shown file: directly on the │ │703 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │704 // renderer's scroll id changes and JS drops the preserved position).│ │688 - const sc = self.detailScroll(); │ │705 sc.x = 0; │ │706 sc.y = 0; │ │707 sc.getFocus().version +%= 1; │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │740 } │ │741 if (direction == .up and self.contentAtTop() and self.focusHeader(root_focus)) return;│ │742 if (self.detailActive()) { │ │727 - try self.detailInput(key, root_focus); │ │743 + try self.detailInput(allocator, key, root_focus); │ │744 } else { │ │745 try self.listInput(key, root_focus); │ │746 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │772 // whether focus is on the content box (vs. a "previous"/"next" nav link). │ │773 fn focusOnContent(self: *View, root_focus: *Focus) bool { │ │774 const g = root_focus.grandchild_id orelse return false; │ │775 + if (self.markdownView()) |view| return view.owns(g); │ │776 return self.detailInner().children.contains(g); │ │777 + } │ │778 + │ │779 + // the rendered markdown in the detail pane, if that's what it shows │ │780 + fn markdownView(self: *View) ?*Markdown.View { │ │781 + const inner = self.detailInner(); │ │782 + if (inner.children.count() == 0) return null; │ │783 + return switch (inner.children.values()[0].widget) { │ │784 + .markdown => |*view| view, │ │785 + else => null, │ │786 + }; │ │787 } │ │788 │ │762 - fn detailInput(self: *View, key: Key, root_focus: *Focus) !void { │ │789 + fn detailInput(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void {│ │790 const sc = self.detailScroll(); │ │791 const on_content = self.focusOnContent(root_focus); │ │792 // the "next" link sits above the content scroll. on the link, left returns │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │849 self.detailScroll().clampToContent(); │ │850 }, │ │851 else => {}, │ │852 + }, │ │853 + // step between the rendered markdown's links; the web leaves tab to the browser │ │854 + .tab, .back_tab => if (on_content and self.session.is_terminal) if (self.markdownView()) |view| {│ │855 + try view.stepLink(allocator, root_focus, sc, key == .tab); │ │856 }, │ │857 else => {}, │ │858 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯