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 { 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) {

View file

@ -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.
*/

View file

@ -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(() => {

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

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
}