Files
overleaf-cep/services/web/frontend/js/features/command-palette/hooks/use-file-tree-command-source.ts
T
Mathias Jakobsen 33e2a1e282 Merge pull request #36526 from overleaf/mj-command-palette-network-stall-files
[web] Avoid searching files when network is stalled

GitOrigin-RevId: 08707b1c8a7e0088e8fee6ead3e74bdd807514c7
2026-09-17 11:55:12 +00:00

162 lines
4.5 KiB
TypeScript

import { useCallback, useMemo } from 'react'
import { CommandPaletteSearchResult, CommandPaletteSource } from '../types'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { Folder } from '@ol-types/folder'
import MiniSearch from 'minisearch'
import { findInTree } from '@/features/file-tree/util/find-in-tree'
import { debugConsole } from '@/utils/debugging'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import useIsNetworkStalled from '@/features/ide-react/hooks/use-is-network-stalled'
type FlatFileTree = { path: string; name: string; id: string }[]
const FILE_POSITION_REGEX = /:(\d+)(?:,(\d+))?$/
const useFileTreeCommandSource = (): CommandPaletteSource => {
const { fileTreeData } = useFileTreeData()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const networkStalled = useIsNetworkStalled()
const flatFileTree = useMemo(
() => flattenFileTree(fileTreeData),
[fileTreeData]
)
const index = useMemo(
() => flatFileTree && indexFromFlatFileTree(flatFileTree),
[flatFileTree]
)
const onSelect = useCallback(
async (
id: string,
line: number | undefined,
column: number | undefined
) => {
if (!fileTreeData) {
return
}
const file = findInTree(fileTreeData, id)
if (!file) {
return
}
if (file.type === 'doc') {
await openDocWithId(file.entity._id, {
gotoLine: line,
gotoColumn: column,
})
} else if (file.type === 'fileRef') {
openFileWithId(file.entity._id)
} else {
debugConsole.error('Attempting to open invalid entity type')
}
},
[fileTreeData, openDocWithId, openFileWithId]
)
const defaults = useCallback((): CommandPaletteSearchResult[] => {
if (networkStalled) {
return []
}
if (!flatFileTree) {
return []
}
const files: CommandPaletteSearchResult[] = flatFileTree
.slice(0, 10)
.map(({ path, name, id }) => ({
title: name,
description:
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
onSelect: () => onSelect(id, undefined, undefined),
score: 1,
eventSegmentation: { source: 'file' },
}))
return files
}, [flatFileTree, onSelect, networkStalled])
const source: CommandPaletteSource = useMemo(
() => ({
id: 'file-tree',
search(query) {
if (networkStalled) {
return []
}
if (!index) {
return []
}
const fileLocationMatch = query.match(FILE_POSITION_REGEX)
let line: number | undefined = undefined
let column: number | undefined = undefined
if (fileLocationMatch) {
line = parseInt(fileLocationMatch[1], 10)
column = fileLocationMatch[2]
? parseInt(fileLocationMatch[2], 10)
: undefined
query = query.slice(0, fileLocationMatch.index)
}
const result = index.search(query, {
prefix: true,
fuzzy: term => (term.length > 3 ? 0.2 : false),
})
return result.map(({ path, name, id, score }) => ({
title: name,
description:
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
onSelect: () => onSelect(id, line, column),
score,
eventSegmentation: { source: 'file' },
}))
},
defaults,
}),
[index, onSelect, defaults, networkStalled]
)
return source
}
export default useFileTreeCommandSource
const flattenFileTree = (
folder: Folder,
parentPath = ''
): FlatFileTree | null => {
if (!folder) {
return null
}
const currentPath = `${parentPath}/${folder.name}`
const files = folder.fileRefs.map(file => ({
path: `${currentPath}/${file.name}`.replace(/^\/rootFolder\//, ''),
name: file.name,
id: file._id,
}))
const docs = folder.docs.map(doc => ({
path: `${currentPath}/${doc.name}`.replace(/^\/rootFolder\//, ''),
name: doc.name,
id: doc._id,
}))
const subFolders = folder.folders
.flatMap(subFolder => flattenFileTree(subFolder, currentPath))
.filter(x => x !== null)
return [...docs, ...files, ...subFolders]
}
function indexFromFlatFileTree(flatFileTree: FlatFileTree): MiniSearch {
const miniSearch = new MiniSearch({
fields: ['path', 'name'],
storeFields: ['path', 'name'],
})
if (!flatFileTree) {
return miniSearch
}
miniSearch.addAll(flatFileTree)
return miniSearch
}