@prosekit/extensions 0.12.2 → 0.13.0
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.
- package/dist/list/style.css +5 -5
- package/dist/list/style.css.map +1 -1
- package/dist/prosekit-extensions-autocomplete.d.ts +11 -3
- package/dist/prosekit-extensions-autocomplete.d.ts.map +1 -1
- package/dist/prosekit-extensions-autocomplete.js +171 -60
- package/dist/prosekit-extensions-autocomplete.js.map +1 -1
- package/dist/prosekit-extensions-blockquote.js +1 -1
- package/dist/prosekit-extensions-blockquote.js.map +1 -1
- package/dist/prosekit-extensions-code.d.ts.map +1 -1
- package/dist/prosekit-extensions-commit.js +1 -1
- package/dist/prosekit-extensions-commit.js.map +1 -1
- package/dist/prosekit-extensions-heading.d.ts.map +1 -1
- package/dist/prosekit-extensions-heading.js +6 -6
- package/dist/prosekit-extensions-heading.js.map +1 -1
- package/dist/prosekit-extensions-loro.d.ts +16 -17
- package/dist/prosekit-extensions-loro.d.ts.map +1 -1
- package/dist/prosekit-extensions-loro.js +13 -6
- package/dist/prosekit-extensions-loro.js.map +1 -1
- package/dist/prosekit-extensions-paragraph.js +1 -1
- package/dist/prosekit-extensions-paragraph.js.map +1 -1
- package/dist/prosekit-extensions-placeholder.d.ts.map +1 -1
- package/dist/prosekit-extensions-placeholder.js +2 -3
- package/dist/prosekit-extensions-placeholder.js.map +1 -1
- package/dist/prosekit-extensions-strike.js +2 -2
- package/dist/prosekit-extensions-strike.js.map +1 -1
- package/dist/prosekit-extensions-table.js +0 -1
- package/dist/prosekit-extensions-text-align.js +4 -4
- package/dist/prosekit-extensions-text-align.js.map +1 -1
- package/dist/prosekit-extensions-yjs.js +1 -1
- package/dist/prosekit-extensions-yjs.js.map +1 -1
- package/package.json +15 -14
- package/src/autocomplete/autocomplete-helpers.ts +18 -9
- package/src/autocomplete/autocomplete-plugin.ts +261 -117
- package/src/autocomplete/autocomplete-rule.ts +3 -3
- package/src/autocomplete/autocomplete.spec.ts +239 -20
- package/src/autocomplete/autocomplete.ts +8 -0
- package/src/blockquote/blockquote-keymap.spec.ts +4 -4
- package/src/blockquote/blockquote-keymap.ts +1 -1
- package/src/commit/index.ts +1 -1
- package/src/hard-break/hard-break-keymap.spec.ts +5 -7
- package/src/heading/heading-keymap.spec.ts +7 -7
- package/src/heading/heading-keymap.ts +6 -6
- package/src/link/index.spec.ts +9 -8
- package/src/list/list-keymap.spec.ts +5 -5
- package/src/list/style.css +5 -5
- package/src/loro/loro-cursor-plugin.ts +23 -13
- package/src/loro/loro-keymap.ts +1 -1
- package/src/loro/loro.ts +14 -10
- package/src/paragraph/paragraph-keymap.ts +1 -1
- package/src/placeholder/index.ts +2 -1
- package/src/strike/index.ts +2 -2
- package/src/testing/index.ts +2 -2
- package/src/testing/keyboard.ts +0 -30
- package/src/text-align/index.ts +4 -4
- package/src/yjs/yjs-keymap.ts +1 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { OBJECT_REPLACEMENT_CHARACTER } from '@prosekit/core'
|
|
2
|
+
import type {
|
|
3
|
+
ProseMirrorNode,
|
|
4
|
+
ResolvedPos,
|
|
5
|
+
} from '@prosekit/pm/model'
|
|
2
6
|
import {
|
|
3
7
|
Plugin,
|
|
4
8
|
type EditorState,
|
|
5
9
|
type Transaction,
|
|
6
10
|
} from '@prosekit/pm/state'
|
|
11
|
+
import type { Mapping } from '@prosekit/pm/transform'
|
|
12
|
+
import type { EditorView } from '@prosekit/pm/view'
|
|
7
13
|
import {
|
|
8
14
|
Decoration,
|
|
9
15
|
DecorationSet,
|
|
@@ -16,9 +22,27 @@ import {
|
|
|
16
22
|
setTrMeta,
|
|
17
23
|
type PredictionPluginMatching,
|
|
18
24
|
type PredictionPluginState,
|
|
25
|
+
type PredictionTransactionMeta,
|
|
19
26
|
} from './autocomplete-helpers'
|
|
20
27
|
import type { AutocompleteRule } from './autocomplete-rule'
|
|
21
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates a plugin that handles autocomplete functionality.
|
|
31
|
+
*
|
|
32
|
+
* Workflow:
|
|
33
|
+
*
|
|
34
|
+
* 1. {@link handleTextInput}: called when text is going to be input, but the
|
|
35
|
+
* transaction is not yet created. Injects a new matching as a transaction
|
|
36
|
+
* meta if applicable. This is the only place to create a new matching if
|
|
37
|
+
* there is no existing matching.
|
|
38
|
+
* 2. {@link handleTransaction}: called when a transaction is going to be
|
|
39
|
+
* applied. Updates the plugin state based on the transaction. This step
|
|
40
|
+
* determines if a matching should be created, updated or removed.
|
|
41
|
+
* 3. {@link handleUpdate}: called when the editor state is updated. This is the
|
|
42
|
+
* place to call `onMatch` and register `deleteMatch` and `ignoreMatch`
|
|
43
|
+
* callbacks.
|
|
44
|
+
* 4. {@link getDecorations}: creates the decorations for the current matching.
|
|
45
|
+
*/
|
|
22
46
|
export function createAutocompletePlugin({
|
|
23
47
|
getRules,
|
|
24
48
|
}: {
|
|
@@ -31,139 +55,252 @@ export function createAutocompletePlugin({
|
|
|
31
55
|
init: (): PredictionPluginState => {
|
|
32
56
|
return { ignores: [], matching: null }
|
|
33
57
|
},
|
|
34
|
-
apply: (
|
|
35
|
-
tr
|
|
36
|
-
prevValue: PredictionPluginState,
|
|
37
|
-
oldState: EditorState,
|
|
38
|
-
newState: EditorState,
|
|
39
|
-
): PredictionPluginState => {
|
|
40
|
-
const meta = getTrMeta(tr)
|
|
41
|
-
|
|
42
|
-
// No changes
|
|
43
|
-
if (
|
|
44
|
-
!tr.docChanged
|
|
45
|
-
&& oldState.selection.eq(newState.selection)
|
|
46
|
-
&& !meta
|
|
47
|
-
) {
|
|
48
|
-
return prevValue
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Receiving a meta means that we are ignoring a match
|
|
52
|
-
if (meta) {
|
|
53
|
-
let ignores = prevValue.ignores
|
|
54
|
-
if (!ignores.includes(meta.ignore)) {
|
|
55
|
-
ignores = [...ignores, meta.ignore]
|
|
56
|
-
}
|
|
57
|
-
return { matching: null, ignores }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Calculate the new ignores
|
|
61
|
-
const ignoreSet = new Set(prevValue.ignores.map(pos => tr.mapping.map(pos)))
|
|
62
|
-
|
|
63
|
-
// Calculate the new matching
|
|
64
|
-
let matching = calcPluginStateMatching(newState, getRules())
|
|
65
|
-
|
|
66
|
-
// Check if the matching should be ignored
|
|
67
|
-
if (matching && ignoreSet.has(matching.from)) {
|
|
68
|
-
matching = null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Return the new matching and ignores
|
|
72
|
-
return { matching, ignores: Array.from(ignoreSet) }
|
|
58
|
+
apply: (tr, prevValue, oldState, newState): PredictionPluginState => {
|
|
59
|
+
return handleTransaction(tr, prevValue, oldState, newState, getRules)
|
|
73
60
|
},
|
|
74
61
|
},
|
|
75
62
|
|
|
76
63
|
view: () => ({
|
|
77
|
-
update:
|
|
78
|
-
const prevValue = getPluginState(prevState)
|
|
79
|
-
const currValue = getPluginState(view.state)
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
prevValue?.matching
|
|
83
|
-
&& prevValue.matching.rule !== currValue?.matching?.rule
|
|
84
|
-
) {
|
|
85
|
-
// Deactivate the previous rule
|
|
86
|
-
prevValue.matching.rule.onLeave?.()
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
currValue?.matching
|
|
91
|
-
&& !currValue.ignores.includes(currValue.matching.from)
|
|
92
|
-
) {
|
|
93
|
-
// Activate the current rule
|
|
94
|
-
|
|
95
|
-
const { from, to, match, rule } = currValue.matching
|
|
96
|
-
|
|
97
|
-
const textContent = view.state.doc.textBetween(
|
|
98
|
-
from,
|
|
99
|
-
to,
|
|
100
|
-
null,
|
|
101
|
-
OBJECT_REPLACEMENT_CHARACTER,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const deleteMatch = () => {
|
|
105
|
-
if (
|
|
106
|
-
view.state.doc.textBetween(
|
|
107
|
-
from,
|
|
108
|
-
to,
|
|
109
|
-
null,
|
|
110
|
-
OBJECT_REPLACEMENT_CHARACTER,
|
|
111
|
-
) === textContent
|
|
112
|
-
) {
|
|
113
|
-
view.dispatch(view.state.tr.delete(from, to))
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const ignoreMatch = () => {
|
|
118
|
-
view.dispatch(
|
|
119
|
-
setTrMeta(view.state.tr, { ignore: from }),
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
rule.onMatch({
|
|
124
|
-
state: view.state,
|
|
125
|
-
match,
|
|
126
|
-
from,
|
|
127
|
-
to,
|
|
128
|
-
deleteMatch,
|
|
129
|
-
ignoreMatch,
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
},
|
|
64
|
+
update: handleUpdate,
|
|
133
65
|
}),
|
|
134
66
|
|
|
135
67
|
props: {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return DecorationSet.create(state.doc, [deco])
|
|
68
|
+
handleTextInput: (view, from, to, textAdded, getTr) => {
|
|
69
|
+
const meta = handleTextInput(view, from, to, textAdded, getRules)
|
|
70
|
+
if (meta) {
|
|
71
|
+
const tr = getTr()
|
|
72
|
+
setTrMeta(tr, meta)
|
|
73
|
+
view.dispatch(tr)
|
|
74
|
+
return true
|
|
144
75
|
}
|
|
145
|
-
return
|
|
76
|
+
return false
|
|
146
77
|
},
|
|
78
|
+
decorations: getDecorations,
|
|
147
79
|
},
|
|
148
80
|
})
|
|
149
81
|
}
|
|
150
82
|
|
|
151
|
-
|
|
83
|
+
function handleTextInput(
|
|
84
|
+
view: EditorView,
|
|
85
|
+
from: number,
|
|
86
|
+
to: number,
|
|
87
|
+
textAdded: string,
|
|
88
|
+
getRules: () => AutocompleteRule[],
|
|
89
|
+
): PredictionTransactionMeta | undefined {
|
|
90
|
+
// Only handle insertions
|
|
91
|
+
if (from !== to) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
152
94
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const $pos = state.selection.$from
|
|
95
|
+
const textBackward = getTextBackward(view.state.doc.resolve(from))
|
|
96
|
+
const textFull = textBackward + textAdded
|
|
97
|
+
const textTo = to + textAdded.length
|
|
98
|
+
const textFrom = textTo - textFull.length
|
|
158
99
|
|
|
159
|
-
const
|
|
100
|
+
const pluginState = getPluginState(view.state)
|
|
101
|
+
const ignores = pluginState?.ignores ?? []
|
|
102
|
+
|
|
103
|
+
const currMatching = matchRule(
|
|
104
|
+
view.state,
|
|
105
|
+
getRules(),
|
|
106
|
+
textFull,
|
|
107
|
+
textFrom,
|
|
108
|
+
textTo,
|
|
109
|
+
ignores,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (currMatching) {
|
|
113
|
+
return { type: 'enter', matching: currMatching }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleTransaction(
|
|
118
|
+
tr: Transaction,
|
|
119
|
+
prevValue: PredictionPluginState,
|
|
120
|
+
oldState: EditorState,
|
|
121
|
+
newState: EditorState,
|
|
122
|
+
getRules: () => AutocompleteRule[],
|
|
123
|
+
): PredictionPluginState {
|
|
124
|
+
const meta = getTrMeta(tr)
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
!meta
|
|
128
|
+
&& !tr.docChanged
|
|
129
|
+
&& oldState.selection.eq(newState.selection)
|
|
130
|
+
) {
|
|
131
|
+
// No changes
|
|
132
|
+
return prevValue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle position mapping changes
|
|
136
|
+
const ignoreSet = new Set<number>()
|
|
137
|
+
for (const ignore of prevValue.ignores) {
|
|
138
|
+
const result = tr.mapping.mapResult(ignore)
|
|
139
|
+
if (!result.deletedBefore && !result.deletedAfter) {
|
|
140
|
+
ignoreSet.add(result.pos)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const ignores = Array.from(ignoreSet)
|
|
144
|
+
|
|
145
|
+
const prevMatching = prevValue.matching && mapMatching(prevValue.matching, tr.mapping)
|
|
146
|
+
|
|
147
|
+
// If there is no new matching from `handleTextInput`
|
|
148
|
+
if (!meta) {
|
|
149
|
+
if (!prevMatching) {
|
|
150
|
+
return { matching: null, ignores }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { selection } = newState
|
|
154
|
+
// If the text selection is before the matching or after the matching,
|
|
155
|
+
// we leave the matching
|
|
156
|
+
if (selection.to < prevMatching.from || selection.from > prevMatching.to) {
|
|
157
|
+
ignores.push(prevMatching.from)
|
|
158
|
+
return { matching: null, ignores }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get the text between the existing matching
|
|
162
|
+
const text = getTextBetween(newState.doc, prevMatching.from, prevMatching.to)
|
|
163
|
+
// Check the text again to see if it still matches the rule
|
|
164
|
+
const currMatching = matchRule(
|
|
165
|
+
newState,
|
|
166
|
+
getRules(),
|
|
167
|
+
text,
|
|
168
|
+
prevMatching.from,
|
|
169
|
+
prevMatching.to,
|
|
170
|
+
ignores,
|
|
171
|
+
)
|
|
172
|
+
return { matching: currMatching ?? null, ignores }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If a new matching is being entered from `handleTextInput`
|
|
176
|
+
if (meta.type === 'enter') {
|
|
177
|
+
// Ignore the previous matching if it is not the same as the new matching
|
|
178
|
+
if (prevMatching && prevMatching.from !== meta.matching.from) {
|
|
179
|
+
ignores.push(prevMatching.from)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Return the new matching
|
|
183
|
+
return { matching: meta.matching, ignores }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// If a matching is being exited
|
|
187
|
+
if (meta.type === 'leave') {
|
|
188
|
+
if (prevMatching) {
|
|
189
|
+
ignores.push(prevMatching.from)
|
|
190
|
+
}
|
|
191
|
+
return { matching: null, ignores }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(`Invalid transaction meta: ${meta satisfies never}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleUpdate(view: EditorView, prevState: EditorState): void {
|
|
198
|
+
const prevValue = getPluginState(prevState)
|
|
199
|
+
const currValue = getPluginState(view.state)
|
|
200
|
+
|
|
201
|
+
if (!prevValue || !currValue) {
|
|
202
|
+
// Should not happen
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const prevMatching = prevValue.matching
|
|
207
|
+
const currMatching = currValue.matching
|
|
208
|
+
|
|
209
|
+
// Deactivate the previous rule
|
|
210
|
+
if (prevMatching && prevMatching.rule !== currMatching?.rule) {
|
|
211
|
+
prevMatching.rule.onLeave?.()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Activate the current rule
|
|
215
|
+
if (currMatching) {
|
|
216
|
+
const { from, to, match, rule } = currMatching
|
|
217
|
+
|
|
218
|
+
const textSnapshot = getTextBetween(view.state.doc, from, to)
|
|
219
|
+
|
|
220
|
+
const deleteMatch = () => {
|
|
221
|
+
if (getTextBetween(view.state.doc, from, to) === textSnapshot) {
|
|
222
|
+
view.dispatch(view.state.tr.delete(from, to))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ignoreMatch = () => {
|
|
227
|
+
view.dispatch(
|
|
228
|
+
setTrMeta(view.state.tr, { type: 'leave' }),
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
rule.onMatch({
|
|
233
|
+
state: view.state,
|
|
234
|
+
match,
|
|
235
|
+
from,
|
|
236
|
+
to,
|
|
237
|
+
deleteMatch,
|
|
238
|
+
ignoreMatch,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getDecorations(state: EditorState): DecorationSet | null {
|
|
244
|
+
const pluginState = getPluginState(state)
|
|
245
|
+
if (pluginState?.matching) {
|
|
246
|
+
const { from, to, match } = pluginState.matching
|
|
247
|
+
const deco = Decoration.inline(from, to, {
|
|
248
|
+
'class': 'prosekit-autocomplete-match',
|
|
249
|
+
'data-autocomplete-match-text': match[0],
|
|
250
|
+
})
|
|
251
|
+
return DecorationSet.create(state.doc, [deco])
|
|
252
|
+
}
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const MAX_MATCH = 200
|
|
160
257
|
|
|
161
|
-
|
|
258
|
+
/** Get the text before the given position at the current block. */
|
|
259
|
+
function getTextBackward($pos: ResolvedPos): string {
|
|
260
|
+
const parentOffset: number = $pos.parentOffset
|
|
261
|
+
return getTextBetween(
|
|
262
|
+
$pos.parent,
|
|
162
263
|
Math.max(0, parentOffset - MAX_MATCH),
|
|
163
264
|
parentOffset,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getTextBetween(node: ProseMirrorNode, from: number, to: number): string {
|
|
269
|
+
return node.textBetween(
|
|
270
|
+
from,
|
|
271
|
+
to,
|
|
164
272
|
null,
|
|
165
273
|
OBJECT_REPLACEMENT_CHARACTER,
|
|
166
274
|
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function matchRule(
|
|
278
|
+
state: EditorState,
|
|
279
|
+
rules: AutocompleteRule[],
|
|
280
|
+
text: string,
|
|
281
|
+
textFrom: number,
|
|
282
|
+
textTo: number,
|
|
283
|
+
ignores: Array<number>,
|
|
284
|
+
): PredictionPluginMatching | undefined {
|
|
285
|
+
// Find the rightmost ignore point within the text range
|
|
286
|
+
let maxIgnore = -1
|
|
287
|
+
for (const ignore of ignores) {
|
|
288
|
+
if (ignore >= textFrom && ignore < textTo && ignore > maxIgnore) {
|
|
289
|
+
maxIgnore = ignore
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If an ignore point is within the text range, we ignore the text to the left
|
|
294
|
+
// of the ignore point (including the character right after the ignore point).
|
|
295
|
+
if (maxIgnore >= 0) {
|
|
296
|
+
const cut = maxIgnore + 1 - textFrom
|
|
297
|
+
text = text.slice(cut)
|
|
298
|
+
textFrom += cut
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (textFrom >= textTo || !text) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
167
304
|
|
|
168
305
|
for (const rule of rules) {
|
|
169
306
|
if (!rule.canMatch({ state })) {
|
|
@@ -171,16 +308,23 @@ function calcPluginStateMatching(
|
|
|
171
308
|
}
|
|
172
309
|
|
|
173
310
|
rule.regex.lastIndex = 0
|
|
174
|
-
const match = rule.regex.exec(
|
|
311
|
+
const match = rule.regex.exec(text)
|
|
175
312
|
if (!match) {
|
|
176
313
|
continue
|
|
177
314
|
}
|
|
178
315
|
|
|
179
|
-
const
|
|
180
|
-
const
|
|
316
|
+
const matchTo = textTo
|
|
317
|
+
const matchFrom = textFrom + match.index
|
|
181
318
|
|
|
182
|
-
return { rule, match, from, to }
|
|
319
|
+
return { rule, match, from: matchFrom, to: matchTo }
|
|
183
320
|
}
|
|
321
|
+
}
|
|
184
322
|
|
|
185
|
-
|
|
323
|
+
function mapMatching(matching: PredictionPluginMatching, mapping: Mapping): PredictionPluginMatching {
|
|
324
|
+
return {
|
|
325
|
+
rule: matching.rule,
|
|
326
|
+
match: matching.match,
|
|
327
|
+
from: mapping.map(matching.from),
|
|
328
|
+
to: mapping.map(matching.to, -1),
|
|
329
|
+
}
|
|
186
330
|
}
|
|
@@ -69,7 +69,7 @@ export interface AutocompleteRuleOptions {
|
|
|
69
69
|
* The regular expression to match against the text before the cursor. The
|
|
70
70
|
* last match before the cursor is used.
|
|
71
71
|
*
|
|
72
|
-
* For a slash menu, you might use `/(?<!\S)\/(
|
|
72
|
+
* For a slash menu, you might use `/(?<!\S)\/(\S.*)?$/u`.
|
|
73
73
|
* For a mention, you might use `/@\w*$/`
|
|
74
74
|
*/
|
|
75
75
|
regex: RegExp
|
|
@@ -87,8 +87,8 @@ export interface AutocompleteRuleOptions {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* A predicate to determine if the rule can be applied in the current editor
|
|
90
|
-
* state. If not provided, it defaults to only allowing matches
|
|
91
|
-
*
|
|
90
|
+
* state. If not provided, it defaults to only allowing matches that are not
|
|
91
|
+
* inside a code block or code mark.
|
|
92
92
|
*/
|
|
93
93
|
canMatch?: CanMatchPredicate
|
|
94
94
|
}
|