mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Streamline the project metadata context provider (#19384)
GitOrigin-RevId: 0b75635cb9141983827dfd0fa6a58b6182d47f22
This commit is contained in:
parent
1e1a8c0bb3
commit
2d2746ef24
9 changed files with 88 additions and 295 deletions
|
@ -11,7 +11,6 @@ import {
|
|||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import _ from 'lodash'
|
||||
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
|
@ -22,28 +21,31 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions-
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
|
||||
|
||||
type DocumentMetadata = {
|
||||
export type Command = {
|
||||
caption: string
|
||||
snippet: string
|
||||
meta: string
|
||||
score: number
|
||||
}
|
||||
|
||||
export type DocumentMetadata = {
|
||||
labels: string[]
|
||||
packages: Record<string, any>
|
||||
packages: Record<string, Command[]>
|
||||
packageNames: string[]
|
||||
}
|
||||
|
||||
type DocumentsMetadata = Record<string, DocumentMetadata>
|
||||
|
||||
type MetadataContextValue = {
|
||||
metadata: {
|
||||
state: {
|
||||
documents: DocumentsMetadata
|
||||
}
|
||||
getAllLabels: () => DocumentMetadata['labels']
|
||||
getAllPackages: () => DocumentMetadata['packages']
|
||||
}
|
||||
}
|
||||
|
||||
type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
|
||||
|
||||
export const MetadataContext = createContext<MetadataContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
export const MetadataContext = createContext<
|
||||
| {
|
||||
commands: Command[]
|
||||
labels: Set<string>
|
||||
packageNames: Set<string>
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const MetadataProvider: FC = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -65,7 +67,8 @@ export const MetadataProvider: FC = ({ children }) => {
|
|||
}: CustomEvent<IdeEvents['entity:deleted']>) => {
|
||||
if (entity.type === 'doc') {
|
||||
setDocuments(documents => {
|
||||
return _.omit(documents, entity.entity._id)
|
||||
delete documents[entity.entity._id]
|
||||
return { ...documents }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -90,23 +93,6 @@ export const MetadataProvider: FC = ({ children }) => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const getAllLabels = useCallback(
|
||||
() => _.flattenDeep(Object.values(documents).map(meta => meta.labels)),
|
||||
[documents]
|
||||
)
|
||||
|
||||
const getAllPackages = useCallback(() => {
|
||||
const packageCommandMapping: Record<string, any> = {}
|
||||
for (const meta of Object.values(documents)) {
|
||||
for (const [packageName, commandSnippets] of Object.entries(
|
||||
meta.packages
|
||||
)) {
|
||||
packageCommandMapping[packageName] = commandSnippets
|
||||
}
|
||||
}
|
||||
return packageCommandMapping
|
||||
}, [documents])
|
||||
|
||||
const loadProjectMetaFromServer = useCallback(() => {
|
||||
getJSON(`/project/${projectId}/metadata`).then(
|
||||
(response: { projectMeta: DocumentsMetadata }) => {
|
||||
|
@ -212,16 +198,15 @@ export const MetadataProvider: FC = ({ children }) => {
|
|||
}
|
||||
}, [eventEmitter, loadProjectMetaFromServer, showGenericMessageModal, t])
|
||||
|
||||
const value = useMemo<MetadataContextValue>(
|
||||
() => ({
|
||||
metadata: {
|
||||
state: { documents },
|
||||
getAllLabels,
|
||||
getAllPackages,
|
||||
},
|
||||
}),
|
||||
[documents, getAllLabels, getAllPackages]
|
||||
)
|
||||
const value = useMemo(() => {
|
||||
const docs = Object.values(documents)
|
||||
|
||||
return {
|
||||
commands: docs.flatMap(doc => Object.values(doc.packages).flat()),
|
||||
labels: new Set(docs.flatMap(doc => doc.labels)),
|
||||
packageNames: new Set(docs.flatMap(doc => doc.packageNames)),
|
||||
}
|
||||
}, [documents])
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={value}>
|
||||
|
@ -230,7 +215,7 @@ export const MetadataProvider: FC = ({ children }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export function useMetadataContext(): MetadataContextValue {
|
||||
export function useMetadataContext() {
|
||||
const context = useContext(MetadataContext)
|
||||
|
||||
if (!context) {
|
||||
|
|
|
@ -7,8 +7,9 @@ import {
|
|||
import { languages } from '../languages'
|
||||
import { ViewPlugin } from '@codemirror/view'
|
||||
import { indentUnit, LanguageDescription } from '@codemirror/language'
|
||||
import { Metadata } from '../../../../../types/metadata'
|
||||
import { updateHasEffect } from '../utils/effects'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { Command } from '@/features/ide-react/context/metadata-context'
|
||||
|
||||
export const languageLoadedEffect = StateEffect.define()
|
||||
export const hasLanguageLoadedEffect = updateHasEffect(languageLoadedEffect)
|
||||
|
@ -19,6 +20,14 @@ type Options = {
|
|||
syntaxValidation: boolean
|
||||
}
|
||||
|
||||
type Metadata = {
|
||||
labels: Set<string>
|
||||
packageNames: Set<string>
|
||||
commands: Command[]
|
||||
references: string[]
|
||||
fileTreeData: Folder
|
||||
}
|
||||
|
||||
/**
|
||||
* A state field that stores the metadata parsed from a project on the server.
|
||||
*/
|
||||
|
|
|
@ -73,7 +73,7 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
|
||||
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
|
||||
|
||||
const { metadata } = useMetadataContext()
|
||||
const metadata = useMetadataContext()
|
||||
|
||||
const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
|
||||
|
||||
|
@ -213,16 +213,16 @@ function useCodeMirrorScope(view: EditorView) {
|
|||
// set the project metadata, mostly for use in autocomplete
|
||||
// TODO: read this data from the scope?
|
||||
const metadataRef = useRef({
|
||||
documents: metadata.state.documents,
|
||||
...metadata,
|
||||
references: references.keys,
|
||||
fileTreeData,
|
||||
})
|
||||
|
||||
// listen to project metadata (docs + packages) updates
|
||||
// listen to project metadata (commands, labels and package names) updates
|
||||
useEffect(() => {
|
||||
metadataRef.current.documents = metadata.state.documents
|
||||
metadataRef.current = { ...metadataRef.current, ...metadata }
|
||||
view.dispatch(setMetadata(metadataRef.current))
|
||||
}, [view, metadata.state.documents])
|
||||
}, [view, metadata])
|
||||
|
||||
// listen to project reference keys updates
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,13 +16,7 @@ export function buildLabelCompletions(
|
|||
return
|
||||
}
|
||||
|
||||
const uniqueLabels = new Set(
|
||||
Object.values(metadata.documents)
|
||||
.map(doc => doc.labels)
|
||||
.flat(1)
|
||||
)
|
||||
|
||||
for (const label of uniqueLabels) {
|
||||
for (const label of metadata.labels) {
|
||||
completions.labels.push({
|
||||
type: 'label',
|
||||
label,
|
||||
|
|
|
@ -21,24 +21,24 @@ export function buildPackageCompletions(
|
|||
return
|
||||
}
|
||||
|
||||
const uniquePackageNames = new Set<string>(packageNames)
|
||||
|
||||
// package names and commands from packages in the project
|
||||
for (const doc of Object.values(metadata.documents)) {
|
||||
for (const [packageName, commands] of Object.entries(doc.packages)) {
|
||||
uniquePackageNames.add(packageName)
|
||||
|
||||
for (const item of commands) {
|
||||
completions.commands.push({
|
||||
type: item.meta,
|
||||
label: item.caption,
|
||||
apply: applySnippet(item.snippet),
|
||||
extend: extendOverUnpairedClosingBrace,
|
||||
})
|
||||
}
|
||||
}
|
||||
// commands from packages in the project
|
||||
for (const command of metadata.commands) {
|
||||
completions.commands.push({
|
||||
type: command.meta,
|
||||
label: command.caption,
|
||||
apply: applySnippet(command.snippet),
|
||||
extend: extendOverUnpairedClosingBrace,
|
||||
})
|
||||
}
|
||||
|
||||
const uniquePackageNames = new Set<string>(packageNames)
|
||||
|
||||
// package names from packages in the project
|
||||
for (const packageName of metadata.packageNames) {
|
||||
uniquePackageNames.add(packageName)
|
||||
}
|
||||
|
||||
// exclude package names that are already in this document
|
||||
const existingPackageNames = findExistingPackageNames(context)
|
||||
|
||||
for (const item of uniquePackageNames) {
|
||||
|
|
|
@ -133,15 +133,6 @@ const initialize = () => {
|
|||
console.log('open doc', id, options)
|
||||
},
|
||||
},
|
||||
metadataManager: {
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
'test-file-id': { labels: ['sec:section-label'], packages: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// window.metaAttributesCache is reset in preview.tsx
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Folder } from '../../../../../types/folder'
|
||||
import { docId, mockDocContent } from '../helpers/mock-doc'
|
||||
import { Metadata } from '../../../../../types/metadata'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
|
@ -62,42 +61,13 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
},
|
||||
]
|
||||
|
||||
const metadataManager: { metadata: { state: Metadata } } = {
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
[docId]: {
|
||||
labels: ['fig:frog'],
|
||||
// TODO: add tests for packages and referencesKeys autocompletions
|
||||
packages: {
|
||||
foo: [
|
||||
{
|
||||
caption: 'a caption',
|
||||
meta: 'foo-cmd',
|
||||
score: 0.1,
|
||||
snippet: 'a caption{$1}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
fileTreeData: rootFolder[0],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const scope = mockScope()
|
||||
scope.$root._references.keys = ['foo']
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
metadataManager={metadataManager}
|
||||
rootFolder={rootFolder as any}
|
||||
>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
@ -238,41 +208,12 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
},
|
||||
]
|
||||
|
||||
const metadataManager: { metadata: { state: Metadata } } = {
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
[docId]: {
|
||||
labels: ['fig:frog'],
|
||||
// TODO: add tests for packages and referencesKeys autocompletions
|
||||
packages: {
|
||||
foo: [
|
||||
{
|
||||
caption: 'a caption',
|
||||
meta: 'foo-cmd',
|
||||
score: 0.1,
|
||||
snippet: 'a caption{$1}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
fileTreeData: rootFolder[0],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const scope = mockScope()
|
||||
scope.$root._references.keys = ['foo']
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
metadataManager={metadataManager}
|
||||
rootFolder={rootFolder as any}
|
||||
>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
@ -358,33 +299,15 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
},
|
||||
]
|
||||
|
||||
const metadata = {
|
||||
commands: [],
|
||||
labels: new Set<string>(),
|
||||
packageNames: new Set(['foo']),
|
||||
}
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider
|
||||
value={{
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
[docId]: {
|
||||
labels: [],
|
||||
packages: {
|
||||
foo: [
|
||||
{
|
||||
caption: 'a caption',
|
||||
meta: 'foo-cmd',
|
||||
score: 0.1,
|
||||
snippet: 'a caption{$1}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAllLabels: () => [],
|
||||
getAllPackages: () => ({ foo: {} }),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MetadataContext.Provider value={metadata}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
|
@ -452,36 +375,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
const scope = mockScope()
|
||||
scope.$root._references.keys = ['ref-1', 'ref-2', 'ref-3']
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider
|
||||
value={{
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
[docId]: {
|
||||
labels: [],
|
||||
packages: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAllLabels: () => [],
|
||||
getAllPackages: () => ({}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={MetadataProvider}
|
||||
rootFolder={rootFolder as any}
|
||||
>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
@ -535,31 +431,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
scope.$root._references.keys = ['foo']
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider
|
||||
value={{
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {},
|
||||
},
|
||||
getAllLabels: () => [],
|
||||
getAllPackages: () => ({}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
rootFolder={rootFolder as any}
|
||||
providers={MetadataProvider}
|
||||
>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
@ -824,33 +698,22 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
it('displays unique completions for commands', function () {
|
||||
const scope = mockScope()
|
||||
|
||||
const metadata = {
|
||||
commands: [
|
||||
{
|
||||
caption: '\\label{}', // label{} is also included in top-hundred-snippets
|
||||
meta: 'amsmath-cmd',
|
||||
score: 1,
|
||||
snippet: '\\label{$1}',
|
||||
},
|
||||
],
|
||||
labels: new Set<string>(),
|
||||
packageNames: new Set<string>('amsmath'),
|
||||
}
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider
|
||||
value={{
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {
|
||||
[docId]: {
|
||||
labels: [],
|
||||
packages: {
|
||||
amsmath: [
|
||||
{
|
||||
caption: '\\label{}',
|
||||
meta: 'amsmath-cmd',
|
||||
score: 1,
|
||||
snippet: '\\label{$1}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAllLabels: () => [],
|
||||
getAllPackages: () => ({}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MetadataContext.Provider value={metadata}>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
|
@ -1033,31 +896,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
scope.$root._references.keys = ['foo']
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
const MetadataProvider: FC = ({ children }) => {
|
||||
return (
|
||||
<MetadataContext.Provider
|
||||
value={{
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {},
|
||||
},
|
||||
getAllLabels: () => [],
|
||||
getAllPackages: () => ({}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MetadataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={MetadataProvider}
|
||||
rootFolder={rootFolder as any}
|
||||
>
|
||||
<EditorProviders scope={scope} rootFolder={rootFolder as any}>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
|
|
@ -95,13 +95,6 @@ export function EditorProviders({
|
|||
getCurrentDocValue: () => {},
|
||||
openDoc: sinon.stub(),
|
||||
},
|
||||
metadataManager = {
|
||||
metadata: {
|
||||
state: {
|
||||
documents: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
userSettings = {},
|
||||
providers = {},
|
||||
}) {
|
||||
|
@ -153,7 +146,6 @@ export function EditorProviders({
|
|||
clsiServerId,
|
||||
editorManager,
|
||||
fileTreeManager,
|
||||
metadataManager,
|
||||
}
|
||||
|
||||
// Add details for useUserContext
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { Folder } from './folder'
|
||||
|
||||
export type Package = {
|
||||
caption: string
|
||||
meta: string
|
||||
score: number
|
||||
snippet: string
|
||||
}
|
||||
|
||||
export type MetadataDocument = {
|
||||
labels: string[]
|
||||
packages: Record<string, Package[]>
|
||||
}
|
||||
|
||||
export type Metadata = {
|
||||
documents: Record<string, MetadataDocument>
|
||||
references: string[]
|
||||
fileTreeData: Folder
|
||||
}
|
Loading…
Reference in a new issue