From d44d938a7dc77cc18af7835aa654cddee859fb04 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Mon, 22 Sep 2025 10:24:48 +0200 Subject: [PATCH 01/17] Module store and adjustments --- .../components/AppContainer/RightPanel.vue | 11 +- app/gui/src/components/WithCurrentProject.vue | 65 ++-- app/gui/src/entrypoint.ts | 2 +- app/gui/src/project-view/ProjectView.vue | 2 +- .../components/CodeEditor/CodeEditorImpl.vue | 22 +- .../CodeEditor/CodeEditorTooltip.vue | 6 +- .../components/CodeEditor/diagnostics.ts | 33 +- .../components/CodeEditor/sync.ts | 56 ++-- .../components/CodeEditor/tooltips.ts | 15 +- .../components/ColorPickerMenu.vue | 14 +- .../components/ComponentBrowser.vue | 4 +- .../ComponentBrowser/ComponentEditorLabel.vue | 2 +- .../__tests__/component.test.ts | 18 +- .../__tests__/filtering.bench.ts | 8 +- .../__tests__/filtering.test.ts | 10 +- .../ComponentBrowser/__tests__/input.test.ts | 9 +- .../components/ComponentBrowser/ai.ts | 2 +- .../components/ComponentBrowser/component.ts | 6 +- .../components/ComponentBrowser/filtering.ts | 9 +- .../components/ComponentBrowser/input.ts | 53 +-- .../project-view/components/ComponentHelp.vue | 12 +- .../components/ComponentHelp/history.ts | 2 +- .../components/ComponentHelp/ir.ts | 11 +- .../components/ComponentHelpPanel.vue | 38 ++- .../project-view/components/ContextMenu.vue | 2 +- .../components/DocumentationEditor.vue | 141 -------- .../ClosedProjectDocumentationEditor.vue | 91 ++++++ .../DocumentationEditor.vue | 36 +++ .../OpenedProjectDocumentationEditor.vue | 66 ++++ .../components/DocumentationEditor/index.ts | 3 + .../components/FunctionSignatureEditor.vue | 22 +- .../project-view/components/GraphEditor.vue | 26 +- .../GraphEditor/CodeMirrorWidgetBase.vue | 8 +- .../GraphEditor/ComponentWidgetTree.vue | 28 +- .../GraphEditor/CreateNodeFromPortButton.vue | 2 +- .../components/GraphEditor/GraphEdge.vue | 4 +- .../components/GraphEditor/GraphEdges.vue | 82 ++--- .../components/GraphEditor/GraphNode.vue | 17 +- .../GraphEditor/GraphNode/nodeMessage.ts | 10 +- .../GraphNode/nodeVisualization.ts | 10 +- .../GraphEditor/GraphNodeComment.vue | 2 +- .../GraphEditor/GraphNodeMessage.vue | 11 +- .../GraphEditor/GraphNodeOutputPorts.vue | 2 +- .../components/GraphEditor/GraphNodes.vue | 2 +- .../GraphEditor/GraphVisualization.vue | 2 +- .../VisualizationToolbar.vue | 2 +- .../GraphVisualization/visualizationData.ts | 6 +- .../components/GraphEditor/NodeWidget.vue | 4 +- .../GraphEditor/ResizableWidget.vue | 2 +- .../components/GraphEditor/WidgetTreeRoot.vue | 9 +- .../GraphEditor/__tests__/collapsing.test.ts | 4 +- .../components/GraphEditor/collapsing.ts | 4 +- .../components/GraphEditor/graphClipboard.ts | 2 +- .../components/GraphEditor/nodesDragging.ts | 22 +- .../GraphEditor/selectionActions.ts | 4 +- .../components/GraphEditor/toasts.ts | 2 +- .../GraphEditor/widgets/WidgetAnyToTarget.vue | 12 +- .../GraphEditor/widgets/WidgetApplication.vue | 10 +- .../widgets/WidgetArgumentName.vue | 9 +- .../GraphEditor/widgets/WidgetBlank.vue | 7 +- .../GraphEditor/widgets/WidgetCheckbox.vue | 24 +- .../widgets/WidgetEnsoExpression.vue | 9 +- .../GraphEditor/widgets/WidgetFileBrowser.vue | 24 +- .../WidgetFileBrowser/browsableTypes.ts | 4 +- .../widgets/WidgetFileBrowser/cloudBrowser.ts | 16 +- .../widgets/WidgetFileBrowser/localBrowser.ts | 10 +- .../GraphEditor/widgets/WidgetFunction.vue | 29 +- .../__tests__/widgetFunctionCallInfo.test.ts | 24 +- .../WidgetFunction/widgetFunctionCallInfo.ts | 52 +-- .../widgets/WidgetFunctionDef/ArgumentRow.vue | 18 +- .../widgets/WidgetFunctionName.vue | 7 +- .../GraphEditor/widgets/WidgetGroup.vue | 7 +- .../GraphEditor/widgets/WidgetHierarchy.vue | 2 +- .../GraphEditor/widgets/WidgetIcon.vue | 6 +- .../widgets/WidgetMultiSelection.vue | 18 +- .../GraphEditor/widgets/WidgetNumber.vue | 9 +- .../GraphEditor/widgets/WidgetPort.vue | 9 +- .../GraphEditor/widgets/WidgetSelection.vue | 25 +- .../widgets/WidgetSelection/tags.ts | 46 +-- .../widgets/WidgetSelectionArrow.vue | 7 +- .../widgets/WidgetSelfAccessChain.vue | 7 +- .../GraphEditor/widgets/WidgetTableEditor.vue | 13 +- .../__tests__/editHandler.test.ts | 7 +- .../__tests__/tableInputArgument.test.ts | 8 +- .../widgets/WidgetTableEditor/editHandler.ts | 11 +- .../WidgetTableEditor/tableInputArgument.ts | 36 +-- .../GraphEditor/widgets/WidgetTableMethod.vue | 4 +- .../GraphEditor/widgets/WidgetText.vue | 11 +- .../GraphEditor/widgets/WidgetToken.vue | 7 +- .../widgets/WidgetTopLevelArgument.vue | 2 +- .../GraphEditor/widgets/WidgetTypeCast.vue | 7 +- .../widgets/WidgetTypeCastPort.vue | 8 +- .../widgets/WidgetTypeExpression.vue | 4 +- .../GraphEditor/widgets/WidgetVector.vue | 15 +- .../components/widgets/FileBrowserWidget.vue | 2 +- .../FileBrowserWidget/FileBrowserNameBar.vue | 2 +- .../FileBrowserWidget/fileExtensions.ts | 12 +- .../composables/componentColors.ts | 4 +- .../project-view/composables/nodeColors.ts | 6 +- .../project-view/composables/nodeCreation.ts | 30 +- .../src/project-view/composables/selection.ts | 2 +- .../composables/stackNavigator.ts | 6 +- .../project-view/composables/tableColumns.ts | 6 +- .../__tests__/widgetRegistry.test.ts | 2 +- .../providers/asyncResources/context.ts | 2 +- .../project-view/providers/functionInfo.ts | 8 +- .../project-view/providers/graphSelection.ts | 2 +- .../providers/languageSupportExtensions.ts | 6 +- .../src/project-view/providers/widgetTree.ts | 4 +- .../project-view/providers/widgetUsageInfo.ts | 3 +- app/gui/src/project-view/stores/persisted.ts | 2 +- .../src/project-view/stores/projectFiles.ts | 4 +- .../stores/visualization/index.ts | 4 +- .../util/__tests__/callTree.test.ts | 12 +- .../util/__tests__/projectPath.test.ts | 2 +- .../util/__tests__/qualifiedName.test.ts | 2 +- app/gui/src/project-view/util/ast/node.ts | 7 +- app/gui/src/project-view/util/callTree.ts | 14 +- .../language/tableExpression/index.ts | 8 +- app/gui/src/project-view/util/getIconName.ts | 25 +- .../src/project-view/util/methodPointer.ts | 6 +- app/gui/src/providers/openedProjects.ts | 22 +- .../graph/__tests__/graphDatabase.test.ts | 0 .../graph/__tests__/imports.test.ts | 18 +- .../openedProjects/graph/graph.ts} | 302 ++++-------------- .../openedProjects}/graph/graphDatabase.ts | 13 +- .../providers/openedProjects/graph/index.ts | 10 + .../openedProjects}/graph/unconnectedEdges.ts | 2 +- .../openedProjects/module}/imports.ts | 9 +- .../providers/openedProjects/module/index.ts | 1 + .../providers/openedProjects/module/module.ts | 225 +++++++++++++ .../project/computedValueRegistry.ts | 7 +- .../project/executionContext.ts | 2 +- .../providers/openedProjects/project/index.ts | 1 + .../openedProjects}/project/nodeExecution.ts | 4 +- .../openedProjects/project/project.ts} | 74 ++--- .../project/visualizationDataRegistry.ts | 2 +- .../openedProjects}/projectNames.ts | 0 .../__tests__/documentation.test.ts | 7 +- .../__tests__/lsUpdate.test.ts | 11 +- .../suggestionDatabase/documentation.ts | 2 +- .../suggestionDatabase/entry.ts | 6 +- .../suggestionDatabase/index.ts | 11 +- .../suggestionDatabase/lsUpdate.ts | 8 +- .../suggestionDatabase/mockSuggestion.ts | 11 +- .../openedProjects}/widgetRegistry.ts | 83 ++--- .../__tests__/configuration.test.ts | 0 .../__tests__/editHandler.test.ts | 0 .../widgetRegistry/configuration.ts | 0 .../widgetRegistry/devtools.ts | 2 +- .../widgetRegistry/editHandler.ts | 2 +- 151 files changed, 1425 insertions(+), 1155 deletions(-) delete mode 100644 app/gui/src/project-view/components/DocumentationEditor.vue create mode 100644 app/gui/src/project-view/components/DocumentationEditor/ClosedProjectDocumentationEditor.vue create mode 100644 app/gui/src/project-view/components/DocumentationEditor/DocumentationEditor.vue create mode 100644 app/gui/src/project-view/components/DocumentationEditor/OpenedProjectDocumentationEditor.vue create mode 100644 app/gui/src/project-view/components/DocumentationEditor/index.ts rename app/gui/src/{project-view/stores => providers/openedProjects}/graph/__tests__/graphDatabase.test.ts (100%) rename app/gui/src/{project-view/stores => providers/openedProjects}/graph/__tests__/imports.test.ts (98%) rename app/gui/src/{project-view/stores/graph/index.ts => providers/openedProjects/graph/graph.ts} (71%) rename app/gui/src/{project-view/stores => providers/openedProjects}/graph/graphDatabase.ts (98%) create mode 100644 app/gui/src/providers/openedProjects/graph/index.ts rename app/gui/src/{project-view/stores => providers/openedProjects}/graph/unconnectedEdges.ts (98%) rename app/gui/src/{project-view/stores/graph => providers/openedProjects/module}/imports.ts (97%) create mode 100644 app/gui/src/providers/openedProjects/module/index.ts create mode 100644 app/gui/src/providers/openedProjects/module/module.ts rename app/gui/src/{project-view/stores => providers/openedProjects}/project/computedValueRegistry.ts (97%) rename app/gui/src/{project-view/stores => providers/openedProjects}/project/executionContext.ts (99%) create mode 100644 app/gui/src/providers/openedProjects/project/index.ts rename app/gui/src/{project-view/stores => providers/openedProjects}/project/nodeExecution.ts (93%) rename app/gui/src/{project-view/stores/project/index.ts => providers/openedProjects/project/project.ts} (98%) rename app/gui/src/{project-view/stores => providers/openedProjects}/project/visualizationDataRegistry.ts (97%) rename app/gui/src/{project-view/stores => providers/openedProjects}/projectNames.ts (100%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/__tests__/documentation.test.ts (91%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/__tests__/lsUpdate.test.ts (98%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/documentation.ts (97%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/entry.ts (96%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/index.ts (97%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/lsUpdate.ts (98%) rename app/gui/src/{project-view/stores => providers/openedProjects}/suggestionDatabase/mockSuggestion.ts (95%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry.ts (90%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry/__tests__/configuration.test.ts (100%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry/__tests__/editHandler.test.ts (100%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry/configuration.ts (100%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry/devtools.ts (97%) rename app/gui/src/{project-view/providers => providers/openedProjects}/widgetRegistry/editHandler.ts (99%) diff --git a/app/gui/src/components/AppContainer/RightPanel.vue b/app/gui/src/components/AppContainer/RightPanel.vue index 290f1d2075af..45608114135a 100644 --- a/app/gui/src/components/AppContainer/RightPanel.vue +++ b/app/gui/src/components/AppContainer/RightPanel.vue @@ -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' @@ -70,11 +69,9 @@ const style = computed(() => (data.width == null ? {} : { '--panel-width': `${da
- -
- -
-
+
+ +
diff --git a/app/gui/src/components/WithCurrentProject.vue b/app/gui/src/components/WithCurrentProject.vue index 58665eaf561c..bb34bc912e8a 100644 --- a/app/gui/src/components/WithCurrentProject.vue +++ b/app/gui/src/components/WithCurrentProject.vue @@ -4,6 +4,7 @@ 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' @@ -18,43 +19,50 @@ import { computed, toValue, watch, type ToRefs } from 'vue' */ const [provideCurrentProject, useCurrentProject] = createContextStore( 'currentProject', - (projectId: ToValue>) => { - 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 - }) - - const ref = computed((): OpenedProject | undefined => { - const id = hybridResolvedProjectId.value - return id != null ? openedProjects.get(id) : undefined + (project: ToValue) => { + const ref = computed(() => { + const proj = toValue(project) + assert(proj != null) + return proj }) - 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 }>, + store: computed(() => ref.value.store), + names: computed(() => ref.value.names), + 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] }>, } }, ) export { useCurrentProject } +function useOpenedProject(projectId: ToValue>) { + 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 + }) + + return computed((): OpenedProject | undefined => { + const id = hybridResolvedProjectId.value + return id != null ? openedProjects.get(id) : undefined + }) +} + function useStoreTemplate( storeKey: K, ): () => NonNullable { @@ -92,7 +100,9 @@ export const useWidgetRegistry = useStoreTemplate('widgetRegistry') diff --git a/app/gui/src/project-view/components/DocumentationEditor/ClosedProjectDocumentationEditor.vue b/app/gui/src/project-view/components/DocumentationEditor/ClosedProjectDocumentationEditor.vue new file mode 100644 index 000000000000..4ab18ff53bd8 --- /dev/null +++ b/app/gui/src/project-view/components/DocumentationEditor/ClosedProjectDocumentationEditor.vue @@ -0,0 +1,91 @@ + + + diff --git a/app/gui/src/project-view/components/DocumentationEditor/DocumentationEditor.vue b/app/gui/src/project-view/components/DocumentationEditor/DocumentationEditor.vue new file mode 100644 index 000000000000..6850c6172698 --- /dev/null +++ b/app/gui/src/project-view/components/DocumentationEditor/DocumentationEditor.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/app/gui/src/project-view/components/DocumentationEditor/OpenedProjectDocumentationEditor.vue b/app/gui/src/project-view/components/DocumentationEditor/OpenedProjectDocumentationEditor.vue new file mode 100644 index 000000000000..153f10fa2ae9 --- /dev/null +++ b/app/gui/src/project-view/components/DocumentationEditor/OpenedProjectDocumentationEditor.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/gui/src/project-view/components/DocumentationEditor/index.ts b/app/gui/src/project-view/components/DocumentationEditor/index.ts new file mode 100644 index 000000000000..71752a89f252 --- /dev/null +++ b/app/gui/src/project-view/components/DocumentationEditor/index.ts @@ -0,0 +1,3 @@ +import * as DocumentationEditor from './DocumentationEditor.vue' + +export default DocumentationEditor diff --git a/app/gui/src/project-view/components/FunctionSignatureEditor.vue b/app/gui/src/project-view/components/FunctionSignatureEditor.vue index e3eb6e122a1b..f475a0012cd0 100644 --- a/app/gui/src/project-view/components/FunctionSignatureEditor.vue +++ b/app/gui/src/project-view/components/FunctionSignatureEditor.vue @@ -1,11 +1,15 @@ diff --git a/app/gui/src/project-view/components/GraphEditor/GraphNodeOutputPorts.vue b/app/gui/src/project-view/components/GraphEditor/GraphNodeOutputPorts.vue index 92a806894e11..22f5944a57be 100644 --- a/app/gui/src/project-view/components/GraphEditor/GraphNodeOutputPorts.vue +++ b/app/gui/src/project-view/components/GraphEditor/GraphNodeOutputPorts.vue @@ -1,12 +1,12 @@ diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue index 934c6284bbae..f80d8530ea37 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue @@ -1,9 +1,14 @@ diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue index f80d8530ea37..f9d238a5d0b4 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue @@ -44,27 +44,28 @@ const value = computed({ return WidgetInput.valueRepr(props.input)?.endsWith('True') ?? false }, set(value) { - const edit = module.value.startEdit() - const theImport = value ? trueImport.value : falseImport.value - const inputValue: Ast.Expression | string | undefined = props.input.value - if (inputValue instanceof Ast.Ast) { - const { requiresImport } = setBoolNode( - edit.getVersion(inputValue), - value ? ('True' as Identifier) : ('False' as Identifier), - ) - if (requiresImport) module.value.addMissingImports(edit, theImport) - props.updateCallback({ edit, directInteraction: true }) - } else { - module.value.addMissingImports(edit, theImport) - props.updateCallback({ - edit, - portUpdate: { - value: value ? 'True' : 'False', - origin: props.input.portId, - }, - directInteraction: true, - }) - } + module.value.edit((edit) => { + const theImport = value ? trueImport.value : falseImport.value + const inputValue: Ast.Expression | string | undefined = props.input.value + if (inputValue instanceof Ast.Ast) { + const { requiresImport } = setBoolNode( + edit.getVersion(inputValue), + value ? ('True' as Identifier) : ('False' as Identifier), + ) + if (requiresImport) module.value.addMissingImports(edit, theImport) + return props.updateCallback({ edit, directInteraction: true }) + } else { + module.value.addMissingImports(edit, theImport) + return props.updateCallback({ + edit, + portUpdate: { + value: value ? 'True' : 'False', + origin: props.input.portId, + }, + directInteraction: true, + }) + } + }) }, }) diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue index e214c5f02524..5482dea8d15c 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue @@ -63,7 +63,6 @@ const makeSetPathUpdate = useSetPath({ currentPath: currentPathAst, preferRawPath: () => !!typeInfo.value.rawPath?.prefer, portId: () => props.input.portId, - edit: () => module.value.startEdit(), addMissingConstructorImports: (edit, type) => module.value.addMissingImports( edit, @@ -72,7 +71,7 @@ const makeSetPathUpdate = useSetPath({ }) function setPath(type: 'file' | 'secret', path: string) { - props.updateCallback(makeSetPathUpdate(type, path)) + module.value.edit((edit) => props.updateCallback(makeSetPathUpdate(type, path, edit))) } const write = computed(() => typeInfo.value.write) diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser/browsableTypes.ts b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser/browsableTypes.ts index d7e08ee9bce4..012b4c3d9fcb 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser/browsableTypes.ts +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser/browsableTypes.ts @@ -217,13 +217,11 @@ export function useSetPath({ currentPath, preferRawPath, portId, - edit, addMissingConstructorImports, }: { currentPath: ToValue preferRawPath: ToValue portId: ToValue - edit: ToValue addMissingConstructorImports: (module: Ast.MutableModule, type: ProjectPath) => boolean }) { const FILE_CONSTRUCTOR_PATTERN = { @@ -264,6 +262,7 @@ export function useSetPath({ makeValue, preferRawPath, }: { type: AbsoluteProjectPath; makeValue: MakeValue; preferRawPath: boolean }, + edit: Ast.MutableModule, ): WidgetUpdate { if (currentPath || preferRawPath) { return { @@ -274,22 +273,21 @@ export function useSetPath({ directInteraction: true, } } else { - const module = toValue(edit) - const importsOk = addMissingConstructorImports(module, type) - const pathText = Ast.TextLiteral.new(path, module) + const importsOk = addMissingConstructorImports(edit, type) + const pathText = Ast.TextLiteral.new(path, edit) const pathStyle = importsOk ? 'default' : 'full' return { portUpdate: { - value: makeValue(module, pathText, pathStyle), + value: makeValue(edit, pathText, pathStyle), origin: toValue(portId), }, - edit: module, + edit, directInteraction: true, } } } - function setPath(type: 'file' | 'secret', path: string) { + function setPath(type: 'file' | 'secret', path: string, edit: Ast.MutableModule) { const oldPathInfo = toValue(currentPath) const oldPath = oldPathInfo?.type === type ? oldPathInfo.path : undefined return widgetUpdate( @@ -306,6 +304,7 @@ export function useSetPath({ makeValue: makeFile, preferRawPath: toValue(preferRawPath), }, + edit, ) } diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction.vue index b747aef25b41..9967e1e09234 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction.vue @@ -1,5 +1,5 @@ diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetSelection.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetSelection.vue index 60fc350d1113..ded06de53bb5 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetSelection.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetSelection.vue @@ -215,12 +215,13 @@ function onClick(clickedEntry: Entry, keepOpen: boolean) { } function expressionTagClicked(tag: ExpressionTag) { - const edit = module.value.startEdit() - const tagValue = tag.resolveExpression(edit, module.value) - props.updateCallback({ - edit, - portUpdate: { value: tagValue, origin: props.input.portId }, - directInteraction: true, + module.value.edit((edit) => { + const tagValue = tag.resolveExpression(edit, module.value) + return props.updateCallback({ + edit, + portUpdate: { value: tagValue, origin: props.input.portId }, + directInteraction: true, + }) }) } diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor.vue index 774653556d3d..6852c6d0b148 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor.vue @@ -11,6 +11,7 @@ import { } from '@/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument' import AgGridTableView from '@/components/shared/AgGridTableView.vue' import { targetIsOutside } from '@/util/autoBlur' +import { Result } from '@/util/data/result' import { ProjectPath } from '@/util/projectPath' import type { Identifier, QualifiedName } from '@/util/qualifiedName' import { proxyRefs } from '@/util/reactivity' @@ -23,7 +24,7 @@ import type { ProcessDataFromClipboardParams, RowDragEndEvent, } from 'ag-grid-enterprise' -import { computed, ref, watch, type ComponentInstance, type ComputedRef } from 'vue' +import { ComponentInstance, computed, ComputedRef, ref, watch } from 'vue' import type { ComponentExposed } from 'vue-component-type-helpers' import { z } from 'zod' import ResizableWidget from '../ResizableWidget.vue' @@ -116,13 +117,20 @@ function processDataFromClipboard({ data, api }: ProcessDataFromClipboardParams< const focusedCell = api.getFocusedCell() if (focusedCell === null) console.warn('Pasting while no cell is focused!') else { + const checkAndWarn = (pasted: Result<{ rows: number; columns: number }>) => { + if ( + pasted.ok && + (pasted.value.rows < data.length || pasted.value.columns < (data[0]?.length ?? 0)) + ) { + pasteWarning.show(`Truncated pasted data to keep table within ${CELLS_LIMIT} limit`) + } + } const pasted = pasteFromClipboard(data, { rowIndex: focusedCell.rowIndex, colId: focusedCell.column.getColId(), }) - if (pasted.rows < data.length || pasted.columns < (data[0]?.length ?? 0)) { - pasteWarning.show(`Truncated pasted data to keep table within ${CELLS_LIMIT} limit`) - } + if (pasted instanceof Promise) pasted.then(checkAndWarn) + else checkAndWarn(pasted) } return [] } diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument.ts b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument.ts index a9c2a30125e6..07cebf73765c 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument.ts +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument.ts @@ -1,7 +1,7 @@ import { type ModuleStore } from '$/providers/openedProjects/module' import { requiredImportsByProjectPath } from '$/providers/openedProjects/module/imports' import type { SuggestionDb } from '$/providers/openedProjects/suggestionDatabase' -import type { WidgetInput, WidgetUpdate } from '$/providers/openedProjects/widgetRegistry' +import type { UpdateHandler, WidgetInput } from '$/providers/openedProjects/widgetRegistry' import { commonContextMenuActions, type MenuItem } from '@/components/shared/AgGridTableView.vue' import { Ast } from '@/util/ast' import { findIndexOpt } from '@/util/data/array' @@ -12,6 +12,7 @@ import { qnLastSegment, type QualifiedName } from '@/util/qualifiedName' import { cachedGetter, type ToValue } from '@/util/reactivity' import type { ColDef } from 'ag-grid-enterprise' import * as iter from 'enso-common/src/utilities/data/iter' +import { isPromise } from 'util/types' import { computed, toValue } from 'vue' import type { ColumnSpecificParams } from './TableHeader.vue' @@ -140,9 +141,9 @@ export function tableInputCallMayBeHandled(call: Ast.Expression) { */ export function useTableInputArgument( input: ToValue, - module: ToValue>, + module: ToValue>, suggestions: ToValue, - onUpdate: (update: WidgetUpdate) => void, + onUpdate: UpdateHandler, ) { const errorMessagePreamble = 'Table Editor Widget should not have been matched' const columnsAst = computed(() => retrieveColumnsAst(toValue(input).value)) @@ -270,21 +271,29 @@ export function useTableInputArgument( const removeRowMenuItem = { name: 'Remove Row', action: ({ node }: { node: { data: RowData | undefined } | null }) => { - if (!node?.data) return - const edit = toValue(module).startEdit() - fixColumns(edit) - removeRow(edit, node.data.index) - onUpdate({ edit, directInteraction: true }) + toValue(module).edit( + (edit) => { + if (!node?.data) return Err('No node data') + fixColumns(edit) + removeRow(edit, node.data.index) + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot remove row in Table Widget' }, + ) }, } const removeColumnMenuItem = (colId: Ast.AstId) => ({ name: 'Remove Column', action: () => { - const edit = toValue(module).startEdit() - fixColumns(edit) - removeColumn(edit, colId) - onUpdate({ edit, directInteraction: true }) + toValue(module).edit( + (edit) => { + fixColumns(edit) + removeColumn(edit, colId) + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot remove column in Table Widget' }, + ) }, }) @@ -302,10 +311,14 @@ export function useTableInputArgument( type: 'newColumn', enabled: mayAddNewColumnCurrently.value, newColumnRequested: () => { - const edit = toValue(module).startEdit() - fixColumns(edit) - addColumn(edit, `${DEFAULT_COLUMN_PREFIX}${columns.value.length + 1}`) - onUpdate({ edit, directInteraction: true }) + toValue(module).edit( + (edit) => { + fixColumns(edit) + addColumn(edit, `${DEFAULT_COLUMN_PREFIX}${columns.value.length + 1}`) + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot add new column' }, + ) }, }, }, @@ -362,31 +375,40 @@ export function useTableInputArgument( return false } const ast = colData.at(data.index) - const edit = toValue(module).startEdit() - fixColumns(edit) - if (data.index === rowCount.value) { - addRow(edit, (colId) => (colId === col.id ? newValue : null)) - } else { - const newValueAst = convertWithImport(newValue, edit) - if (ast != null) edit.getVersion(ast).replace(newValueAst) - else edit.getVersion(colData).set(data.index, newValueAst) - } - onUpdate({ edit, directInteraction: true }) - return true + const result = toValue(module).edit( + (edit) => { + fixColumns(edit) + if (data.index === rowCount.value) { + addRow(edit, (colId) => (colId === col.id ? newValue : null)) + } else { + const newValueAst = convertWithImport(newValue, edit) + if (ast != null) edit.getVersion(ast).replace(newValueAst) + else edit.getVersion(colData).set(data.index, newValueAst) + } + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot set value on table cell' }, + ) + return isPromise(result) ? true : result.ok }, headerComponentParams: { columnParams: { type: 'astColumn', nameSetter: (newName: string) => { - const edit = toValue(module).startEdit() - const column = columns.value[i] - if (column == null) { - console.error('Tried to rename column no longer existing in code') - return - } - fixColumns(edit) - edit.getVersion(column.name).setRawTextContent(newName) - onUpdate({ edit, directInteraction: true }) + toValue(module).edit( + (edit) => { + const column = columns.value[i] + if (column == null) { + const err = Err('Tried to rename column no longer existing in code') + err.error.log() + return err + } + fixColumns(edit) + edit.getVersion(column.name).setRawTextContent(newName) + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot rename header' }, + ) }, }, }, @@ -431,52 +453,58 @@ export function useTableInputArgument( } function moveColumn(colId: string, toIndex: number) { - if (!columnsAst.value.ok) { - columnsAst.value.error.log('Cannot reorder columns: The table AST is not available') - return - } - if (!columnsAst.value.value) { - console.error('Cannot reorder columns on placeholders! This should not be possible in the UI') - return - } - const edit = toValue(module).startEdit() - const columns = edit.getVersion(columnsAst.value.value) - const fromIndex = iter.find(columns.enumerate(), ([, ast]) => ast?.id === colId)?.[0] - if (fromIndex != null) { - columns.move(fromIndex, toIndex - 1) - onUpdate({ edit, directInteraction: true }) - } + toValue(module).edit( + (edit) => { + if (!columnsAst.value.ok) { + return columnsAst.value + } + if (!columnsAst.value.value) { + return Err( + 'Cannot reorder columns on placeholders! This should not be possible in the UI', + ) + } + const columns = edit.getVersion(columnsAst.value.value) + const fromIndex = iter.find(columns.enumerate(), ([, ast]) => ast?.id === colId)?.[0] + if (fromIndex != null) { + columns.move(fromIndex, toIndex - 1) + return onUpdate({ edit, directInteraction: true }) + } + return Err(`Uknown columnId ${colId}`) + }, + { logPreamble: 'Cannot reorder columns' }, + ) } function moveRow(rowIndex: number, overIndex: number) { - if (!columnsAst.value.ok) { - columnsAst.value.error.log('Cannot reorder rows: The table AST is not available') - return - } - if (!columnsAst.value.value) { - console.error('Cannot reorder rows on placeholders! This should not be possible in the UI') - return - } // If dragged out of grid, we do nothing. if (overIndex === -1) return - const edit = toValue(module).startEdit() - for (const col of columns.value) { - const editedCol = edit.getVersion(col.data) - editedCol.move(rowIndex, overIndex) - } - onUpdate({ edit, directInteraction: true }) + toValue(module).edit( + (edit) => { + if (!columnsAst.value.ok) { + return columnsAst.value + } + if (!columnsAst.value.value) { + return Err('Cannot reorder rows on placeholders! This should not be possible in the UI') + } + for (const col of columns.value) { + const editedCol = edit.getVersion(col.data) + editedCol.move(rowIndex, overIndex) + } + return onUpdate({ edit, directInteraction: true }) + }, + { logPreamble: 'Cannot move row' }, + ) } function pasteFromClipboard(data: string[][], focusedCell: { rowIndex: number; colId: string }) { - if (data.length === 0) return { rows: 0, columns: 0 } - const edit = toValue(module).startEdit() + if (data.length === 0) return Ok({ rows: 0, columns: 0 }) const focusedColIndex = findIndexOpt(columns.value, ({ id }) => id === focusedCell.colId) ?? columns.value.length const newValueGetter = (rowIndex: number, colIndex: number) => { if (rowIndex < focusedCell.rowIndex) return undefined if (colIndex < focusedColIndex) return undefined - return data[rowIndex - focusedCell.rowIndex]?.[colIndex - focusedColIndex] + return Ok(data[rowIndex - focusedCell.rowIndex]?.[colIndex - focusedColIndex]) } const pastedRowsEnd = focusedCell.rowIndex + data.length const pastedColsEnd = focusedColIndex + data[0]!.length @@ -484,53 +512,56 @@ export function useTableInputArgument( let actuallyPastedRowsEnd = pastedRowsEnd let actuallyPastedColsEnd = pastedColsEnd - // Set data in existing cells. - for ( - let rowIndex = focusedCell.rowIndex; - rowIndex < Math.min(pastedRowsEnd, rowCount.value); - ++rowIndex - ) { + return toValue(module).edit(async (edit) => { + // Set data in existing cells. for ( - let colIndex = focusedColIndex; - colIndex < Math.min(pastedColsEnd, columns.value.length); - ++colIndex + let rowIndex = focusedCell.rowIndex; + rowIndex < Math.min(pastedRowsEnd, rowCount.value); + ++rowIndex ) { - const column = columns.value[colIndex]! - const newValueAst = convertWithImport(newValueGetter(rowIndex, colIndex), edit) - edit.getVersion(column.data).set(rowIndex, newValueAst) + for ( + let colIndex = focusedColIndex; + colIndex < Math.min(pastedColsEnd, columns.value.length); + ++colIndex + ) { + const column = columns.value[colIndex]! + const newValueAst = convertWithImport(newValueGetter(rowIndex, colIndex), edit) + edit.getVersion(column.data).set(rowIndex, newValueAst) + } } - } - // Extend the table if necessary. - const newRowCount = Math.max(pastedRowsEnd, rowCount.value) - for (let i = rowCount.value; i < newRowCount; ++i) { - if (!mayAddNewRow(i)) { - actuallyPastedRowsEnd = i - break - } + // Extend the table if necessary. + const newRowCount = Math.max(pastedRowsEnd, rowCount.value) + for (let i = rowCount.value; i < newRowCount; ++i) { + if (!mayAddNewRow(i)) { + actuallyPastedRowsEnd = i + break + } - addRow(edit, (_colId, index) => newValueGetter(i, index)) - } - const newColCount = Math.max(pastedColsEnd, columns.value.length) - let modifiedColumnsAst: Ast.Vector | undefined = undefined - for (let i = columns.value.length; i < newColCount; ++i) { - if (!mayAddNewColumn(newRowCount, i)) { - actuallyPastedColsEnd = i - break + addRow(edit, (_colId, index) => newValueGetter(i, index)) } - modifiedColumnsAst = addColumn( - edit, - `${DEFAULT_COLUMN_PREFIX}${i + 1}`, - (index) => newValueGetter(index, i), - newRowCount, - modifiedColumnsAst, - ) - } - onUpdate({ edit, directInteraction: true }) - return { - rows: actuallyPastedRowsEnd - focusedCell.rowIndex, - columns: actuallyPastedColsEnd - focusedColIndex, - } + const newColCount = Math.max(pastedColsEnd, columns.value.length) + let modifiedColumnsAst: Ast.Vector | undefined = undefined + for (let i = columns.value.length; i < newColCount; ++i) { + if (!mayAddNewColumn(newRowCount, i)) { + actuallyPastedColsEnd = i + break + } + modifiedColumnsAst = addColumn( + edit, + `${DEFAULT_COLUMN_PREFIX}${i + 1}`, + (index) => newValueGetter(index, i), + newRowCount, + modifiedColumnsAst, + ) + } + const updateResult = await onUpdate({ edit, directInteraction: true }) + if (!updateResult.ok) return updateResult + return Ok({ + rows: actuallyPastedRowsEnd - focusedCell.rowIndex, + columns: actuallyPastedColsEnd - focusedColIndex, + }) + }) } return { diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetText.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetText.vue index 4ab53705d94b..089fa27520b9 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetText.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetText.vue @@ -28,28 +28,29 @@ const textContents = computed(() => function acceptValue(text: string): HandledUpdate { const module = currentProject.value.module - if (props.input.value instanceof Ast.TextLiteral) { - const edit = module.startEdit() - const value = edit.getVersion(props.input.value) - if (value.rawTextContent === text) return Ok() - value.setRawTextContent(text) - return props.updateCallback({ edit, directInteraction: true }) - } else { - let value: Ast.Owned - if (inputTextLiteral.value) { - value = Ast.copyIntoNewModule(inputTextLiteral.value) + return module.edit((edit) => { + if (props.input.value instanceof Ast.TextLiteral) { + const value = edit.getVersion(props.input.value) + if (value.rawTextContent === text) return Ok() value.setRawTextContent(text) + return props.updateCallback({ edit, directInteraction: true }) } else { - value = Ast.TextLiteral.new(text) + let value: Ast.Owned + if (inputTextLiteral.value) { + value = Ast.copyIntoNewModule(inputTextLiteral.value) + value.setRawTextContent(text) + } else { + value = Ast.TextLiteral.new(text) + } + return props.updateCallback({ + portUpdate: { + value, + origin: props.input.portId, + }, + directInteraction: true, + }) } - return props.updateCallback({ - portUpdate: { - value, - origin: props.input.portId, - }, - directInteraction: true, - }) - } + }) } /** Widget Input as Text Literal; undefined if there's no value, or the value is not a Text literal. */ diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetVector.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetVector.vue index 69b5865a9138..90e5b1f33ca9 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetVector.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetVector.vue @@ -21,18 +21,20 @@ const project = useCurrentProject().ref const tree = injectWidgetTree() function doEdit(editFn: (ast: Ast.MutableVector) => void) { - if (props.input.value instanceof Ast.Vector) { - const edit = project.value.module.startEdit() - editFn(edit.getVersion(props.input.value)) - props.updateCallback({ edit, directInteraction: true }) - } else { - const value = Ast.Vector.new(MutableModule.Transient(), []) - editFn(value) - props.updateCallback({ - portUpdate: { value, origin: props.input.portId }, - directInteraction: true, - }) - } + project.value.module.edit((edit) => { + if (props.input.value instanceof Ast.Vector) { + editFn(edit.getVersion(props.input.value)) + return props.updateCallback({ edit, directInteraction: true }) + } else { + const value = Ast.Vector.new(MutableModule.Transient(), []) + editFn(value) + return props.updateCallback({ + edit, + portUpdate: { value, origin: props.input.portId }, + directInteraction: true, + }) + } + }) } const itemConfig = computed(() => diff --git a/app/gui/src/project-view/composables/nodeCreation.ts b/app/gui/src/project-view/composables/nodeCreation.ts index 7cb956b2ca5a..11fa8931da13 100644 --- a/app/gui/src/project-view/composables/nodeCreation.ts +++ b/app/gui/src/project-view/composables/nodeCreation.ts @@ -14,6 +14,7 @@ import { Ast } from '@/util/ast' import { isIdentifier, substituteIdentifier, type Identifier } from '@/util/ast/abstract' import { partition } from '@/util/data/array' import { Rect } from '@/util/data/rect' +import { Ok } from '@/util/data/result' import { Vec2 } from '@/util/data/vec2' import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName' import type { ToValue } from '@/util/reactivity' @@ -150,6 +151,7 @@ export function useNodeCreation( graph.nodeRects.set(id, new Rect(Vec2.FromXY(options.metadata.position), Vec2.Zero)) } insertNodeStatements(edit.getVersion(methodAst.value).bodyAsBlock(), statements) + return Ok() }) graph.doAfterUpdate(() => onCreated(created)) } diff --git a/app/gui/src/providers/openedProjects/graph/graph.ts b/app/gui/src/providers/openedProjects/graph/graph.ts index 7a9f4d30ca24..316f41585842 100644 --- a/app/gui/src/providers/openedProjects/graph/graph.ts +++ b/app/gui/src/providers/openedProjects/graph/graph.ts @@ -11,12 +11,8 @@ import { type RequiredImport } from '$/providers/openedProjects/module/imports' import { type ProjectStore } from '$/providers/openedProjects/project' import { type ProjectNameStore } from '$/providers/openedProjects/projectNames' import { type SuggestionDbStore } from '$/providers/openedProjects/suggestionDatabase' -import { type Typename } from '$/providers/openedProjects/suggestionDatabase/entry' -import type { - UpdateHandler, - UpdateResult, - WidgetUpdate, -} from '$/providers/openedProjects/widgetRegistry' +import { Typename } from '$/providers/openedProjects/suggestionDatabase/entry' +import type { UpdateHandler, UpdateResult } from '$/providers/openedProjects/widgetRegistry' import { usePlacement } from '@/components/ComponentBrowser/placement' import type { PortId } from '@/providers/portInfo' import { assert, assertNever } from '@/util/assert' @@ -30,7 +26,6 @@ import { andThen, Err, Ok, unwrap, type Result } from '@/util/data/result' import { Vec2 } from '@/util/data/vec2' import type { MethodPointer } from '@/util/methodPointer' import { proxyRefs, useWatchContext } from '@/util/reactivity' -import { computedAsync } from '@vueuse/core' import { useCallbackRegistry } from 'enso-common/src/utilities/data/callbacks' import * as iter from 'enso-common/src/utilities/data/iter' import { map, set } from 'lib0' @@ -49,7 +44,7 @@ import { type ShallowReactive, type ShallowRef, } from 'vue' -import type { ExpressionUpdate, Path as LsPath } from 'ydoc-shared/languageServerTypes' +import type { ExpressionUpdate } from 'ydoc-shared/languageServerTypes' import { reachable } from 'ydoc-shared/util/data/graph' import type { ExternalId, VisualizationMetadata } from 'ydoc-shared/yjsModel' import { visMetadataEquals } from 'ydoc-shared/yjsModel' @@ -665,16 +660,6 @@ export function createGraphStore( return true } - const modulePath: Ref = computedAsync( - async () => { - const rootId = await proj.projectRootId - const segments = ['src', 'Main.enso'] - return rootId ? { rootId, segments } : undefined - }, - undefined, - { onError: console.error }, - ) - function onBeforeEdit(f: (transaction: Y.Transaction) => void): { unregister: () => void } { proj.module?.doc.ydoc.on('beforeTransaction', f) return { unregister: () => proj.module?.doc.ydoc.off('beforeTransaction', f) } @@ -714,7 +699,6 @@ export function createGraphStore( isConnectedSource, isConnectedTarget, nodeCanBeEntered, - modulePath, connectedEdges, currentMethod: proxyRefs({ ast: methodAst, diff --git a/app/gui/src/providers/openedProjects/module/module.ts b/app/gui/src/providers/openedProjects/module/module.ts index 36283a479926..96148f3cf848 100644 --- a/app/gui/src/providers/openedProjects/module/module.ts +++ b/app/gui/src/providers/openedProjects/module/module.ts @@ -6,11 +6,9 @@ import { reactiveModule } from '@/util/ast/reactive' import { Err, Ok, type Result } from '@/util/data/result' import { type MethodPointer } from '@/util/methodPointer' import { proxyRefs } from '@/util/reactivity' -import { computedAsync } from '@vueuse/core' import { isPromise } from 'util/types' -import { computed, reactive, type Ref, ref, watch } from 'vue' +import { computed, reactive, ref, watch } from 'vue' import { SourceDocument } from 'ydoc-shared/ast/sourceDocument' -import type { Path as LsPath } from 'ydoc-shared/languageServerTypes' import { defaultLocalOrigin, type Origin } from 'ydoc-shared/yjsModel' import * as Y from 'yjs' import { type ProjectNameStore } from '../projectNames' @@ -38,15 +36,6 @@ export function createModuleStore( const root = ref() const synced = computed(() => root.value?.module as Ast.MutableModule | undefined) const ast = computed((): Ast.Module => synced.value!) - const modulePath: Ref = computedAsync( - async () => { - const rootId = await proj.projectRootId - const segments = ['src', 'Main.enso'] - return rootId ? { rootId, segments } : undefined - }, - undefined, - { onError: console.error }, - ) watch( () => proj.module, @@ -93,10 +82,16 @@ export function createModuleStore( */ function edit | Promise>>( f: (edit: MutableModule) => R, - options: { skipTreeRepair?: boolean; origin?: Origin } = {}, + options: { + skipTreeRepair?: boolean + origin?: Origin + logLevel?: 'none' | 'info' | 'warn' | 'error' + logPreamble?: string + } = {}, ): R { assertDefined(synced.value) const edit = synced.value.edit() + const logLevel = options.logLevel ?? 'error' const treeRepair = (result: Result) => { if (result.ok && options.skipTreeRepair !== true) { @@ -109,6 +104,8 @@ export function createModuleStore( const applyEdit = (result: Result) => { if (result.ok) synced.value?.applyEdit(edit, options.origin) + else if (logLevel !== 'none') + console[logLevel](result.error.message(options.logPreamble ?? 'Cannot commit AST edit.')) return result } @@ -157,7 +154,10 @@ export function createModuleStore( ast.setWidgetMetadata(widgetKey, md) } - /* Try adding imports. Does nothing if conflict is detected, and returns `DectedConflict` in such case. */ + /** + * Try adding imports. Do not add those conflicting with existing imports - return + * `DectedConflict` in such case. + */ function addMissingImports( edit: MutableModule, newImports: RequiredImport[], diff --git a/app/ydoc-shared/src/util/data/result.ts b/app/ydoc-shared/src/util/data/result.ts index 50e2e24a21f6..00c40764ec66 100644 --- a/app/ydoc-shared/src/util/data/result.ts +++ b/app/ydoc-shared/src/util/data/result.ts @@ -3,6 +3,7 @@ * or an error. */ +import { isPromise } from 'util/types' import { isSome, type Opt } from './opt' /** @@ -60,6 +61,26 @@ export function unwrap(result: Result): T { else throw result.error } +export function logIfError(result: Result, preamble?: string): Result +export function logIfError( + result: Promise>, + preamble?: string, +): Promise> +export function logIfError( + result: Result | Promise>, + preamble?: string, +): Result | Promise> +/** Log if the result of this potentially asynchronous operation is an error. */ +export function logIfError(result: Result | Promise>, preamble?: string) { + if (isPromise(result)) + return result.then((result) => { + if (!result.ok) result.error.log(preamble) + return result + }) + else if (!result.ok) result.error.log(preamble) + return result +} + /** * Unwraps the {@link Result} value. If the result is absent or is an error value, an alternative is * returned. From d273631cf520e13dffd38861e0302947bc5fbb15 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Wed, 24 Sep 2025 12:03:14 +0200 Subject: [PATCH 03/17] Fix for WithCurrentProject --- app/gui/src/components/WithCurrentProject.vue | 69 ++++++++++++------ .../components/ComponentBrowser/input.ts | 9 ++- .../components/DocumentationEditor/index.ts | 3 +- .../project-view/components/GraphEditor.vue | 4 +- .../WidgetTableEditor/tableInputArgument.ts | 3 +- app/gui/src/project-view/providers/index.ts | 2 + .../providers/openedProjects/graph/graph.ts | 70 ++++++++++++------- .../providers/openedProjects/module/module.ts | 30 ++++---- app/ydoc-shared/src/util/data/result.ts | 3 +- 9 files changed, 123 insertions(+), 70 deletions(-) diff --git a/app/gui/src/components/WithCurrentProject.vue b/app/gui/src/components/WithCurrentProject.vue index bb34bc912e8a..2a8419964224 100644 --- a/app/gui/src/components/WithCurrentProject.vue +++ b/app/gui/src/components/WithCurrentProject.vue @@ -6,26 +6,20 @@ 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 { Opt } from '@/util/data/opt' +import { ToValue } from '@/util/reactivity' +import { computed, Ref, shallowRef, ToRefs, toValue, watch } from 'vue' -/** - * 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. - */ -const [provideCurrentProject, useCurrentProject] = createContextStore( +export type CurrentProjectStore = ReturnType +const [provideCurrentProject, useCurrentProjectRaw] = createContextStore( 'currentProject', - (project: ToValue) => { + (project: Ref) => { const ref = computed(() => { - const proj = toValue(project) - assert(proj != null) - return proj + assert(project.value != null) + return project.value }) return { + maybeRef: project, /* Current project as a single ref */ ref, /* Current project's stores decomposed to separate refs. */ @@ -41,7 +35,25 @@ const [provideCurrentProject, useCurrentProject] = createContextStore( }, ) -export { useCurrentProject } +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 injects context, makes sure the project is available, and sets proper css properties). + */ +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 +} function useOpenedProject(projectId: ToValue>) { const openedProjects = injectOpenedProjects() @@ -101,12 +113,27 @@ export const useWidgetRegistry = useStoreTemplate('widgetRegistry') const { id } = defineProps<{ id: Opt }>() const project = useOpenedProject(() => id) +const providedProject = shallowRef(project.value) +watch( + project, + (project) => { + if (project != null) providedProject.value = project + }, + { flush: 'pre' }, +) +watch( + project, + (project) => { + if (project == null) providedProject.value = project + }, + { flush: 'post' }, +) -const provided = provideCurrentProject(project).ref +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) } @@ -115,10 +142,10 @@ const groupColors = computed(() => {