Skip to content

Commit c3ca526

Browse files
committed
Release 1.5.0: Words Classes
1 parent f88bae7 commit c3ca526

File tree

5 files changed

+251
-146
lines changed

5 files changed

+251
-146
lines changed

main.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,29 @@ export default class OccuraPlugin extends Plugin {
185185

186186
// Method to dynamic update the highlight style based on settings
187187
updateHighlightStyle() {
188-
if (this.styleEl) {
189-
this.styleEl.remove();
190-
}
188+
// remove previous style tag
189+
if (this.styleEl) this.styleEl.remove();
190+
191+
const groups = Array.isArray(this.settings.keywordGroups)
192+
? this.settings.keywordGroups
193+
: [];
194+
195+
const css =
196+
[
197+
// selection highlight
198+
`.found-occurrence { background-color: ${this.settings.highlightColorOccurrences}; }`,
199+
// per-class colors (works whether you include 'keyword-occurrence' or not)
200+
...groups.map(g => `.occura-kw-${g.id} { background-color: ${g.color} !important; }`),
201+
...groups.map(g => `.keyword-occurrence.occura-kw-${g.id} { background-color: ${g.color} !important; }`),
202+
].join('\n');
203+
191204
this.styleEl = document.createElement('style');
192-
this.styleEl.textContent = `
193-
.found-occurrence {background-color: ${this.settings.highlightColorOccurrences};}
194-
.keyword-occurrence { background-color: ${this.settings.highlightColorKeywords}; }
195-
`;
205+
this.styleEl.id = 'occura-style';
206+
this.styleEl.textContent = css;
196207
document.head.appendChild(this.styleEl);
197208
}
198209

210+
199211
// Force all editors to update
200212
updateEditors() {
201213
const markdownViews = this.app.workspace.getLeavesOfType('markdown')

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "occura-word-highlighter",
33
"name": "Occura",
4-
"version": "1.4.1",
4+
"version": "1.5.0",
55
"minAppVersion": "0.15.0",
66
"description": "Find and highlight all occurrences of selected text in notes, similar to Notepad++ or IDEs.",
77
"author": "Alexey Sedoykin",

src/highlighter.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)