@singapore-editor/spellcheck 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +106 -0
  2. package/THIRD_PARTY_NOTICES +611 -0
  3. package/dist/assets/spellcheck.worker-Dx-kt277.js +8971 -0
  4. package/dist/assets/spellcheck.worker-Dx-kt277.js.map +1 -0
  5. package/dist/controller.d.ts +51 -0
  6. package/dist/controller.d.ts.map +1 -0
  7. package/dist/controller.js +307 -0
  8. package/dist/controller.js.map +1 -0
  9. package/dist/dictionaryAssets.d.ts +4 -0
  10. package/dist/dictionaryAssets.d.ts.map +1 -0
  11. package/dist/dictionaryData.d.ts +13 -0
  12. package/dist/dictionaryData.d.ts.map +1 -0
  13. package/dist/engine.d.ts +18 -0
  14. package/dist/engine.d.ts.map +1 -0
  15. package/dist/feature.d.ts +19 -0
  16. package/dist/feature.d.ts.map +1 -0
  17. package/dist/feature.js +8 -0
  18. package/dist/feature.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +5 -0
  22. package/dist/plugin.d.ts +12 -0
  23. package/dist/plugin.d.ts.map +1 -0
  24. package/dist/plugin.js +27 -0
  25. package/dist/plugin.js.map +1 -0
  26. package/dist/proseRanges.d.ts +25 -0
  27. package/dist/proseRanges.d.ts.map +1 -0
  28. package/dist/proseRanges.js +75 -0
  29. package/dist/proseRanges.js.map +1 -0
  30. package/dist/protocol.d.ts +27 -0
  31. package/dist/protocol.d.ts.map +1 -0
  32. package/dist/service.d.ts +38 -0
  33. package/dist/service.d.ts.map +1 -0
  34. package/dist/service.js +128 -0
  35. package/dist/service.js.map +1 -0
  36. package/dist/spellcheck.worker.d.ts +2 -0
  37. package/dist/spellcheck.worker.d.ts.map +1 -0
  38. package/dist/styles.d.ts +7 -0
  39. package/dist/styles.d.ts.map +1 -0
  40. package/dist/styles.js +13 -0
  41. package/dist/styles.js.map +1 -0
  42. package/dist/tokenizer.d.ts +23 -0
  43. package/dist/tokenizer.d.ts.map +1 -0
  44. package/dist/tokenizer.js +140 -0
  45. package/dist/tokenizer.js.map +1 -0
  46. package/package.json +52 -0
