import { useCallback, useEffect, useMemo, useState } from "react"; import { Debouncer } from "../../lib/debouncer"; import { nullableCompatibleEqualityCheck } from "../../lib/utils"; import { AutosuggestionsBareFunction } from "../../types/base"; import { AutosuggestionState } from "../../types/base/autosuggestion-state"; import { EditorAutocompleteState, areEqual_autocompleteState, } from "../../types/base/editor-autocomplete-state"; export interface UseAutosuggestionsResult { currentAutocompleteSuggestion: AutosuggestionState | null; onChangeHandler: (newEditorState: EditorAutocompleteState | null) => void; onKeyDownHandler: (event: React.KeyboardEvent) => void; onTouchStartHandler: (event: React.TouchEvent) => void; } export function useAutosuggestions( debounceTime: number, shouldAcceptAutosuggestionOnKeyPress: (event: React.KeyboardEvent) => boolean, shouldAcceptAutosuggestionOnTouch: (event: React.TouchEvent) => boolean, autosuggestionFunction: AutosuggestionsBareFunction, insertAutocompleteSuggestion: (suggestion: AutosuggestionState) => void, disableWhenEmpty: boolean, disabled: boolean, ): UseAutosuggestionsResult { const [previousAutocompleteState, setPreviousAutocompleteState] = useState(null); const [currentAutocompleteSuggestion, setCurrentAutocompleteSuggestion] = useState(null); const awaitForAndAppendSuggestion: ( editorAutocompleteState: EditorAutocompleteState, abortSignal: AbortSignal, ) => Promise = useCallback( async (editorAutocompleteState: EditorAutocompleteState, abortSignal: AbortSignal) => { // early return if disabled if (disabled) { return; } if ( disableWhenEmpty && editorAutocompleteState.textBeforeCursor === "" && editorAutocompleteState.textAfterCursor === "" ) { return; } // fetch the suggestion const suggestion = await autosuggestionFunction(editorAutocompleteState, abortSignal); // We'll assume for now that the autocomplete function might or might not respect the abort signal. if (!suggestion || abortSignal.aborted) { throw new DOMException("Aborted", "AbortError"); } setCurrentAutocompleteSuggestion({ text: suggestion, point: editorAutocompleteState.cursorPoint, }); }, [autosuggestionFunction, setCurrentAutocompleteSuggestion, disableWhenEmpty, disabled], ); const debouncedFunction = useMemo( () => new Debouncer<[editorAutocompleteState: EditorAutocompleteState]>(debounceTime), [debounceTime], ); // clean current state when unmounting or disabling useEffect(() => { return () => { debouncedFunction.cancel(); setCurrentAutocompleteSuggestion(null); }; }, [debouncedFunction, disabled]); const onChange = useCallback( (newEditorState: EditorAutocompleteState | null) => { const editorStateHasChanged = !nullableCompatibleEqualityCheck( areEqual_autocompleteState, previousAutocompleteState, newEditorState, ); setPreviousAutocompleteState(newEditorState); // if no change, do nothing if (!editorStateHasChanged) { return; } // if change, then first null out the current suggestion setCurrentAutocompleteSuggestion(null); // then try to get a new suggestion, debouncing to avoid too many requests while typing if (newEditorState) { debouncedFunction.debounce(awaitForAndAppendSuggestion, newEditorState); } else { debouncedFunction.cancel(); } }, [ previousAutocompleteState, setPreviousAutocompleteState, debouncedFunction, awaitForAndAppendSuggestion, setCurrentAutocompleteSuggestion, ], ); const keyDownOrTouchHandler = useCallback( (event: React.KeyboardEvent | React.TouchEvent) => { if (currentAutocompleteSuggestion) { const shouldAcceptSuggestion = event.type === "touchstart" ? shouldAcceptAutosuggestionOnTouch(event as React.TouchEvent) : shouldAcceptAutosuggestionOnKeyPress(event as React.KeyboardEvent); if (shouldAcceptSuggestion) { event.preventDefault(); insertAutocompleteSuggestion(currentAutocompleteSuggestion); setCurrentAutocompleteSuggestion(null); } } }, [ currentAutocompleteSuggestion, setCurrentAutocompleteSuggestion, insertAutocompleteSuggestion, shouldAcceptAutosuggestionOnKeyPress, ], ); return { currentAutocompleteSuggestion, onChangeHandler: onChange, onKeyDownHandler: keyDownOrTouchHandler, onTouchStartHandler: keyDownOrTouchHandler, }; }