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, }; }