@@ -55,6 +55,8 @@ export function highlightOccurrenceExtension(plugin: OccuraPlugin) {
5555 lastEnabled = plugin . settings . occuraPluginEnabled ;
5656 lastAutoHL = plugin . settings . autoKeywordsHighlightEnabled ;
5757
58+ private groupDecoCache = new Map < string , Decoration > ( ) ;
59+
5860 constructor ( public view : EditorView ) {
5961 this . decorations = this . buildDecorations ( ) ;
6062 }
@@ -65,34 +67,41 @@ export function highlightOccurrenceExtension(plugin: OccuraPlugin) {
6567 u . docChanged ||
6668 u . viewportChanged ||
6769 plugin . settings . occuraPluginEnabled !== this . lastEnabled ||
68- plugin . settings . autoKeywordsHighlightEnabled !==
69- this . lastAutoHL
70+ plugin . settings . autoKeywordsHighlightEnabled !== this . lastAutoHL
7071 ) {
7172 this . decorations = this . buildDecorations ( ) ;
7273 }
7374 }
7475
75- /* Scan the visible ranges and prepare the decoration set */
76+ private getGroupDecoration ( groupId : string ) : Decoration {
77+ let deco = this . groupDecoCache . get ( groupId ) ;
78+ if ( ! deco ) {
79+ deco = Decoration . mark ( {
80+ class : `occura-kw-${ groupId } ` ,
81+ priority : 50 ,
82+ } ) ;
83+ this . groupDecoCache . set ( groupId , deco ) ;
84+ }
85+ return deco ;
86+ }
87+
7688 private buildDecorations ( ) {
7789 this . lastEnabled = plugin . settings . occuraPluginEnabled ;
7890 this . lastAutoHL = plugin . settings . autoKeywordsHighlightEnabled ;
7991
80- const { state } = this . view ;
81- const builder = new RangeSetBuilder < Decoration > ( ) ;
92+ const matches : { from : number ; to : number ; deco : Decoration ; startSide : number } [ ] = [ ] ;
93+ const addedSpans = new Set < string > ( ) ;
8294 foundCount = 0 ;
8395
84- /* --- highlight the currently selected text --- */
96+ /* --- selected text occurrences --- */
8597 if ( plugin . settings . occuraPluginEnabled ) {
98+ const { state } = this . view ;
8699 const sel = state . selection . main ;
87100 if ( ! sel . empty ) {
88101 const txt = state . doc . sliceString ( sel . from , sel . to ) . trim ( ) ;
89102 if ( txt && ! / \s / . test ( txt ) ) {
90- const re = buildRegex (
91- txt ,
92- plugin . settings . occuraCaseSensitive ,
93- /*wholeWord*/ false ,
94- ) ;
95- this . searchVisibleRanges ( re , selectedTextDecoration , builder ) ;
103+ const re = buildRegex ( txt , plugin . settings . occuraCaseSensitive , false ) ;
104+ this . collectVisibleMatches ( re , selectedTextDecoration , matches , addedSpans ) ;
96105 this . updateStatusBar ( txt ) ;
97106 } else {
98107 this . updateStatusBar ( '' ) ;
@@ -102,52 +111,64 @@ export function highlightOccurrenceExtension(plugin: OccuraPlugin) {
102111 }
103112 }
104113
105- /* --- highlight keywords from the user list --- */
114+ /* --- class-based keywords --- */
106115 if (
107116 plugin . settings . occuraPluginEnabled &&
108- plugin . settings . autoKeywordsHighlightEnabled
117+ plugin . settings . autoKeywordsHighlightEnabled &&
118+ Array . isArray ( plugin . settings . keywordGroups )
109119 ) {
110- const words = plugin . settings . keywords
111- . map ( k => k . trim ( ) )
112- . filter ( k => k !== '' ) ;
113-
114- if ( words . length ) {
115- const regexes = words . map ( word =>
116- buildRegex (
117- word ,
118- plugin . settings . keywordsCaseSensitive ,
119- /*wholeWord*/ true ,
120- ) ,
121- ) ;
122- regexes . forEach ( re =>
123- this . searchVisibleRanges ( re , keywordDecoration , builder ) ,
124- ) ;
120+ for ( const group of plugin . settings . keywordGroups ) {
121+ if ( ! group ?. enabled ) continue ;
122+
123+ const words = ( group . keywords ?? [ ] )
124+ . map ( w => w . trim ( ) )
125+ . filter ( Boolean ) ;
126+
127+ if ( words . length === 0 ) continue ;
128+
129+ const deco = this . getGroupDecoration ( group . id ) ;
130+ for ( const w of words ) {
131+ const re = buildRegex ( w , ! ! group . caseSensitive , true ) ;
132+ this . collectVisibleMatches ( re , deco , matches , addedSpans ) ;
133+ }
125134 }
126135 }
127136
137+ /* --- sort THEN add to builder --- */
138+ matches . sort ( ( a , b ) => ( a . from - b . from ) || ( a . startSide - b . startSide ) || ( a . to - b . to ) ) ;
139+
140+ const builder = new RangeSetBuilder < Decoration > ( ) ;
141+ for ( const m of matches ) builder . add ( m . from , m . to , m . deco ) ;
142+
128143 return builder . finish ( ) ;
129144 }
130145
131- /* Search every visible range with `re` and add decorations to `builder` . */
132- private searchVisibleRanges (
146+ /** Collect matches (do not add directly). Keeps them sorted later . */
147+ private collectVisibleMatches (
133148 re : RegExp ,
134149 deco : Decoration ,
135- builder : RangeSetBuilder < Decoration > ,
150+ out : { from : number ; to : number ; deco : Decoration ; startSide : number } [ ] ,
151+ addedSpans : Set < string > ,
136152 ) {
153+ const startSide = ( deco as any ) ?. spec ?. startSide ?? 0 ; // default 0
137154 const { state } = this . view ;
155+
138156 for ( const { from, to } of this . view . visibleRanges ) {
139157 const text = state . doc . sliceString ( from , to ) ;
158+ re . lastIndex = 0 ; // important for /g
140159 let m : RegExpExecArray | null ;
141160 while ( ( m = re . exec ( text ) ) ) {
142- const start = from + m . index ;
143- const end = start + m [ 0 ] . length ;
144- builder . add ( start , end , deco ) ;
161+ const s = from + m . index ;
162+ const e = s + m [ 0 ] . length ;
163+ const key = `${ s } :${ e } ` ;
164+ if ( addedSpans . has ( key ) ) continue ; // avoid duplicate exact spans
165+ addedSpans . add ( key ) ;
166+ out . push ( { from : s , to : e , deco, startSide } ) ;
145167 foundCount ++ ;
146168 }
147169 }
148170 }
149171
150- /* Write “Occura found: …” into the status bar (or clear it). */
151172 private updateStatusBar ( message : string ) {
152173 if (
153174 plugin . statusBarOccurrencesNumber &&
0 commit comments