Streamline the project metadata context provider (#19384)

GitOrigin-RevId: 0b75635cb9141983827dfd0fa6a58b6182d47f22
This commit is contained in:
Alf Eaton 2024-07-15 10:13:34 +01:00 committed by Copybot
parent 1e1a8c0bb3
commit 2d2746ef24
9 changed files with 88 additions and 295 deletions

View file

@ -11,7 +11,6 @@ import {
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useConnectionContext } from '@/features/ide-react/context/connection-context' import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import _ from 'lodash'
import { getJSON, postJSON } from '@/infrastructure/fetch-json' import { getJSON, postJSON } from '@/infrastructure/fetch-json'
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context' import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
import { useEditorContext } from '@/shared/context/editor-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 { useTranslation } from 'react-i18next'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter' 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[] labels: string[]
packages: Record<string, any> packages: Record<string, Command[]>
packageNames: string[]
} }
type DocumentsMetadata = Record<string, DocumentMetadata> type DocumentsMetadata = Record<string, DocumentMetadata>
type MetadataContextValue = {
metadata: {
state: {
documents: DocumentsMetadata
}
getAllLabels: () => DocumentMetadata['labels']
getAllPackages: () => DocumentMetadata['packages']
}
}
type DocMetadataResponse = { docId: string; meta: DocumentMetadata } type DocMetadataResponse = { docId: string; meta: DocumentMetadata }
export const MetadataContext = createContext<MetadataContextValue | undefined>( export const MetadataContext = createContext<
undefined | {
) commands: Command[]
labels: Set<string>
packageNames: Set<string>
}
| undefined
>(undefined)
export const MetadataProvider: FC = ({ children }) => { export const MetadataProvider: FC = ({ children }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -65,7 +67,8 @@ export const MetadataProvider: FC = ({ children }) => {
}: CustomEvent<IdeEvents['entity:deleted']>) => { }: CustomEvent<IdeEvents['entity:deleted']>) => {
if (entity.type === 'doc') { if (entity.type === 'doc') {
setDocuments(documents => { 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(() => { const loadProjectMetaFromServer = useCallback(() => {
getJSON(`/project/${projectId}/metadata`).then( getJSON(`/project/${projectId}/metadata`).then(
(response: { projectMeta: DocumentsMetadata }) => { (response: { projectMeta: DocumentsMetadata }) => {
@ -212,16 +198,15 @@ export const MetadataProvider: FC = ({ children }) => {
} }
}, [eventEmitter, loadProjectMetaFromServer, showGenericMessageModal, t]) }, [eventEmitter, loadProjectMetaFromServer, showGenericMessageModal, t])
const value = useMemo<MetadataContextValue>( const value = useMemo(() => {
() => ({ const docs = Object.values(documents)
metadata: {
state: { documents }, return {
getAllLabels, commands: docs.flatMap(doc => Object.values(doc.packages).flat()),
getAllPackages, labels: new Set(docs.flatMap(doc => doc.labels)),
}, packageNames: new Set(docs.flatMap(doc => doc.packageNames)),
}), }
[documents, getAllLabels, getAllPackages] }, [documents])
)
return ( return (
<MetadataContext.Provider value={value}> <MetadataContext.Provider value={value}>
@ -230,7 +215,7 @@ export const MetadataProvider: FC = ({ children }) => {
) )
} }
export function useMetadataContext(): MetadataContextValue { export function useMetadataContext() {
const context = useContext(MetadataContext) const context = useContext(MetadataContext)
if (!context) { if (!context) {

View file

@ -7,8 +7,9 @@ import {
import { languages } from '../languages' import { languages } from '../languages'
import { ViewPlugin } from '@codemirror/view' import { ViewPlugin } from '@codemirror/view'
import { indentUnit, LanguageDescription } from '@codemirror/language' import { indentUnit, LanguageDescription } from '@codemirror/language'
import { Metadata } from '../../../../../types/metadata'
import { updateHasEffect } from '../utils/effects' 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 languageLoadedEffect = StateEffect.define()
export const hasLanguageLoadedEffect = updateHasEffect(languageLoadedEffect) export const hasLanguageLoadedEffect = updateHasEffect(languageLoadedEffect)
@ -19,6 +20,14 @@ type Options = {
syntaxValidation: boolean 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. * A state field that stores the metadata parsed from a project on the server.
*/ */

View file

@ -73,7 +73,7 @@ function useCodeMirrorScope(view: EditorView) {
const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext() const { reviewPanelOpen, miniReviewPanelVisible } = useLayoutContext()
const { metadata } = useMetadataContext() const metadata = useMetadataContext()
const [loadingThreads] = useScopeValue<boolean>('loadingThreads') const [loadingThreads] = useScopeValue<boolean>('loadingThreads')
@ -213,16 +213,16 @@ function useCodeMirrorScope(view: EditorView) {
// set the project metadata, mostly for use in autocomplete // set the project metadata, mostly for use in autocomplete
// TODO: read this data from the scope? // TODO: read this data from the scope?
const metadataRef = useRef({ const metadataRef = useRef({
documents: metadata.state.documents, ...metadata,
references: references.keys, references: references.keys,
fileTreeData, fileTreeData,
}) })
// listen to project metadata (docs + packages) updates // listen to project metadata (commands, labels and package names) updates
useEffect(() => { useEffect(() => {
metadataRef.current.documents = metadata.state.documents metadataRef.current = { ...metadataRef.current, ...metadata }
view.dispatch(setMetadata(metadataRef.current)) view.dispatch(setMetadata(metadataRef.current))
}, [view, metadata.state.documents]) }, [view, metadata])
// listen to project reference keys updates // listen to project reference keys updates
useEffect(() => { useEffect(() => {

View file

@ -16,13 +16,7 @@ export function buildLabelCompletions(
return return
} }
const uniqueLabels = new Set( for (const label of metadata.labels) {
Object.values(metadata.documents)
.map(doc => doc.labels)
.flat(1)
)
for (const label of uniqueLabels) {
completions.labels.push({ completions.labels.push({
type: 'label', type: 'label',
label, label,

View file

@ -21,24 +21,24 @@ export function buildPackageCompletions(
return return
} }
const uniquePackageNames = new Set<string>(packageNames) // commands from packages in the project
for (const command of metadata.commands) {
// package names and commands from packages in the project completions.commands.push({
for (const doc of Object.values(metadata.documents)) { type: command.meta,
for (const [packageName, commands] of Object.entries(doc.packages)) { label: command.caption,
uniquePackageNames.add(packageName) apply: applySnippet(command.snippet),
extend: extendOverUnpairedClosingBrace,
for (const item of commands) { })
completions.commands.push({
type: item.meta,
label: item.caption,
apply: applySnippet(item.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) const existingPackageNames = findExistingPackageNames(context)
for (const item of uniquePackageNames) { for (const item of uniquePackageNames) {

View file

@ -133,15 +133,6 @@ const initialize = () => {
console.log('open doc', id, options) 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 // window.metaAttributesCache is reset in preview.tsx

View file

@ -1,6 +1,5 @@
import { Folder } from '../../../../../types/folder' import { Folder } from '../../../../../types/folder'
import { docId, mockDocContent } from '../helpers/mock-doc' import { docId, mockDocContent } from '../helpers/mock-doc'
import { Metadata } from '../../../../../types/metadata'
import { mockScope } from '../helpers/mock-scope' import { mockScope } from '../helpers/mock-scope'
import { EditorProviders } from '../../../helpers/editor-providers' import { EditorProviders } from '../../../helpers/editor-providers'
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor' 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() const scope = mockScope()
scope.$root._references.keys = ['foo'] scope.$root._references.keys = ['foo']
scope.project.rootFolder = rootFolder scope.project.rootFolder = rootFolder
cy.mount( cy.mount(
<TestContainer> <TestContainer>
<EditorProviders <EditorProviders scope={scope} rootFolder={rootFolder as any}>
scope={scope}
metadataManager={metadataManager}
rootFolder={rootFolder as any}
>
<CodeMirrorEditor /> <CodeMirrorEditor />
</EditorProviders> </EditorProviders>
</TestContainer> </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() const scope = mockScope()
scope.$root._references.keys = ['foo'] scope.$root._references.keys = ['foo']
cy.mount( cy.mount(
<TestContainer> <TestContainer>
<EditorProviders <EditorProviders scope={scope} rootFolder={rootFolder as any}>
scope={scope}
metadataManager={metadataManager}
rootFolder={rootFolder as any}
>
<CodeMirrorEditor /> <CodeMirrorEditor />
</EditorProviders> </EditorProviders>
</TestContainer> </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 }) => { const MetadataProvider: FC = ({ children }) => {
return ( return (
<MetadataContext.Provider <MetadataContext.Provider value={metadata}>
value={{
metadata: {
state: {
documents: {
[docId]: {
labels: [],
packages: {
foo: [
{
caption: 'a caption',
meta: 'foo-cmd',
score: 0.1,
snippet: 'a caption{$1}',
},
],
},
},
},
},
getAllLabels: () => [],
getAllPackages: () => ({ foo: {} }),
},
}}
>
{children} {children}
</MetadataContext.Provider> </MetadataContext.Provider>
) )
@ -452,36 +375,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
const scope = mockScope() const scope = mockScope()
scope.$root._references.keys = ['ref-1', 'ref-2', 'ref-3'] 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( cy.mount(
<TestContainer> <TestContainer>
<EditorProviders <EditorProviders scope={scope} rootFolder={rootFolder as any}>
scope={scope}
providers={MetadataProvider}
rootFolder={rootFolder as any}
>
<CodeMirrorEditor /> <CodeMirrorEditor />
</EditorProviders> </EditorProviders>
</TestContainer> </TestContainer>
@ -535,31 +431,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
scope.$root._references.keys = ['foo'] scope.$root._references.keys = ['foo']
scope.project.rootFolder = rootFolder scope.project.rootFolder = rootFolder
const MetadataProvider: FC = ({ children }) => {
return (
<MetadataContext.Provider
value={{
metadata: {
state: {
documents: {},
},
getAllLabels: () => [],
getAllPackages: () => ({}),
},
}}
>
{children}
</MetadataContext.Provider>
)
}
cy.mount( cy.mount(
<TestContainer> <TestContainer>
<EditorProviders <EditorProviders scope={scope} rootFolder={rootFolder as any}>
scope={scope}
rootFolder={rootFolder as any}
providers={MetadataProvider}
>
<CodeMirrorEditor /> <CodeMirrorEditor />
</EditorProviders> </EditorProviders>
</TestContainer> </TestContainer>
@ -824,33 +698,22 @@ describe('autocomplete', { scrollBehavior: false }, function () {
it('displays unique completions for commands', function () { it('displays unique completions for commands', function () {
const scope = mockScope() 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 }) => { const MetadataProvider: FC = ({ children }) => {
return ( return (
<MetadataContext.Provider <MetadataContext.Provider value={metadata}>
value={{
metadata: {
state: {
documents: {
[docId]: {
labels: [],
packages: {
amsmath: [
{
caption: '\\label{}',
meta: 'amsmath-cmd',
score: 1,
snippet: '\\label{$1}',
},
],
},
},
},
},
getAllLabels: () => [],
getAllPackages: () => ({}),
},
}}
>
{children} {children}
</MetadataContext.Provider> </MetadataContext.Provider>
) )
@ -1033,31 +896,9 @@ describe('autocomplete', { scrollBehavior: false }, function () {
scope.$root._references.keys = ['foo'] scope.$root._references.keys = ['foo']
scope.project.rootFolder = rootFolder scope.project.rootFolder = rootFolder
const MetadataProvider: FC = ({ children }) => {
return (
<MetadataContext.Provider
value={{
metadata: {
state: {
documents: {},
},
getAllLabels: () => [],
getAllPackages: () => ({}),
},
}}
>
{children}
</MetadataContext.Provider>
)
}
cy.mount( cy.mount(
<TestContainer> <TestContainer>
<EditorProviders <EditorProviders scope={scope} rootFolder={rootFolder as any}>
scope={scope}
providers={MetadataProvider}
rootFolder={rootFolder as any}
>
<CodeMirrorEditor /> <CodeMirrorEditor />
</EditorProviders> </EditorProviders>
</TestContainer> </TestContainer>

View file

@ -95,13 +95,6 @@ export function EditorProviders({
getCurrentDocValue: () => {}, getCurrentDocValue: () => {},
openDoc: sinon.stub(), openDoc: sinon.stub(),
}, },
metadataManager = {
metadata: {
state: {
documents: {},
},
},
},
userSettings = {}, userSettings = {},
providers = {}, providers = {},
}) { }) {
@ -153,7 +146,6 @@ export function EditorProviders({
clsiServerId, clsiServerId,
editorManager, editorManager,
fileTreeManager, fileTreeManager,
metadataManager,
} }
// Add details for useUserContext // Add details for useUserContext

View file

@ -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
}