@@ -18,22 +18,35 @@ let linter: WorkerLinter;
1818
1919// Live list of lints from the framework's lint callback
2020let 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 }
0 commit comments