Skip to content

Commit b873d8c

Browse files
feat(web): apply feedback from #1878 (#1942)
1 parent a33ced2 commit b873d8c

File tree

3 files changed

+125
-26
lines changed

3 files changed

+125
-26
lines changed

packages/web/src/lib/Editor.svelte

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,35 @@ let linter: WorkerLinter;
1818
1919
// Live list of lints from the framework's lint callback
2020
let lints: UnpackedLint[] = [];
21-
let openIndex: number | null = null;
22-
23-
let lfw = new LintFramework(async (text) => {
24-
// Guard until the linter is ready
25-
if (!linter) return [];
26-
27-
const raw = await linter.lint(text);
28-
// The framework expects "unpacked" lints with plain fields
29-
const unpacked = await Promise.all(
30-
raw.map((lint) => unpackLint(window.location.hostname, lint, linter)),
31-
);
32-
33-
lints = unpacked;
34-
35-
return unpacked;
36-
}, {});
21+
// Track which lint cards are open by index
22+
let openSet: Set<number> = new Set();
23+
24+
let lfw = new LintFramework(
25+
async (text) => {
26+
if (!linter) return [];
27+
28+
const raw = await linter.lint(text);
29+
// The framework expects "unpacked" lints with plain fields
30+
const unpacked = await Promise.all(raw.map((lint) => unpackLint(text, lint, linter)));
31+
32+
lints = unpacked;
33+
34+
return unpacked;
35+
},
36+
{
37+
ignoreLint: async (hash: string) => {
38+
if (!linter) return;
39+
try {
40+
await linter.ignoreLintHash(BigInt(hash));
41+
console.log(`Ignored ${hash}`);
42+
// Re-run linting to hide ignored lint immediately
43+
lfw.update();
44+
} catch (e) {
45+
console.error('Failed to ignore lint', e);
46+
}
47+
},
48+
},
49+
);
3750
3851
(async () => {
3952
let { WorkerLinter, binary } = await import('harper.js');
@@ -74,6 +87,68 @@ function createSnippetFor(lint: UnpackedLint) {
7487
suffixEllipsis: end < content.length,
7588
};
7689
}
90+
91+
function jumpTo(lint: UnpackedLint) {
92+
if (!editor) return;
93+
const start = lint.span.start;
94+
const end = lint.span.end;
95+
// Focus and select; most browsers will scroll selection into view on focus
96+
editor.focus();
97+
editor.setSelectionRange(start, end);
98+
// As a fallback, nudge scroll to selection if needed
99+
try {
100+
const approxLineHeight = 20;
101+
const beforeText = content.slice(0, start);
102+
const line = (beforeText.match(/\n/g)?.length ?? 0) + 1;
103+
const targetTop = Math.max(0, (line - 3) * approxLineHeight);
104+
(editor as HTMLTextAreaElement).scrollTop = targetTop;
105+
} catch {}
106+
}
107+
108+
function toggleCard(i: number) {
109+
const wasOpen = openSet.has(i);
110+
if (wasOpen) {
111+
const ns = new Set(openSet);
112+
ns.delete(i);
113+
openSet = ns;
114+
} else {
115+
const ns = new Set(openSet);
116+
ns.add(i);
117+
openSet = ns;
118+
}
119+
}
120+
121+
$: allOpen = lints.length > 0 && openSet.size === lints.length;
122+
123+
function toggleAll() {
124+
if (allOpen) {
125+
openSet = new Set();
126+
} else {
127+
openSet = new Set(lints.map((_, i) => i));
128+
}
129+
}
130+
131+
async function ignoreAll() {
132+
if (!linter || lints.length === 0) return;
133+
try {
134+
const hashes = Array.from(new Set(lints.map((l) => l.context_hash)));
135+
await Promise.all(hashes.map((h) => linter.ignoreLintHash(BigInt(h))));
136+
// Refresh to hide ignored lints immediately
137+
lfw.update();
138+
} catch (e) {
139+
console.error('Failed to ignore all lints', e);
140+
}
141+
}
142+
143+
// Keep openSet in range if lint list changes
144+
$: if (openSet.size > 0) {
145+
const max = lints.length;
146+
const next = new Set<number>();
147+
for (const idx of openSet) {
148+
if (idx >= 0 && idx < max) next.add(idx);
149+
}
150+
if (next.size !== openSet.size) openSet = next;
151+
}
77152
</script>
78153

79154
<div class="flex flex-row h-full max-w-full">
@@ -86,7 +161,26 @@ function createSnippetFor(lint: UnpackedLint) {
86161
</Card>
87162

88163
<Card class="hidden md:flex md:flex-col md:w-1/3 h-full p-5 z-10">
89-
<div class="text-base font-semibold mb-3">Problems</div>
164+
<div class="flex items-center justify-between mb-3">
165+
<div class="text-base font-semibold">Problems</div>
166+
<div class="flex items-center gap-2">
167+
<button
168+
class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-[#0b0f14]"
169+
on:click={toggleAll}
170+
aria-label={allOpen ? 'Collapse all lint cards' : 'Open all lint cards'}
171+
>
172+
{allOpen ? 'Collapse all' : 'Open all'}
173+
</button>
174+
<button
175+
class="text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-[#0b0f14]"
176+
on:click={ignoreAll}
177+
disabled={lints.length === 0}
178+
aria-label="Ignore all current lints"
179+
>
180+
Ignore all
181+
</button>
182+
</div>
183+
</div>
90184
<div class="flex-1 overflow-y-auto pr-1">
91185
{#if lints.length === 0}
92186
<p class="text-sm text-gray-500">No lints yet.</p>
@@ -96,8 +190,9 @@ function createSnippetFor(lint: UnpackedLint) {
96190
<LintCard
97191
{lint}
98192
snippet={createSnippetFor(lint)}
99-
open={openIndex === i}
100-
onToggle={() => (openIndex = openIndex === i ? null : i)}
193+
open={openSet.has(i)}
194+
onToggleOpen={() => toggleCard(i)}
195+
focusError={() => jumpTo(lint)}
101196
onApply={(s) => applySug(lint, s)}
102197
/>
103198
{/each}

packages/web/src/lib/LintCard.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { slide } from 'svelte/transition';
44
55
export let lint: UnpackedLint;
66
export let open = false;
7-
export let onToggle: () => void;
7+
export let onToggleOpen: () => void;
8+
export let focusError: () => void;
89
export let onApply: (s: UnpackedSuggestion) => void;
910
export let snippet: {
1011
prefix: string;
@@ -19,14 +20,17 @@ function suggestionText(s: UnpackedSuggestion): string {
1920
}
2021
</script>
2122

22-
<div class="rounded-lg border border-gray-300 dark:border-gray-700 shadow-sm bg-white dark:bg-[#0d1117]">
23+
<div
24+
class="rounded-lg border border-gray-300 dark:border-gray-700 shadow-sm bg-white dark:bg-[#0d1117]"
25+
on:click={() => focusError?.()}
26+
>
2327
<div
2428
role="button"
2529
tabindex="0"
2630
class="flex items-center justify-between p-3 cursor-pointer select-none"
2731
aria-expanded={open}
28-
on:click={() => onToggle?.()}
29-
on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), onToggle?.())}
32+
on:click={() => onToggleOpen?.()}
33+
on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), onToggleOpen?.())}
3034
>
3135
<div
3236
class="text-sm font-semibold pb-1"

packages/web/src/routes/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ function agentHas(keyword) {
110110
>, so you can get fantastic grammar checking anywhere you work.
111111
<br /><br /> That said, we take extra care to make sure the
112112
<a href="/docs/integrations/visual-studio-code"
113-
>Install in Visual Studio Code</a
114-
>, <a href="/docs/integrations/neovim">Use in Neovim</a>,
115-
<a href="/docs/integrations/obsidian">Install in Obsidian</a>, and <a href="/docs/integrations/chrome-extension">Add to Chrome</a> integrations are amazing.
113+
>Visual Studio Code</a
114+
>, <a href="/docs/integrations/neovim">Neovim</a>,
115+
<a href="/docs/integrations/obsidian">Obsidian</a>, and <a href="/docs/integrations/chrome-extension">Chrome</a> extensions are amazing.
116116
</span>
117117

118118
<img

0 commit comments

Comments
 (0)