commit 7f0d3892d8c1c3402aa9d760184b5ad002788dbb
Author: tri <tri@thac.loan>
Date:   Sun Oct 5 15:56:47 2025 +0700

    shift-click line num to select range

diff --git a/editor-integrations/vim/plugin/khoe.vim b/editor-integrations/vim/plugin/khoe.vim
index 30eb43f..820dce6 100644
--- a/editor-integrations/vim/plugin/khoe.vim
+++ b/editor-integrations/vim/plugin/khoe.vim
@@ -19,6 +19,9 @@ function! KhoeHandler(opts)
 
     if a:opts.line1
         let l:url = l:url . '#L' . a:opts.line1
+        if a:opts.line2
+            let l:url = l:url . '-' . a:opts.line2
+        endif
     endif
 
     return l:url
diff --git a/src/assets/script.js b/src/assets/script.js
index 6d3bc13..d81afb4 100644
--- a/src/assets/script.js
+++ b/src/assets/script.js
@@ -55,4 +55,46 @@ document.addEventListener("DOMContentLoaded", function () {
     const relativeTimeStr = formatRelativeTime(timeStr);
     element.innerHTML = relativeTimeStr;
   });
+
+  // Shift-click to select a line range
+  const SELECTED_RANGE = "selected-range";
+  document.querySelectorAll(".line-number > a").forEach((el) => {
+    el.addEventListener("click", (evt) => {
+      if (evt.shiftKey && location.hash.match("^#L[0-9]+$")) {
+        evt.preventDefault();
+        document.querySelectorAll("." + SELECTED_RANGE).forEach((el) => {
+          el.classList.remove(SELECTED_RANGE);
+        });
+
+        const line1 = new Number(location.hash.slice(2));
+        const line2 = new Number(evt.target.innerHTML);
+        const [startLineNum, endLineNum] = [line1, line2].sort((a, b) => a - b);
+
+        for (let i = startLineNum; i <= endLineNum; i++) {
+          const selector = `a[href="#L${i}"]`;
+          const el = document.querySelector(selector);
+          el.parentElement.parentElement.classList.add(SELECTED_RANGE);
+        }
+
+        document.location.hash = `#L${startLineNum}-${endLineNum}`;
+      } else {
+        document.querySelectorAll("." + SELECTED_RANGE).forEach((el) => {
+          el.classList.remove(SELECTED_RANGE);
+        });
+      }
+    });
+  });
+
+  // On page load, focus on and scroll to selected range
+  const selectedRange = location.hash.match("^#L([0-9]+)-([0-9]+)$");
+  if (selectedRange) {
+    const startLineNum = new Number(selectedRange[1]);
+    const endLineNum = new Number(selectedRange[2]);
+    for (let i = startLineNum; i <= endLineNum; i++) {
+      const selector = `a[href="#L${i}"]`;
+      const el = document.querySelector(selector);
+      el.parentElement.parentElement.classList.add(SELECTED_RANGE);
+    }
+    document.querySelector(`#L${startLineNum}`).scrollIntoView();
+  }
 });
diff --git a/src/assets/style.css b/src/assets/style.css
index 0c2643f..8be0b4b 100644
--- a/src/assets/style.css
+++ b/src/assets/style.css
@@ -233,7 +233,8 @@ table.text-blob-content {
     user-select: none;
   }
 
-  tr:has(a:target) {
+  tr:has(a:target),
+  tr.selected-range {
     background-color: var(--selected-line-bg);
   }
 }