Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions app/gui/src/components/AppContainer/RightPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import {
ProjectSessions,
} from '$/components/AppContainer/reactTabs'
import SelectableTab from '$/components/AppContainer/SelectableTab.vue'
import WithCurrentProject from '$/components/WithCurrentProject.vue'
import { useRightPanelData, type RightPanelTabId } from '$/providers/rightPanel'
import ComponentHelpPanel from '@/components/ComponentHelpPanel.vue'
import DescriptionEditor from '@/components/DescriptionEditor.vue'
import DocumentationEditor from '@/components/DocumentationEditor.vue'
import DocumentationEditor from '@/components/DocumentationEditor'
import ResizeHandles from '@/components/ResizeHandles.vue'
import SizeTransition from '@/components/SizeTransition.vue'
import WithFullscreenMode from '@/components/WithFullscreenMode.vue'
Expand Down Expand Up @@ -70,11 +69,9 @@ const style = computed(() => (data.width == null ? {} : { '--panel-width': `${da
<div v-if="component != null" class="sizeWrapper">
<div ref="contentElement" class="content">
<WithFullscreenMode v-model="data.fullscreen">
<WithCurrentProject :id="data.focusedProject">
<div class="contentInner withBackgroundColor">
<component :is="component" />
</div>
</WithCurrentProject>
<div class="contentInner withBackgroundColor">
<component :is="component" />
</div>
</WithFullscreenMode>
<ResizeHandles left :modelValue="bounds" @update:modelValue="data.width = $event.width" />
</div>
Expand Down
126 changes: 84 additions & 42 deletions app/gui/src/components/WithCurrentProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,82 @@ import { isLocalProjectId } from '#/services/LocalBackend'
import { injectOpenedProjects, type OpenedProject } from '$/providers/openedProjects'
import { groupColorVar } from '@/composables/nodeColors'
import { createContextStore } from '@/providers'
import { assert } from '@/util/assert'
import { colorFromString } from '@/util/colors'
import type { Opt } from '@/util/data/opt'
import type { ToValue } from '@/util/reactivity'
import { computed, toValue, watch, type ToRefs } from 'vue'
import { computed, type Ref, shallowRef, type ToRefs, toValue, watch } from 'vue'

export type CurrentProjectStore = ReturnType<typeof useCurrentProjectRaw>
const [provideCurrentProject, useCurrentProjectRaw] = createContextStore(
'currentProject',
(project: Ref<OpenedProject | undefined>) => {
const ref = computed(() => {
assert(project.value != null)
return project.value
})
return {
maybeRef: project,
/* Current project as a single ref */
store: computed(() => ref.value.store),
projectNames: computed(() => ref.value.projectNames),
suggestionDb: computed(() => ref.value.suggestionDb),
module: computed(() => ref.value.module),
graph: computed(() => ref.value.graph),
widgetRegistry: computed(() => ref.value.widgetRegistry),
} satisfies ToRefs<{ [K in keyof OpenedProject]: OpenedProject[K] }> & {
maybeRef: Ref<OpenedProject | undefined>
}
},
)

export function useCurrentProject(allowMissing: true): CurrentProjectStore | undefined
export function useCurrentProject(allowMissing?: false): CurrentProjectStore
export function useCurrentProject(allowMissing?: boolean): CurrentProjectStore | undefined
/**
* A context of a single opened project.
*
* Use `WithCurrentProject` component to provide which project is the current for entire component
* tree (it's injects context and also sets proper css properties). Inside, inject will bring all
* project-related stores. If the project is closed, all stores becomes undefined.
* tree (it injects context, makes sure the project is available, and sets proper css properties).
*
* The refs inside aren't proxied, so this store may be deconstructed.
*/
const [provideCurrentProject, useCurrentProject] = createContextStore(
'currentProject',
(projectId: ToValue<Opt<ProjectId>>) => {
const openedProjects = injectOpenedProjects()

const hybridResolvedProjectId = computed(() => {
const id = toValue(projectId)
// When we have a hybrid project opened, we have to translate cloud project ID to corresponding hybrid project.
if (id && openedProjects.get(id) == null && !isLocalProjectId(id)) {
for (const openedId of openedProjects.listIds()) {
if (openedId.includes('/cloud-' + id) && isLocalProjectId(openedId)) return openedId
}
}
return id
})
export function useCurrentProject(allowMissing?: boolean) {
const currentProjectStore = useCurrentProjectRaw(allowMissing)
if (currentProjectStore == null) return undefined
// If the store is defined, but there is no project in it, it has to be fallback component.
if (currentProjectStore.maybeRef.value == null) {
if (allowMissing) return undefined
else throw new Error(`Trying to inject currentProject in WithProject's fallback component`)
}
return currentProjectStore
}

const ref = computed((): OpenedProject | undefined => {
const id = hybridResolvedProjectId.value
return id != null ? openedProjects.get(id) : undefined
})
function useOpenedProject(projectId: ToValue<Opt<ProjectId>>) {
const openedProjects = injectOpenedProjects()

return {
id: hybridResolvedProjectId,
/* Current project as a single ref */
ref,
/* Current project's stores decomposed to separate refs. */
storesRefs: {
store: computed(() => ref.value?.store),
names: computed(() => ref.value?.names),
suggestionDb: computed(() => ref.value?.suggestionDb),
graph: computed(() => ref.value?.graph),
widgetRegistry: computed(() => ref.value?.widgetRegistry),
} satisfies ToRefs<{ [K in keyof OpenedProject]: OpenedProject[K] | undefined }>,
const hybridResolvedProjectId = computed(() => {
const id = toValue(projectId)
// When we have a hybrid project opened, we have to translate cloud project ID to corresponding hybrid project.
if (id && openedProjects.get(id) == null && !isLocalProjectId(id)) {
for (const openedId of openedProjects.listIds()) {
if (openedId.includes('/cloud-' + id) && isLocalProjectId(openedId)) return openedId
}
}
},
)
return id
})

export { useCurrentProject }
return computed((): OpenedProject | undefined => {
const id = hybridResolvedProjectId.value
return id != null ? openedProjects.get(id) : undefined
})
}

