diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx index 6424c48f2f..11662f7d9a 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-search-form.tsx @@ -22,8 +22,12 @@ import Icon from '../../../shared/components/icon' import classnames from 'classnames' import useScopeValue from '../../../shared/hooks/use-scope-value' import { getStoredSelection, setStoredSelection } from '../extensions/search' +import { debounce } from 'lodash' +import { EditorState } from '@codemirror/state' -const MAX_MATCH_COUNT = 1000 +const MATCH_COUNT_DEBOUNCE_WAIT = 100 // the amount of ms to wait before counting matches +const MAX_MATCH_COUNT = 999 // the maximum number of matches to count +const MAX_MATCH_TIME = 100 // the maximum amount of ms allowed for counting matches type ActiveSearchOption = | 'caseSensitive' @@ -32,6 +36,12 @@ type ActiveSearchOption = | 'withinSelection' | null +type MatchPositions = { + current: number | null + total: number + interrupted: boolean +} + const CodeMirrorSearchForm: FC = () => { const view = useCodeMirrorViewContext() const state = useCodeMirrorStateContext() @@ -52,10 +62,7 @@ const CodeMirrorSearchForm: FC = () => { const { t } = useTranslation() - const [position, setPosition] = useState<{ - current: number - total: number - } | null>(null) + const [position, setPosition] = useState(null) const formRef = useRef(null) const inputRef = useRef(null) @@ -80,34 +87,7 @@ const CodeMirrorSearchForm: FC = () => { }, []) useEffect(() => { - const { from, to } = state.selection.main - - const query = getSearchQuery(state) - - if (query.valid) { - const cursor = query.getCursor(state.doc) as SearchCursor - - let total = 0 - let current = 0 - - while (!cursor.next().done) { - total++ - - if (total >= MAX_MATCH_COUNT) { - break - } - - const item = cursor.value - - if (current === 0 && item.from === from && item.to === to) { - current = total - } - } - - setPosition({ current, total }) - } else { - setPosition(null) - } + buildPosition(state, setPosition) }, [state]) const handleChange = useCallback(() => { @@ -435,9 +415,9 @@ const CodeMirrorSearchForm: FC = () => { {position !== null && (
- {position.total === MAX_MATCH_COUNT - ? `${position.current} ${t('of')} ${MAX_MATCH_COUNT}+` - : `${position.current} ${t('of')} ${position.total}`} + {position.current === null ? '?' : position.current} {t('of')}{' '} + {position.total} + {position.interrupted && '+'}
)} @@ -483,3 +463,60 @@ function isInvalidRegExp(source: string) { } export default CodeMirrorSearchForm + +const buildPosition = debounce( + ( + state: EditorState, + setPosition: (position: MatchPositions | null) => void + ) => { + const { main } = state.selection + + const query = getSearchQuery(state) + + if (!query.valid) { + return setPosition(null) + } + + const cursor = query.getCursor(state.doc) as SearchCursor + + const startTime = Date.now() + + let total = 0 + let current = null + + while (!cursor.next().done) { + total++ + + // if there are too many matches, bail out + if (total >= MAX_MATCH_COUNT) { + return setPosition({ + current, + total, + interrupted: true, + }) + } + + const { from, to } = cursor.value + + if (current === null && main.from === from && main.to === to) { + current = total + } + + // if finding matches is taking too long, bail out + if (Date.now() - startTime > MAX_MATCH_TIME) { + return setPosition({ + current, + total, + interrupted: true, + }) + } + } + + setPosition({ + current: current ?? 0, + total, + interrupted: false, + }) + }, + MATCH_COUNT_DEBOUNCE_WAIT +)