import type { Editor } from '@tiptap/core' import type { distance, FuzzballTokenSetOptions, partial_ratio, partial_token_similarity_sort_ratio, WRatio } from 'fuzzball' import { action } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import { extract, partial_token_set_ratio } from 'fuzzball' import { defaultAutoCompleteCallback, setAutoCompleteCallback, setAutoCompleteOptions } from '../components/BlockTree' import { getLazyAtTags, getLazyHashTags, getLazyPlusTags, useCurrentThread } from '../ui/reactive' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export const suggestionMenuTriggers = ['/', '#', '@', '+'] export function isNeedingSuggestionMenu(eventKey, currentWordStart: string) { VERBOSE({ eventKey }) if (suggestionMenuTriggers.includes(eventKey) || suggestionMenuTriggers.includes(currentWordStart)) { DEBUG('suggestionMenuTrigger:', { eventKey }) return true } return false } export function tagMatcherForAutoComplete(editorMap, refs) { const currentThread = useCurrentThread() return action((event: KeyboardEvent) => { const editor: Editor = editorMap.get(refs().editorRef) // const text = editor.getText() // const textLength = text.length // const currentPos = editor.state.selection.anchor - 1 // let nextStart = 0 // const words = text.split(' ').map(eachWord => { // const retObj = { // word: eachWord, // st: nextStart, // ed: eachWord.length + nextStart, // } // nextStart = retObj.ed + 1 // include the space // return retObj // }) // const currentWordInfo = words.find(eachWord => currentPos >= eachWord.st && currentPos <= eachWord.ed) // if (!currentWordInfo) return // const currentWordSpan = { from: currentWordInfo.st + 1, to: currentWordInfo.ed + 1 } // const currentWord = currentWordInfo?.word // const currentWordStart = currentWord.substring(0, 1) // const currentWordWithoutFirst = currentWord.substring(1) // VERBOSE({ editor, focused: editor.isFocused, text, textLength, currentPos, currentWord }) const { currentWordInfo, currentWordSpan } = editor.commands.getWordInfo() const currentWord = currentWordInfo?.word const currentWordStart = currentWord.substring(0, 1) const currentWordWithoutFirst = currentWord.substring(1) type scorerTypes = | typeof distance | typeof WRatio | typeof partial_token_similarity_sort_ratio | typeof partial_token_set_ratio | typeof partial_ratio const prioritizeStart = (str1: string, str2: string) => { let extra = 0 for (let eachChar = 1; eachChar <= str1.length; eachChar++) { const queryStart = str1.substring(0, eachChar) const foundIndex = str2.indexOf(queryStart) if (foundIndex === 0) extra += 10 if (foundIndex === 1) extra += 5 if (foundIndex > 1) extra += Math.ceil((1 / foundIndex) * 8) } VERBOSE({ extra }) return extra } const scorer: scorerTypes = (str1: string, str2: string, opts?: FuzzballTokenSetOptions) => partial_token_set_ratio(str1, str2, opts) + prioritizeStart(str1, str2) const options = { trySimple: true, /* sortBySimilarity: true, */ scorer } if (isNeedingSuggestionMenu(event.key, currentWordStart)) { const whichTagArray // ? formatting = currentWordStart === '#' ? Array.from(getLazyHashTags(currentThread).keys()) : currentWordStart === '@' ? Array.from(getLazyAtTags(currentThread).keys()) : currentWordStart === '+' ? Array.from(getLazyPlusTags(currentThread).keys()) : [] if (whichTagArray.length) { const matches = extract(currentWordWithoutFirst, whichTagArray, options).slice(0, 5) VERBOSE({ matches, whichTagArray }) setAutoCompleteOptions(matches.map(m => `${currentWordStart}${m[0]}`)) setAutoCompleteCallback(() => (chosenWord) => { DEBUG('replacing', { currentWord, chosenWord }, editor) editor // https://tiptap.dev/docs/editor/api/commands#inline-commands .chain() .focus() .command(({ tr }) => !!tr.insertText(chosenWord, currentWordSpan.from, currentWordSpan.to)) .run() setAutoCompleteOptions([]) }) } } else { setAutoCompleteOptions([]) setAutoCompleteCallback(() => defaultAutoCompleteCallback) } }) }