mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
[cm6] Handle multiple selection ranges when applying snippets (#12830)
* Use fork of @codemirror/autocomplete * Handle multiple selection ranges when applying snippets GitOrigin-RevId: 04afc087ac127206463ea3d4950284a50308364a
This commit is contained in:
Generated
+9
-8
@@ -3145,9 +3145,10 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@codemirror/autocomplete": {
|
"node_modules/@codemirror/autocomplete": {
|
||||||
"version": "6.5.1",
|
"version": "6.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.5.1.tgz",
|
"resolved": "git+ssh://git@github.com/overleaf/codemirror-autocomplete.git#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"integrity": "sha512-/Sv9yJmqyILbZ26U4LBHnAtbikuVxWUp+rQ8BXuRGtxZfbfKOY/WPbsUtvSP2h0ZUZMlkxV/hqbKRFzowlA6xw==",
|
"integrity": "sha512-Uf8Tv/wLUmPyxTwxrp/+t+f5sognQs+qHyQ+AvCFhS1wWa2Tw9+Ko/84EsPXQpYwTgN8xvejBCSHlRzubogHLA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.0.0",
|
"@codemirror/language": "^6.0.0",
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
@@ -35168,7 +35169,7 @@
|
|||||||
"@babel/preset-env": "^7.14.5",
|
"@babel/preset-env": "^7.14.5",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@babel/preset-typescript": "^7.16.0",
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
"@codemirror/autocomplete": "^6.5.1",
|
"@codemirror/autocomplete": "github:overleaf/codemirror-autocomplete#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"@codemirror/commands": "^6.2.3",
|
"@codemirror/commands": "^6.2.3",
|
||||||
"@codemirror/lang-markdown": "^6.1.1",
|
"@codemirror/lang-markdown": "^6.1.1",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
@@ -39929,9 +39930,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@codemirror/autocomplete": {
|
"@codemirror/autocomplete": {
|
||||||
"version": "6.5.1",
|
"version": "git+ssh://git@github.com/overleaf/codemirror-autocomplete.git#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.5.1.tgz",
|
"integrity": "sha512-Uf8Tv/wLUmPyxTwxrp/+t+f5sognQs+qHyQ+AvCFhS1wWa2Tw9+Ko/84EsPXQpYwTgN8xvejBCSHlRzubogHLA==",
|
||||||
"integrity": "sha512-/Sv9yJmqyILbZ26U4LBHnAtbikuVxWUp+rQ8BXuRGtxZfbfKOY/WPbsUtvSP2h0ZUZMlkxV/hqbKRFzowlA6xw==",
|
"from": "@codemirror/autocomplete@github:overleaf/codemirror-autocomplete#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@codemirror/language": "^6.0.0",
|
"@codemirror/language": "^6.0.0",
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
@@ -44848,7 +44849,7 @@
|
|||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@babel/preset-typescript": "^7.16.0",
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
"@babel/register": "^7.14.5",
|
"@babel/register": "^7.14.5",
|
||||||
"@codemirror/autocomplete": "^6.5.1",
|
"@codemirror/autocomplete": "github:overleaf/codemirror-autocomplete#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"@codemirror/commands": "^6.2.3",
|
"@codemirror/commands": "^6.2.3",
|
||||||
"@codemirror/lang-markdown": "^6.1.1",
|
"@codemirror/lang-markdown": "^6.1.1",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { keymap } from '@codemirror/view'
|
import { keymap } from '@codemirror/view'
|
||||||
import { Compartment, Prec, TransactionSpec } from '@codemirror/state'
|
import { Compartment, Prec, TransactionSpec } from '@codemirror/state'
|
||||||
import { closeBrackets, closeBracketsKeymap } from './close-brackets'
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'
|
||||||
|
|
||||||
const autoPairConf = new Compartment()
|
const autoPairConf = new Compartment()
|
||||||
|
|
||||||
@@ -8,22 +8,16 @@ export const autoPair = ({
|
|||||||
autoPairDelimiters,
|
autoPairDelimiters,
|
||||||
}: {
|
}: {
|
||||||
autoPairDelimiters: boolean
|
autoPairDelimiters: boolean
|
||||||
}) => autoPairConf.of(createAutoPair(autoPairDelimiters))
|
}) => autoPairConf.of(autoPairDelimiters ? extension : [])
|
||||||
|
|
||||||
export const setAutoPair = (autoPairDelimiters: boolean): TransactionSpec => {
|
export const setAutoPair = (autoPairDelimiters: boolean): TransactionSpec => {
|
||||||
return {
|
return {
|
||||||
effects: autoPairConf.reconfigure(createAutoPair(autoPairDelimiters)),
|
effects: autoPairConf.reconfigure(autoPairDelimiters ? extension : []),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAutoPair = (enabled: boolean) => {
|
const extension = [
|
||||||
if (!enabled) {
|
closeBrackets(),
|
||||||
return []
|
// NOTE: using Prec.highest as this needs to run before the default Backspace handler
|
||||||
}
|
Prec.highest(keymap.of(closeBracketsKeymap)),
|
||||||
|
]
|
||||||
return [
|
|
||||||
closeBrackets(),
|
|
||||||
// NOTE: using Prec.highest as this needs to run before the default Backspace handler
|
|
||||||
Prec.highest(keymap.of(closeBracketsKeymap)),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file is adapted from CodeMirror 6, licensed under the MIT license:
|
|
||||||
* https://github.com/codemirror/autocomplete/blob/main/src/closebrackets.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EditorView, KeyBinding } from '@codemirror/view'
|
|
||||||
import {
|
|
||||||
EditorState,
|
|
||||||
EditorSelection,
|
|
||||||
Transaction,
|
|
||||||
Extension,
|
|
||||||
StateCommand,
|
|
||||||
StateField,
|
|
||||||
StateEffect,
|
|
||||||
MapMode,
|
|
||||||
CharCategory,
|
|
||||||
Text,
|
|
||||||
codePointAt,
|
|
||||||
fromCodePoint,
|
|
||||||
codePointSize,
|
|
||||||
RangeSet,
|
|
||||||
RangeValue,
|
|
||||||
SelectionRange,
|
|
||||||
} from '@codemirror/state'
|
|
||||||
import { syntaxTree } from '@codemirror/language'
|
|
||||||
|
|
||||||
/// Configures bracket closing behavior for a syntax (via
|
|
||||||
/// [language data](#state.EditorState.languageDataAt)) using the `"closeBrackets"`
|
|
||||||
/// identifier.
|
|
||||||
export interface CloseBracketConfig {
|
|
||||||
/// The opening and closing tokens.
|
|
||||||
brackets?: string[]
|
|
||||||
/// Characters in front of which newly opened brackets are
|
|
||||||
/// automatically closed. Closing always happens in front of
|
|
||||||
/// whitespace. Defaults to `")]}:;>"`.
|
|
||||||
before?: string
|
|
||||||
/// When determining whether a given node may be a string, recognize
|
|
||||||
/// these prefixes before the opening quote.
|
|
||||||
stringPrefixes?: string[]
|
|
||||||
/// An optional callback for overriding the content that's inserted
|
|
||||||
/// based on surrounding characters
|
|
||||||
// added by Overleaf
|
|
||||||
buildInsert?: (
|
|
||||||
state: EditorState,
|
|
||||||
range: SelectionRange,
|
|
||||||
open: string,
|
|
||||||
close: string
|
|
||||||
) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults: Required<CloseBracketConfig> = {
|
|
||||||
brackets: ['(', '[', '{', "'", '"'],
|
|
||||||
before: ')]}:;>',
|
|
||||||
stringPrefixes: [],
|
|
||||||
// added by Overleaf
|
|
||||||
buildInsert: (state, range, open, close) => open + close,
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeBracketEffect = StateEffect.define<number>({
|
|
||||||
map(value, mapping) {
|
|
||||||
const mapped = mapping.mapPos(value, -1, MapMode.TrackAfter)
|
|
||||||
return mapped === null ? undefined : mapped
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const closedBracket = new (class extends RangeValue {})()
|
|
||||||
closedBracket.startSide = 1
|
|
||||||
closedBracket.endSide = -1
|
|
||||||
|
|
||||||
const bracketState = StateField.define<RangeSet<typeof closedBracket>>({
|
|
||||||
create() {
|
|
||||||
return RangeSet.empty
|
|
||||||
},
|
|
||||||
update(value, tr) {
|
|
||||||
if (tr.selection) {
|
|
||||||
const lineStart = tr.state.doc.lineAt(tr.selection.main.head).from
|
|
||||||
const prevLineStart = tr.startState.doc.lineAt(
|
|
||||||
tr.startState.selection.main.head
|
|
||||||
).from
|
|
||||||
if (lineStart !== tr.changes.mapPos(prevLineStart, -1))
|
|
||||||
value = RangeSet.empty
|
|
||||||
}
|
|
||||||
value = value.map(tr.changes)
|
|
||||||
for (const effect of tr.effects)
|
|
||||||
if (effect.is(closeBracketEffect))
|
|
||||||
value = value.update({
|
|
||||||
add: [closedBracket.range(effect.value, effect.value + 1)],
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Extension to enable bracket-closing behavior. When a closeable
|
|
||||||
/// bracket is typed, its closing bracket is immediately inserted
|
|
||||||
/// after the cursor. When closing a bracket directly in front of a
|
|
||||||
/// closing bracket inserted by the extension, the cursor moves over
|
|
||||||
/// that bracket.
|
|
||||||
export function closeBrackets(): Extension {
|
|
||||||
return [inputHandler, bracketState]
|
|
||||||
}
|
|
||||||
|
|
||||||
const definedClosing = '()[]{}<>'
|
|
||||||
|
|
||||||
function closing(ch: number) {
|
|
||||||
for (let i = 0; i < definedClosing.length; i += 2)
|
|
||||||
if (definedClosing.charCodeAt(i) === ch) return definedClosing.charAt(i + 1)
|
|
||||||
return fromCodePoint(ch < 128 ? ch : ch + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function config(state: EditorState, pos: number) {
|
|
||||||
return (
|
|
||||||
state.languageDataAt<CloseBracketConfig>('closeBrackets', pos)[0] ||
|
|
||||||
defaults
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const android =
|
|
||||||
typeof navigator === 'object' && /Android\b/.test(navigator.userAgent)
|
|
||||||
|
|
||||||
const inputHandler = EditorView.inputHandler.of((view, from, to, insert) => {
|
|
||||||
if (
|
|
||||||
(android ? view.composing : view.compositionStarted) ||
|
|
||||||
view.state.readOnly
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
const sel = view.state.selection.main
|
|
||||||
if (
|
|
||||||
insert.length > 2 ||
|
|
||||||
(insert.length === 2 && codePointSize(codePointAt(insert, 0)) === 1) ||
|
|
||||||
from !== sel.from ||
|
|
||||||
to !== sel.to
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
const tr = insertBracket(view.state, insert)
|
|
||||||
if (!tr) return false
|
|
||||||
view.dispatch(tr)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
/// Command that implements deleting a pair of matching brackets when
|
|
||||||
/// the cursor is between them.
|
|
||||||
export const deleteBracketPair: StateCommand = ({ state, dispatch }) => {
|
|
||||||
if (state.readOnly) return false
|
|
||||||
const conf = config(state, state.selection.main.head)
|
|
||||||
const tokens = conf.brackets || defaults.brackets
|
|
||||||
let dont = null
|
|
||||||
const changes = state.changeByRange(range => {
|
|
||||||
if (range.empty) {
|
|
||||||
const before = prevChar(state.doc, range.head)
|
|
||||||
for (const token of tokens) {
|
|
||||||
if (
|
|
||||||
token === before &&
|
|
||||||
nextChar(state.doc, range.head) === closing(codePointAt(token, 0))
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
changes: {
|
|
||||||
from: range.head - token.length,
|
|
||||||
to: range.head + token.length,
|
|
||||||
},
|
|
||||||
range: EditorSelection.cursor(range.head - token.length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { range: (dont = range) }
|
|
||||||
})
|
|
||||||
if (!dont)
|
|
||||||
dispatch(
|
|
||||||
state.update(changes, {
|
|
||||||
scrollIntoView: true,
|
|
||||||
userEvent: 'delete.backward',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return !dont
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close-brackets related key bindings. Binds Backspace to
|
|
||||||
/// [`deleteBracketPair`](#autocomplete.deleteBracketPair).
|
|
||||||
export const closeBracketsKeymap: readonly KeyBinding[] = [
|
|
||||||
{ key: 'Backspace', run: deleteBracketPair },
|
|
||||||
]
|
|
||||||
|
|
||||||
/// Implements the extension's behavior on text insertion. If the
|
|
||||||
/// given string counts as a bracket in the language around the
|
|
||||||
/// selection, and replacing the selection with it requires custom
|
|
||||||
/// behavior (inserting a closing version or skipping past a
|
|
||||||
/// previously-closed bracket), this function returns a transaction
|
|
||||||
/// representing that custom behavior. (You only need this if you want
|
|
||||||
/// to programmatically insert brackets—the
|
|
||||||
/// [`closeBrackets`](#autocomplete.closeBrackets) extension will
|
|
||||||
/// take care of running this for user input.)
|
|
||||||
export function insertBracket(
|
|
||||||
state: EditorState,
|
|
||||||
bracket: string
|
|
||||||
): Transaction | null {
|
|
||||||
const conf = config(state, state.selection.main.head)
|
|
||||||
const tokens = conf.brackets || defaults.brackets
|
|
||||||
for (const tok of tokens) {
|
|
||||||
const closed = closing(codePointAt(tok, 0))
|
|
||||||
if (bracket === tok)
|
|
||||||
return closed === tok
|
|
||||||
? handleSame(
|
|
||||||
state,
|
|
||||||
tok,
|
|
||||||
tokens.indexOf(tok + tok) > -1,
|
|
||||||
tokens.indexOf(tok + tok + tok) > -1,
|
|
||||||
conf
|
|
||||||
)
|
|
||||||
: handleOpen(state, tok, closed, conf.before || defaults.before, conf)
|
|
||||||
|
|
||||||
if (bracket === closed && closedBracketAt(state, state.selection.main.from))
|
|
||||||
return handleClose(state, tok, closed)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function closedBracketAt(state: EditorState, pos: number) {
|
|
||||||
let found = false
|
|
||||||
state.field(bracketState).between(0, state.doc.length, from => {
|
|
||||||
if (from === pos) found = true
|
|
||||||
})
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextChar(doc: Text, pos: number) {
|
|
||||||
const next = doc.sliceString(pos, pos + 2)
|
|
||||||
return next.slice(0, codePointSize(codePointAt(next, 0)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevChar(doc: Text, pos: number) {
|
|
||||||
const prev = doc.sliceString(pos - 2, pos)
|
|
||||||
return codePointSize(codePointAt(prev, 0)) === prev.length
|
|
||||||
? prev
|
|
||||||
: prev.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpen(
|
|
||||||
state: EditorState,
|
|
||||||
open: string,
|
|
||||||
close: string,
|
|
||||||
closeBefore: string,
|
|
||||||
config: CloseBracketConfig
|
|
||||||
) {
|
|
||||||
// added by Overleaf
|
|
||||||
const buildInsert = config.buildInsert || defaults.buildInsert
|
|
||||||
|
|
||||||
let dont = null
|
|
||||||
const changes = state.changeByRange(range => {
|
|
||||||
if (!range.empty)
|
|
||||||
return {
|
|
||||||
changes: [
|
|
||||||
{ insert: open, from: range.from },
|
|
||||||
{ insert: close, from: range.to },
|
|
||||||
],
|
|
||||||
effects: closeBracketEffect.of(range.to + open.length),
|
|
||||||
range: EditorSelection.range(
|
|
||||||
range.anchor + open.length,
|
|
||||||
range.head + open.length
|
|
||||||
),
|
|
||||||
}
|
|
||||||
const next = nextChar(state.doc, range.head)
|
|
||||||
if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) {
|
|
||||||
// added by Overleaf
|
|
||||||
const insert = buildInsert(state, range, open, close) ?? open + close
|
|
||||||
|
|
||||||
return {
|
|
||||||
changes: { insert, from: range.head },
|
|
||||||
effects:
|
|
||||||
// modified by Overleaf
|
|
||||||
insert === open
|
|
||||||
? []
|
|
||||||
: closeBracketEffect.of(range.head + open.length),
|
|
||||||
range: EditorSelection.cursor(range.head + open.length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { range: (dont = range) }
|
|
||||||
})
|
|
||||||
return dont
|
|
||||||
? null
|
|
||||||
: state.update(changes, {
|
|
||||||
scrollIntoView: true,
|
|
||||||
userEvent: 'input.type',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose(state: EditorState, _open: string, close: string) {
|
|
||||||
let dont = null
|
|
||||||
const changes = state.changeByRange(range => {
|
|
||||||
if (range.empty && nextChar(state.doc, range.head) === close)
|
|
||||||
return {
|
|
||||||
changes: {
|
|
||||||
from: range.head,
|
|
||||||
to: range.head + close.length,
|
|
||||||
insert: close,
|
|
||||||
},
|
|
||||||
range: EditorSelection.cursor(range.head + close.length),
|
|
||||||
}
|
|
||||||
return (dont = { range })
|
|
||||||
})
|
|
||||||
return dont
|
|
||||||
? null
|
|
||||||
: state.update(changes, {
|
|
||||||
scrollIntoView: true,
|
|
||||||
userEvent: 'input.type',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles cases where the open and close token are the same, and
|
|
||||||
// possibly triple quotes (as in `"""abc"""`-style quoting).
|
|
||||||
function handleSame(
|
|
||||||
state: EditorState,
|
|
||||||
token: string,
|
|
||||||
// added by Overleaf
|
|
||||||
allowDouble: boolean,
|
|
||||||
allowTriple: boolean,
|
|
||||||
config: CloseBracketConfig
|
|
||||||
) {
|
|
||||||
const stringPrefixes = config.stringPrefixes || defaults.stringPrefixes
|
|
||||||
// added by Overleaf
|
|
||||||
const buildInsert = config.buildInsert || defaults.buildInsert
|
|
||||||
|
|
||||||
let dont = null
|
|
||||||
const changes = state.changeByRange(range => {
|
|
||||||
if (!range.empty)
|
|
||||||
return {
|
|
||||||
changes: [
|
|
||||||
{ insert: token, from: range.from },
|
|
||||||
{ insert: token, from: range.to },
|
|
||||||
],
|
|
||||||
effects: closeBracketEffect.of(range.to + token.length),
|
|
||||||
range: EditorSelection.range(
|
|
||||||
range.anchor + token.length,
|
|
||||||
range.head + token.length
|
|
||||||
),
|
|
||||||
}
|
|
||||||
const pos = range.head
|
|
||||||
const next = nextChar(state.doc, pos)
|
|
||||||
|
|
||||||
let start
|
|
||||||
if (
|
|
||||||
allowTriple &&
|
|
||||||
state.sliceDoc(pos - 2 * token.length, pos) === token + token &&
|
|
||||||
(start = canStartStringAt(
|
|
||||||
state,
|
|
||||||
pos - 2 * token.length,
|
|
||||||
stringPrefixes
|
|
||||||
)) > -1 &&
|
|
||||||
nodeStart(state, start)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
changes: { insert: token + token + token + token, from: pos },
|
|
||||||
effects: closeBracketEffect.of(pos + token.length),
|
|
||||||
range: EditorSelection.cursor(pos + token.length),
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
// added by Overleaf, for $$
|
|
||||||
allowDouble &&
|
|
||||||
state.sliceDoc(pos - token.length, pos) === token &&
|
|
||||||
(start = canStartStringAt(state, pos - token.length, stringPrefixes)) >
|
|
||||||
-1 &&
|
|
||||||
nodeStart(state, start)
|
|
||||||
) {
|
|
||||||
// added by Overleaf
|
|
||||||
const insert = buildInsert(state, range, token, token) ?? token + token
|
|
||||||
|
|
||||||
return {
|
|
||||||
changes: { insert, from: pos },
|
|
||||||
effects:
|
|
||||||
// modified by Overleaf
|
|
||||||
insert === token ? [] : closeBracketEffect.of(pos + token.length),
|
|
||||||
range: EditorSelection.cursor(pos + token.length),
|
|
||||||
}
|
|
||||||
} else if (next === token) {
|
|
||||||
if (nodeStart(state, pos)) {
|
|
||||||
// added by Overleaf
|
|
||||||
const insert = buildInsert(state, range, token, token) ?? token + token
|
|
||||||
|
|
||||||
return {
|
|
||||||
changes: { insert, from: pos },
|
|
||||||
effects:
|
|
||||||
// modified by Overleaf
|
|
||||||
insert === token ? [] : closeBracketEffect.of(pos + token.length),
|
|
||||||
range: EditorSelection.cursor(pos + token.length),
|
|
||||||
}
|
|
||||||
} else if (closedBracketAt(state, pos)) {
|
|
||||||
const isTriple =
|
|
||||||
allowTriple &&
|
|
||||||
state.sliceDoc(pos, pos + token.length * 3) === token + token + token
|
|
||||||
const content = isTriple ? token + token + token : token
|
|
||||||
return {
|
|
||||||
changes: { from: pos, to: pos + content.length, insert: content },
|
|
||||||
range: EditorSelection.cursor(pos + content.length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (state.charCategorizer(pos)(next) !== CharCategory.Word) {
|
|
||||||
if (
|
|
||||||
canStartStringAt(state, pos, stringPrefixes) > -1 &&
|
|
||||||
!probablyInString(state, pos, token, stringPrefixes)
|
|
||||||
) {
|
|
||||||
// added by Overleaf
|
|
||||||
const insert = buildInsert(state, range, token, token) ?? token + token
|
|
||||||
|
|
||||||
return {
|
|
||||||
changes: { insert, from: pos },
|
|
||||||
effects:
|
|
||||||
// modified by Overleaf
|
|
||||||
insert === token ? [] : closeBracketEffect.of(pos + token.length),
|
|
||||||
range: EditorSelection.cursor(pos + token.length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { range: (dont = range) }
|
|
||||||
})
|
|
||||||
return dont
|
|
||||||
? null
|
|
||||||
: state.update(changes, {
|
|
||||||
scrollIntoView: true,
|
|
||||||
userEvent: 'input.type',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeStart(state: EditorState, pos: number) {
|
|
||||||
const tree = syntaxTree(state).resolveInner(pos + 1)
|
|
||||||
return tree.parent && tree.from === pos
|
|
||||||
}
|
|
||||||
|
|
||||||
function probablyInString(
|
|
||||||
state: EditorState,
|
|
||||||
pos: number,
|
|
||||||
quoteToken: string,
|
|
||||||
prefixes: readonly string[]
|
|
||||||
) {
|
|
||||||
let node = syntaxTree(state).resolveInner(pos, -1)
|
|
||||||
const maxPrefix = prefixes.reduce((m, p) => Math.max(m, p.length), 0)
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const start = state.sliceDoc(
|
|
||||||
node.from,
|
|
||||||
Math.min(node.to, node.from + quoteToken.length + maxPrefix)
|
|
||||||
)
|
|
||||||
const quotePos = start.indexOf(quoteToken)
|
|
||||||
if (
|
|
||||||
!quotePos ||
|
|
||||||
(quotePos > -1 && prefixes.indexOf(start.slice(0, quotePos)) > -1)
|
|
||||||
) {
|
|
||||||
let first = node.firstChild
|
|
||||||
while (
|
|
||||||
first &&
|
|
||||||
first.from === node.from &&
|
|
||||||
first.to - first.from > quoteToken.length + quotePos
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
state.sliceDoc(first.to - quoteToken.length, first.to) === quoteToken
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
first = first.firstChild
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const parent = node.to === pos && node.parent
|
|
||||||
if (!parent) break
|
|
||||||
node = parent
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function canStartStringAt(
|
|
||||||
state: EditorState,
|
|
||||||
pos: number,
|
|
||||||
prefixes: readonly string[]
|
|
||||||
) {
|
|
||||||
const charCat = state.charCategorizer(pos)
|
|
||||||
if (charCat(state.sliceDoc(pos - 1, pos)) !== CharCategory.Word) return pos
|
|
||||||
for (const prefix of prefixes) {
|
|
||||||
const start = pos - prefix.length
|
|
||||||
if (
|
|
||||||
state.sliceDoc(start, pos) === prefix &&
|
|
||||||
charCat(state.sliceDoc(start - 1, start)) !== CharCategory.Word
|
|
||||||
)
|
|
||||||
return start
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
+5
-15
@@ -1,12 +1,9 @@
|
|||||||
import { CloseBracketConfig } from '../../extensions/close-brackets'
|
import { EditorState, SelectionRange, Text } from '@codemirror/state'
|
||||||
import {
|
import {
|
||||||
codePointAt,
|
CloseBracketConfig,
|
||||||
codePointSize,
|
completionStatus,
|
||||||
EditorState,
|
prevChar,
|
||||||
SelectionRange,
|
} from '@codemirror/autocomplete'
|
||||||
Text,
|
|
||||||
} from '@codemirror/state'
|
|
||||||
import { completionStatus } from '@codemirror/autocomplete'
|
|
||||||
|
|
||||||
export const closeBracketConfig: CloseBracketConfig = {
|
export const closeBracketConfig: CloseBracketConfig = {
|
||||||
brackets: ['$', '$$', '[', '{', '('],
|
brackets: ['$', '$$', '[', '{', '('],
|
||||||
@@ -90,13 +87,6 @@ export const closeBracketConfig: CloseBracketConfig = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevChar(doc: Text, pos: number) {
|
|
||||||
const prev = doc.sliceString(pos - 2, pos)
|
|
||||||
return codePointSize(codePointAt(prev, 0)) === prev.length
|
|
||||||
? prev
|
|
||||||
: prev.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function countSurroundingCharacters(doc: Text, pos: number, insert: string) {
|
function countSurroundingCharacters(doc: Text, pos: number, insert: string) {
|
||||||
let count = 0
|
let count = 0
|
||||||
// count backwards
|
// count backwards
|
||||||
|
|||||||
+88
-141
@@ -1,82 +1,27 @@
|
|||||||
|
import { EditorState, Text } from '@codemirror/state'
|
||||||
import {
|
import {
|
||||||
codePointAt,
|
clearSnippet,
|
||||||
codePointSize,
|
Completion,
|
||||||
EditorSelection,
|
snippet,
|
||||||
EditorState,
|
nextChar,
|
||||||
Text,
|
} from '@codemirror/autocomplete'
|
||||||
TransactionSpec,
|
|
||||||
} from '@codemirror/state'
|
|
||||||
import { clearSnippet, Completion, snippet } from '@codemirror/autocomplete'
|
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { prepareSnippetTemplate } from '../snippets'
|
import { prepareSnippetTemplate } from '../snippets'
|
||||||
import { ancestorNodeOfType } from '../../../utils/tree-query'
|
import { ancestorNodeOfType } from '../../../utils/tree-query'
|
||||||
|
|
||||||
// from https://github.com/codemirror/autocomplete/blob/main/src/closebrackets.ts
|
export const applySnippet = (template: string, clear = false) => {
|
||||||
export const nextChar = (doc: Text, pos: number) => {
|
return (
|
||||||
const next = doc.sliceString(pos, pos + 2)
|
view: EditorView,
|
||||||
return next.slice(0, codePointSize(codePointAt(next, 0)))
|
completion: Completion,
|
||||||
}
|
from: number,
|
||||||
|
to: number
|
||||||
export const prevChar = (doc: Text, pos: number) => {
|
) => {
|
||||||
const prev = doc.sliceString(pos - 2, pos)
|
snippet(prepareSnippetTemplate(template))(view, completion, from, to)
|
||||||
return codePointSize(codePointAt(prev, 0)) === prev.length
|
if (clear) {
|
||||||
? prev
|
clearSnippet(view)
|
||||||
: prev.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// from https://github.com/codemirror/autocomplete/blob/6.4.2/src/completion.ts
|
|
||||||
// forked due to an issue with `to` in https://github.com/codemirror/autocomplete/commit/a4cce022daea903c8b9ffcb7ca2fb598b17bfb66
|
|
||||||
export function insertCompletionText(
|
|
||||||
state: EditorState,
|
|
||||||
text: string,
|
|
||||||
from: number,
|
|
||||||
to: number
|
|
||||||
): TransactionSpec {
|
|
||||||
return {
|
|
||||||
...state.changeByRange(range => {
|
|
||||||
if (range === state.selection.main) {
|
|
||||||
return {
|
|
||||||
changes: { from, to, insert: text },
|
|
||||||
range: EditorSelection.cursor(from + text.length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!range.empty) {
|
|
||||||
return { range }
|
|
||||||
}
|
|
||||||
const len = to - from
|
|
||||||
if (
|
|
||||||
len &&
|
|
||||||
state.sliceDoc(range.from - len, range.from) !==
|
|
||||||
state.sliceDoc(from, to)
|
|
||||||
) {
|
|
||||||
return { range }
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
changes: { from: range.from - len, to: range.from, insert: text },
|
|
||||||
range: EditorSelection.cursor(range.from - len + text.length),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
userEvent: 'input.complete',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply a completed command, removing any subsequent closing brace, optionally
|
|
||||||
// providing a function that generates the completion text. If missing, the
|
|
||||||
// completion label is used.
|
|
||||||
export const createCommandApplier =
|
|
||||||
(text: string) =>
|
|
||||||
(view: EditorView, completion: Completion, from: number, to: number) => {
|
|
||||||
const { doc } = view.state
|
|
||||||
|
|
||||||
// extend forwards to cover an unpaired closing brace
|
|
||||||
if (nextChar(doc, to) === '}') {
|
|
||||||
if (countUnclosedBraces(doc, from, to) < 0) {
|
|
||||||
to++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: extend `to` to cover more subsequent characters?
|
|
||||||
view.dispatch(insertCompletionText(view.state, text, from, to))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const longestCommonPrefix = (...strs: string[]) => {
|
const longestCommonPrefix = (...strs: string[]) => {
|
||||||
if (strs.length === 0) {
|
if (strs.length === 0) {
|
||||||
@@ -93,68 +38,82 @@ const longestCommonPrefix = (...strs: string[]) => {
|
|||||||
return minLength
|
return minLength
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply a completed required parameter, adding a closing brace and extending the range if needed
|
// extend forwards to cover an unpaired closing brace
|
||||||
export const createRequiredParameterApplier =
|
export const extendRequiredParameter = (
|
||||||
(text: string) =>
|
state: EditorState,
|
||||||
(view: EditorView, completion: Completion, from: number, to: number) => {
|
change: {
|
||||||
const { doc } = view.state
|
from: number
|
||||||
const argumentNode = ancestorNodeOfType(view.state, from, '$Argument')
|
|
||||||
const isWellFormedArgumentNode =
|
|
||||||
argumentNode &&
|
|
||||||
argumentNode.getChild('OpenBrace') &&
|
|
||||||
argumentNode.getChild('CloseBrace')
|
|
||||||
|
|
||||||
// add a closing brace if needed
|
|
||||||
if (nextChar(doc, to) !== '}') {
|
|
||||||
if (countUnclosedBraces(doc, from, to) > 0) {
|
|
||||||
text += '}'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWellFormedArgumentNode) {
|
|
||||||
// extend over subsequent text that isn't a brace, space, or comma
|
|
||||||
const match = doc
|
|
||||||
.sliceString(to, Math.min(doc.lineAt(from).to, argumentNode.to))
|
|
||||||
.match(/^[^}\s,]+/)
|
|
||||||
if (match) {
|
|
||||||
to += match[0].length
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Ensure we don't swallow a closing brace
|
|
||||||
const restOfLine = doc
|
|
||||||
.sliceString(to, Math.min(doc.lineAt(from).to, from + text.length))
|
|
||||||
.split('}')[0]
|
|
||||||
|
|
||||||
to += longestCommonPrefix(text.slice(to - from), restOfLine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.dispatch(insertCompletionText(view.state, text, from, to))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Handle trailing '}' characters and tabbing back from final placeholder when
|
|
||||||
* inserting snippets
|
|
||||||
*/
|
|
||||||
const customSnippetApply = (template: string, clear = false) => {
|
|
||||||
return (
|
|
||||||
view: EditorView,
|
|
||||||
completion: Completion,
|
|
||||||
from: number,
|
|
||||||
to: number
|
to: number
|
||||||
) => {
|
insert: string | Text
|
||||||
// extend forwards to cover an unpaired closing brace
|
}
|
||||||
if (nextChar(view.state.doc, to) === '}') {
|
) => {
|
||||||
if (countUnclosedBraces(view.state.doc, from, to) < 0) {
|
if (typeof change.insert !== 'string') {
|
||||||
to++
|
return change
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const argumentNode = ancestorNodeOfType(state, change.from, '$Argument')
|
||||||
|
const isWellFormedArgumentNode =
|
||||||
|
argumentNode &&
|
||||||
|
argumentNode.getChild('OpenBrace') &&
|
||||||
|
argumentNode.getChild('CloseBrace')
|
||||||
|
|
||||||
|
// add a closing brace if needed
|
||||||
|
if (nextChar(state.doc, change.to) !== '}') {
|
||||||
|
if (countUnclosedBraces(state.doc, change.from, change.to) > 0) {
|
||||||
|
change.insert += '}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const snippetApply = snippet(prepareSnippetTemplate(template))
|
if (isWellFormedArgumentNode) {
|
||||||
snippetApply(view, completion, from, to)
|
// extend over subsequent text that isn't a brace, space, or comma
|
||||||
if (clear) {
|
const match = state.doc
|
||||||
clearSnippet(view)
|
.sliceString(
|
||||||
|
change.to,
|
||||||
|
Math.min(state.doc.lineAt(change.from).to, argumentNode.to)
|
||||||
|
)
|
||||||
|
.match(/^[^}\s,]+/)
|
||||||
|
if (match) {
|
||||||
|
change.to += match[0].length
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ensure we don't swallow a closing brace
|
||||||
|
const restOfLine = state.doc
|
||||||
|
.sliceString(
|
||||||
|
change.to,
|
||||||
|
Math.min(
|
||||||
|
state.doc.lineAt(change.from).to,
|
||||||
|
change.from + change.insert.length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.split('}')[0]
|
||||||
|
|
||||||
|
change.to += longestCommonPrefix(
|
||||||
|
change.insert.slice(change.to - change.from),
|
||||||
|
restOfLine
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return change
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend forwards to cover an unpaired closing brace
|
||||||
|
export const extendOverUnpairedClosingBrace = (
|
||||||
|
state: EditorState,
|
||||||
|
change: {
|
||||||
|
from: number
|
||||||
|
to: number
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (nextChar(state.doc, change.to) === '}') {
|
||||||
|
const unclosedBraces = countUnclosedBraces(
|
||||||
|
state.doc,
|
||||||
|
change.from,
|
||||||
|
change.to
|
||||||
|
)
|
||||||
|
if (unclosedBraces < 0) {
|
||||||
|
change.to++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return change
|
||||||
}
|
}
|
||||||
|
|
||||||
const countUnclosedBraces = (doc: Text, from: number, to: number): number => {
|
const countUnclosedBraces = (doc: Text, from: number, to: number): number => {
|
||||||
@@ -176,15 +135,3 @@ const countUnclosedBraces = (doc: Text, from: number, to: number): number => {
|
|||||||
|
|
||||||
return openBraces - closedBraces
|
return openBraces - closedBraces
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert from Ace `$1` to CodeMirror numbered placeholder format `${1}` or `#{1}` in snippets.
|
|
||||||
// Note: metadata from the server still uses the old format, so it's not enough to convert all
|
|
||||||
// the bundled data to the new format.
|
|
||||||
export const customSnippetCompletion = (
|
|
||||||
template: string,
|
|
||||||
completion: Completion,
|
|
||||||
clear = false
|
|
||||||
) => {
|
|
||||||
completion.apply = customSnippetApply(template, clear)
|
|
||||||
return completion
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
import { createRequiredParameterApplier } from './apply'
|
import { extendRequiredParameter } from './apply'
|
||||||
import { bibliographyStyles } from './data/bibliography-styles'
|
import { bibliographyStyles } from './data/bibliography-styles'
|
||||||
|
|
||||||
const values = Object.values(bibliographyStyles).flat()
|
const values = Object.values(bibliographyStyles).flat()
|
||||||
@@ -11,7 +11,7 @@ export function buildBibliographyStyleCompletions(completions: Completions) {
|
|||||||
completions.bibliographyStyles.push({
|
completions.bibliographyStyles.push({
|
||||||
type: 'bib',
|
type: 'bib',
|
||||||
label: item,
|
label: item,
|
||||||
apply: createRequiredParameterApplier(item),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { createRequiredParameterApplier } from './apply'
|
import { extendRequiredParameter } from './apply'
|
||||||
import { classNames } from './data/class-names'
|
import { classNames } from './data/class-names'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ export function buildClassCompletions(completions: Completions) {
|
|||||||
completions.classes.push({
|
completions.classes.push({
|
||||||
type: 'pkg',
|
type: 'pkg',
|
||||||
label: item,
|
label: item,
|
||||||
apply: createRequiredParameterApplier(item),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
import { customSnippetCompletion } from './apply'
|
import { applySnippet, extendOverUnpairedClosingBrace } from './apply'
|
||||||
import { Completion, CompletionContext } from '@codemirror/autocomplete'
|
import { Completion, CompletionContext } from '@codemirror/autocomplete'
|
||||||
import { documentCommands } from '../document-commands'
|
import { documentCommands } from '../document-commands'
|
||||||
import { Command } from '../../../utils/tree-operations/commands'
|
import { Command } from '../../../utils/tree-operations/commands'
|
||||||
@@ -16,19 +16,19 @@ export function customCommandCompletions(
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
)
|
)
|
||||||
|
|
||||||
const output = []
|
const output: Completion[] = []
|
||||||
|
|
||||||
const items = countCommandUsage(context)
|
const items = countCommandUsage(context)
|
||||||
|
|
||||||
for (const item of items.values()) {
|
for (const item of items.values()) {
|
||||||
if (!existingCommands.has(commandNameFromLabel(item.label))) {
|
if (!existingCommands.has(commandNameFromLabel(item.label))) {
|
||||||
output.push(
|
output.push({
|
||||||
customSnippetCompletion(item.snippet, {
|
type: 'cmd',
|
||||||
type: 'cmd',
|
label: item.label,
|
||||||
label: item.label,
|
boost: item.count - 10,
|
||||||
boost: item.count - 10,
|
apply: applySnippet(item.snippet),
|
||||||
})
|
extend: extendOverUnpairedClosingBrace,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-16
@@ -1,5 +1,5 @@
|
|||||||
import { environments, snippet } from './data/environments'
|
import { environments, snippet } from './data/environments'
|
||||||
import { customSnippetCompletion, createCommandApplier } from './apply'
|
import { applySnippet, extendOverUnpairedClosingBrace } from './apply'
|
||||||
import { Completion, CompletionContext } from '@codemirror/autocomplete'
|
import { Completion, CompletionContext } from '@codemirror/autocomplete'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
|
|
||||||
@@ -8,31 +8,31 @@ import { Completions } from './types'
|
|||||||
*/
|
*/
|
||||||
export function buildEnvironmentCompletions(completions: Completions) {
|
export function buildEnvironmentCompletions(completions: Completions) {
|
||||||
for (const [item, snippet] of environments) {
|
for (const [item, snippet] of environments) {
|
||||||
completions.commands.push(
|
// clear snippet for some environments after inserting
|
||||||
customSnippetCompletion(
|
const clear =
|
||||||
snippet,
|
item === 'abstract' || item === 'itemize' || item === 'enumerate'
|
||||||
{
|
completions.commands.push({
|
||||||
type: 'env',
|
type: 'env',
|
||||||
label: `\\begin{${item}} …`,
|
label: `\\begin{${item}} …`,
|
||||||
},
|
apply: applySnippet(snippet, clear),
|
||||||
// clear snippet for some environments after inserting
|
extend: extendOverUnpairedClosingBrace,
|
||||||
item === 'itemize' || item === 'enumerate'
|
})
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `begin` environment completion with a snippet, for the current context
|
* A `begin` environment completion with a snippet, for the current context
|
||||||
*/
|
*/
|
||||||
export function customBeginCompletion(name: string) {
|
export function customBeginCompletion(name: string): Completion | null {
|
||||||
if (environments.has(name)) {
|
if (environments.has(name)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return customSnippetCompletion(snippet(name), {
|
return {
|
||||||
label: `\\begin{${name}} …`,
|
label: `\\begin{${name}} …`,
|
||||||
})
|
apply: applySnippet(snippet(name)),
|
||||||
|
extend: extendOverUnpairedClosingBrace,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +61,6 @@ export function customEndCompletions(context: CompletionContext): Completion[] {
|
|||||||
completions.push({
|
completions.push({
|
||||||
label: env,
|
label: env,
|
||||||
boost: boost++, // environments opened later rank higher
|
boost: boost++, // environments opened later rank higher
|
||||||
apply: createCommandApplier(env),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-11
@@ -1,5 +1,8 @@
|
|||||||
import { CompletionContext } from '@codemirror/autocomplete'
|
import { CompletionContext } from '@codemirror/autocomplete'
|
||||||
import { createCommandApplier, createRequiredParameterApplier } from './apply'
|
import {
|
||||||
|
extendOverUnpairedClosingBrace,
|
||||||
|
extendRequiredParameter,
|
||||||
|
} from './apply'
|
||||||
import { Folder } from '../../../../../../../types/folder'
|
import { Folder } from '../../../../../../../types/folder'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
import { metadataState } from '../../../extensions/language'
|
import { metadataState } from '../../../extensions/language'
|
||||||
@@ -34,21 +37,24 @@ export function buildIncludeCompletions(
|
|||||||
completions.includes.push({
|
completions.includes.push({
|
||||||
type: 'file',
|
type: 'file',
|
||||||
label: path,
|
label: path,
|
||||||
apply: createRequiredParameterApplier(removeTexExtension(path)),
|
apply: removeTexExtension(path),
|
||||||
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// \include{path}
|
// \include{path}
|
||||||
completions.commands.push({
|
completions.commands.push({
|
||||||
type: 'cmd',
|
type: 'cmd',
|
||||||
label: `\\include{${path}}`,
|
label: `\\include{${path}}`,
|
||||||
apply: createCommandApplier(`\\include{${removeTexExtension(path)}}`),
|
apply: `\\include{${removeTexExtension(path)}}`,
|
||||||
|
extend: extendOverUnpairedClosingBrace,
|
||||||
})
|
})
|
||||||
|
|
||||||
// \input{path}
|
// \input{path}
|
||||||
completions.commands.push({
|
completions.commands.push({
|
||||||
type: 'cmd',
|
type: 'cmd',
|
||||||
label: `\\input{${path}}`,
|
label: `\\input{${path}}`,
|
||||||
apply: createCommandApplier(`\\input{${removeTexExtension(path)}}`),
|
apply: `\\input{${removeTexExtension(path)}}`,
|
||||||
|
extend: extendOverUnpairedClosingBrace,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,16 +64,13 @@ export function buildIncludeCompletions(
|
|||||||
completions.graphics.push({
|
completions.graphics.push({
|
||||||
type: 'file',
|
type: 'file',
|
||||||
label: path,
|
label: path,
|
||||||
apply: createRequiredParameterApplier(path), // TODO: remove extension?
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = `\\includegraphics{${path}}`
|
|
||||||
|
|
||||||
// \includegraphics{path}
|
|
||||||
completions.commands.push({
|
completions.commands.push({
|
||||||
type: 'cmd',
|
type: 'cmd',
|
||||||
label,
|
label: `\\includegraphics{${path}}`,
|
||||||
apply: createCommandApplier(label),
|
extend: extendOverUnpairedClosingBrace,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ export function buildIncludeCompletions(
|
|||||||
completions.bibliographies.push({
|
completions.bibliographies.push({
|
||||||
type: 'bib',
|
type: 'bib',
|
||||||
label,
|
label,
|
||||||
apply: createRequiredParameterApplier(label),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { createRequiredParameterApplier } from './apply'
|
import { extendRequiredParameter } from './apply'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
import { metadataState } from '../../../extensions/language'
|
import { metadataState } from '../../../extensions/language'
|
||||||
import { CompletionContext } from '@codemirror/autocomplete'
|
import { CompletionContext } from '@codemirror/autocomplete'
|
||||||
@@ -26,7 +26,7 @@ export function buildLabelCompletions(
|
|||||||
completions.labels.push({
|
completions.labels.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
label,
|
label,
|
||||||
apply: createRequiredParameterApplier(label),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
customSnippetCompletion,
|
applySnippet,
|
||||||
createRequiredParameterApplier,
|
extendOverUnpairedClosingBrace,
|
||||||
createCommandApplier,
|
extendRequiredParameter,
|
||||||
} from './apply'
|
} from './apply'
|
||||||
import { packageNames } from './data/package-names'
|
import { packageNames } from './data/package-names'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
@@ -29,12 +29,12 @@ export function buildPackageCompletions(
|
|||||||
uniquePackageNames.add(packageName)
|
uniquePackageNames.add(packageName)
|
||||||
|
|
||||||
for (const item of commands) {
|
for (const item of commands) {
|
||||||
completions.commands.push(
|
completions.commands.push({
|
||||||
customSnippetCompletion(item.snippet, {
|
type: item.meta,
|
||||||
type: item.meta,
|
label: item.caption,
|
||||||
label: item.caption,
|
apply: applySnippet(item.snippet),
|
||||||
})
|
extend: extendOverUnpairedClosingBrace,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ export function buildPackageCompletions(
|
|||||||
completions.packages.push({
|
completions.packages.push({
|
||||||
type: 'pkg',
|
type: 'pkg',
|
||||||
label: item,
|
label: item,
|
||||||
apply: createRequiredParameterApplier(item),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = `\\usepackage{${item}}`
|
const label = `\\usepackage{${item}}`
|
||||||
@@ -56,19 +56,19 @@ export function buildPackageCompletions(
|
|||||||
completions.commands.push({
|
completions.commands.push({
|
||||||
type: 'pkg',
|
type: 'pkg',
|
||||||
label,
|
label,
|
||||||
apply: createCommandApplier(label),
|
extend: extendOverUnpairedClosingBrace,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty \\usepackage{…} snippet
|
// empty \\usepackage{…} snippet
|
||||||
completions.commands.push(
|
completions.commands.push({
|
||||||
customSnippetCompletion('\\usepackage{#{}}', {
|
type: 'pkg',
|
||||||
type: 'pkg',
|
label: '\\usepackage{}',
|
||||||
label: '\\usepackage{}',
|
boost: 10,
|
||||||
boost: 10,
|
apply: applySnippet('\\usepackage{#{}}'),
|
||||||
})
|
extend: extendOverUnpairedClosingBrace,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const findExistingPackageNames = (context: CompletionContext) => {
|
const findExistingPackageNames = (context: CompletionContext) => {
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
import { CompletionContext } from '@codemirror/autocomplete'
|
import { CompletionContext } from '@codemirror/autocomplete'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
import { metadataState } from '../../../extensions/language'
|
import { metadataState } from '../../../extensions/language'
|
||||||
import { createRequiredParameterApplier } from './apply'
|
import { extendRequiredParameter } from './apply'
|
||||||
|
|
||||||
export function buildReferenceCompletions(
|
export function buildReferenceCompletions(
|
||||||
completions: Completions,
|
completions: Completions,
|
||||||
@@ -20,7 +20,7 @@ export function buildReferenceCompletions(
|
|||||||
completions.references.push({
|
completions.references.push({
|
||||||
type: 'reference',
|
type: 'reference',
|
||||||
label: reference,
|
label: reference,
|
||||||
apply: createRequiredParameterApplier(reference),
|
extend: extendRequiredParameter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
import topHundredSnippets from './data/top-hundred-snippets'
|
import topHundredSnippets from './data/top-hundred-snippets'
|
||||||
import snippets from './data/snippets'
|
import snippets from './data/snippets'
|
||||||
import { customSnippetCompletion } from './apply'
|
import { applySnippet, extendOverUnpairedClosingBrace } from './apply'
|
||||||
import { Completions } from './types'
|
import { Completions } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,21 +8,21 @@ import { Completions } from './types'
|
|||||||
*/
|
*/
|
||||||
export function buildSnippetCompletions(completions: Completions) {
|
export function buildSnippetCompletions(completions: Completions) {
|
||||||
for (const item of topHundredSnippets) {
|
for (const item of topHundredSnippets) {
|
||||||
completions.commands.push(
|
completions.commands.push({
|
||||||
customSnippetCompletion(item.snippet, {
|
type: item.meta,
|
||||||
type: item.meta,
|
label: item.caption,
|
||||||
label: item.caption,
|
boost: item.score,
|
||||||
boost: item.score,
|
apply: applySnippet(item.snippet),
|
||||||
})
|
extend: extendOverUnpairedClosingBrace,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of snippets) {
|
for (const item of snippets) {
|
||||||
completions.commands.push(
|
completions.commands.push({
|
||||||
customSnippetCompletion(item.snippet, {
|
type: item.type,
|
||||||
type: item.type,
|
label: item.label,
|
||||||
label: item.label,
|
apply: applySnippet(item.snippet),
|
||||||
})
|
extend: extendOverUnpairedClosingBrace,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
// Add a final placeholder at the end of the snippet to allow for
|
// 1. Convert from Ace `$1` to CodeMirror numbered placeholder format `${1}` or `#{1}` in snippets.
|
||||||
|
// Note: metadata from the server still uses the old format, so it's not enough to convert all
|
||||||
|
// the bundled data to the new format.
|
||||||
|
// 2. Add a final placeholder at the end of the snippet to allow for
|
||||||
// shift-tabbing back from the penultimate placeholder. See #8697.
|
// shift-tabbing back from the penultimate placeholder. See #8697.
|
||||||
export const prepareSnippetTemplate = (template: string): string => {
|
export const prepareSnippetTemplate = (template: string): string => {
|
||||||
return template.replace(/\$(\d+)/g, '#{$1}') + '${}'
|
return template.replace(/\$(\d+)/g, '#{$1}') + '${}'
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
"@babel/preset-env": "^7.14.5",
|
"@babel/preset-env": "^7.14.5",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@babel/preset-typescript": "^7.16.0",
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
"@codemirror/autocomplete": "^6.5.1",
|
"@codemirror/autocomplete": "github:overleaf/codemirror-autocomplete#cd13c2c15a6f89b207f7cc4523e38fd5ef0efc47",
|
||||||
"@codemirror/commands": "^6.2.3",
|
"@codemirror/commands": "^6.2.3",
|
||||||
"@codemirror/lang-markdown": "^6.1.1",
|
"@codemirror/lang-markdown": "^6.1.1",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
|
|||||||
+11
-11
@@ -1003,46 +1003,46 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||||||
cy.get('@line').type('\\include{e', { delay: 100 })
|
cy.get('@line').type('\\include{e', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('example.tex').click()
|
cy.findAllByRole('option').contains('example.tex').click()
|
||||||
activeEditorLine().contains('\\include{example')
|
activeEditorLine().contains('\\include{example')
|
||||||
activeEditorLine().type('{}}{Enter}')
|
activeEditorLine().type('}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().type('\\include{s', { delay: 100 })
|
activeEditorLine().type('\\include{s', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||||
activeEditorLine().contains('\\include{sometext.txt')
|
activeEditorLine().should('have.text', '\\include{sometext.txt}')
|
||||||
activeEditorLine().type('{}}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().type('\\inclu', { delay: 100 })
|
activeEditorLine().type('\\inclu', { delay: 100 })
|
||||||
cy.contains('\\include{}').click()
|
cy.contains('\\include{}').click()
|
||||||
cy.contains('example.tex').click()
|
cy.contains('example.tex').click()
|
||||||
activeEditorLine().contains('\\include{example}')
|
activeEditorLine().should('have.text', '\\include{example}')
|
||||||
activeEditorLine().type('{rightArrow}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().type('\\inclu', { delay: 100 })
|
activeEditorLine().type('\\inclu', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('\\include{}').click()
|
cy.findAllByRole('option').contains('\\include{}').click()
|
||||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||||
activeEditorLine().contains('\\include{sometext.txt}')
|
activeEditorLine().should('have.text', '\\include{sometext.txt}')
|
||||||
activeEditorLine().type('{rightArrow}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().click().as('line')
|
activeEditorLine().click().as('line')
|
||||||
activeEditorLine().type('\\input{e', { delay: 100 })
|
activeEditorLine().type('\\input{e', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('example.tex').click()
|
cy.findAllByRole('option').contains('example.tex').click()
|
||||||
activeEditorLine().contains('\\input{example')
|
activeEditorLine().should('have.text', '\\input{example}')
|
||||||
activeEditorLine().type('{}}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().click().as('line')
|
activeEditorLine().click().as('line')
|
||||||
activeEditorLine().type('\\input{s', { delay: 100 })
|
activeEditorLine().type('\\input{s', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||||
activeEditorLine().contains('\\input{sometext.txt')
|
activeEditorLine().should('have.text', '\\input{sometext.txt}')
|
||||||
activeEditorLine().type('{}}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().type('\\inpu', { delay: 100 })
|
activeEditorLine().type('\\inpu', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('\\input{}').click()
|
cy.findAllByRole('option').contains('\\input{}').click()
|
||||||
cy.findAllByRole('option').contains('example.tex').click()
|
cy.findAllByRole('option').contains('example.tex').click()
|
||||||
activeEditorLine().contains('\\input{example}')
|
activeEditorLine().should('have.text', '\\input{example}')
|
||||||
activeEditorLine().type('{rightArrow}{Enter}')
|
activeEditorLine().type('{rightArrow}{Enter}')
|
||||||
|
|
||||||
activeEditorLine().type('\\inpu', { delay: 100 })
|
activeEditorLine().type('\\inpu', { delay: 100 })
|
||||||
cy.findAllByRole('option').contains('\\input{}').click()
|
cy.findAllByRole('option').contains('\\input{}').click()
|
||||||
cy.findAllByRole('option').contains('sometext.txt').click()
|
cy.findAllByRole('option').contains('sometext.txt').click()
|
||||||
activeEditorLine().contains('\\input{sometext.txt}')
|
activeEditorLine().should('have.text', '\\input{sometext.txt}')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-1
@@ -3,6 +3,8 @@ import { EditorProviders } from '../../../helpers/editor-providers'
|
|||||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||||
import { mockScope } from '../helpers/mock-scope'
|
import { mockScope } from '../helpers/mock-scope'
|
||||||
|
|
||||||
|
const isMac = /Mac/.test(window.navigator?.platform)
|
||||||
|
|
||||||
const Container: FC = ({ children }) => (
|
const Container: FC = ({ children }) => (
|
||||||
<div style={{ width: 785, height: 785 }}>{children}</div>
|
<div style={{ width: 785, height: 785 }}>{children}</div>
|
||||||
)
|
)
|
||||||
@@ -110,7 +112,7 @@ ${'long line '.repeat(200)}`
|
|||||||
it('has no active line highlight when there is a selection', function () {
|
it('has no active line highlight when there is a selection', function () {
|
||||||
// Put the cursor on a blank line
|
// Put the cursor on a blank line
|
||||||
cy.get('.cm-line').eq(1).click().as('line')
|
cy.get('.cm-line').eq(1).click().as('line')
|
||||||
cy.get('@line').type('{ctrl}A')
|
cy.get('@line').type(isMac ? '{cmd}A' : '{ctrl}A')
|
||||||
|
|
||||||
cy.get('.cm-activeLine').should('not.exist')
|
cy.get('.cm-activeLine').should('not.exist')
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user