@@ -0,0 +1,51 @@
1
+ import type { EditorDisposable, EditorEditContributionContext, EditorViewContributionContext, EditorViewContributionUpdateKind, EditorViewSnapshot } from '@singapore-editor/core/extensions';
2
+ import type { EditorSpellcheckFeature } from './feature';
3
+ import { type SpellcheckScope } from './proseRanges';
4
+ import type { SpellcheckService } from './service';
5
+ export type SpellcheckChecker = Pick<SpellcheckService, 'check' | 'suggest' | 'isAccepted' | 'setAcceptedWords' | 'onDidChangeAcceptedWords'>;
6
+ /**
7
+ * One editor's spellcheck. Marks are always derived from the current text: the worker answers with
8
+ * verdicts per word, never with offsets, so a reply that lands after an edit cannot paint stale ranges.
9
+ */
10
+ export declare class SpellcheckController {
11
+ private readonly checker;
12
+ private readonly scope;
13
+ private view;
14
+ private edit;
15
+ private highlightName;
16
+ private readonly verdicts;
17
+ private readonly pending;
18
+ private readonly lines;
19
+ private issues;
20
+ private held;
21
+ private paintedKey;
22
+ private recheckInputs;
23
+ private dictionaryVersion;
24
+ private verdictVersion;
25
+ private wordsKey;
26
+ private wordsCaptures;
27
+ private cachedWords;
28
+ private failed;
29
+ private disposed;
30
+ constructor(checker: SpellcheckChecker, scope: SpellcheckScope);
31
+ attachView(context: EditorViewContributionContext): EditorDisposable;
32
+ attachEdit(context: EditorEditContributionContext): EditorDisposable;
33
+ feature(): EditorSpellcheckFeature;
34
+ update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void;
35
+ private refresh;
36
+ private recheck;
37
+ /** Selection and scroll updates outnumber edits; they reuse the words while the window holds. */
38
+ private windowWords;
39
+ private words;
40
+ private proseWords;
41
+ private lineWords;
42
+ private request;
43
+ private settle;
44
+ /** A failed worker is not asked again by this editor: every keystroke would start another. */
45
+ private fail;
46
+ /** Every issue in the window answers `issueAt`; only those on mounted rows are painted. */
47
+ private paint;
48
+ private issueAt;
49
+ private replace;
50
+ }
51
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,gCAAgC,EAChC,kBAAkB,EACnB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,KAAK,EAAE,uBAAuB,EAAc,MAAM,WAAW,CAAA;AACpE,OAAO,EAA6C,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAUlD,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAClC,iBAAiB,EACjB,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,kBAAkB,GAAG,0BAA0B,CACrF,CAAA;AAcD;;;GAGG;AACH,qBAAa,oBAAoB;IAoB7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IApBxB,OAAO,CAAC,IAAI,CAA6C;IACzD,OAAO,CAAC,IAAI,CAA6C;IACzD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAQ;gBAGL,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,eAAe;IAGlC,UAAU,CAAC,OAAO,EAAE,6BAA6B,GAAG,gBAAgB;IAsBpE,UAAU,CAAC,OAAO,EAAE,6BAA6B,GAAG,gBAAgB;IAKpE,OAAO,IAAI,uBAAuB;IAYlC,MAAM,CAAC,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,gCAAgC,GAAG,IAAI;IAUzF,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAgCf,iGAAiG;IACjG,OAAO,CAAC,WAAW;IA8BnB,OAAO,CAAC,KAAK;IAoBb,OAAO,CAAC,UAAU;IAiBlB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,OAAO;IAiBf,OAAO,CAAC,MAAM;IASd,8FAA8F;IAC9F,OAAO,CAAC,IAAI;IAYZ,2FAA2F;IAC3F,OAAO,CAAC,KAAK;IAcb,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;CAWhB"}
@@ -0,0 +1,307 @@
1
+ import { spellcheckRegions } from "./proseRanges.js";
2
+ import { SPELLING_STYLE } from "./styles.js";
3
+ import { foldApostrophes, tokenizeSpellWords } from "./tokenizer.js";
4
+ //#region src/controller.ts
5
+ var DEFAULT_SUGGESTIONS = 5;
6
+ var MAX_CACHED_LINES = 4096;
7
+ var RECHECK_KINDS = /* @__PURE__ */ new Set([
8
+ "document",
9
+ "content",
10
+ "tokens",
11
+ "selection",
12
+ "viewport",
13
+ "layout"
14
+ ]);
15
+ /**
16
+ * One editor's spellcheck. Marks are always derived from the current text: the worker answers with
17
+ * verdicts per word, never with offsets, so a reply that lands after an edit cannot paint stale ranges.
18
+ */
19
+ var SpellcheckController = class {
20
+ checker;
21
+ scope;
22
+ view = null;
23
+ edit = null;
24
+ highlightName = "";
25
+ verdicts = /* @__PURE__ */ new Map();
26
+ pending = /* @__PURE__ */ new Set();
27
+ lines = /* @__PURE__ */ new Map();
28
+ issues = [];
29
+ held = [];
30
+ paintedKey = "";
31
+ recheckInputs = "";
32
+ dictionaryVersion = 0;
33
+ verdictVersion = 0;
34
+ wordsKey = "";
35
+ wordsCaptures = null;
36
+ cachedWords = [];
37
+ failed = false;
38
+ disposed = false;
39
+ constructor(checker, scope) {
40
+ this.checker = checker;
41
+ this.scope = scope;
42
+ }
43
+ attachView(context) {
44
+ this.view = context;
45
+ this.highlightName = `${context.highlightPrefix}-spelling`;
46
+ const captures = context.requestSyntaxCaptures();
47
+ const accepted = this.checker.onDidChangeAcceptedWords(() => {
48
+ this.dictionaryVersion += 1;
49
+ this.verdicts.clear();
50
+ this.pending.clear();
51
+ this.verdictVersion += 1;
52
+ this.refresh();
53
+ });
54
+ return { dispose: () => {
55
+ captures.dispose();
56
+ accepted.dispose();
57
+ context.clearRangeHighlight(this.highlightName);
58
+ this.view = null;
59
+ this.disposed = true;
60
+ } };
61
+ }
62
+ attachEdit(context) {
63
+ this.edit = context;
64
+ return { dispose: () => this.edit = null };
65
+ }
66
+ feature() {
67
+ return {
68
+ issueAt: (offset) => this.issueAt(offset),
69
+ suggestions: (offset, limit = DEFAULT_SUGGESTIONS) => {
70
+ const issue = this.issueAt(offset);
71
+ return issue ? this.checker.suggest(issue.word, limit) : Promise.resolve([]);
72
+ },
73
+ replace: (offset, word) => this.replace(offset, word),
74
+ setAcceptedWords: (words) => this.checker.setAcceptedWords(words)
75
+ };
76
+ }
77
+ update(snapshot, kind) {
78
+ if (kind === "clear") {
79
+ this.paint([], null);
80
+ return;
81
+ }
82
+ if (!RECHECK_KINDS.has(kind)) return;
83
+ if (kind === "document") this.held = [];
84
+ this.recheck(snapshot, kind === "content");
85
+ }
86
+ refresh() {
87
+ if (this.view) this.recheck(this.view.getSnapshot(), false);
88
+ }
89
+ recheck(snapshot, edited) {
90
+ const context = this.view;
91
+ if (!context || !context.hasDocument()) return this.paint([], null);
92
+ const window = checkWindow(snapshot);
93
+ const words = window ? this.windowWords(snapshot, context, window) : [];
94
+ if (!words) {
95
+ this.recheckInputs = "";
96
+ return this.paint([], null);
97
+ }
98
+ const carets = caretOffsets(snapshot);
99
+ const mounted = mountedRange(snapshot);
100
+ const inputs = `${this.wordsKey}|${carets.join(",")}|${mounted?.start}:${mounted?.end}|${this.verdictVersion}`;
101
+ if (!edited && inputs === this.recheckInputs) return;
102
+ this.recheckInputs = inputs;
103
+ this.held = edited ? wordsAtCarets(words, carets) : stillHeld(this.held, carets);
104
+ const issues = [];
105
+ const unknown = /* @__PURE__ */ new Set();
106
+ for (const word of words) {
107
+ const verdict = this.verdicts.get(word.word);
108
+ if (verdict === false) continue;
109
+ if (isHeld(word, this.held)) continue;
110
+ if (verdict === void 0) unknown.add(word.word);
111
+ else if (!this.checker.isAccepted(word.word)) issues.push(word);
112
+ }
113
+ this.paint(issues, mounted);
114
+ this.request(unknown);
115
+ }
116
+ /** Selection and scroll updates outnumber edits; they reuse the words while the window holds. */
117
+ windowWords(snapshot, context, window) {
118
+ const captures = context.getSyntaxCaptures();
119
+ const replacements = context.getInlineReplacementRanges();
120
+ const key = [
121
+ snapshot.languageId,
122
+ snapshot.textVersion,
123
+ window.start,
124
+ window.end,
125
+ ...replacements.flatMap((range) => [range.start, range.end])
126
+ ].join(",");
127
+ if (key === this.wordsKey && captures === this.wordsCaptures) return this.cachedWords;
128
+ const regions = spellcheckRegions({
129
+ languageId: snapshot.languageId,
130
+ captures,
131
+ window,
132
+ scope: this.scope
133
+ });
134
+ if (!regions) return null;
135
+ this.recheckInputs = "";
136
+ this.cachedWords = this.words(snapshot, regions, replacements);
137
+ this.wordsKey = key;
138
+ this.wordsCaptures = captures;
139
+ return this.cachedWords;
140
+ }
141
+ words(snapshot, regions, replacements) {
142
+ const text = snapshot.textSnapshot;
143
+ const excluded = [...regions.excluded, ...replacements];
144
+ const words = [];
145
+ for (const region of regions.prose) this.proseWords(text, region, excluded, words);
146
+ for (const region of regions.code) {
147
+ if (region.end - region.start > 16384) continue;
148
+ const source = text.readRange(region.start, region.end);
149
+ const local = localRanges(excluded, region);
150
+ for (const word of tokenizeSpellWords(source, {
151
+ mode: "code",
152
+ excluded: local
153
+ })) words.push(shifted(word, region.start));
154
+ }
155
+ return words;
156
+ }
157
+ proseWords(text, region, excluded, words) {
158
+ const firstLine = text.lineAt(region.start);
159
+ const lastLine = text.lineAt(region.end);
160
+ for (let line = firstLine; line <= lastLine; line++) {
161
+ const range = text.lineRange(line);
162
+ if (range.end - range.start > 16384) continue;
163
+ const local = localRanges(excluded, range);
164
+ const lineWords = this.lineWords(text.readRange(range.start, range.end), local);
165
+ for (const word of lineWords) words.push(shifted(word, range.start));
166
+ }
167
+ }
168
+ lineWords(line, excluded) {
169
+ if (excluded.length > 0) return tokenizeSpellWords(line, { excluded });
170
+ const cached = this.lines.get(line);
171
+ if (cached) return cached;
172
+ if (this.lines.size >= MAX_CACHED_LINES) this.lines.clear();
173
+ const words = tokenizeSpellWords(line);
174
+ this.lines.set(line, words);
175
+ return words;
176
+ }
177
+ request(unknown) {
178
+ if (this.failed) return;
179
+ const words = [...unknown].filter((word) => !this.pending.has(word));
180
+ if (words.length === 0) return;
181
+ for (const word of words) this.pending.add(word);
182
+ const dictionaryVersion = this.dictionaryVersion;
183
+ this.checker.check(words).then((misspelled) => {
184
+ if (dictionaryVersion === this.dictionaryVersion) this.settle(words, new Set(misspelled));
185
+ }, (error) => {
186
+ if (dictionaryVersion === this.dictionaryVersion) this.fail(words, error);
187
+ });
188
+ }
189
+ settle(words, misspelled) {
190
+ for (const word of words) {
191
+ this.pending.delete(word);
192
+ this.verdicts.set(word, misspelled.has(word));
193
+ }
194
+ this.verdictVersion += 1;
195
+ if (!this.disposed) this.refresh();
196
+ }
197
+ /** A failed worker is not asked again by this editor: every keystroke would start another. */
198
+ fail(words, error) {
199
+ for (const word of words) this.pending.delete(word);
200
+ if (this.disposed || this.failed) return;
201
+ this.failed = true;
202
+ this.view?.log({
203
+ action: "spellcheck.check",
204
+ level: "warn",
205
+ message: "Spellcheck stopped: the dictionary worker failed",
206
+ reason: error instanceof Error ? error.message : String(error)
207
+ });
208
+ }
209
+ /** Every issue in the window answers `issueAt`; only those on mounted rows are painted. */
210
+ paint(issues, mounted) {
211
+ this.issues = issues;
212
+ const view = this.view;
213
+ if (!view) return;
214
+ const painted = mounted ? issues.filter((issue) => issue.end > mounted.start && issue.start < mounted.end) : [];
215
+ const key = painted.map((issue) => `${issue.start}:${issue.end}`).join(",");
216
+ if (key === this.paintedKey) return;
217
+ this.paintedKey = key;
218
+ if (painted.length === 0) view.clearRangeHighlight(this.highlightName);
219
+ else view.setRangeHighlight(this.highlightName, painted, SPELLING_STYLE);
220
+ }
221
+ issueAt(offset) {
222
+ return this.issues.find((issue) => issue.start <= offset && offset <= issue.end) ?? null;
223
+ }
224
+ replace(offset, word) {
225
+ const issue = this.issueAt(offset);
226
+ const edit = this.edit;
227
+ if (!issue || !edit) return false;
228
+ const text = edit.getTextSnapshot();
229
+ if (!text || foldApostrophes(text.readRange(issue.start, issue.end)) !== issue.word) return false;
230
+ edit.applyEdits([{
231
+ from: issue.start,
232
+ to: issue.end,
233
+ text: word
234
+ }], "spellcheck.replace");
235
+ return true;
236
+ }
237
+ };
238
+ function mountedRange(snapshot) {
239
+ let start = Number.POSITIVE_INFINITY;
240
+ let end = -1;
241
+ for (const row of snapshot.visibleRows) {
242
+ if (row.source !== "document") continue;
243
+ start = Math.min(start, row.startOffset);
244
+ end = Math.max(end, row.endOffset);
245
+ }
246
+ return end < 0 ? null : {
247
+ start,
248
+ end
249
+ };
250
+ }
251
+ /** The mounted rows plus one screen above and below them. */
252
+ function checkWindow(snapshot) {
253
+ let first = Number.POSITIVE_INFINITY;
254
+ let last = -1;
255
+ for (const row of snapshot.visibleRows) {
256
+ if (row.source !== "document") continue;
257
+ first = Math.min(first, row.bufferRow);
258
+ last = Math.max(last, row.bufferRow);
259
+ }
260
+ if (last < 0) return null;
261
+ const text = snapshot.textSnapshot;
262
+ const span = last - first + 1;
263
+ const startLine = Math.max(0, first - span);
264
+ const endLine = Math.min(text.lineCount - 1, last + span);
265
+ return {
266
+ start: text.lineStart(startLine),
267
+ end: text.lineRange(endLine).end
268
+ };
269
+ }
270
+ function caretOffsets(snapshot) {
271
+ return snapshot.selections.filter((selection) => selection.startOffset === selection.endOffset).map((selection) => selection.headOffset);
272
+ }
273
+ function containsCaret(range, carets) {
274
+ return carets.some((caret) => range.start <= caret && caret <= range.end);
275
+ }
276
+ /** The words being typed: held back until the caret leaves them. */
277
+ function wordsAtCarets(words, carets) {
278
+ return words.filter((word) => containsCaret(word, carets));
279
+ }
280
+ function stillHeld(held, carets) {
281
+ return held.filter((range) => containsCaret(range, carets));
282
+ }
283
+ function isHeld(word, held) {
284
+ return held.some((range) => range.start === word.start && range.end === word.end);
285
+ }
286
+ function localRanges(ranges, within) {
287
+ const local = [];
288
+ for (const range of ranges) {
289
+ if (range.end <= within.start || range.start >= within.end) continue;
290
+ local.push({
291
+ start: range.start - within.start,
292
+ end: range.end - within.start
293
+ });
294
+ }
295
+ return local;
296
+ }
297
+ function shifted(word, by) {
298
+ return {
299
+ start: word.start + by,
300
+ end: word.end + by,
301
+ word: word.word
302
+ };
303
+ }
304
+ //#endregion
305
+ export { SpellcheckController };
306
+
307
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","names":[],"sources":["../src/controller.ts"],"sourcesContent":["import type {\n EditorDisposable,\n EditorEditContributionContext,\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport type { EditorSpellcheckFeature, SpellIssue } from './feature'\nimport { spellcheckRegions, type SpellcheckRegions, type SpellcheckScope } from './proseRanges'\nimport type { SpellcheckService } from './service'\nimport { SPELLING_STYLE } from './styles'\nimport {\n foldApostrophes,\n MAX_SPELLCHECK_LINE_LENGTH,\n tokenizeSpellWords,\n type SpellTextRange,\n type SpellWord,\n} from './tokenizer'\n\nexport type SpellcheckChecker = Pick<\n SpellcheckService,\n 'check' | 'suggest' | 'isAccepted' | 'setAcceptedWords' | 'onDidChangeAcceptedWords'\n>\n\nconst DEFAULT_SUGGESTIONS = 5\n// Lines keyed by their text: typing re-tokenizes the line it changes, never the whole window.\nconst MAX_CACHED_LINES = 4096\nconst RECHECK_KINDS: ReadonlySet<EditorViewContributionUpdateKind> = new Set([\n 'document',\n 'content',\n 'tokens',\n 'selection',\n 'viewport',\n 'layout',\n])\n\n/**\n * One editor's spellcheck. Marks are always derived from the current text: the worker answers with\n * verdicts per word, never with offsets, so a reply that lands after an edit cannot paint stale ranges.\n */\nexport class SpellcheckController {\n private view: EditorViewContributionContext | null = null\n private edit: EditorEditContributionContext | null = null\n private highlightName = ''\n private readonly verdicts = new Map<string, boolean>()\n private readonly pending = new Set<string>()\n private readonly lines = new Map<string, readonly SpellWord[]>()\n private issues: readonly SpellIssue[] = []\n private held: readonly SpellTextRange[] = []\n private paintedKey = ''\n private recheckInputs = ''\n private dictionaryVersion = 0\n private verdictVersion = 0\n private wordsKey = ''\n private wordsCaptures: readonly unknown[] | null = null\n private cachedWords: readonly SpellWord[] = []\n private failed = false\n private disposed = false\n\n public constructor(\n private readonly checker: SpellcheckChecker,\n private readonly scope: SpellcheckScope,\n ) {}\n\n public attachView(context: EditorViewContributionContext): EditorDisposable {\n this.view = context\n this.highlightName = `${context.highlightPrefix}-spelling`\n const captures = context.requestSyntaxCaptures()\n const accepted = this.checker.onDidChangeAcceptedWords(() => {\n this.dictionaryVersion += 1\n this.verdicts.clear()\n this.pending.clear()\n this.verdictVersion += 1\n this.refresh()\n })\n return {\n dispose: () => {\n captures.dispose()\n accepted.dispose()\n context.clearRangeHighlight(this.highlightName)\n this.view = null\n this.disposed = true\n },\n }\n }\n\n public attachEdit(context: EditorEditContributionContext): EditorDisposable {\n this.edit = context\n return { dispose: () => (this.edit = null) }\n }\n\n public feature(): EditorSpellcheckFeature {\n return {\n issueAt: (offset) => this.issueAt(offset),\n suggestions: (offset, limit = DEFAULT_SUGGESTIONS) => {\n const issue = this.issueAt(offset)\n return issue ? this.checker.suggest(issue.word, limit) : Promise.resolve([])\n },\n replace: (offset, word) => this.replace(offset, word),\n setAcceptedWords: (words) => this.checker.setAcceptedWords(words),\n }\n }\n\n public update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void {\n if (kind === 'clear') {\n this.paint([], null)\n return\n }\n if (!RECHECK_KINDS.has(kind)) return\n if (kind === 'document') this.held = []\n this.recheck(snapshot, kind === 'content')\n }\n\n private refresh(): void {\n if (this.view) this.recheck(this.view.getSnapshot(), false)\n }\n\n private recheck(snapshot: EditorViewSnapshot, edited: boolean): void {\n const context = this.view\n if (!context || !context.hasDocument()) return this.paint([], null)\n\n const window = checkWindow(snapshot)\n const words = window ? this.windowWords(snapshot, context, window) : []\n if (!words) {\n this.recheckInputs = ''\n return this.paint([], null)\n }\n\n const carets = caretOffsets(snapshot)\n const mounted = mountedRange(snapshot)\n const inputs = `${this.wordsKey}|${carets.join(',')}|${mounted?.start}:${mounted?.end}|${this.verdictVersion}`\n if (!edited && inputs === this.recheckInputs) return\n this.recheckInputs = inputs\n this.held = edited ? wordsAtCarets(words, carets) : stillHeld(this.held, carets)\n\n const issues: SpellIssue[] = []\n const unknown = new Set<string>()\n for (const word of words) {\n const verdict = this.verdicts.get(word.word)\n if (verdict === false) continue\n if (isHeld(word, this.held)) continue\n if (verdict === undefined) unknown.add(word.word)\n else if (!this.checker.isAccepted(word.word)) issues.push(word)\n }\n\n this.paint(issues, mounted)\n this.request(unknown)\n }\n\n /** Selection and scroll updates outnumber edits; they reuse the words while the window holds. */\n private windowWords(\n snapshot: EditorViewSnapshot,\n context: EditorViewContributionContext,\n window: SpellTextRange,\n ): readonly SpellWord[] | null {\n const captures = context.getSyntaxCaptures()\n const replacements = context.getInlineReplacementRanges()\n const key = [\n snapshot.languageId,\n snapshot.textVersion,\n window.start,\n window.end,\n ...replacements.flatMap((range) => [range.start, range.end]),\n ].join(',')\n if (key === this.wordsKey && captures === this.wordsCaptures) return this.cachedWords\n\n const regions = spellcheckRegions({\n languageId: snapshot.languageId,\n captures,\n window,\n scope: this.scope,\n })\n if (!regions) return null\n this.recheckInputs = ''\n this.cachedWords = this.words(snapshot, regions, replacements)\n this.wordsKey = key\n this.wordsCaptures = captures\n return this.cachedWords\n }\n\n private words(\n snapshot: EditorViewSnapshot,\n regions: SpellcheckRegions,\n replacements: readonly SpellTextRange[],\n ): readonly SpellWord[] {\n const text = snapshot.textSnapshot\n const excluded = [...regions.excluded, ...replacements]\n const words: SpellWord[] = []\n for (const region of regions.prose) this.proseWords(text, region, excluded, words)\n for (const region of regions.code) {\n if (region.end - region.start > MAX_SPELLCHECK_LINE_LENGTH) continue\n const source = text.readRange(region.start, region.end)\n const local = localRanges(excluded, region)\n for (const word of tokenizeSpellWords(source, { mode: 'code', excluded: local })) {\n words.push(shifted(word, region.start))\n }\n }\n return words\n }\n\n private proseWords(\n text: EditorViewSnapshot['textSnapshot'],\n region: SpellTextRange,\n excluded: readonly SpellTextRange[],\n words: SpellWord[],\n ): void {\n const firstLine = text.lineAt(region.start)\n const lastLine = text.lineAt(region.end)\n for (let line = firstLine; line <= lastLine; line++) {\n const range = text.lineRange(line)\n if (range.end - range.start > MAX_SPELLCHECK_LINE_LENGTH) continue\n const local = localRanges(excluded, range)\n const lineWords = this.lineWords(text.readRange(range.start, range.end), local)\n for (const word of lineWords) words.push(shifted(word, range.start))\n }\n }\n\n private lineWords(line: string, excluded: readonly SpellTextRange[]): readonly SpellWord[] {\n if (excluded.length > 0) return tokenizeSpellWords(line, { excluded })\n\n const cached = this.lines.get(line)\n if (cached) return cached\n if (this.lines.size >= MAX_CACHED_LINES) this.lines.clear()\n const words = tokenizeSpellWords(line)\n this.lines.set(line, words)\n return words\n }\n\n private request(unknown: ReadonlySet<string>): void {\n if (this.failed) return\n const words = [...unknown].filter((word) => !this.pending.has(word))\n if (words.length === 0) return\n\n for (const word of words) this.pending.add(word)\n const dictionaryVersion = this.dictionaryVersion\n this.checker.check(words).then(\n (misspelled) => {\n if (dictionaryVersion === this.dictionaryVersion) this.settle(words, new Set(misspelled))\n },\n (error: unknown) => {\n if (dictionaryVersion === this.dictionaryVersion) this.fail(words, error)\n },\n )\n }\n\n private settle(words: readonly string[], misspelled: ReadonlySet<string>): void {\n for (const word of words) {\n this.pending.delete(word)\n this.verdicts.set(word, misspelled.has(word))\n }\n this.verdictVersion += 1\n if (!this.disposed) this.refresh()\n }\n\n /** A failed worker is not asked again by this editor: every keystroke would start another. */\n private fail(words: readonly string[], error: unknown): void {\n for (const word of words) this.pending.delete(word)\n if (this.disposed || this.failed) return\n this.failed = true\n this.view?.log({\n action: 'spellcheck.check',\n level: 'warn',\n message: 'Spellcheck stopped: the dictionary worker failed',\n reason: error instanceof Error ? error.message : String(error),\n })\n }\n\n /** Every issue in the window answers `issueAt`; only those on mounted rows are painted. */\n private paint(issues: readonly SpellIssue[], mounted: SpellTextRange | null): void {\n this.issues = issues\n const view = this.view\n if (!view) return\n const painted = mounted\n ? issues.filter((issue) => issue.end > mounted.start && issue.start < mounted.end)\n : []\n const key = painted.map((issue) => `${issue.start}:${issue.end}`).join(',')\n if (key === this.paintedKey) return\n this.paintedKey = key\n if (painted.length === 0) view.clearRangeHighlight(this.highlightName)\n else view.setRangeHighlight(this.highlightName, painted, SPELLING_STYLE)\n }\n\n private issueAt(offset: number): SpellIssue | null {\n return this.issues.find((issue) => issue.start <= offset && offset <= issue.end) ?? null\n }\n\n private replace(offset: number, word: string): boolean {\n const issue = this.issueAt(offset)\n const edit = this.edit\n if (!issue || !edit) return false\n const text = edit.getTextSnapshot()\n if (!text || foldApostrophes(text.readRange(issue.start, issue.end)) !== issue.word)\n return false\n\n edit.applyEdits([{ from: issue.start, to: issue.end, text: word }], 'spellcheck.replace')\n return true\n }\n}\n\nfunction mountedRange(snapshot: EditorViewSnapshot): SpellTextRange | null {\n let start = Number.POSITIVE_INFINITY\n let end = -1\n for (const row of snapshot.visibleRows) {\n if (row.source !== 'document') continue\n start = Math.min(start, row.startOffset)\n end = Math.max(end, row.endOffset)\n }\n return end < 0 ? null : { start, end }\n}\n\n/** The mounted rows plus one screen above and below them. */\nfunction checkWindow(snapshot: EditorViewSnapshot): SpellTextRange | null {\n let first = Number.POSITIVE_INFINITY\n let last = -1\n for (const row of snapshot.visibleRows) {\n if (row.source !== 'document') continue\n first = Math.min(first, row.bufferRow)\n last = Math.max(last, row.bufferRow)\n }\n if (last < 0) return null\n\n const text = snapshot.textSnapshot\n const span = last - first + 1\n const startLine = Math.max(0, first - span)\n const endLine = Math.min(text.lineCount - 1, last + span)\n return { start: text.lineStart(startLine), end: text.lineRange(endLine).end }\n}\n\nfunction caretOffsets(snapshot: EditorViewSnapshot): readonly number[] {\n return snapshot.selections\n .filter((selection) => selection.startOffset === selection.endOffset)\n .map((selection) => selection.headOffset)\n}\n\nfunction containsCaret(range: SpellTextRange, carets: readonly number[]): boolean {\n return carets.some((caret) => range.start <= caret && caret <= range.end)\n}\n\n/** The words being typed: held back until the caret leaves them. */\nfunction wordsAtCarets(\n words: readonly SpellWord[],\n carets: readonly number[],\n): readonly SpellTextRange[] {\n return words.filter((word) => containsCaret(word, carets))\n}\n\nfunction stillHeld(\n held: readonly SpellTextRange[],\n carets: readonly number[],\n): readonly SpellTextRange[] {\n return held.filter((range) => containsCaret(range, carets))\n}\n\nfunction isHeld(word: SpellTextRange, held: readonly SpellTextRange[]): boolean {\n return held.some((range) => range.start === word.start && range.end === word.end)\n}\n\nfunction localRanges(\n ranges: readonly SpellTextRange[],\n within: SpellTextRange,\n): readonly SpellTextRange[] {\n const local: SpellTextRange[] = []\n for (const range of ranges) {\n if (range.end <= within.start || range.start >= within.end) continue\n local.push({ start: range.start - within.start, end: range.end - within.start })\n }\n return local\n}\n\nfunction shifted(word: SpellWord, by: number): SpellWord {\n return { start: word.start + by, end: word.end + by, word: word.word }\n}\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { SpellDictionaryUrls } from './dictionaryData';
2
+ /** US and British spellings are both accepted, with software vocabulary on top. */
3
+ export declare const ENGLISH_DICTIONARIES: SpellDictionaryUrls;
4
+ //# sourceMappingURL=dictionaryAssets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dictionaryAssets.d.ts","sourceRoot":"","sources":["../src/dictionaryAssets.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,mFAAmF;AACnF,eAAO,MAAM,oBAAoB,EAAE,mBAGlC,CAAA"}
@@ -0,0 +1,13 @@
1
+ import type { SpellEngineSource } from './engine';
2
+ export type SpellDictionaryUrls = {
3
+ readonly dictionaries: readonly string[];
4
+ readonly suggestionWeights: SpellEngineSource['suggestionWeights'];
5
+ };
6
+ /** Fetches and inflates each dictionary. Every fetch starts before any is awaited. */
7
+ export declare function loadSpellEngineSource(urls: SpellDictionaryUrls): Promise<SpellEngineSource>;
8
+ /**
9
+ * A server that labels `.gz` files `Content-Encoding: gzip` hands fetch the inflated bytes, so the
10
+ * gzip magic number decides whether there is anything left to inflate.
11
+ */
12
+ export declare function inflateDictionary(bytes: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer>>;
13
+ //# sourceMappingURL=dictionaryData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dictionaryData.d.ts","sourceRoot":"","sources":["../src/dictionaryData.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,mBAAmB,CAAC,CAAA;CACnE,CAAA;AAED,sFAAsF;AACtF,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAGjG;AAQD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAC7B,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAKlC"}
@@ -0,0 +1,18 @@
1
+ import { mapDictionaryInformationToWeightMap } from 'cspell-trie-lib';
2
+ type DictionaryInformation = Parameters<typeof mapDictionaryInformationToWeightMap>[0];
3
+ export type SpellEngine = {
4
+ isCorrect(word: string): boolean;
5
+ /** The words among `words` that are misspelled, each once, in first-seen order. */
6
+ check(words: readonly string[]): readonly string[];
7
+ suggest(word: string, limit: number): readonly string[];
8
+ /** Replaces the accepted list. Accepted words are never misspelled and can be suggested. */
9
+ setAcceptedWords(words: readonly string[]): void;
10
+ };
11
+ export type SpellEngineSource = {
12
+ /** Decompressed tries, in either of cspell-trie-lib's encodings. */
13
+ readonly dictionaries: readonly Uint8Array<ArrayBuffer>[];
14
+ readonly suggestionWeights: DictionaryInformation;
15
+ };
16
+ export declare function createSpellEngine(source: SpellEngineSource): SpellEngine;
17
+ export {};
18
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,mCAAmC,EAGpC,MAAM,iBAAiB,CAAA;AAExB,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,mCAAmC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEtF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,mFAAmF;IACnF,KAAK,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAAA;IAClD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IACvD,4FAA4F;IAC5F,gBAAgB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;CACjD,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,SAAS,UAAU,CAAC,WAAW,CAAC,EAAE,CAAA;IACzD,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAA;CAClD,CAAA;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,WAAW,CAuBxE"}
@@ -0,0 +1,19 @@
1
+ export type SpellIssue = {
2
+ readonly start: number;
3
+ readonly end: number;
4
+ readonly word: string;
5
+ };
6
+ /** What a host reaches through `editor.getFeature(EDITOR_SPELLCHECK_FEATURE)`. */
7
+ export type EditorSpellcheckFeature = {
8
+ /** The marked word at `offset`, edges included; only words on or near the screen are marked. */
9
+ issueAt(offset: number): SpellIssue | null;
10
+ /** Replacements for the marked word at `offset`, best first; empty when nothing is marked there. */
11
+ suggestions(offset: number, limit?: number): Promise<readonly string[]>;
12
+ /** Replaces the marked word at `offset` as one undoable edit. False when nothing is marked there. */
13
+ replace(offset: number, word: string): boolean;
14
+ /** Words never marked, in every editor sharing this editor's spellcheck service. */
15
+ setAcceptedWords(words: readonly string[]): void;
16
+ };
17
+ export declare const EDITOR_SPELLCHECK_FEATURE_ID = "editor.spellcheck";
18
+ export declare const EDITOR_SPELLCHECK_FEATURE: import("@singapore-editor/core").EditorCapabilityToken<EditorSpellcheckFeature>;
19
+ //# sourceMappingURL=feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../src/feature.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,kFAAkF;AAClF,MAAM,MAAM,uBAAuB,GAAG;IACpC,gGAAgG;IAChG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAA;IAC1C,oGAAoG;IACpG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;IACvE,qGAAqG;IACrG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9C,oFAAoF;IACpF,gBAAgB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;CACjD,CAAA;AAED,eAAO,MAAM,4BAA4B,sBAAsB,CAAA;AAE/D,eAAO,MAAM,yBAAyB,iFAErC,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { createEditorCapabilityToken } from "@singapore-editor/core/extensions";
2
+ //#region src/feature.ts
3
+ var EDITOR_SPELLCHECK_FEATURE_ID = "editor.spellcheck";
4
+ var EDITOR_SPELLCHECK_FEATURE = createEditorCapabilityToken(EDITOR_SPELLCHECK_FEATURE_ID);
5
+ //#endregion
6
+ export { EDITOR_SPELLCHECK_FEATURE, EDITOR_SPELLCHECK_FEATURE_ID };
7
+
8
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature.js","names":[],"sources":["../src/feature.ts"],"sourcesContent":["import { createEditorCapabilityToken } from '@singapore-editor/core/extensions'\n\nexport type SpellIssue = {\n readonly start: number\n readonly end: number\n readonly word: string\n}\n\n/** What a host reaches through `editor.getFeature(EDITOR_SPELLCHECK_FEATURE)`. */\nexport type EditorSpellcheckFeature = {\n /** The marked word at `offset`, edges included; only words on or near the screen are marked. */\n issueAt(offset: number): SpellIssue | null\n /** Replacements for the marked word at `offset`, best first; empty when nothing is marked there. */\n suggestions(offset: number, limit?: number): Promise<readonly string[]>\n /** Replaces the marked word at `offset` as one undoable edit. False when nothing is marked there. */\n replace(offset: number, word: string): boolean\n /** Words never marked, in every editor sharing this editor's spellcheck service. */\n setAcceptedWords(words: readonly string[]): void\n}\n\nexport const EDITOR_SPELLCHECK_FEATURE_ID = 'editor.spellcheck'\n\nexport const EDITOR_SPELLCHECK_FEATURE = createEditorCapabilityToken<EditorSpellcheckFeature>(\n EDITOR_SPELLCHECK_FEATURE_ID,\n)\n"],"mappings":";;AAoBA,IAAa,+BAA+B;AAE5C,IAAa,4BAA4B,4BACvC,4BACF"}
@@ -0,0 +1,7 @@
1
+ export type { SpellcheckChecker } from './controller';
2
+ export { EDITOR_SPELLCHECK_FEATURE, EDITOR_SPELLCHECK_FEATURE_ID, type EditorSpellcheckFeature, type SpellIssue, } from './feature';
3
+ export { createSpellcheckPlugin, type SpellcheckPluginOptions } from './plugin';
4
+ export type { SpellcheckScope } from './proseRanges';
5
+ export { SpellcheckService, type SpellcheckServiceOptions } from './service';
6
+ export { tokenizeSpellWords, type SpellTextRange, type SpellTokenizeMode, type SpellTokenizeOptions, type SpellWord, } from './tokenizer';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACrD,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,sBAAsB,EAAE,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAC5E,OAAO,EACL,kBAAkB,EAClB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { EDITOR_SPELLCHECK_FEATURE, EDITOR_SPELLCHECK_FEATURE_ID } from "./feature.js";
2
+ import { tokenizeSpellWords } from "./tokenizer.js";
3
+ import { createSpellcheckPlugin } from "./plugin.js";
4
+ import { SpellcheckService } from "./service.js";
5
+ export { EDITOR_SPELLCHECK_FEATURE, EDITOR_SPELLCHECK_FEATURE_ID, SpellcheckService, createSpellcheckPlugin, tokenizeSpellWords };
@@ -0,0 +1,12 @@
1
+ import type { EditorPlugin } from '@singapore-editor/core/extensions';
2
+ import { type SpellcheckChecker } from './controller';
3
+ import type { SpellcheckScope } from './proseRanges';
4
+ export type SpellcheckPluginOptions = {
5
+ /** The page's one service, shared by every editor. */
6
+ readonly service: SpellcheckChecker;
7
+ /** Defaults to `prose`: plain text and Markdown. */
8
+ readonly scope?: SpellcheckScope;
9
+ };
10
+ /** Marks misspelled words on and near the screen, and registers `EDITOR_SPELLCHECK_FEATURE`. */
11
+ export declare function createSpellcheckPlugin(options: SpellcheckPluginOptions): EditorPlugin;
12
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AACrE,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAE3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,MAAM,MAAM,uBAAuB,GAAG;IACpC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CACjC,CAAA;AAED,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,YAAY,CAyBrF"}
package/dist/plugin.js ADDED
@@ -0,0 +1,27 @@
1
+ import { EDITOR_SPELLCHECK_FEATURE } from "./feature.js";
2
+ import { SpellcheckController } from "./controller.js";
3
+ //#region src/plugin.ts
4
+ /** Marks misspelled words on and near the screen, and registers `EDITOR_SPELLCHECK_FEATURE`. */
5
+ function createSpellcheckPlugin(options) {
6
+ return {
7
+ name: "editor.spellcheck",
8
+ activate(context) {
9
+ const controller = new SpellcheckController(options.service, options.scope ?? "prose");
10
+ return [
11
+ context.registerViewContribution({ createContribution: (view) => {
12
+ const attachment = controller.attachView(view);
13
+ return {
14
+ update: (snapshot, kind) => controller.update(snapshot, kind),
15
+ dispose: () => attachment.dispose()
16
+ };
17
+ } }),
18
+ context.registerEditContribution({ createContribution: (edit) => controller.attachEdit(edit) }),
19
+ context.registerCapabilityContribution({ createContribution: (capability) => capability.registerFeature(EDITOR_SPELLCHECK_FEATURE, controller.feature()) })
20
+ ];
21
+ }
22
+ };
23
+ }
24
+ //#endregion
25
+ export { createSpellcheckPlugin };
26
+
27
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import type { EditorPlugin } from '@singapore-editor/core/extensions'\nimport { SpellcheckController, type SpellcheckChecker } from './controller'\nimport { EDITOR_SPELLCHECK_FEATURE } from './feature'\nimport type { SpellcheckScope } from './proseRanges'\n\nexport type SpellcheckPluginOptions = {\n /** The page's one service, shared by every editor. */\n readonly service: SpellcheckChecker\n /** Defaults to `prose`: plain text and Markdown. */\n readonly scope?: SpellcheckScope\n}\n\n/** Marks misspelled words on and near the screen, and registers `EDITOR_SPELLCHECK_FEATURE`. */\nexport function createSpellcheckPlugin(options: SpellcheckPluginOptions): EditorPlugin {\n return {\n name: 'editor.spellcheck',\n activate(context) {\n const controller = new SpellcheckController(options.service, options.scope ?? 'prose')\n return [\n context.registerViewContribution({\n createContribution: (view) => {\n const attachment = controller.attachView(view)\n return {\n update: (snapshot, kind) => controller.update(snapshot, kind),\n dispose: () => attachment.dispose(),\n }\n },\n }),\n context.registerEditContribution({\n createContribution: (edit) => controller.attachEdit(edit),\n }),\n context.registerCapabilityContribution({\n createContribution: (capability) =>\n capability.registerFeature(EDITOR_SPELLCHECK_FEATURE, controller.feature()),\n }),\n ]\n },\n }\n}\n"],"mappings":";;;;AAaA,SAAgB,uBAAuB,SAAgD;CACrF,OAAO;EACL,MAAM;EACN,SAAS,SAAS;GAChB,MAAM,aAAa,IAAI,qBAAqB,QAAQ,SAAS,QAAQ,SAAS,OAAO;GACrF,OAAO;IACL,QAAQ,yBAAyB,EAC/B,qBAAqB,SAAS;KAC5B,MAAM,aAAa,WAAW,WAAW,IAAI;KAC7C,OAAO;MACL,SAAS,UAAU,SAAS,WAAW,OAAO,UAAU,IAAI;MAC5D,eAAe,WAAW,QAAQ;KACpC;IACF,EACF,CAAC;IACD,QAAQ,yBAAyB,EAC/B,qBAAqB,SAAS,WAAW,WAAW,IAAI,EAC1D,CAAC;IACD,QAAQ,+BAA+B,EACrC,qBAAqB,eACnB,WAAW,gBAAgB,2BAA2B,WAAW,QAAQ,CAAC,EAC9E,CAAC;GACH;EACF;CACF;AACF"}
@@ -0,0 +1,25 @@
1
+ import type { SpellTextRange } from './tokenizer';
2
+ /** `prose` checks plain text and Markdown only; `proseAndCode` also checks comments and strings. */
3
+ export type SpellcheckScope = 'prose' | 'proseAndCode';
4
+ export type SpellcheckCapture = {
5
+ readonly startIndex: number;
6
+ readonly endIndex: number;
7
+ readonly captureName: string;
8
+ };
9
+ export type SpellcheckRegionInput = {
10
+ readonly languageId: string | null;
11
+ /** Null while the parse that decides which parts are prose has not landed. */
12
+ readonly captures: readonly SpellcheckCapture[] | null;
13
+ readonly window: SpellTextRange;
14
+ readonly scope: SpellcheckScope;
15
+ };
16
+ export type SpellcheckRegions = {
17
+ /** Checked word by word as written. */
18
+ readonly prose: readonly SpellTextRange[];
19
+ /** Comments and strings: camelCase and snake_case are split. */
20
+ readonly code: readonly SpellTextRange[];
21
+ readonly excluded: readonly SpellTextRange[];
22
+ };
23
+ /** Which parts of the window are checked, or null while the syntax that decides it is loading. */
24
+ export declare function spellcheckRegions(input: SpellcheckRegionInput): SpellcheckRegions | null;
25
+ //# sourceMappingURL=proseRanges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proseRanges.d.ts","sourceRoot":"","sources":["../src/proseRanges.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,oGAAoG;AACpG,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,cAAc,CAAA;AAEtD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,IAAI,CAAA;IACtD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;IACzC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAA;CAC7C,CAAA;AAaD,kGAAkG;AAClG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,GAAG,iBAAiB,GAAG,IAAI,CAiBxF"}