mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
Merge pull request #35056 from overleaf/mj-command-palette-improvements
[web] Command palette improvements GitOrigin-RevId: 3d6e11697ed4726b7ecada1744d24a15291ad646
This commit is contained in:
committed by
Copybot
parent
4ef7324263
commit
912fb2dcd8
+6
-6
@@ -111,17 +111,17 @@ const CommandPaletteBody: FC<CommandPaletteBodyProps> = ({ show, onHide }) => {
|
||||
>
|
||||
<button
|
||||
role="menuitem"
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onMouseMove={() => setSelectedIndex(index)}
|
||||
onClick={() => runResult(result)}
|
||||
>
|
||||
<div className="command-palette-result-title">
|
||||
{result.title}
|
||||
{result.description && (
|
||||
<span className="command-palette-result-description">
|
||||
{result.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{result.description && (
|
||||
<div className="command-palette-result-description">
|
||||
{result.description}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
+30
-10
@@ -1,7 +1,7 @@
|
||||
import useDebounce from '@/shared/hooks/use-debounce'
|
||||
import useCommandPaletteSources from './use-command-palette-sources'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CommandPaletteSearchResult } from '../types'
|
||||
import { CommandPaletteSearchResult, CommandPaletteSource } from '../types'
|
||||
|
||||
const useCommandPaletteResults = (query: string) => {
|
||||
const debouncedQuery = useDebounce(query, 25)
|
||||
@@ -9,17 +9,37 @@ const useCommandPaletteResults = (query: string) => {
|
||||
const [results, setResults] = useState<CommandPaletteSearchResult[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery) {
|
||||
const res = sources.map(source => source.search(debouncedQuery))
|
||||
setResults(res.flat().sort((a, b) => b.score - a.score))
|
||||
} else {
|
||||
const res = sources
|
||||
.filter(s => 'defaults' in s)
|
||||
.map(source => source.defaults!())
|
||||
setResults(res.flat().sort((a, b) => b.score - a.score))
|
||||
}
|
||||
const res = getSourcesMatchingQuery(debouncedQuery, sources).map(
|
||||
({ source, query }) =>
|
||||
query ? source.search(query) : (source.defaults?.() ?? [])
|
||||
)
|
||||
setResults(res.flat().sort((a, b) => b.score - a.score))
|
||||
}, [debouncedQuery, sources])
|
||||
return results
|
||||
}
|
||||
|
||||
const getSourcesMatchingQuery = (
|
||||
query: string,
|
||||
sources: CommandPaletteSource[]
|
||||
): { query: string; source: CommandPaletteSource }[] => {
|
||||
// No query matches all sources
|
||||
if (!query) return sources.map(source => ({ source, query }))
|
||||
|
||||
// If the query matches a prefix, return all the sources with that prefix
|
||||
const matchingSources = sources.filter(
|
||||
source => source.prefix && query.startsWith(source.prefix)
|
||||
)
|
||||
if (matchingSources.length > 0) {
|
||||
return matchingSources.map(source => ({
|
||||
source,
|
||||
query: query.slice(source.prefix!.length).trim(),
|
||||
}))
|
||||
}
|
||||
|
||||
// otherwise return all sources that don't require a prefix match
|
||||
return sources
|
||||
.filter(source => !source.prefixRequired)
|
||||
.map(source => ({ source, query: query.trim() }))
|
||||
}
|
||||
|
||||
export default useCommandPaletteResults
|
||||
|
||||
+5
-2
@@ -2,13 +2,16 @@ import { useMemo } from 'react'
|
||||
import { CommandPaletteSource } from '../types'
|
||||
import useCommandRegistrySource from './use-command-registry-source'
|
||||
import useFileTreeCommandSource from './use-file-tree-command-source'
|
||||
import { useJumpToLineCommandSource } from './use-jump-to-line-command-source'
|
||||
|
||||
const useCommandPaletteSources = (): CommandPaletteSource[] => {
|
||||
const fileTreeSource = useFileTreeCommandSource()
|
||||
const commandRegistrySource = useCommandRegistrySource()
|
||||
const jumpToLineSource = useJumpToLineCommandSource()
|
||||
|
||||
const sources = useMemo(
|
||||
() => [fileTreeSource, commandRegistrySource],
|
||||
[fileTreeSource, commandRegistrySource]
|
||||
() => [fileTreeSource, commandRegistrySource, jumpToLineSource],
|
||||
[fileTreeSource, commandRegistrySource, jumpToLineSource]
|
||||
)
|
||||
return sources
|
||||
}
|
||||
|
||||
+77
-33
@@ -5,42 +5,85 @@ import {
|
||||
Command,
|
||||
useCommandRegistry,
|
||||
} from '@/features/ide-react/context/command-registry-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ENABLED_COMMANDS: string[] = [
|
||||
'new_file',
|
||||
'new_folder',
|
||||
'upload_file',
|
||||
'open-settings',
|
||||
'show_version_history',
|
||||
'word_count',
|
||||
'view-pdf-presentation-mode',
|
||||
'comment',
|
||||
'compile',
|
||||
'stop-compile',
|
||||
'recompile-from-scratch',
|
||||
'synctex-sync-to-pdf',
|
||||
'synctex-sync-to-code',
|
||||
'insert-inline-math',
|
||||
'insert-display-math',
|
||||
'insert-figure-from-computer',
|
||||
'insert-figure-from-project-files',
|
||||
'insert-figure-from-another-project',
|
||||
'insert-figure-from-url',
|
||||
'insert-table',
|
||||
'insert-citation',
|
||||
'insert-link',
|
||||
'insert-cross-reference',
|
||||
]
|
||||
type CommandGroup = {
|
||||
label: string
|
||||
actions: string[]
|
||||
}
|
||||
|
||||
const useCommandRegistrySource = (): CommandPaletteSource => {
|
||||
const { t } = useTranslation()
|
||||
const elements: (CommandGroup | string)[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('file'),
|
||||
actions: ['new_file', 'new_folder', 'upload_file'],
|
||||
},
|
||||
{
|
||||
label: t('view'),
|
||||
actions: [
|
||||
'change-layout-side-by-side',
|
||||
'change-layout-editor-only',
|
||||
'change-layout-pdf-only',
|
||||
'change-layout-detached-pdf',
|
||||
'change-layout-focus-mode',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('insert'),
|
||||
actions: [
|
||||
'insert-inline-math',
|
||||
'insert-display-math',
|
||||
'insert-figure-from-computer',
|
||||
'insert-figure-from-project-files',
|
||||
'insert-figure-from-another-project',
|
||||
'insert-figure-from-url',
|
||||
'insert-table',
|
||||
'insert-citation',
|
||||
'insert-link',
|
||||
'insert-cross-reference',
|
||||
],
|
||||
},
|
||||
'open-settings',
|
||||
'show_version_history',
|
||||
'word_count',
|
||||
'view-pdf-presentation-mode',
|
||||
'comment',
|
||||
'compile',
|
||||
'stop-compile',
|
||||
'recompile-from-scratch',
|
||||
'synctex-sync-to-pdf',
|
||||
'synctex-sync-to-code',
|
||||
'close-tab',
|
||||
'close-other-tabs',
|
||||
],
|
||||
[t]
|
||||
)
|
||||
const { registry } = useCommandRegistry()
|
||||
|
||||
const commands = useMemo(() => {
|
||||
const enabled = new Set(ENABLED_COMMANDS)
|
||||
return [...registry.values()].filter(
|
||||
c => enabled.has(c.id) && !c.disabled && c.handler
|
||||
)
|
||||
}, [registry])
|
||||
const ret: Command[] = []
|
||||
for (const element of elements) {
|
||||
if (typeof element === 'string') {
|
||||
const command = registry.get(element)
|
||||
if (command && !command.disabled && command.handler) {
|
||||
ret.push(command)
|
||||
}
|
||||
} else {
|
||||
for (const action of element.actions) {
|
||||
const command = registry.get(action)
|
||||
if (command && !command.disabled && command.handler) {
|
||||
ret.push({
|
||||
...command,
|
||||
label: `${element.label}: ${command.label}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}, [registry, elements])
|
||||
|
||||
const defaults = useCallback((): CommandPaletteSearchResult[] => {
|
||||
return commands.map(command => ({
|
||||
@@ -53,7 +96,7 @@ const useCommandRegistrySource = (): CommandPaletteSource => {
|
||||
const index = useMemo(() => {
|
||||
const miniSearch = new MiniSearch<Command>({
|
||||
fields: ['label'],
|
||||
storeFields: ['id'],
|
||||
storeFields: ['id', 'label'],
|
||||
idField: 'id',
|
||||
})
|
||||
miniSearch.addAll(commands)
|
||||
@@ -63,17 +106,18 @@ const useCommandRegistrySource = (): CommandPaletteSource => {
|
||||
return useMemo<CommandPaletteSource>(
|
||||
() => ({
|
||||
id: 'command-registry',
|
||||
prefix: '>',
|
||||
search(query) {
|
||||
const results = index.search(query, {
|
||||
prefix: true,
|
||||
fuzzy: term => (term.length > 3 ? 0.2 : false),
|
||||
})
|
||||
return results.flatMap(({ id, score }) => {
|
||||
return results.flatMap(({ id, label, score }) => {
|
||||
const command = registry.get(id)
|
||||
if (!command?.handler) return []
|
||||
return [
|
||||
{
|
||||
title: command.label,
|
||||
title: label,
|
||||
onSelect: () => command.handler!({ location: 'command-palette' }),
|
||||
score,
|
||||
},
|
||||
|
||||
+30
-6
@@ -9,9 +9,12 @@ import { useEditorManagerContext } from '@/features/ide-react/context/editor-man
|
||||
|
||||
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 flatFileTree = useMemo(
|
||||
() => flattenFileTree(fileTreeData),
|
||||
[fileTreeData]
|
||||
@@ -22,7 +25,11 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
|
||||
)
|
||||
|
||||
const onSelect = useCallback(
|
||||
async (id: string) => {
|
||||
async (
|
||||
id: string,
|
||||
line: number | undefined,
|
||||
column: number | undefined
|
||||
) => {
|
||||
if (!fileTreeData) {
|
||||
return
|
||||
}
|
||||
@@ -31,7 +38,10 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
|
||||
return
|
||||
}
|
||||
if (file.type === 'doc') {
|
||||
await openDocWithId(file.entity._id)
|
||||
await openDocWithId(file.entity._id, {
|
||||
gotoLine: line,
|
||||
gotoColumn: column,
|
||||
})
|
||||
} else if (file.type === 'fileRef') {
|
||||
openFileWithId(file.entity._id)
|
||||
} else {
|
||||
@@ -49,8 +59,9 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
|
||||
.slice(0, 10)
|
||||
.map(({ path, name, id }) => ({
|
||||
title: name,
|
||||
description: path === name ? undefined : path,
|
||||
onSelect: () => onSelect(id),
|
||||
description:
|
||||
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
|
||||
onSelect: () => onSelect(id, undefined, undefined),
|
||||
score: 1,
|
||||
}))
|
||||
|
||||
@@ -64,14 +75,27 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
|
||||
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,
|
||||
onSelect: () => onSelect(id),
|
||||
description:
|
||||
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
|
||||
onSelect: () => onSelect(id, line, column),
|
||||
score,
|
||||
}))
|
||||
},
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import { CommandPaletteSource } from '../types'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const JUMP_REGEX = /^(\d+)(?:,(\d+))?$/
|
||||
|
||||
export const useJumpToLineCommandSource = (): CommandPaletteSource => {
|
||||
const { jumpToLine } = useEditorManagerContext()
|
||||
const { t } = useTranslation()
|
||||
return useMemo(
|
||||
() => ({
|
||||
id: 'jump-to-line-source',
|
||||
prefix: ':',
|
||||
prefixRequired: true,
|
||||
search: (query: string) => {
|
||||
const match = query.match(JUMP_REGEX)
|
||||
if (!match) {
|
||||
return []
|
||||
}
|
||||
const line = parseInt(match[1], 10)
|
||||
const column = match[2] ? parseInt(match[2], 10) : undefined
|
||||
|
||||
return [
|
||||
{
|
||||
onSelect: () => jumpToLine({ gotoLine: line, gotoColumn: column }),
|
||||
title:
|
||||
column != null
|
||||
? t('jump_to_line_x_column_y', {
|
||||
line,
|
||||
column,
|
||||
})
|
||||
: t('jump_to_line_x', {
|
||||
line,
|
||||
}),
|
||||
score: 1,
|
||||
},
|
||||
]
|
||||
},
|
||||
}),
|
||||
[jumpToLine, t]
|
||||
)
|
||||
}
|
||||
@@ -9,4 +9,6 @@ export type CommandPaletteSource = {
|
||||
id: string
|
||||
search(query: string): CommandPaletteSearchResult[]
|
||||
defaults?(): CommandPaletteSearchResult[]
|
||||
prefix?: string
|
||||
prefixRequired?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user