function useStoreTemplate<K extends keyof OpenedProject>(
storeKey: K,
): () => NonNullable<OpenedProject[K]> {
return () => {
const currentProject = useCurrentProject().ref
const currentProject = useCurrentProject().maybeRef
const store: Opt<OpenedProject[K]> = currentProject.value?.[storeKey]
if (store == null) {
throw new Error('Current Project missing, probably closed.')
Expand All @@ -77,7 +97,7 @@ function useStoreTemplate<K extends keyof OpenedProject>(
export const useProjectStore = useStoreTemplate('store')

/** @deprecated it expects the current project will not change. Use {@link useCurrentProject} instead. */
export const useProjectNames = useStoreTemplate('names')
export const useProjectNames = useStoreTemplate('projectNames')

/** @deprecated it expects the current project will not change. Use {@link useCurrentProject} instead. */
export const useSuggestionDbStore = useStoreTemplate('suggestionDb')
Expand All @@ -92,11 +112,32 @@ export const useWidgetRegistry = useStoreTemplate('widgetRegistry')
<script setup lang="ts">
const { id } = defineProps<{ id: Opt<ProjectId> }>()

const provided = provideCurrentProject(() => id).ref
const project = useOpenedProject(() => id)
const providedProject = shallowRef<OpenedProject | undefined>(project.value)

// When project appears, the setup and mount handlers should already see it in context. But when project disappears,
// we want to keep stores while unmounting (because unmount handlers may still read some computed values).
// That's why we use two separate watches.
watch(
project,
(project) => {
if (project != null) providedProject.value = project
},
{ flush: 'pre' },
)
watch(
project,
(project) => {
if (project == null) providedProject.value = project
},
{ flush: 'post' },
)

provideCurrentProject(providedProject)

const groupColors = computed(() => {
const styles: { [key: string]: string } = {}
const groups = provided.value?.suggestionDb.groups ?? []
const groups = project.value?.suggestionDb.groups ?? []
for (const group of groups) {
styles[groupColorVar(group)] = group.color ?? colorFromString(group.name)
}
Expand All @@ -106,7 +147,8 @@ const groupColors = computed(() => {

<template>
<div class="WithCurrentProject" :style="groupColors">
<slot />
<slot v-if="project != null" />
<slot v-else name="fallback" />
</div>
</template>

Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/dashboard/layouts/AssetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import * as backendModule from '#/services/Backend'
import * as permissions from '#/utilities/permissions'
import { useMutationCallback } from '#/utilities/tanstackQuery'
import { useBackends, useFullUserSession, useRouter, useText } from '$/providers/react'
import { useRightPanelData } from '$/providers/react/container'
import * as featureFlagsProvider from '$/providers/react/featureFlags'
import { useRightPanelData } from '$/providers/rightPanel'
import {
TEAMS_DIRECTORY_ID,
USERS_DIRECTORY_ID,
Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import '#/styles.css'
import '#/tailwind.css'
import App from '$/App.vue'
import { setupLogger } from '$/log'
import { widgetDevtools } from '$/providers/openedProjects/widgetRegistry/devtools'
import router from '$/router'
import { widgetDevtools } from '@/providers/widgetRegistry/devtools'
import * as sentry from '@sentry/vue'
import type { Vue } from '@sentry/vue/types/types'
import { VueQueryPlugin } from '@tanstack/vue-query'
Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/project-view/ProjectView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import Backend, { ProjectId } from '#/services/Backend'
import WithCurrentProject from '$/components/WithCurrentProject.vue'
import { injectOpenedProjects } from '$/providers/openedProjects'
import { type LsUrls } from '$/providers/openedProjects/project'
import GraphEditor from '@/components/GraphEditor.vue'
import { provideEventLogger } from '@/providers/eventLogging'
import { provideProjectBackend } from '@/providers/projectBackend'
import { provideVisibility } from '@/providers/visibility'
import type { LsUrls } from '@/stores/project'
import { provideSettings } from '@/stores/settings'
import type { Opt } from '@/util/data/opt'
import { useEventListener } from '@vueuse/core'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
<script setup lang="ts">
import {
useGraphStore,
useProjectStore,
useSuggestionDbStore,
} from '$/components/WithCurrentProject.vue'
import { useCurrentProject } from '$/components/WithCurrentProject.vue'
import { useEnsoDiagnostics } from '@/components/CodeEditor/diagnostics'
import { ensoSyntax } from '@/components/CodeEditor/ensoSyntax'
import { useEnsoSourceSync } from '@/components/CodeEditor/sync'
Expand All @@ -26,9 +22,7 @@ import { highlightSelectionMatches } from '@codemirror/search'
import { drawSelection, keymap } from '@codemirror/view'
import { onMounted, toRef, useTemplateRef, type ComponentInstance } from 'vue'

const projectStore = useProjectStore()
const graphStore = useGraphStore()
const suggestionDbStore = useSuggestionDbStore()
const { store: project, suggestionDb, module, graph } = useCurrentProject()

const editorRoot = useTemplateRef<ComponentInstance<typeof CodeMirrorRoot>>('editorRoot')

Expand All @@ -47,21 +41,17 @@ const { editorView, setExtraExtensions } = useCodeMirror(editorRoot, {
foldGutter(),
lintGutter(),
highlightSelectionMatches(),
ensoSyntax(toRef(graphStore, 'moduleRoot')),
ensoHoverTooltip(graphStore, suggestionDbStore, vueHost),
ensoSyntax(toRef(module.value, 'root')),
ensoHoverTooltip(graph, suggestionDb, vueHost),
() => (editorRoot.value ? highlightStyle(editorRoot.value.highlightClasses) : []),
],
vueHost: () => vueHost,
lineMode: 'multi',
})
;(window as any).__codeEditorApi = testSupport(editorView)
useAutoBlur(editorView.dom)
const { updateListener, connectModuleListener } = useEnsoSourceSync(
projectStore,
graphStore,
editorView,
)
const ensoDiagnostics = useEnsoDiagnostics(projectStore, graphStore, editorView)
const { updateListener, connectModuleListener } = useEnsoSourceSync(project, module, editorView)
const ensoDiagnostics = useEnsoDiagnostics(project, module, graph, editorView)
setExtraExtensions([updateListener, ensoDiagnostics])
connectModuleListener()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useCurrentProject } from '$/components/WithCurrentProject.vue'
import type { NodeId } from '@/stores/graph'
import type { GraphDb } from '@/stores/graph/graphDatabase'
import type { SuggestionDbStore } from '@/stores/suggestionDatabase'
import type { NodeId } from '$/providers/openedProjects/graph'
import type { GraphDb } from '$/providers/openedProjects/graph/graphDatabase'
import type { SuggestionDbStore } from '$/providers/openedProjects/suggestionDatabase'
import { computed } from 'vue'

const { nodeId, syntax, graphDb, suggestionDbStore } = defineProps<{
Expand All @@ -12,7 +12,7 @@ const { nodeId, syntax, graphDb, suggestionDbStore } = defineProps<{
suggestionDbStore: SuggestionDbStore
}>()

const { names: projectNames } = useCurrentProject().storesRefs
const { projectNames: projectNames } = useCurrentProject()

const expressionInfo = computed(() => nodeId && graphDb.getExpressionInfo(nodeId))
const typeName = computed(() => {
Expand Down
33 changes: 18 additions & 15 deletions app/gui/src/project-view/components/CodeEditor/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { GraphStore } from '@/stores/graph'
import type { ProjectStore } from '@/stores/project'
import type { ComputedValueRegistry } from '@/stores/project/computedValueRegistry'
import type { GraphStore } from '$/providers/openedProjects/graph'
import type { ModuleStore } from '$/providers/openedProjects/module'
import type { ProjectStore } from '$/providers/openedProjects/project'
import type { ComputedValueRegistry } from '$/providers/openedProjects/project/computedValueRegistry'
import { valueExt, type ValueExt } from '@/util/codemirror/stateEffect'
import type { ToValue } from '@/util/reactivity'
import { forceLinting, linter, type Diagnostic } from '@codemirror/lint'
import type { Extension } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import * as iter from 'enso-common/src/utilities/data/iter'
import { computed, toValue, watchEffect, type ComputedRef } from 'vue'
import { computed, toValue, watchEffect, type ComputedRef, type Ref } from 'vue'
import type { Diagnostic as LSDiagnostic } from 'ydoc-shared/languageServerTypes'
import type { SourceRange } from 'ydoc-shared/util/data/text'
import type { ExternalId } from 'ydoc-shared/yjsModel'
Expand All @@ -17,31 +18,32 @@ import type { ExternalId } from 'ydoc-shared/yjsModel'
* based on dataflow errors and diagnostics the LS provided in an `executionStatus` message.
*/
export function useEnsoDiagnostics(
projectStore: Pick<ProjectStore, 'computedValueRegistry' | 'dataflowErrors' | 'diagnostics'>,
graphStore: Pick<GraphStore, 'moduleSource' | 'db'>,
projectStore: Ref<Pick<ProjectStore, 'computedValueRegistry' | 'dataflowErrors' | 'diagnostics'>>,
moduleStore: Ref<Pick<ModuleStore, 'source'>>,
graphStore: Ref<Pick<GraphStore, 'db'>>,
view: EditorView,
): Extension {
function spanOfExternalId(externalId: ExternalId): SourceRange | undefined {
const astId = graphStore.db.idFromExternal(externalId)
return astId && graphStore.moduleSource.getSpan(astId)
const astId = graphStore.value.db.idFromExternal(externalId)
return astId && moduleStore.value.source.getSpan(astId)
}
return [
reactiveDiagnostics(
view,
expressionUpdateDiagnostics,
useExpressionUpdateDiagnostics({
spanOfExternalId,
computedValueDb: projectStore.computedValueRegistry.db,
computedValueDb: () => projectStore.value.computedValueRegistry.db,
getDataflowError: (externalId: ExternalId) =>
projectStore.dataflowErrors.lookup(externalId)?.value?.message,
projectStore.value.dataflowErrors.lookup(externalId)?.value?.message,
}),
),
reactiveDiagnostics(
view,
executionContextDiagnostics,
useExecutionContextDiagnostics({
spanOfExternalId,
lsDiagnostics: () => projectStore.diagnostics,
lsDiagnostics: () => projectStore.value.diagnostics,
}),
),
]
Expand Down Expand Up @@ -74,15 +76,16 @@ function useExpressionUpdateDiagnostics({
getDataflowError,
}: {
spanOfExternalId: (externalId: ExternalId) => SourceRange | undefined
computedValueDb: ComputedValueRegistry['db']
computedValueDb: ToValue<ComputedValueRegistry['db']>
getDataflowError: (externalId: ExternalId) => string | undefined
}): ComputedRef<Diagnostic[]> {
return computed<Diagnostic[]>(() => {
const panics = computedValueDb.type.reverseLookup('Panic')
const errors = computedValueDb.type.reverseLookup('DataflowError')
const db = toValue(computedValueDb)
const panics = db.type.reverseLookup('Panic')
const errors = db.type.reverseLookup('DataflowError')
const diagnostics: Diagnostic[] = []
for (const externalId of iter.chain(panics, errors)) {
const update = computedValueDb.get(externalId)
const update = db.get(externalId)
if (!update) continue
const span = spanOfExternalId(externalId)
if (!span) continue
Expand Down
Loading
Loading