detached 39ca311

feat(web): expand directories in place in the Files view

commit
39ca311756f5cde2a82d8cd77938f885034a3d50
author
brice · · 2026-10-11 18:29 UTC
parent
bfab294
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
43 files · +534 -331
M cmd/ermite/testdata/web/blob-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-code.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-image.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-markdown.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-svg.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-text.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-too-large.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/blob-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-big.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-binary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-merge.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify-split.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-modify.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-rename.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commit-trojan.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-page2.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-path.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-root.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/commits-small.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/empty.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/home.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found-page.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/not-found.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="" data-sha="" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="" data-sha="" data-sel="" data-nav="">22 <body data-ref="" data-sha="" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/refs.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/summary.html +2 -0
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
M cmd/ermite/testdata/web/tree-nested.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106108 <th class="c-when">date</th>
107109 </tr>
108110 </thead>
109-<tbody id="tree-rows" data-rows>
111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110112 <tr data-cursor>
111113 <td class="cur" aria-hidden="true">
112114 </td>
@@ -341,12 +343,6 @@
341343 <span class="k">tag</span>
342344 </a>
343345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350346 </ul>
351347 <p class="panel-foot">
352348 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -106,7 +108,7 @@
106 <th class="c-when">date</th>108 <th class="c-when">date</th>
107 </tr>109 </tr>
108 </thead>110 </thead>
109-<tbody id="tree-rows" data-rows>111+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
110 <tr data-cursor>112 <tr data-cursor>
111 <td class="cur" aria-hidden="true">113 <td class="cur" aria-hidden="true">
112 </td>114 </td>
@@ -341,12 +343,6 @@
341 <span class="k">tag</span>343 <span class="k">tag</span>
342 </a>344 </a>
343 </li>345 </li>
344-<li data-k="file src/lib/util.go">
345-<a href="/files/blob/main/src/lib/util.go" tabindex="-1">
346-<span class="lbl">src/lib/util.go</span>
347-<span class="k">file</span>
348-</a>
349-</li>
350 </ul>346 </ul>
351 <p class="panel-foot">347 <p class="panel-foot">
352 <span>348 <span>
M cmd/ermite/testdata/web/tree-root.html +7 -83
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100102 <th class="c-when">date</th>
101103 </tr>
102104 </thead>
103-<tbody id="tree-rows" data-rows>
105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104106 <tr data-cursor>
105107 <td class="cur" aria-hidden="true">
106108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>
109+<td class="c-kind" aria-hidden="true">▸</td>
108110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>
111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110112 </td>
111113 <td class="c-size num dim">-</td>
112114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120122 <tr>
121123 <td class="cur" aria-hidden="true">
122124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>
125+<td class="c-kind" aria-hidden="true">▸</td>
124126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>
127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126128 </td>
127129 <td class="c-size num dim">-</td>
128130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568570 <span class="k">tag</span>
569571 </a>
570572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649573 </ul>
650574 <p class="panel-foot">
651575 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -100,13 +102,13 @@
100 <th class="c-when">date</th>102 <th class="c-when">date</th>
101 </tr>103 </tr>
102 </thead>104 </thead>
103-<tbody id="tree-rows" data-rows>105+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
104 <tr data-cursor>106 <tr data-cursor>
105 <td class="cur" aria-hidden="true">107 <td class="cur" aria-hidden="true">
106 </td>108 </td>
107-<td class="c-kind" aria-hidden="true">▾</td>109+<td class="c-kind" aria-hidden="true">▸</td>
108 <td class="c-name">110 <td class="c-name">
109-<a href="/files/tree/main/docs">docs/</a>111+<a href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false">docs/</a>
110 </td>112 </td>
111 <td class="c-size num dim">-</td>113 <td class="c-size num dim">-</td>
112 <td class="c-sub dim">114 <td class="c-sub dim">
@@ -120,9 +122,9 @@
120 <tr>122 <tr>
121 <td class="cur" aria-hidden="true">123 <td class="cur" aria-hidden="true">
122 </td>124 </td>
123-<td class="c-kind" aria-hidden="true">▾</td>125+<td class="c-kind" aria-hidden="true">▸</td>
124 <td class="c-name">126 <td class="c-name">
125-<a href="/files/tree/main/src">src/</a>127+<a href="/files/tree/main/src" data-expand="/files/tree/main/src?rows" aria-expanded="false">src/</a>
126 </td>128 </td>
127 <td class="c-size num dim">-</td>129 <td class="c-size num dim">-</td>
128 <td class="c-sub dim">130 <td class="c-sub dim">
@@ -568,84 +570,6 @@
568 <span class="k">tag</span>570 <span class="k">tag</span>
569 </a>571 </a>
570 </li>572 </li>
571-<li data-k="file readme.md">
572-<a href="/files/blob/main/README.md" tabindex="-1">
573-<span class="lbl">README.md</span>
574-<span class="k">file</span>
575-</a>
576-</li>
577-<li data-k="file big.txt">
578-<a href="/files/blob/main/big.txt" tabindex="-1">
579-<span class="lbl">big.txt</span>
580-<span class="k">file</span>
581-</a>
582-</li>
583-<li data-k="file data.bin">
584-<a href="/files/blob/main/data.bin" tabindex="-1">
585-<span class="lbl">data.bin</span>
586-<span class="k">file</span>
587-</a>
588-</li>
589-<li data-k="file empty.txt">
590-<a href="/files/blob/main/empty.txt" tabindex="-1">
591-<span class="lbl">empty.txt</span>
592-<span class="k">file</span>
593-</a>
594-</li>
595-<li data-k="file evil.go">
596-<a href="/files/blob/main/evil.go" tabindex="-1">
597-<span class="lbl">evil.go</span>
598-<span class="k">file</span>
599-</a>
600-</li>
601-<li data-k="file fake.jpg">
602-<a href="/files/blob/main/fake.jpg" tabindex="-1">
603-<span class="lbl">fake.jpg</span>
604-<span class="k">file</span>
605-</a>
606-</li>
607-<li data-k="file fake.png">
608-<a href="/files/blob/main/fake.png" tabindex="-1">
609-<span class="lbl">fake.png</span>
610-<span class="k">file</span>
611-</a>
612-</li>
613-<li data-k="file lines.txt">
614-<a href="/files/blob/main/lines.txt" tabindex="-1">
615-<span class="lbl">lines.txt</span>
616-<span class="k">file</span>
617-</a>
618-</li>
619-<li data-k="file logo.svg">
620-<a href="/files/blob/main/logo.svg" tabindex="-1">
621-<span class="lbl">logo.svg</span>
622-<span class="k">file</span>
623-</a>
624-</li>
625-<li data-k="file main.go">
626-<a href="/files/blob/main/main.go" tabindex="-1">
627-<span class="lbl">main.go</span>
628-<span class="k">file</span>
629-</a>
630-</li>
631-<li data-k="file notes.txt">
632-<a href="/files/blob/main/notes.txt" tabindex="-1">
633-<span class="lbl">notes.txt</span>
634-<span class="k">file</span>
635-</a>
636-</li>
637-<li data-k="file page.html">
638-<a href="/files/blob/main/page.html" tabindex="-1">
639-<span class="lbl">page.html</span>
640-<span class="k">file</span>
641-</a>
642-</li>
643-<li data-k="file pic.png">
644-<a href="/files/blob/main/pic.png" tabindex="-1">
645-<span class="lbl">pic.png</span>
646-<span class="k">file</span>
647-</a>
648-</li>
649 </ul>573 </ul>
650 <p class="panel-foot">574 <p class="panel-foot">
651 <span>575 <span>
M cmd/ermite/testdata/web/tree-slash-ref.html +3 -7
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -338,12 +340,6 @@
338340 <span class="k">tag</span>
339341 </a>
340342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347343 </ul>
348344 <p class="panel-foot">
349345 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="feature/x" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -338,12 +340,6 @@
338 <span class="k">tag</span>340 <span class="k">tag</span>
339 </a>341 </a>
340 </li>342 </li>
341-<li data-k="file x/y.txt">
342-<a href="/files/blob/feature/x/x/y.txt" tabindex="-1">
343-<span class="lbl">x/y.txt</span>
344-<span class="k">file</span>
345-</a>
346-</li>
347 </ul>343 </ul>
348 <p class="panel-foot">344 <p class="panel-foot">
349 <span>345 <span>
M cmd/ermite/testdata/web/tree-subdir.html +3 -13
@@ -16,6 +16,8 @@
1616 </script>
1717 <script src="/-/static/js/ermite.HASH.js" defer>
1818 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
1921 </head>
2022 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
2123 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103105 <th class="c-when">date</th>
104106 </tr>
105107 </thead>
106-<tbody id="tree-rows" data-rows>
108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107109 <tr data-cursor>
108110 <td class="cur" aria-hidden="true">
109111 </td>
@@ -363,18 +365,6 @@
363365 <span class="k">tag</span>
364366 </a>
365367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378368 </ul>
379369 <p class="panel-foot">
380370 <span>
@@ -16,6 +16,8 @@
16 </script>16 </script>
17 <script src="/-/static/js/ermite.HASH.js" defer>17 <script src="/-/static/js/ermite.HASH.js" defer>
18 </script>18 </script>
19+<script src="/-/static/js/files.HASH.js" defer>
20+</script>
19 </head>21 </head>
20 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">22 <body data-ref="main" data-sha="SHA40" data-sel="" data-nav="">
21 <a class="skip" href="#main">skip to content</a>23 <a class="skip" href="#main">skip to content</a>
@@ -103,7 +105,7 @@
103 <th class="c-when">date</th>105 <th class="c-when">date</th>
104 </tr>106 </tr>
105 </thead>107 </thead>
106-<tbody id="tree-rows" data-rows>108+<tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
107 <tr data-cursor>109 <tr data-cursor>
108 <td class="cur" aria-hidden="true">110 <td class="cur" aria-hidden="true">
109 </td>111 </td>
@@ -363,18 +365,6 @@
363 <span class="k">tag</span>365 <span class="k">tag</span>
364 </a>366 </a>
365 </li>367 </li>
366-<li data-k="file docs/readme.md">
367-<a href="/files/blob/main/docs/README.md" tabindex="-1">
368-<span class="lbl">docs/README.md</span>
369-<span class="k">file</span>
370-</a>
371-</li>
372-<li data-k="file docs/guide.md">
373-<a href="/files/blob/main/docs/guide.md" tabindex="-1">
374-<span class="lbl">docs/guide.md</span>
375-<span class="k">file</span>
376-</a>
377-</li>
378 </ul>368 </ul>
379 <p class="panel-foot">369 <p class="panel-foot">
380 <span>370 <span>
M cmd/ermite/web_files_test.go +42 -0
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100100 }
101101 }
102102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103145 // Ref names with a slash are found by their longest prefix, and a commit id
104146 // works where a ref does.
105147 func TestWebFilesRefs(t *testing.T) {
@@ -100,6 +100,48 @@ func TestWebFilesGolden(t *testing.T) {
100 }100 }
101 }101 }
102 102
103+// AC1, AC2, AC7: ?rows answers the entries of a directory as table rows only,
104+// for the Files view to insert under the directory; a private repository is
105+// the 404 of a missing one.
106+func TestWebTreeRows(t *testing.T) {
107+ h := newHarness(t, harnessOpts{})
108+ h.webFilesRepo("files")
109+ h.seed("secret-sauce", domain.VisibilityPrivate, "dir/a.txt")
110+
111+ // The page links its directories to their rows.
112+ _, _, page := h.get("/files/tree/main")
113+ assert.Contains(t, page, `href="/files/tree/main/docs" data-expand="/files/tree/main/docs?rows" aria-expanded="false"`)
114+ assert.Contains(t, page, `<script src="/-/static/js/files.`)
115+ assert.NotContains(t, page, "main.go</span>", "the palette lists no file")
116+
117+ status, hdr, body := h.get("/files/tree/main/docs?rows")
118+ require.Equal(t, 200, status)
119+ assert.Equal(t, "no-cache", hdr.Get("Cache-Control"))
120+ assert.Contains(t, hdr.Get("Content-Type"), "text/html")
121+ assert.NotContains(t, body, "<html")
122+ assert.NotContains(t, body, "<table")
123+ assert.Equal(t, 2, strings.Count(body, "<tr"), "one row per entry")
124+ assert.Contains(t, body, `<a href="/files/blob/main/docs/README.md">README.md</a>`)
125+ assert.Contains(t, body, `<a href="/files/blob/main/docs/guide.md">guide.md</a>`)
126+ assert.NotContains(t, body, "data-cursor", "the cursor stays on the page")
127+ assertNoInline(t, body)
128+
129+ // A subdirectory row can be opened in turn.
130+ _, _, body = h.get("/files/tree/main/src?rows")
131+ assert.Contains(t, body, `href="/files/tree/main/src/lib" data-expand="/files/tree/main/src/lib?rows" aria-expanded="false"`)
132+ assert.Contains(t, body, "▸")
133+ _, _, body = h.get("/files/tree/main/src/lib?rows")
134+ assert.Contains(t, body, `href="/files/blob/main/src/lib/util.go"`)
135+ assert.NotContains(t, body, "data-expand")
136+
137+ // Not found, and private as anonymous: the 404 page, nothing of the rows.
138+ for _, p := range []string{"/files/tree/main/nope?rows", "/secret-sauce/tree/main/dir?rows", "/secret-sauce/tree/main?rows"} {
139+ status, _, body = h.get(p)
140+ assert.Equal(t, 404, status, p)
141+ assert.NotContains(t, body, "a.txt", p)
142+ }
143+}
144+
103 // Ref names with a slash are found by their longest prefix, and a commit id145 // Ref names with a slash are found by their longest prefix, and a commit id
104 // works where a ref does.146 // works where a ref does.
105 func TestWebFilesRefs(t *testing.T) {147 func TestWebFilesRefs(t *testing.T) {
M internal/adapters/web/ASSETS.md +1 -0
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
99 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
1010 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
1111 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
1213 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
1314 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
1415 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
@@ -9,6 +9,7 @@ content-hashed names. Nothing is fetched at runtime.
9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |9 | `static/js/hx-live.min.js` | htmx 4 `hx-live` extension | `mockups/assets/htmx/ext/hx-live.min.js` | `adf9b90949d57baf20dc2eaf1d1fd46c81347ac82c090a5e2912ce6b37726734` |
10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |10 | `static/js/hx-sse.min.js` | htmx 4 `hx-sse` extension | `mockups/assets/htmx/ext/hx-sse.min.js` | `8a834680c4000a9034d79228872372a92e140c810a075cb6d4a76690dfc13085` |
11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |11 | `static/js/ermite.js` | key sequences to `k-*` events | `mockups/d/js/ermite.js` | |
12+| `static/js/files.js` | the Files view: directories open in place (`window.ermiteTree`) | written here | |
12 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |13 | `static/css/56-chroma.css` | syntax classes of the code view, mapped to the `--syn-*` tokens (no colour of its own) | generated by `go run scripts/chroma-css.go` from chroma's token classes | |
13 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |14 | `static/css/*.css` | native CSS, served as one stylesheet in name order; relative `url(fonts/…)` and `url(icons/…)` become hashed URLs | written here | |
14 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |15 | `static/fonts/atkinson-next.woff2`, `atkinson-next-italic.woff2` | Atkinson Hyperlegible Next, variable weight, latin subset (SIL OFL 1.1, `static/fonts/OFL-next.txt`) | Google Fonts | `18b2a1a39a2fa298b0ba5390aca68462669826c90925656f1c1f6796e0e1bbaf`, `4a5037bfaf6680f40147407407ec09fa42925774bde809a579283d27f9f08106` |
M internal/adapters/web/assets.go +2 -1
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
2323 // Assets are the URLs of the assets a page loads. Their names carry a hash of
2424 // the content, so they can be cached for ever.
2525 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string
26+ CSS, HTMX, Live, SSE, App, Files, Icon string
2727 // Font is the UI font, preloaded by every page.
2828 Font string
2929 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
5252 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
5353 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
5454 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
5556 }
5657
5758 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
@@ -23,7 +23,7 @@ const StaticPrefix = "/-/static/"
23 // Assets are the URLs of the assets a page loads. Their names carry a hash of23 // Assets are the URLs of the assets a page loads. Their names carry a hash of
24 // the content, so they can be cached for ever.24 // the content, so they can be cached for ever.
25 type Assets struct {25 type Assets struct {
26- CSS, HTMX, Live, SSE, App, Icon string26+ CSS, HTMX, Live, SSE, App, Files, Icon string
27 // Font is the UI font, preloaded by every page.27 // Font is the UI font, preloaded by every page.
28 Font string28 Font string
29 // Rules is the speculation rules file named by the Speculation-Rules header.29 // Rules is the speculation rules file named by the Speculation-Rules header.
@@ -52,6 +52,7 @@ var scripts = []struct {
52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},52 {"hx-live.min.js", func(a *Assets, u string) { a.Live = u }},
53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},53 {"hx-sse.min.js", func(a *Assets, u string) { a.SSE = u }},
54 {"ermite.js", func(a *Assets, u string) { a.App = u }},54 {"ermite.js", func(a *Assets, u string) { a.App = u }},
55+ {"files.js", func(a *Assets, u string) { a.Files = u }},
55 }56 }
56 57
57 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):58 // sheetAssets are the files the stylesheet references as url(<dir>/<file>):
M internal/adapters/web/overlays.templ +1 -1
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
8484 }
8585
8686 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.
87+// refs and repos.
8888 templ OverlayCmd(items []CmdItem) {
8989 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
9090 <div class="panel-head">
@@ -84,7 +84,7 @@ templ refItem(r RefItem) {
84 }84 }
85 85
86 // OverlayCmd is the ":" command line: one filterable list mixing sections,86 // OverlayCmd is the ":" command line: one filterable list mixing sections,
87-// refs, files of this directory and repos.87+// refs and repos.
88 templ OverlayCmd(items []CmdItem) {88 templ OverlayCmd(items []CmdItem) {
89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">89 <dialog id="panel-cmd" class="panel modal" aria-label="Go to" hx-on:keydown="event.key === 'Escape' && (event.preventDefault(), this.close())" hx-on:click="event.target === this && this.close()">
90 <div class="panel-head">90 <div class="panel-head">
M internal/adapters/web/overlays_templ.go +1 -1
@@ -214,7 +214,7 @@ func refItem(r RefItem) templ.Component {
214214 }
215215
216216 // OverlayCmd is the ":" command line: one filterable list mixing sections,
217-// refs, files of this directory and repos.
217+// refs and repos.
218218 func OverlayCmd(items []CmdItem) templ.Component {
219219 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
220220 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -214,7 +214,7 @@ func refItem(r RefItem) templ.Component {
214 }214 }
215 215
216 // OverlayCmd is the ":" command line: one filterable list mixing sections,216 // OverlayCmd is the ":" command line: one filterable list mixing sections,
217-// refs, files of this directory and repos.217+// refs and repos.
218 func OverlayCmd(items []CmdItem) templ.Component {218 func OverlayCmd(items []CmdItem) templ.Component {
219 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {219 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
220 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context220 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
M internal/adapters/web/shell.templ +1 -0
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
2020 <script src={ l.Assets.Live } defer></script>
2121 <script src={ l.Assets.SSE } defer></script>
2222 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
2324 </head>
2425 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
2526 <a class="skip" href="#main">skip to content</a>
@@ -20,6 +20,7 @@ templ Shell(l Layout, rail templ.Component) {
20 <script src={ l.Assets.Live } defer></script>20 <script src={ l.Assets.Live } defer></script>
21 <script src={ l.Assets.SSE } defer></script>21 <script src={ l.Assets.SSE } defer></script>
22 <script src={ l.Assets.App } defer></script>22 <script src={ l.Assets.App } defer></script>
23+ <script src={ l.Assets.Files } defer></script>
23 </head>24 </head>
24 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">25 <body data-ref={ l.RefPath } data-sha={ l.SHA } data-sel="" data-nav="">
25 <a class="skip" href="#main">skip to content</a>26 <a class="skip" href="#main">skip to content</a>
M internal/adapters/web/shell_templ.go +138 -125 generated file: collapsed
M internal/adapters/web/static/css/30-list.css +4 -1
@@ -38,7 +38,10 @@
3838 }
3939 .list.tree {
4040 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }
41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
4245 & .c-size { width: 90px; text-align: right; }
4346 & .c-when { width: 100px; }
4447 & .c-sub { width: auto; }
@@ -38,7 +38,10 @@
38 }38 }
39 .list.tree {39 .list.tree {
40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }40 & .c-kind { width: 36px; color: var(--ink-mute); padding-right: 0; text-align: center; }
41- & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); }41+ /* --depth is set by files.js on the rows opened in place */
42+ & .c-name { width: 32%; font-family: var(--mono); font-size: var(--fs-s); padding-inline-start: calc(var(--s3) + var(--depth, 0) * 1.25ch); }
43+ & tr[aria-busy=true] .c-kind { opacity: .4; }
44+ & .tree-more td { padding-inline-start: calc(var(--s3) + 36px + var(--depth, 0) * 1.25ch); color: var(--ink-dim); font-size: var(--fs-s); }
42 & .c-size { width: 90px; text-align: right; }45 & .c-size { width: 90px; text-align: right; }
43 & .c-when { width: 100px; }46 & .c-when { width: 100px; }
44 & .c-sub { width: auto; }47 & .c-sub { width: auto; }
A internal/adapters/web/static/js/files.js +81 -0
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
@@ -0,0 +1,81 @@
1+// files.js: the Files view. A directory opens in place: its entries are
2+// fetched once as table rows (the tree page with ?rows), inserted under it
3+// and kept in the DOM, so closing and opening it again needs no request.
4+//
5+// Rows opened in place carry data-depth and the --depth custom property
6+// (set through the CSSOM: the CSP forbids style attributes). Rows of the
7+// page itself have depth 0. Nothing here touches the document at load; the
8+// handlers are declared in the templates (hx-on:click on #tree-rows).
9+window.ermiteTree = (() => {
10+ const depthOf = (tr) => Number(tr.dataset.depth || 0);
11+ const linkOf = (tr) => tr.querySelector('a[data-expand]');
12+ const isOpen = (a) => a.getAttribute('aria-expanded') === 'true';
13+
14+ function setOpen(tr, a, open) {
15+ a.setAttribute('aria-expanded', open ? 'true' : 'false');
16+ const kind = tr.querySelector('.c-kind');
17+ if (kind) kind.textContent = open ? '▾' : '▸';
18+ }
19+
20+ // collapse hides every row under tr.
21+ function collapse(tr, a) {
22+ setOpen(tr, a, false);
23+ const d = depthOf(tr);
24+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) r.hidden = true;
25+ }
26+
27+ // expand shows the rows under tr that no collapsed directory between hides.
28+ function expand(tr, a) {
29+ setOpen(tr, a, true);
30+ const d = depthOf(tr);
31+ let closedAt = Infinity; // depth of the collapsed directory whose rows are skipped
32+ for (let r = tr.nextElementSibling; r && depthOf(r) > d; r = r.nextElementSibling) {
33+ const rd = depthOf(r);
34+ if (rd <= closedAt) closedAt = Infinity;
35+ r.hidden = rd > closedAt;
36+ const la = linkOf(r);
37+ if (closedAt === Infinity && la && !isOpen(la)) closedAt = rd;
38+ }
39+ }
40+
41+ async function load(tr, a) {
42+ tr.setAttribute('aria-busy', 'true');
43+ try {
44+ const res = await fetch(a.dataset.expand, { headers: { Accept: 'text/html' } });
45+ if (!res.ok) throw new Error('HTTP ' + res.status);
46+ const t = document.createElement('template');
47+ t.innerHTML = await res.text();
48+ const d = depthOf(tr) + 1;
49+ const rows = Array.from(t.content.children);
50+ for (const r of rows) {
51+ r.dataset.depth = String(d);
52+ r.style.setProperty('--depth', String(d));
53+ }
54+ tr.after(...rows);
55+ tr.dataset.loaded = '1';
56+ tr.removeAttribute('aria-busy');
57+ expand(tr, a);
58+ } catch (e) {
59+ location.href = a.href;
60+ }
61+ }
62+
63+ function toggle(tr) {
64+ const a = linkOf(tr);
65+ if (!a || tr.getAttribute('aria-busy') === 'true') return;
66+ if (isOpen(a)) collapse(tr, a);
67+ else if (tr.dataset.loaded) expand(tr, a);
68+ else load(tr, a);
69+ }
70+
71+ return {
72+ // click opens or closes the directory under a plain left click; any
73+ // modified or other click keeps the link's own navigation.
74+ click(event) {
75+ const a = event.target.closest('a[data-expand]');
76+ if (!a || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
77+ event.preventDefault();
78+ toggle(a.closest('tr'));
79+ },
80+ };
81+})();
M internal/adapters/web/tree.go +21 -12
@@ -12,14 +12,14 @@ import (
1212 pb "bnema.dev/ermite-api/gen/ermite"
1313 )
1414
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
1815 // TreeRow is one entry of a directory.
1916 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file
21- Name string
22- Href string // "" for a submodule
17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
18+ Name string
19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
2323 Size string
2424 Subject string
2525 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
3535 Readme *Readme
3636 // Notes say what the listing leaves out.
3737 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
3841 }
3942
4043 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
5760 entries := res.Msg.GetEntries()
5861 now := s.cfg.Now()
5962
63+ rowsOnly := r.URL.Query().Has("rows")
6064 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
6265 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}
66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
6467 switch e.GetKind() {
6568 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"
69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
6770 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
6872 case pb.EntryKind_ENTRY_KIND_SYMLINK:
6973 row.Glyph = "@"
7074 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
7276 row.Glyph, row.Size = "+", "-"
7377 default:
7478 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
7879 }
7980 if c := e.GetLastCommit(); c != nil {
8081 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
8283 }
8384 p.Rows = append(p.Rows, row)
8485 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
8593 if res.Msg.GetTruncated() {
8694 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
8795 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96104 p.Readme = rd.readme
97105 }
98106
107+ l := s.repoLayout(ctx, d, "tree")
99108 pinRef(&l, t)
100109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101110 if t.path == "" {
@@ -12,14 +12,14 @@ import (
12 pb "bnema.dev/ermite-api/gen/ermite"12 pb "bnema.dev/ermite-api/gen/ermite"
13 )13 )
14 14
15-// maxFileTargets bounds the files of a directory offered in the ":" list.
16-const maxFileTargets = 200
17-
18 // TreeRow is one entry of a directory.15 // TreeRow is one entry of a directory.
19 type TreeRow struct {16 type TreeRow struct {
20- Glyph string // ▾ directory, @ symbolic link, + submodule, · file17+ Glyph string // ▸ directory, @ symbolic link, + submodule, · file
21- Name string18+ Name string
22- Href string // "" for a submodule19+ Href string // "" for a submodule
20+ // Expand is the URL of the rows of a directory (Href?rows); "" for any
21+ // other entry.
22+ Expand string
23 Size string23 Size string
24 Subject string24 Subject string
25 CHref string25 CHref string
@@ -35,6 +35,9 @@ type TreePage struct {
35 Readme *Readme35 Readme *Readme
36 // Notes say what the listing leaves out.36 // Notes say what the listing leaves out.
37 Notes []string37 Notes []string
38+ // More, for the rows of a directory expanded in place, is the page of the
39+ // directory when it has more entries than are listed; "" otherwise.
40+ More string
38 }41 }
39 42
40 // tree lists a directory of a ref, with the last commit of each entry, and43 // tree lists a directory of a ref, with the last commit of each entry, and
@@ -57,14 +60,15 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
57 entries := res.Msg.GetEntries()60 entries := res.Msg.GetEntries()
58 now := s.cfg.Now()61 now := s.cfg.Now()
59 62
63+ rowsOnly := r.URL.Query().Has("rows")
60 p := TreePage{}64 p := TreePage{}
61- l := s.repoLayout(ctx, d, "tree")
62 for i, e := range entries {65 for i, e := range entries {
63- row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0}66+ row := TreeRow{Glyph: "·", Name: visibleText(e.GetName()), Size: humanSize(e.GetSize()), Cursor: i == 0 && !rowsOnly}
64 switch e.GetKind() {67 switch e.GetKind() {
65 case pb.EntryKind_ENTRY_KIND_DIRECTORY:68 case pb.EntryKind_ENTRY_KIND_DIRECTORY:
66- row.Glyph, row.Name, row.Size = "▾", row.Name+"/", "-"69+ row.Glyph, row.Name, row.Size = "▸", row.Name+"/", "-"
67 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())70 row.Href = repoHref(d.name(), "tree", t.name, e.GetPath())
71+ row.Expand = row.Href + "?rows"
68 case pb.EntryKind_ENTRY_KIND_SYMLINK:72 case pb.EntryKind_ENTRY_KIND_SYMLINK:
69 row.Glyph = "@"73 row.Glyph = "@"
70 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
@@ -72,9 +76,6 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
72 row.Glyph, row.Size = "+", "-"76 row.Glyph, row.Size = "+", "-"
73 default:77 default:
74 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())78 row.Href = repoHref(d.name(), "blob", t.name, e.GetPath())
75- if len(l.Cmd) < maxFileTargets {
76- l.Cmd = append(l.Cmd, CmdItem{Kind: "file", Label: visibleText(e.GetPath()), Href: row.Href})
77- }
78 }79 }
79 if c := e.GetLastCommit(); c != nil {80 if c := e.GetLastCommit(); c != nil {
80 last := commitRow(d.name(), c, now)81 last := commitRow(d.name(), c, now)
@@ -82,6 +83,13 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
82 }83 }
83 p.Rows = append(p.Rows, row)84 p.Rows = append(p.Rows, row)
84 }85 }
86+ if rowsOnly {
87+ // The rows alone, for a directory opened in place (files.js).
88+ if res.Msg.GetTruncated() {
89+ p.More = repoHref(d.name(), "tree", t.name, t.path)
90+ }
91+ return TreeRows(p), http.StatusOK, nil
92+ }
85 if res.Msg.GetTruncated() {93 if res.Msg.GetTruncated() {
86 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")94 p.Notes = append(p.Notes, "this directory has more entries than are listed here: the first "+itoa(len(entries))+" are shown.")
87 }95 }
@@ -96,6 +104,7 @@ func (s *Server) tree(ctx context.Context, r *http.Request) (templ.Component, in
96 p.Readme = rd.readme104 p.Readme = rd.readme
97 }105 }
98 106
107+ l := s.repoLayout(ctx, d, "tree")
99 pinRef(&l, t)108 pinRef(&l, t)
100 l.Crumbs = pathCrumbs(d.name(), "tree", t)109 l.Crumbs = pathCrumbs(d.name(), "tree", t)
101 if t.path == "" {110 if t.path == "" {
M internal/adapters/web/tree.templ +24 -7
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
2121 <thead>
2222 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
2323 </thead>
24- <tbody id="tree-rows" data-rows>
25- for _, r := range p.Rows {
26- @TreeRowView(r)
27- }
24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25+ @TreeRows(p)
2826 </tbody>
2927 </table>
3028 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
3937 }
4038 }
4139
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic
43-// link, + submodule, · file.
40+// TreeRows is the rows of a directory alone: what files.js inserts under a
41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
4455 templ TreeRowView(r TreeRow) {
4556 <tr data-cursor?={ r.Cursor }>
4657 <td class="cur" aria-hidden="true"></td>
4758 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
4859 <td class="c-name">
4960 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>
61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
5168 } else {
5269 { r.Name }
5370 }
@@ -21,10 +21,8 @@ templ Tree(l Layout, p TreePage) {
21 <thead>21 <thead>
22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>22 <tr><th class="cur"></th><th class="c-kind"></th><th class="c-name">name</th><th class="c-size">size</th><th class="c-sub">last commit</th><th class="c-who">author</th><th class="c-when">date</th></tr>
23 </thead>23 </thead>
24- <tbody id="tree-rows" data-rows>24+ <tbody id="tree-rows" data-rows hx-on:click="ermiteTree.click(event)">
25- for _, r := range p.Rows {25+ @TreeRows(p)
26- @TreeRowView(r)
27- }
28 </tbody>26 </tbody>
29 </table>27 </table>
30 if len(p.Rows) == 0 {28 if len(p.Rows) == 0 {
@@ -39,15 +37,34 @@ templ Tree(l Layout, p TreePage) {
39 }37 }
40 }38 }
41 39
42-// TreeRowView is one entry of a directory. Glyph: ▾ directory, @ symbolic40+// TreeRows is the rows of a directory alone: what files.js inserts under a
43-// link, + submodule, · file.41+// directory it opens in place. When the directory has more entries than are
42+// listed, the last row links to its page.
43+templ TreeRows(p TreePage) {
44+ for _, r := range p.Rows {
45+ @TreeRowView(r)
46+ }
47+ if p.More != "" {
48+ <tr class="tree-more"><td colspan="7"><a href={ templ.SafeURL(p.More) }>more entries: open the directory</a></td></tr>
49+ }
50+}
51+
52+// TreeRowView is one entry of a directory. Glyph: ▸ directory (▾ once opened
53+// in place), @ symbolic link, + submodule, · file. A directory link carries
54+// data-expand, the URL of its rows.
44 templ TreeRowView(r TreeRow) {55 templ TreeRowView(r TreeRow) {
45 <tr data-cursor?={ r.Cursor }>56 <tr data-cursor?={ r.Cursor }>
46 <td class="cur" aria-hidden="true"></td>57 <td class="cur" aria-hidden="true"></td>
47 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>58 <td class="c-kind" aria-hidden="true">{ r.Glyph }</td>
48 <td class="c-name">59 <td class="c-name">
49 if r.Href != "" {60 if r.Href != "" {
50- <a href={ templ.SafeURL(r.Href) }>{ r.Name }</a>61+ <a
62+ href={ templ.SafeURL(r.Href) }
63+ if r.Expand != "" {
64+ data-expand={ r.Expand }
65+ aria-expanded="false"
66+ }
67+ >{ r.Name }</a>
51 } else {68 } else {
52 { r.Name }69 { r.Name }
53 }70 }
M internal/adapters/web/tree_templ.go +148 -73 generated file: collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 44

↑↓ moveenter openesc close