diff --git a/CHANGELOG.md b/CHANGELOG.md index 1433b2b50868..8a9d04e1eeff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ is included][14028] - [Function docs in autocomplete in table expressions][14059] - [Many CLI arguments removed][14069] +- [Added immediate rename of user created components][14209] - [Multiple opened projects' tabs are now allowed][14215] [13685]: https://github.com/enso-org/enso/pull/13685 @@ -27,6 +28,7 @@ [13976]: https://github.com/enso-org/enso/pull/13976 [14059]: https://github.com/enso-org/enso/pull/14059 [14069]: https://github.com/enso-org/enso/pull/14069 +[14209]: https://github.com/enso-org/enso/pull/14209 [14215]: https://github.com/enso-org/enso/pull/14215 #### Enso Standard Library diff --git a/app/electron-client/tests/localWorkflow.spec.ts b/app/electron-client/tests/localWorkflow.spec.ts index 030c688df0b7..8bdfa00d3ea2 100644 --- a/app/electron-client/tests/localWorkflow.spec.ts +++ b/app/electron-client/tests/localWorkflow.spec.ts @@ -76,7 +76,7 @@ test('Local Workflow', async ({ page, app, projectsDir }) => { .getByRole('button', { name: 'Create User Defined Component from Selected Components' }) .click() await expect(page.locator('.GraphNode')).toHaveCount(1) - await expect(page.locator('.GraphNode')).toHaveText(/Main.user_defined_component/) + await expect(page.locator('.GraphNode')).toHaveText(/user_defined_component/) await page.locator('.GraphNode').click() await page.getByRole('button', { name: 'Visualization' }).click() await expect(page.locator('.TableVisualization')).toBeVisible() @@ -104,7 +104,7 @@ test('Local Workflow', async ({ page, app, projectsDir }) => { // Leave function await page.locator('.ProjectView').getByText('New Project').dblclick() await expect(page.locator('.GraphNode')).toHaveCount(1) - await expect(page.locator('.GraphNode')).toHaveText(/Main.new_name/) + await expect(page.locator('.GraphNode')).toHaveText(/new_name/) // Create new text literal node. await page.keyboard.press('Escape') // deselect. diff --git a/app/gui/integration-test/actions/EditorPageActions.ts b/app/gui/integration-test/actions/EditorPageActions.ts index 92d2e372d9c9..fd61cd0a138d 100644 --- a/app/gui/integration-test/actions/EditorPageActions.ts +++ b/app/gui/integration-test/actions/EditorPageActions.ts @@ -155,6 +155,13 @@ export default class EditorPageActions extends PageActions { + await expect(node.locator('.WidgetArgumentName .name')).toHaveText(expectedPlaceholders) + }) + } + /** Expect count of nodes to reachto given value. */ expectNodesToExist(nodeBindings: string[]) { return this.do(async () => { @@ -258,7 +265,7 @@ export default class EditorPageActions extends PageActions { - await this.locateNodes(binding).dblclick() + await this.locateNodes(binding).locator('.grab-handle').dblclick() }) } diff --git a/app/gui/integration-test/mock/lsHandler.ts b/app/gui/integration-test/mock/lsHandler.ts index a439007d0388..d276a203a4a7 100644 --- a/app/gui/integration-test/mock/lsHandler.ts +++ b/app/gui/integration-test/mock/lsHandler.ts @@ -50,7 +50,7 @@ const mainFile = `\ from Standard.Base import all ## A User Defined Function -func1 arg = +func1 arg1 = f2 = Main.func2 arg result = f2 - 5 result diff --git a/app/gui/integration-test/project-view/collapsingAndEntering.spec.ts b/app/gui/integration-test/project-view/collapsingAndEntering.spec.ts index bfd6fc903748..1daa33f2f9d3 100644 --- a/app/gui/integration-test/project-view/collapsingAndEntering.spec.ts +++ b/app/gui/integration-test/project-view/collapsingAndEntering.spec.ts @@ -118,7 +118,7 @@ test.describe('Collapsing nodes with multiple inputs', () => { .mockUserDefinedFunctionInfo('prod', 'user_defined_component') .withNode('prod', async (node) => { await expect(node.locator('.WidgetApplication.prefix > .WidgetPort')).toHaveText( - 'Main.user_defined_component', + 'user_defined_component', ) }) .enterNode('prod') @@ -150,7 +150,7 @@ test('Collapsing nodes', async ({ editorPage }) => { .withNode('prod', async (node) => { const port = node.locator('.WidgetApplication.prefix > .WidgetPort') await expect(port).toExist() - await expect(port).toHaveText('Main.user_defined_component') + await expect(port).toHaveText('user_defined_component') await expect(node.locator('.WidgetTopLevelArgument')).toHaveText('five') }) .enterNode('prod') @@ -162,8 +162,11 @@ test('Collapsing nodes', async ({ editorPage }) => { .press(COLLAPSE_SHORTCUT) .expectNodeCount(5) .expectNodeCount(1, locate.INPUT_NODE_FILTER) - .expectNodeTokens('sum', ['Main', '.', 'user_defined_component1', 'five', 'twenty']) - .mockUserDefinedFunctionInfo('sum', 'user_defined_component1') + .do((page) => page.keyboard.type('My renamed component')) + .press('Enter') + .expectNodeTokens('sum', ['my_renamed_component']) + .expectArgumentPlaceholders('sum', ['five', 'twenty']) + .mockUserDefinedFunctionInfo('sum', 'my_renamed_component') .enterNode('sum') .expectNodesToExist(['ten']) .expectNodeCount(5) @@ -238,7 +241,9 @@ test('Output node is not collapsed', async ({ editorPage }) => { .call(enterToFunc2) .selectNodes([locate.OUTPUT_NODE_FILTER, 'r']) .clickActionTrigger('components.collapse') - .expectNodeTokens('r', ['Main', '.', 'user_defined_component', 'a']) + .press('Enter') + .expectNodeTokens('r', ['user_defined_component']) + .expectArgumentPlaceholders('r', ['a']) .expectNodeCount(3) }) @@ -247,7 +252,9 @@ test('Input node is not collapsed', async ({ editorPage }) => { .call(enterToFunc2) .selectNodes(['r', locate.INPUT_NODE_FILTER]) .clickActionTrigger('components.collapse') - .expectNodeTokens('r', ['Main', '.', 'user_defined_component', 'a']) + .press('Enter') + .expectNodeTokens('r', ['user_defined_component']) + .expectArgumentPlaceholders('r', ['a']) .expectNodeCount(3) }) diff --git a/app/gui/integration-test/project-view/graphRenderNodes.spec.ts b/app/gui/integration-test/project-view/graphRenderNodes.spec.ts index 5f53dae6243e..09a384d3f023 100644 --- a/app/gui/integration-test/project-view/graphRenderNodes.spec.ts +++ b/app/gui/integration-test/project-view/graphRenderNodes.spec.ts @@ -13,7 +13,7 @@ test('graph can open and render nodes', async ({ editorPage, page }) => { // check documented node's content const finalNode = locate.graphNodeByBinding(page, 'final') - await expect(finalNode.locator('.WidgetToken')).toHaveText(['Main', '.', 'func1', 'prod']) + await expect(finalNode.locator('.WidgetToken')).toHaveText(['func1']) }) test('Component icon indicates evaluation in progress', async ({ editorPage, page }) => { diff --git a/app/gui/integration-test/project-view/undoRedo.spec.ts b/app/gui/integration-test/project-view/undoRedo.spec.ts index a309178e4afb..c3b52bb92351 100644 --- a/app/gui/integration-test/project-view/undoRedo.spec.ts +++ b/app/gui/integration-test/project-view/undoRedo.spec.ts @@ -67,7 +67,7 @@ test('Removing node', async ({ editorPage, page }) => { await page.keyboard.press(`ControlOrMeta+Z`) await expect(locate.graphNode(page)).toHaveCount(nodesCount) - await expect(deletedNode.locator('.WidgetToken')).toHaveText(['Main', '.', 'func1', 'prod']) + await expect(deletedNode.locator('.WidgetToken')).toHaveText(['func1']) await expect(locate.nodeCommentContent(deletedNode)).toHaveText('This node can be entered') const restoredBBox = await deletedNode.boundingBox() diff --git a/app/gui/package.json b/app/gui/package.json index e83d2d30a0b6..64df4a2dc15b 100644 --- a/app/gui/package.json +++ b/app/gui/package.json @@ -120,7 +120,7 @@ "veaury": "=2.4.4", "vue": "^3.5.13", "vue-component-type-helpers": "^2.2.0", - "vue-router": "^4.5.0", + "vue-router": "^4.6.3", "y-protocols": "^1.0.6", "y-textarea": "^1.0.2", "y-websocket": "^1.5.4", @@ -193,7 +193,7 @@ "tar-stream": "^3.1.7", "typescript": "catalog:", "vite": "catalog:", - "vite-plugin-vue-devtools": "^8.0.0", + "vite-plugin-vue-devtools": "^8.0.3", "vite-plugin-wasm": "^3.5.0", "vitest": "catalog:", "vue-tsc": "^2.2.0", diff --git a/app/gui/src/project-view/assets/icons.svg b/app/gui/src/project-view/assets/icons.svg index 4c10888d8536..072af2b04112 100644 --- a/app/gui/src/project-view/assets/icons.svg +++ b/app/gui/src/project-view/assets/icons.svg @@ -1,6 +1,6 @@ - + @@ -31,11 +31,11 @@ - + - + @@ -138,11 +138,11 @@ - - + + - + @@ -154,7 +154,7 @@ - + @@ -163,7 +163,7 @@ - + @@ -174,16 +174,16 @@ - + - + - + @@ -196,8 +196,8 @@ - - + + @@ -230,8 +230,8 @@ - - + + @@ -239,11 +239,11 @@ - + - + @@ -259,12 +259,14 @@ - + + - - + + + @@ -272,7 +274,7 @@ - + @@ -297,16 +299,16 @@ - - + + - + - + @@ -332,19 +334,12 @@ - - + + - - - - - - - - + @@ -354,18 +349,18 @@ - + - - + + - - + + @@ -373,14 +368,14 @@ - + - + @@ -389,7 +384,7 @@ - + - + - + @@ -440,7 +435,7 @@ - + @@ -449,17 +444,17 @@ - + - + - - + + @@ -467,11 +462,11 @@ - + - + @@ -479,7 +474,7 @@ - + @@ -497,15 +492,15 @@ - + - + - + @@ -519,7 +514,7 @@ - + @@ -536,12 +531,12 @@ - + - + @@ -559,8 +554,13 @@ + + + + + - + @@ -568,7 +568,7 @@ - + @@ -582,14 +582,13 @@ - - + - + @@ -620,16 +619,16 @@ - + - + - + @@ -639,7 +638,7 @@ - + @@ -651,20 +650,20 @@ - + - + - + @@ -676,7 +675,7 @@ - + @@ -689,7 +688,7 @@ - + @@ -706,12 +705,12 @@ - + - + @@ -747,7 +746,7 @@ - + @@ -768,12 +767,12 @@ - + - + @@ -792,7 +791,7 @@ - + @@ -828,7 +827,7 @@ - + @@ -838,7 +837,7 @@ - + @@ -855,7 +854,7 @@ - + @@ -863,7 +862,7 @@ - + @@ -879,7 +878,7 @@ - + @@ -891,8 +890,8 @@ - - + + @@ -912,7 +911,7 @@ - + @@ -924,11 +923,11 @@ - + - + @@ -991,16 +990,16 @@ - + - - + + - + @@ -1011,7 +1010,7 @@ - + @@ -1019,23 +1018,23 @@ - + - + - + - + - + @@ -1043,7 +1042,7 @@ - + @@ -1052,7 +1051,7 @@ - + @@ -1060,11 +1059,11 @@ - + - + @@ -1072,14 +1071,14 @@ - + - + @@ -1100,21 +1099,21 @@ - + - + - - + + - - + + @@ -1167,6 +1166,19 @@ + + + + + + + + + + + + + @@ -1181,11 +1193,11 @@ - + - + @@ -1198,8 +1210,8 @@ - - + + @@ -1234,11 +1246,11 @@ - + - + @@ -1300,11 +1312,11 @@ - + - + @@ -1320,15 +1332,15 @@ - - - + + + - - - + + + @@ -1340,7 +1352,7 @@ - + @@ -1348,10 +1360,10 @@ - + - + diff --git a/app/gui/src/project-view/components/ActionButton.vue b/app/gui/src/project-view/components/ActionButton.vue index 96bd221c74b1..4880c4fd66f7 100644 --- a/app/gui/src/project-view/components/ActionButton.vue +++ b/app/gui/src/project-view/components/ActionButton.vue @@ -9,11 +9,11 @@ const { action: actionName, label } = defineProps<{ }>() const action = computed(() => resolveAction(actionName)) -const descriptionWithShortcut = computed(() => - action.value.shortcut ? - `${toValue(action.value.description)} (${toValue(action.value.shortcut?.humanReadable)})` - : toValue(action.value.description), -) +const descriptionWithShortcut = computed(() => { + const description = toValue(action.value.description) + const shortcut = toValue(action.value.shortcut) + return shortcut ? `${description} (${shortcut.humanReadable})` : description +}) 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 556ece541390..c33843c5bb15 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 @@ @@ -72,10 +142,33 @@ declare module '$/providers/openedProjects/widgetRegistry' { */ editableNameExpression: ExpressionId methodPointer: MethodPointer + requireUserAction?: boolean } } } +export const [provideRenameSchedule, useRenameSchedule] = createContextStore( + 'functionRenameSchedule', + () => { + let scheduledRename: MethodPointer | null = null + /** Inform the function name widget to start in "renaming" state the next time it is instantiated with a function of given name. */ + function scheduleFunctionRename(pointer: MethodPointer) { + scheduledRename = pointer + } + + /** Check if a function has a scheduled rename. If it does, remove it from schedule. */ + function matchScheduled(pointer: MethodPointer) { + if (scheduledRename && methodPointerEquals(scheduledRename, pointer)) { + scheduledRename = null + return true + } + return false + } + + return { scheduleFunctionRename, matchScheduled } + }, +) + function isFunctionName(input: WidgetInput): input is WidgetInput & { value: Ast.Ast [FunctionName]: { editableNameExpression: ExpressionId } @@ -94,17 +187,24 @@ export const widgetDefinition = defineWidget( diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts index 3a8936342806..5ee1a3de6221 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts @@ -7,11 +7,11 @@ import { DEFAULT_COLUMN_PREFIX, NEW_COLUMN_ID, ROW_INDEX_HEADER, - type RowData, tableInputCallMayBeHandled, useTableInputArgument, + type RowData, } from '@/components/GraphEditor/widgets/WidgetTableEditor/tableInputArgument' -import { MenuItem } from '@/components/shared/AgGridTableView.vue' +import type { MenuItem } from '@/components/shared/AgGridTableView.vue' import { assert } from '@/util/assert' import { Ast } from '@/util/ast' import type { Identifier } from '@/util/ast/abstract' @@ -19,7 +19,7 @@ import { parseAbsoluteProjectPathRaw } from '@/util/projectPath' import type { GetContextMenuItems, GetMainMenuItems } from 'ag-grid-enterprise' import { expect, test, vi } from 'vitest' import { assertDefined } from 'ydoc-shared/util/assert' -import { Ok, unwrap } from 'ydoc-shared/util/data/result' +import { Ok, unwrap, type Result } from 'ydoc-shared/util/data/result' function suggestionDbWithNothing() { const db = new SuggestionDb() @@ -202,11 +202,18 @@ function tableEditFixture(code: string, expectedCode: string) { assert(firstStatement instanceof Ast.MutableExpressionStatement) const inputAst = firstStatement.expression const input = WidgetInput.FromAst(inputAst) - const edit = vi.fn(async (f) => { - const result = await f(ast.module.edit()) - expect(result).toEqual(Ok()) - return result - }) + const edit = >( + f: (module: Ast.MutableModule) => Promise | T, + ): Promise | T => { + const maybeSyncResult = f(ast.module.edit()) + function doExpect(result: T) { + expect(result.ok).toBeTruthy() + return result + } + return maybeSyncResult instanceof Promise ? + maybeSyncResult.then(doExpect) + : doExpect(maybeSyncResult) + } const onUpdate = vi.fn((update) => { const inputAst = [...update.edit.getVersion(ast).statements()][0] expect(inputAst?.code()).toBe(expectedCode) diff --git a/app/gui/src/project-view/components/MarkdownEditor/blockTypeDropdown.ts b/app/gui/src/project-view/components/MarkdownEditor/blockTypeDropdown.ts index 35c614ea7d4c..cbdd5f8cc127 100644 --- a/app/gui/src/project-view/components/MarkdownEditor/blockTypeDropdown.ts +++ b/app/gui/src/project-view/components/MarkdownEditor/blockTypeDropdown.ts @@ -44,10 +44,11 @@ export function useBlockTypeDropdown({ function menuOption(key: BlockType): SelectionMenuOption | undefined { const action = actions[blockTypeAction[key]] if (!toValue(action.available)) return + const shortcut = toValue(action.shortcut) return { icon: toValue(action.icon), label: toValue(action.description), - labelExtension: action.shortcut && `(${action.shortcut.humanReadable})`, + labelExtension: shortcut && `(${shortcut.humanReadable})`, disabled: disableSettingTypes.value ? key !== blockType.value && key !== 'Paragraph' : false, hidden: false, } diff --git a/app/gui/src/project-view/components/MenuEntry.vue b/app/gui/src/project-view/components/MenuEntry.vue index 5045cf301abd..d00c0be3529b 100644 --- a/app/gui/src/project-view/components/MenuEntry.vue +++ b/app/gui/src/project-view/components/MenuEntry.vue @@ -22,7 +22,7 @@ const action = computed(() => resolveAction(actionName)) diff --git a/app/gui/src/project-view/components/visualizations/TableVisualization.vue b/app/gui/src/project-view/components/visualizations/TableVisualization.vue index 64864e8ea5ba..e864529d3977 100644 --- a/app/gui/src/project-view/components/visualizations/TableVisualization.vue +++ b/app/gui/src/project-view/components/visualizations/TableVisualization.vue @@ -836,14 +836,6 @@ function toLinkField(fieldName: string, options: LinkFieldOptions): ColDef { } } -watchEffect(() => { - try { - refresh() - } catch (error) { - console.warn('Error refreshing table.', error) - } -}) - const DEFAULT_DATA = { type: typeof props.data, json: props.data, @@ -870,6 +862,14 @@ const DEFAULT_DATA = { requires_number_format: undefined, } +watchEffect(() => { + try { + refresh() + } catch (error) { + console.warn('Error refreshing table.', error) + } +}) + // Update state computed from the input `data`. function refresh() { // If the user switches from one visualization type to another, we can receive the raw object. diff --git a/app/gui/src/project-view/providers/action.ts b/app/gui/src/project-view/providers/action.ts index c9249f5f99da..63da0661abbf 100644 --- a/app/gui/src/project-view/providers/action.ts +++ b/app/gui/src/project-view/providers/action.ts @@ -15,20 +15,30 @@ import type { Icon } from '@/util/iconMetadata/iconName' import type { ToValue } from '@/util/reactivity' import type { BindingInfo } from '@/util/shortcuts' import { identity } from '@vueuse/core' -import { ref, type Ref } from 'vue' +import { ref, toValue, type Ref } from 'vue' import type { ForbidExcessProps } from 'ydoc-shared/util/types' /** * A definition of some action available via shortcut, button, and/or menu entry. */ export interface Action { - available?: ToValue - enabled?: ToValue - action?: (ctx: ActionContext | undefined) => void - shortcut?: BindingInfo - icon?: ToValue - description?: ToValue - toggled?: Ref | (() => boolean) + /** Decide whether the action is even going to be shown in the menu. */ + available?: ToValue | undefined + /** Whether the action can be performed. Available but disabled actions will be listed, but grayed out. */ + enabled?: ToValue | undefined + /** The action callback, called when action is invoked by the user. */ + action?: ((ctx: ActionContext | undefined) => void) | undefined + /** + * The default action keyboard shortcut or mouse action binding. Displayed in dropdown menus and tooltips. + * Action handler must be bound through appropriate pointer or keyboard event for this to have any effect. + */ + shortcut?: ToValue | undefined + /** Icon displayed on action buttons or next to the description in dropdowns. */ + icon?: ToValue | undefined + /** Short name of the action. Shown in the context menu next to the icon, or as a tooltip hover for icon buttons. */ + description?: ToValue | undefined + /** When true, action buttons will be highlighted, suggesting that whatever the action represents is currently "on". */ + toggled?: Ref | (() => boolean) | undefined } export interface DisplayableAction extends Action { icon: ToValue @@ -105,6 +115,12 @@ const displayableActions = { description: 'Color Component', }, + // === Widget === + 'component.widget.editMethodName': { + icon: 'group_rename', + description: 'Rename User Defined Component', + }, + // === Component Browser === 'componentBrowser.editSuggestion': { @@ -415,16 +431,43 @@ export function registerHandlers(existing: ToValue | undefined, overrides: ToValue): ToValue +function combineToValues(existing: ToValue, overrides: ToValue | undefined): ToValue +function combineToValues( + existing: ToValue | undefined, + overrides: ToValue | undefined, +): ToValue | undefined +function combineToValues( + existing: ToValue | undefined, + overrides: ToValue | undefined, +): ToValue | undefined { + if (existing == null || overrides == null) return overrides ?? existing + return () => toValue(overrides) ?? toValue(existing) +} + /** A helper function for making ActionHandler toggling a boolean ref. */ export function toggledAction(toggleState = ref(false)) { return { @@ -441,7 +484,10 @@ interface ResolvedAction extends Action { action: () => void } -type DisplayableResolvedAction = ResolvedAction & DisplayableAction +interface DisplayableResolvedAction extends ResolvedAction { + icon: ToValue + description: ToValue +} export function resolveAction(actionName: DisplayableActionName): DisplayableResolvedAction export function resolveAction(actionName: ActionName): ResolvedAction diff --git a/app/gui/src/project-view/providers/animationCounter.ts b/app/gui/src/project-view/providers/animationCounter.ts index 455e75a59657..a08f3901836b 100644 --- a/app/gui/src/project-view/providers/animationCounter.ts +++ b/app/gui/src/project-view/providers/animationCounter.ts @@ -1,5 +1,5 @@ import { proxyRefs } from '@/util/reactivity' -import { computed, onScopeDispose, ref } from 'vue' +import { computed, nextTick, onScopeDispose, ref, watch, type WatchSource } from 'vue' import { createContextStore } from '.' /** @@ -18,8 +18,11 @@ const [provideAnimationCounter, injectAnimationCounter] = createContextStore( let disposed = false onScopeDispose(() => { - modifyCount(-reportedCount.value) - disposed = true + // See `{@link useLayoutAnimationReporter}().reportAnimationEnded` for reasoning behind using rAF here. + requestAnimationFrame(() => { + modifyCount(-reportedCount.value) + disposed = true + }) }) function modifyCount(change: number) { @@ -40,19 +43,51 @@ const [provideAnimationCounter, injectAnimationCounter] = createContextStore( * that can affect DOM element layout, so that any parent component can temporarily enable extra * logic necessary to track them. * - * This is NOT supposed to be used forcounting CSS-driven animations, as those can already be discovered + * This is NOT supposed to be used for counting CSS-driven animations, as those can already be discovered * using `animationstart` and similar events. * * See `useLayoutAnimationsState` for use on the receiver end. */ export const useLayoutAnimationReporter = () => { const counter = injectAnimationCounter(true) - return { - reportAnimationStarted: () => counter?.modifyCount(1), + + /** + * Notify that JS-driven animation affecting DOM node positions and sizes has started. Each call must be + * paired with **exactly one** call to {@link reportAnimationEnded}. + * + */ + function reportAnimationStarted() { + counter?.modifyCount(1) + } + + /** + * Notify that an animation previously reported by {@link reportAnimationStarted} has ended and will no longer + * modify DOM node positions. Must be called **exactly once** per each invocation of {@link reportAnimationStarted}. + */ + function reportAnimationEnded() { // RequestAnimationFrame is used to ensure that at least one frame has passed since the animation stop // was triggered on the JS side. That way the active state is maintained for the full duration of the // last animation frame, and any tracking logic has a chance to react to the final element position. - reportAnimationEnded: () => requestAnimationFrame(() => counter?.modifyCount(-1)), + requestAnimationFrame(() => counter?.modifyCount(-1)) + } + + function reportAnimationWhile(source: WatchSource) { + watch( + source, + (report, _, onCleanup) => { + if (report) { + reportAnimationStarted() + onCleanup(() => nextTick(reportAnimationEnded)) + } + }, + { flush: 'post', immediate: true }, + ) + } + + return { + reportAnimationStarted, + reportAnimationEnded, + reportAnimationWhile, } } diff --git a/app/gui/src/project-view/providers/index.ts b/app/gui/src/project-view/providers/index.ts index 87797ce55bd8..9e5648d901d4 100644 --- a/app/gui/src/project-view/providers/index.ts +++ b/app/gui/src/project-view/providers/index.ts @@ -1,7 +1,37 @@ -import { inject, provide, type InjectionKey } from 'vue' +import { assert } from '@/util/assert' +import { + getCurrentInstance, + inject, + provide, + type ComponentInternalInstance, + type InjectionKey, +} from 'vue' const MISSING = Symbol('MISSING') +/** + * A version of vue's {@link inject} that will also inject a value provided earlier + * within the same component's setup function. + */ +export function injectImmediate(key: InjectionKey, defaultValue: T): T { + // NOTE: Usage of Vue internal API. Consult sources in case this breaks during vue update: + // https://github.com/vuejs/core/blob/45547e69b25baa99a0ed52ba5110c5bd8b4a35e4/packages/runtime-core/src/apiInject.ts#L28 + const instance = getCurrentInstance() as + | (ComponentInternalInstance & { provides: Record, T> }) + | null + const instanceProvides = instance?.provides + + // Assertion to check if the internal propery exist when we expect it. If this fails after update, + // this function needs to be updated as well. + DEV: assert( + instance == null || 'provides' in instance, + `Expected vue internal 'provides' property to exist.`, + ) + + if (instanceProvides && key in instanceProvides) return instanceProvides[key] as T + return inject(key, defaultValue) +} + /** * A pair of functions, `provideFn` and `injectFn`. The `provideFn` function creates the * store instance and provides it to the context, allowing child components to access it. The @@ -28,10 +58,10 @@ export type ContextStore any> = [ * has never been provided and `missingBehavior` is `true`. */ injectFn: { - (allowMissing?: false): ReturnType - (allowMissing: true): ReturnType | undefined - (allowMissing?: boolean): ReturnType | undefined - (orProvideWith: () => Parameters): ReturnType + (allowMissing?: false, immediate?: boolean): ReturnType + (allowMissing: true, immediate?: boolean): ReturnType | undefined + (allowMissing?: boolean, immediate?: boolean): ReturnType | undefined + (orProvideWith: () => Readonly>, immediate?: boolean): ReturnType }, ] @@ -65,20 +95,25 @@ export function createContextStore any>( ): ContextStore { const provideKey = Symbol.for(`contextStore-${name}`) as InjectionKey> - function provideFn(...args: Parameters): ReturnType { + function provideFn(...args: Readonly>): ReturnType { const constructed = factory(...args) provide(provideKey, constructed) return constructed } - function injectFn(allowMissing: true): ReturnType | undefined - function injectFn(allowMissing?: false): ReturnType - function injectFn(allowMissing?: boolean): ReturnType | undefined - function injectFn(orProvideWith: () => Parameters): ReturnType + function injectFn(allowMissing: true, immediate?: boolean): ReturnType | undefined + function injectFn(allowMissing?: false, immediate?: boolean): ReturnType + function injectFn(allowMissing?: boolean, immediate?: boolean): ReturnType | undefined + function injectFn( + orProvideWith: () => Readonly>, + immediate?: boolean, + ): ReturnType function injectFn( - missingBehavior: boolean | (() => Parameters) = false, + missingBehavior: boolean | (() => Readonly>) = false, + immediate = false, ): ReturnType | undefined { - const injected = inject | typeof MISSING>(provideKey, MISSING) + const injector = immediate ? injectImmediate : inject + const injected = injector | typeof MISSING>(provideKey, MISSING) if (injected === MISSING) { if (missingBehavior === true) return if (typeof missingBehavior === 'function') { diff --git a/app/gui/src/project-view/providers/widgetActions.ts b/app/gui/src/project-view/providers/widgetActions.ts new file mode 100644 index 000000000000..7102a74c0bdd --- /dev/null +++ b/app/gui/src/project-view/providers/widgetActions.ts @@ -0,0 +1,68 @@ +import { createContextStore } from '@/providers' +import { setIfUndefined } from 'lib0/map.js' +import { computed, onScopeDispose, reactive, toValue } from 'vue' +import type { ActionHandler, DisplayableActionName } from './action' + +const [, useWidgetActions] = createContextStore('widget actions', () => { + const widgetActions = reactive(new Map()) + const widgetControlledActionNames = new Set() + const warnedActionNames = new Set() + + function widgetControlledAction(actionName: N) { + widgetControlledActionNames.add(actionName) + const currentAction = computed(() => { + const candidates = widgetActions.get(actionName) + return candidates?.find((action) => toValue(action.available) ?? true) + }) + return { + [actionName]: { + action: (ctx) => currentAction.value?.action(ctx), + available: () => currentAction.value != null, + enabled: () => toValue(currentAction.value?.enabled) ?? true, + description: () => toValue(currentAction.value?.description) ?? '', + icon: () => toValue(currentAction.value?.icon), + shortcut: () => toValue(currentAction.value?.shortcut), + toggled: () => toValue(currentAction.value?.toggled) ?? false, + } satisfies ActionHandler, + } + } + + function registerHandler(actionName: string, handler: ActionHandler) { + if (!widgetControlledActionNames.has(actionName) && !warnedActionNames.has(actionName)) { + warnedActionNames.add(actionName) + console.warn( + `Widget registered an action handler for '${actionName}', but it is not declared as widget controlled.`, + ) + } + const registry: ActionHandler[] = setIfUndefined(widgetActions, actionName, () => reactive([])) + registry.push(handler) + onScopeDispose(() => registry.splice(registry.lastIndexOf(handler), 1)) + } + + return { widgetControlledAction, registerHandler } +}) + +/** + * Declare actions that have their definition controlled by child widgets + * Widgets can register a handler for those actions using {@link registerWidgetActionHandlers}. + */ +export function provideWidgetControlledActions< + const Actions extends [DisplayableActionName, ...DisplayableActionName[]], +>(actions: Actions): { [N in Actions[number]]: ActionHandler } { + const actionsStore = useWidgetActions(() => [] as const, true) + return Object.assign({}, ...actions.map(actionsStore.widgetControlledAction)) +} + +/** + * Register a handler for node-level actions. Only allows handlers for actions declared in as widget controlled + * at the node level. See `provideWidgetControlledActions`. + */ +export function registerWidgetActionHandlers< + const Handlers extends Partial>, +>(handlers: Handlers) { + const actionsStore = useWidgetActions(true) + if (!actionsStore) return + for (const [actionName, ActionHandler] of Object.entries(handlers)) { + actionsStore.registerHandler(actionName, ActionHandler) + } +} diff --git a/app/gui/src/project-view/util/callTree.ts b/app/gui/src/project-view/util/callTree.ts index bd0becd72798..f1d6dd518c9c 100644 --- a/app/gui/src/project-view/util/callTree.ts +++ b/app/gui/src/project-view/util/callTree.ts @@ -5,16 +5,22 @@ import { DisplayMode } from '$/providers/openedProjects/widgetRegistry/configura import { syntheticPortId, type PortId } from '@/providers/portInfo' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type GraphDb, type MethodCallInfo } from '$/providers/openedProjects/graph/graphDatabase' +import type { GroupInfo } from '$/providers/openedProjects/suggestionDatabase' import { isRequiredArgument, type CallableSuggestionEntry, type SuggestionEntryArgument, } from '$/providers/openedProjects/suggestionDatabase/entry' +import { MethodSuggestionEntryImpl } from '$/providers/openedProjects/suggestionDatabase/lsUpdate' import { Ast } from '@/util/ast' import type { AstId } from '@/util/ast/abstract' import { findLastIndex, tryGetIndex } from '@/util/data/array' +import type { DeepReadonly } from 'vue' +import { toValue } from 'vue' import type { ExternalId } from 'ydoc-shared/yjsModel' import { assert } from './assert' +import type { MethodPointer } from './methodPointer' +import type { ToValue } from './reactivity' export const enum ApplicationKind { Prefix, @@ -81,12 +87,12 @@ abstract class Argument { return false } - toWidgetInput(callInfo: MethodCallInfo | undefined): WidgetInput { + toWidgetInput(): WidgetInput { return { portId: this.portId, value: this.value, expectedType: this.argInfo?.reprType, - [ArgumentInfoKey]: { info: this.argInfo, appKind: this.kind, argId: this.argId, callInfo }, + [ArgumentInfoKey]: { info: this.argInfo, appKind: this.kind, argId: this.argId }, dynamicConfig: this.dynamicConfig, } } @@ -521,43 +527,118 @@ export function getMethodCallInfoRecursively( ast: Ast.Expression, graphDb: { getMethodCallInfo(id: AstId): MethodCallInfo | undefined }, ): MethodCallInfo | undefined { - let appliedArgs = 0 - const appliedNamedArgs: string[] = [] + const topLevelAst = ast for (;;) { const info = graphDb.getMethodCallInfo(ast.id) if (info) { // There is an info available! Stop the recursion and adjust `notAppliedArguments`. // Indices of all named arguments applied so far. - const appliedNamed = - appliedNamedArgs.length > 0 ? - info.suggestion.arguments - .map((arg, index) => (appliedNamedArgs.includes(arg.name) ? index : -1)) - .filter((i) => i !== -1) - : [] - const withoutNamed = info.methodCall.notAppliedArguments.filter( - (idx) => !appliedNamed.includes(idx), - ) return { methodCall: { ...info.methodCall, - notAppliedArguments: withoutNamed.sort().slice(appliedArgs), + notAppliedArguments: filterNotAppliedArguments( + getCallAppliedArguments(topLevelAst), + info.suggestion.arguments, + info.methodCall.notAppliedArguments, + ), }, methodCallSource: ast.id, suggestion: info.suggestion, } } // No info, continue recursion to the next sub-application AST. - if (ast instanceof Ast.App) { - if (ast.argumentName) { - appliedNamedArgs.push(ast.argumentName.code()) - } else { - appliedArgs += 1 - } - ast = ast.function + if (ast instanceof Ast.App) ast = ast.function + else break + } +} + +/** + * Derive a synthetic method call info from locally available module code. + * Potentially integrate non-derivable data from language server response, if available. + */ +export function deriveLocalCallInfoFromCode( + methodPointer: MethodPointer, + call: Ast.Expression, + groups: ToValue>, +): MethodCallInfo | undefined { + const moduleRoot = call.module.root() + if (!moduleRoot) return + const methodAst = Ast.findModuleMethod(moduleRoot, methodPointer.name) + if (!methodAst) return + const suggestion = deriveLocalMethodSuggestion( + methodPointer, + methodAst.statement, + toValue(groups), + ) + const appliedInfo = getCallAppliedArguments(call) + const notAppliedArguments = filterNotAppliedArguments(appliedInfo, suggestion.arguments) + return { + suggestion, + methodCallSource: call.id, + methodCall: { methodPointer, notAppliedArguments }, + } +} + +function deriveLocalMethodSuggestion( + methodPointer: MethodPointer, + ast: Ast.FunctionDef, + groups: DeepReadonly = [], +): CallableSuggestionEntry { + const args: SuggestionEntryArgument[] = ast.argumentDefinitions.map((def) => { + return { + name: def.pattern.node.code(), + reprType: def.type?.type.node.code() ?? 'Any', + isSuspended: def.suspension != null, + hasDefault: def.defaultValue != null, + defaultValue: def.defaultValue?.expression.node.code() ?? null, + } satisfies SuggestionEntryArgument + }) + const annotations = ast.annotations.map((ann) => ann.annotation.node.code()) + const returnType = ast.returnType?.code() ?? 'Any' + return MethodSuggestionEntryImpl.synthesizeLocal( + methodPointer, + args, + returnType, + annotations, + groups, + ) +} + +interface AppliedArgsInfo { + positional: number + named: string[] +} + +function getCallAppliedArguments(ast: Ast.Expression) { + const named: string[] = [] + let positional = 0 + while (ast instanceof Ast.App) { + if (ast.argumentName) { + named.push(ast.argumentName.code()) } else { - break + positional += 1 } + ast = ast.function } + return { positional, named } +} + +function filterNotAppliedArguments( + appliedInfo: AppliedArgsInfo, + methodArgs: SuggestionEntryArgument[], + notApplied: number[] = methodArgs.map((_, i) => i), +) { + // There is an info available! Stop the recursion and adjust `notAppliedArguments`. + // Indices of all named arguments applied so far. + const appliedNamed = + appliedInfo.named.length > 0 ? + methodArgs + .map((arg, index) => (appliedInfo.named.includes(arg.name) ? index : -1)) + .filter((i) => i !== -1) + : [] + + const withoutNamed = notApplied.filter((idx) => !appliedNamed.includes(idx)) + return withoutNamed.sort().slice(appliedInfo.positional) } export const ArgumentApplicationKey: unique symbol = Symbol.for('WidgetInput:ArgumentApplication') @@ -569,8 +650,6 @@ declare module '$/providers/openedProjects/widgetRegistry' { appKind: ApplicationKind info: SuggestionEntryArgument | undefined argId: string | undefined - // Call info inherited from the parent function call. - callInfo: MethodCallInfo | undefined } } } diff --git a/app/gui/src/project-view/util/nameValidation.ts b/app/gui/src/project-view/util/nameValidation.ts index 2b90eb18820e..24f7bfc34052 100644 --- a/app/gui/src/project-view/util/nameValidation.ts +++ b/app/gui/src/project-view/util/nameValidation.ts @@ -31,6 +31,47 @@ export function normalizeName(name: string): Identifier { return onlyAlphanumeric } +/** + * Transforms the given string into a valid function name. + */ +export function normalizeFunctionName(name: string): Identifier { + return (toLowerSnakeCase(name) || 'user_created_component') as Identifier +} + +/** + * Transforms the given string into a valid function name. + */ +export function normalizeArgumentName(name: string): Identifier { + return (toLowerSnakeCase(name) || 'arg') as Identifier +} + +function toLowerSnakeCase(name: string): string { + if (!name) return '' + let result = '' + let lastWasUpper = true + + for (let i = 0; i < name.length; i++) { + const c = name.charAt(i) + if (/^[A-Z]$/.test(c)) { + if (!lastWasUpper) result += '_' + result += c.toLowerCase() + lastWasUpper = true + } else if (/^[a-z]$/.test(c)) { + result += c + lastWasUpper = false + } else if (/^[0-9]$/.test(c)) { + if (i == 0) result += '_' + result += c + lastWasUpper = false + } else if (c == '_' || c == ' ') { + result += '_' + lastWasUpper = false + } + } + // Replace multiple underscores with a single underscore and remove trailing underscores + return result.replaceAll(/__+/g, '_').replace(/_$/, '') +} + /** * Checks if a character is allowed in a project name. */ diff --git a/app/gui/src/providers/openedProjects/graph/graphDatabase.ts b/app/gui/src/providers/openedProjects/graph/graphDatabase.ts index 2e1f0b9f6085..9bb34838c29c 100644 --- a/app/gui/src/providers/openedProjects/graph/graphDatabase.ts +++ b/app/gui/src/providers/openedProjects/graph/graphDatabase.ts @@ -8,7 +8,7 @@ import { type ProjectNameStore, } from '$/providers/openedProjects/projectNames' import { SuggestionDb, type GroupInfo } from '$/providers/openedProjects/suggestionDatabase' -import { type CallableSuggestionEntry } from '$/providers/openedProjects/suggestionDatabase/entry' +import type { CallableSuggestionEntry } from '$/providers/openedProjects/suggestionDatabase/entry' import { computeNodeColor } from '@/composables/nodeColors' import { Ast } from '@/util/ast' import type { AstId, NodeMetadata } from '@/util/ast/abstract' diff --git a/app/gui/src/providers/openedProjects/module/module.ts b/app/gui/src/providers/openedProjects/module/module.ts index de206732cd58..12ae2ffa3641 100644 --- a/app/gui/src/providers/openedProjects/module/module.ts +++ b/app/gui/src/providers/openedProjects/module/module.ts @@ -31,6 +31,13 @@ import { */ export type ModuleStore = ReturnType +export interface EditOptions { + skipTreeRepair?: boolean + origin?: Origin + logLevel?: 'none' | 'info' | 'warn' | 'error' + logPreamble?: string +} + /** Constructor of {@link ModuleStore} */ export function createModuleStore( proj: ProjectStore, @@ -82,6 +89,15 @@ export function createModuleStore( } } + function edit>(f: (edit: MutableModule) => R, options?: EditOptions): R + function edit>( + f: (edit: MutableModule) => Promise, + options?: EditOptions, + ): Promise + function edit>( + f: (edit: MutableModule) => R | Promise, + options?: EditOptions, + ): R | Promise /** * Edit the AST module. * @@ -89,29 +105,24 @@ export function createModuleStore( * @param options.skipTreeRepair - If the edit is certain not to produce incorrect or non-canonical syntax, this may be set * to `true` for better performance. */ - function edit | Promise>>( - f: (edit: MutableModule) => R, - options: { - skipTreeRepair?: boolean - origin?: Origin - logLevel?: 'none' | 'info' | 'warn' | 'error' - logPreamble?: string - } = {}, - ): R { + function edit( + f: (edit: MutableModule) => Promise | Result, + options: EditOptions = {}, + ): Promise | Result { assertDefined(synced.value) const edit = synced.value.edit() const logLevel = options.logLevel ?? 'error' - const treeRepair = (result: Result) => { - if (result.ok && options.skipTreeRepair !== true) { + const treeRepair = (result: Result) => { + if (result.ok) { const root = edit.root() assert(root instanceof Ast.BodyBlock) - Ast.repair(root, edit) + edit.transact(() => Ast.repair(root, edit)) } return result } - const applyEdit = (result: Result) => { + 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.')) @@ -120,14 +131,10 @@ export function createModuleStore( const result = edit.transact(() => { const result = f(edit) - if (result instanceof Promise) { - return result.then(treeRepair) - } else { - return treeRepair(result) - } + if (options.skipTreeRepair === true) return result + return result instanceof Promise ? result.then(treeRepair) : treeRepair(result) }) - if (result instanceof Promise) return result.then(applyEdit) as R - else return applyEdit(result) as R + return result instanceof Promise ? result.then(applyEdit) : applyEdit(result) } function batchEdits(f: () => void, origin: Origin = defaultLocalOrigin) { @@ -135,6 +142,11 @@ export function createModuleStore( synced.value.transact(f, origin) } + function hasMethod(name: string): boolean { + const root = ast.value?.root() + return root != null && Ast.findModuleMethod(root, name) != null + } + function getMethodAst(ptr: MethodPointer, edit?: Ast.Module): Result { const topLevel = (edit ?? ast.value)?.root() if (!topLevel) return Err('Module unavailable') @@ -226,6 +238,7 @@ export function createModuleStore( edit, batchEdits, onBeforeEdit, + hasMethod, getMethodAst, mutableNodeMetadata, setWidgetMetadata, diff --git a/app/gui/src/providers/openedProjects/suggestionDatabase/lsUpdate.ts b/app/gui/src/providers/openedProjects/suggestionDatabase/lsUpdate.ts index 968ebb49466a..d99f531a27de 100644 --- a/app/gui/src/providers/openedProjects/suggestionDatabase/lsUpdate.ts +++ b/app/gui/src/providers/openedProjects/suggestionDatabase/lsUpdate.ts @@ -21,6 +21,7 @@ import { assert, assertNever } from '@/util/assert' import type { Opt } from '@/util/data/opt' import { Err, Ok, withContext, type Result } from '@/util/data/result' import { ANY_TYPE_QN } from '@/util/ensoTypes' +import type { MethodPointer } from '@/util/methodPointer' import type { ProjectPath } from '@/util/projectPath' import { isIdentifierOrOperatorIdentifier, @@ -51,9 +52,9 @@ abstract class BaseSuggestionEntry implements SuggestionEntryCommon { protected constructor( documentation: string | undefined, public definedIn: ProjectPath, - context: UpdateContext, + groups: DeepReadonly, ) { - this.documentationData = documentationData(documentation, definedIn.project, context.groups) + this.documentationData = documentationData(documentation, definedIn.project, groups) } get documentation() { @@ -116,9 +117,9 @@ class FunctionSuggestionEntryImpl extends BaseSuggestionEntry implements Functio definedIn: ProjectPath, private lsReturnType: Typename, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) this.arguments = args } @@ -131,7 +132,7 @@ class FunctionSuggestionEntryImpl extends BaseSuggestionEntry implements Functio context: UpdateContext, ): Result { if (!isIdentifierOrOperatorIdentifier(lsEntry.name)) return Err('Invalid name') - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module return Ok( new FunctionSuggestionEntryImpl( @@ -141,7 +142,7 @@ class FunctionSuggestionEntryImpl extends BaseSuggestionEntry implements Functio module.value, lsEntry.returnType, lsEntry.documentation, - context, + context.groups, ), ) } @@ -163,9 +164,9 @@ class ModuleSuggestionEntryImpl extends BaseSuggestionEntry implements ModuleSug definedIn: ProjectPath, public reexportedIn: ProjectPath | undefined, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) } get name() { @@ -186,12 +187,17 @@ class ModuleSuggestionEntryImpl extends BaseSuggestionEntry implements ModuleSug lsEntry: lsTypes.SuggestionEntry.Module, context: UpdateContext, ): Result { - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module - const reexport = parseProjectPath(lsEntry, 'reexport', context) + const reexport = parseProjectPath(lsEntry, 'reexport', context.projectNames) if (!reexport.ok) return reexport return Ok( - new ModuleSuggestionEntryImpl(module.value, reexport.value, lsEntry.documentation, context), + new ModuleSuggestionEntryImpl( + module.value, + reexport.value, + lsEntry.documentation, + context.groups, + ), ) } @@ -212,9 +218,9 @@ class TypeSuggestionEntryImpl extends BaseSuggestionEntry implements TypeSuggest definedIn: ProjectPath, public reexportedIn: ProjectPath | undefined, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) this.arguments = args } @@ -227,11 +233,11 @@ class TypeSuggestionEntryImpl extends BaseSuggestionEntry implements TypeSuggest context: UpdateContext, ): Result { if (!isIdentifierOrOperatorIdentifier(lsEntry.name)) return Err('Invalid name') - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module - const reexport = parseProjectPath(lsEntry, 'reexport', context) + const reexport = parseProjectPath(lsEntry, 'reexport', context.projectNames) if (!reexport.ok) return reexport - const parentType = parseProjectPath(lsEntry, 'parentType', context) + const parentType = parseProjectPath(lsEntry, 'parentType', context.projectNames) if (!parentType.ok) return parentType return Ok( new TypeSuggestionEntryImpl( @@ -241,7 +247,7 @@ class TypeSuggestionEntryImpl extends BaseSuggestionEntry implements TypeSuggest module.value, reexport.value, lsEntry.documentation, - context, + context.groups, ), ) } @@ -267,9 +273,9 @@ class ConstructorSuggestionEntryImpl definedIn: ProjectPath, public memberOf: ProjectPath, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) this.arguments = args } @@ -285,11 +291,11 @@ class ConstructorSuggestionEntryImpl context: UpdateContext, ): Result { if (!isIdentifierOrOperatorIdentifier(lsEntry.name)) return Err('Invalid name') - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module - const reexport = parseProjectPath(lsEntry, 'reexport', context) + const reexport = parseProjectPath(lsEntry, 'reexport', context.projectNames) if (!reexport.ok) return reexport - const returnType = parseProjectPath(lsEntry, 'returnType', context) + const returnType = parseProjectPath(lsEntry, 'returnType', context.projectNames) if (!returnType.ok) return returnType return Ok( new ConstructorSuggestionEntryImpl( @@ -300,7 +306,7 @@ class ConstructorSuggestionEntryImpl module.value, returnType.value, lsEntry.documentation, - context, + context.groups, ), ) } @@ -317,7 +323,11 @@ class ConstructorSuggestionEntryImpl } } -class MethodSuggestionEntryImpl extends BaseSuggestionEntry implements MethodSuggestionEntry { +/** Suggestion entry implementation for module-level methods. */ +export class MethodSuggestionEntryImpl + extends BaseSuggestionEntry + implements MethodSuggestionEntry +{ readonly kind = SuggestionKind.Method arguments: lsTypes.SuggestionEntryArgument[] @@ -331,32 +341,37 @@ class MethodSuggestionEntryImpl extends BaseSuggestionEntry implements MethodSug definedIn: ProjectPath, private lsReturnType: Typename, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) this.arguments = args } + /** {@link SuggestionEntryCommon.returnType} */ returnType() { return this.lsReturnType } + /** {@link SuggestionEntryCommon.definitionPath} */ override get definitionPath() { return this.memberOf.append(this.name) } + + /** {@link SuggestionEntryCommon.definitionPath} */ get selfType() { return this.isStatic ? undefined : this.memberOf } + /** Create a suggestion instance by using data from language server. */ static parse( lsEntry: lsTypes.SuggestionEntry.Method, context: UpdateContext, ): Result { if (!isIdentifierOrOperatorIdentifier(lsEntry.name)) return Err('Invalid name') - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module - const reexport = parseProjectPath(lsEntry, 'reexport', context) + const reexport = parseProjectPath(lsEntry, 'reexport', context.projectNames) if (!reexport.ok) return reexport - const selfType = parseProjectPath(lsEntry, 'selfType', context) + const selfType = parseProjectPath(lsEntry, 'selfType', context.projectNames) if (!selfType.ok) return selfType return Ok( new MethodSuggestionEntryImpl( @@ -369,19 +384,46 @@ class MethodSuggestionEntryImpl extends BaseSuggestionEntry implements MethodSug module.value, lsEntry.returnType, lsEntry.documentation, - context, + context.groups, ), ) } + /** Create a suggestion instance by using data from module code. */ + static synthesizeLocal( + methodPointer: MethodPointer, + args: SuggestionEntryArgument[], + returnType: string = 'Any', + annotations: string[] = [], + groups: DeepReadonly = [], + ) { + return new MethodSuggestionEntryImpl( + methodPointer.name, + args, + undefined, + annotations, + true, + methodPointer.definedOnType, + methodPointer.module, + returnType, + undefined, + groups, + ) + } + + /** {@link BaseSuggestionEntry.setLsReturnType } */ override setLsReturnType(returnType: Typename) { this.lsReturnType = returnType return Ok() } + + /** {@link BaseSuggestionEntry.setLsReexported } */ override setLsReexported(reexported: ProjectPath | undefined) { this.reexportedIn = reexported return Ok() } + + /** {@link BaseSuggestionEntry.setLsSelfType } */ setLsSelfType(selfType: ProjectPath) { this.memberOf = selfType } @@ -396,9 +438,9 @@ class LocalSuggestionEntryImpl extends BaseSuggestionEntry implements LocalSugge definedIn: ProjectPath, private lsReturnType: Typename, documentation: string | undefined, - context: UpdateContext, + groups: DeepReadonly, ) { - super(documentation, definedIn, context) + super(documentation, definedIn, groups) } returnType() { @@ -410,7 +452,7 @@ class LocalSuggestionEntryImpl extends BaseSuggestionEntry implements LocalSugge context: UpdateContext, ): Result { if (!isIdentifierOrOperatorIdentifier(lsEntry.name)) return Err('Invalid name') - const module = parseProjectPath(lsEntry, 'module', context) + const module = parseProjectPath(lsEntry, 'module', context.projectNames) if (!module.ok) return module return Ok( new LocalSuggestionEntryImpl( @@ -419,7 +461,7 @@ class LocalSuggestionEntryImpl extends BaseSuggestionEntry implements LocalSugge module.value, lsEntry.returnType, lsEntry.documentation, - context, + context.groups, ), ) } @@ -683,23 +725,21 @@ export class SuggestionUpdateProcessor { function parseProjectPath( lsEntry: { [P in K]: string }, field: K, - context: UpdateContext, + projectNames: ProjectNameStore, ): Result function parseProjectPath( lsEntry: { [P in K]?: string }, field: K, - context: UpdateContext, + projectNames: ProjectNameStore, ): Result function parseProjectPath( lsEntry: { [P in K]?: string }, field: K, - context: UpdateContext, + projectNames: ProjectNameStore, ) { return withContext( () => `Parsing ${field}`, () => - lsEntry[field] != null ? - context.projectNames.parseProjectPathRaw(lsEntry[field]) - : Ok(undefined), + lsEntry[field] != null ? projectNames.parseProjectPathRaw(lsEntry[field]) : Ok(undefined), ) } diff --git a/app/ydoc-shared/src/ast/mutableModule.ts b/app/ydoc-shared/src/ast/mutableModule.ts index 3f003e0010eb..7f9a28f9dd9c 100644 --- a/app/ydoc-shared/src/ast/mutableModule.ts +++ b/app/ydoc-shared/src/ast/mutableModule.ts @@ -174,7 +174,9 @@ export class MutableModule implements Module { /** @internal */ importCopy(ast: T): Owned> { assert(ast.module !== this) - visitRecursive(ast, (ast) => this.nodes.set(ast.id, ast.fields.clone() as any)) + visitRecursive(ast, (ast) => { + this.nodes.set(ast.id, ast.fields.clone() as any) + }) const fields = this.nodes.get(ast.id) assertDefined(fields) fields.set('parent', undefined) diff --git a/app/ydoc-shared/src/ast/parse.ts b/app/ydoc-shared/src/ast/parse.ts index 85fb1d70ee5d..09431d429b12 100644 --- a/app/ydoc-shared/src/ast/parse.ts +++ b/app/ydoc-shared/src/ast/parse.ts @@ -19,8 +19,10 @@ import type { NodeChild, Owned, OwnedRefs, + ReturnSpecification, TextElement, TextToken, + TypeSignature, } from './tree' import { App, @@ -355,6 +357,7 @@ class Abstractor { }, close: arg.close && this.abstractToken(arg.close), })) + const returns = tree.returns && this.abstractReturnSpecification(tree.returns) const equals = this.abstractToken(tree.equals) const body = tree.body !== undefined ? this.abstractExpression(tree.body) : undefined return FunctionDef.concrete(this.module, { @@ -366,6 +369,7 @@ class Abstractor { private_, name, argumentDefinitions, + returns, equals, body, } satisfies FunctionDefFields) @@ -436,7 +440,7 @@ class Abstractor { } } - private abstractTypeSignature(signature: RawAst.TypeSignature) { + private abstractTypeSignature(signature: RawAst.TypeSignature): TypeSignature { return { name: this.abstractExpression(signature.name), operator: this.abstractToken(signature.operator), @@ -444,6 +448,15 @@ class Abstractor { } } + private abstractReturnSpecification( + spec: RawAst.ReturnSpecification, + ): ReturnSpecification { + return { + arrow: this.abstractToken(spec.arrow), + type: this.abstractExpression(spec.typeNode), + } + } + private abstractDocLine(docLine: RawAst.DocLine) { return { docs: { diff --git a/app/ydoc-shared/src/ast/tree.ts b/app/ydoc-shared/src/ast/tree.ts index a2375d120b91..d497ff05a4a0 100644 --- a/app/ydoc-shared/src/ast/tree.ts +++ b/app/ydoc-shared/src/ast/tree.ts @@ -484,11 +484,25 @@ export abstract class MutableAst extends Ast { } } -/** TODO: Add docs */ -export function visitRecursive(ast: Ast, visit: (ast: Ast) => void | boolean): void { - if (visit(ast) === false) return - for (const child of ast.children()) { - if (!isToken(child)) visitRecursive(child, visit) +/** + * Visit all AST nodes in depth-first order using given visitor function. + * If visitor returns `false` value, child nodes of currently visited node will be skipped. + * If visitor returns an Iterable of nodes, only those nodes will be visited. + */ +export function visitRecursive( + ast: Ast, + visit: (ast: Ast) => void | boolean | Iterable, +): void { + const visitResult = visit(ast) + if (visitResult === false) return + if (visitResult === true || visitResult == null) { + for (const child of ast.children()) { + if (child != null && !isToken(child)) visitRecursive(child, visit) + } + } else { + for (const child of visitResult) { + if (child != null) visitRecursive(child, visit) + } } } @@ -515,6 +529,7 @@ type StructuralField = | NameSpecification | TextElement | ArgumentDefinition + | ReturnSpecification | VectorElement | TypeSignature | SignatureLine @@ -653,6 +668,18 @@ function mapRefs( field: ArgumentDefinition, f: MapRef, ): ArgumentDefinition +function mapRefs( + field: FunctionAnnotation, + f: MapRef, +): FunctionAnnotation +function mapRefs( + field: TypeSignature, + f: MapRef, +): TypeSignature +function mapRefs( + field: ReturnSpecification, + f: MapRef, +): ReturnSpecification function mapRefs( field: VectorElement, f: MapRef, @@ -1331,10 +1358,10 @@ export class PropertyAccess extends BaseExpression { return this.module.getToken(this.fields.get('operator').node) } /** TODO: Add docs */ - get rhs(): IdentifierOrOperatorIdentifierToken { + get rhs(): Ident { const ast = this.module.get(this.fields.get('rhs').node) assert(ast instanceof Ident) - return ast.token as IdentifierOrOperatorIdentifierToken + return ast } /** TODO: Add docs */ @@ -2387,6 +2414,11 @@ export interface ArgumentDefinition { close?: T['token'] | undefined } +export interface ReturnSpecification { + arrow: T['token'] + type: T['expression'] +} + /** * Create a new function argument definition using provided "name" string as argument's pattern expression. */ @@ -2432,7 +2464,7 @@ interface AnnotationLine { newlines: T['token'][] } -interface TypeSignature { +export interface TypeSignature { name: T['ast'] operator: T['token'] type: T['ast'] @@ -2452,6 +2484,7 @@ export interface FunctionDefFields { private_: T['token'] | undefined name: T['ast'] argumentDefinitions: ArgumentDefinition[] + returns: ReturnSpecification | undefined equals: T['token'] body: T['ast'] | undefined } @@ -2491,6 +2524,24 @@ export class FunctionDef extends BaseStatement { .map((def) => mapRefs(def, rawToConcrete(this.module))) } + /** Get annotations attached to this function. */ + get annotations(): FunctionAnnotation[] { + return this.fields + .get('annotationLines') + .map((line) => mapRefs(line.annotation, rawToConcrete(this.module))) + } + + /** Get function's type signature AST, if it is present. */ + get signature(): TypeSignature | undefined { + const line = this.fields.get('signatureLine') + return line && mapRefs(line.signature, rawToConcrete(this.module)) + } + + /** Get function's type signature AST, if it is present. */ + get returnType(): Expression | undefined { + return this.module.get(this.fields.get('returns')?.type.node) as Expression | undefined + } + /** TODO: Add docs */ static concrete( module: MutableModule, @@ -2515,6 +2566,7 @@ export class FunctionDef extends BaseStatement { argumentDefinitions: (fields.argumentDefinitions ?? []).map((def) => mapRefs(def, ownedToRaw(module, id_)), ), + returns: fields.returns && mapRefs(fields.returns, ownedToRaw(module, id_)), equals: fields.equals, body: concreteChild(module, fields.body, id_), }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33dd88462c5b..7fbe40afbe11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -523,8 +523,8 @@ importers: specifier: ^2.2.0 version: 2.2.10 vue-router: - specifier: ^4.5.0 - version: 4.5.0(vue@3.5.13(typescript@5.7.2)) + specifier: ^4.6.3 + version: 4.6.3(vue@3.5.13(typescript@5.7.2)) y-protocols: specifier: ^1.0.6 version: 1.0.6(yjs@13.6.21) @@ -719,8 +719,8 @@ importers: specifier: 'catalog:' version: 7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) vite-plugin-vue-devtools: - specifier: ^8.0.0 - version: 8.0.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)) + specifier: ^8.0.3 + version: 8.0.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)) vite-plugin-wasm: specifier: ^3.5.0 version: 3.5.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) @@ -2659,9 +2659,6 @@ packages: cpu: [x64] os: [win32] - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry-internal/feedback@7.120.3': resolution: {integrity: sha512-ewJJIQ0mbsOX6jfiVFvqMjokxNtgP3dNwUv+4nenN+iJJPQsM6a0ocro3iscxwVdbkjw5hY3BUV2ICI5Q0UWoA==} engines: {node: '>=12'} @@ -2778,10 +2775,6 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} @@ -3288,22 +3281,22 @@ packages: '@vue/devtools-api@7.7.7': resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} - '@vue/devtools-core@8.0.0': - resolution: {integrity: sha512-5bPtF0jAFnaGs4C/4+3vGRR5U+cf6Y8UWK0nJflutEDGepHxl5L9JRaPdHQYCUgrzUaf4cY4waNBEEGXrfcs3A==} + '@vue/devtools-core@8.0.3': + resolution: {integrity: sha512-gCEQN7aMmeaigEWJQ2Z2o3g7/CMqGTPvNS1U3n/kzpLoAZ1hkAHNgi4ml/POn/9uqGILBk65GGOUdrraHXRj5Q==} peerDependencies: vue: ^3.0.0 '@vue/devtools-kit@7.7.7': resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} - '@vue/devtools-kit@8.0.0': - resolution: {integrity: sha512-b11OeQODkE0bctdT0RhL684pEV2DPXJ80bjpywVCbFn1PxuL3bmMPDoJKjbMnnoWbrnUYXYzFfmMWBZAMhORkQ==} + '@vue/devtools-kit@8.0.3': + resolution: {integrity: sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg==} '@vue/devtools-shared@7.7.7': resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/devtools-shared@8.0.0': - resolution: {integrity: sha512-jrKnbjshQCiOAJanoeJjTU7WaCg0Dz2BUal6SaR6VM/P3hiFdX5Q6Pxl73ZMnrhCxNK9nAg5hvvRGqs+6dtU1g==} + '@vue/devtools-shared@8.0.3': + resolution: {integrity: sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw==} '@vue/eslint-config-typescript@14.2.0': resolution: {integrity: sha512-JJ4wHuTJa2faQsBOUeWzuHOSFizVS7RWG2eH2noABk2LcT4wVcTOMZKM/lFobKBcgwADIPAKVRGFHVKooXImoA==} @@ -3638,8 +3631,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - birpc@2.5.0: - resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + birpc@2.7.0: + resolution: {integrity: sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4632,10 +4625,6 @@ packages: resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} engines: {node: ^8.12.0 || >=9.7.0} - execa@9.6.0: - resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} - engines: {node: ^18.19.0 || >=20.5.0} - expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -4707,10 +4696,6 @@ packages: picomatch: optional: true - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -4853,10 +4838,6 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -5016,10 +4997,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-signals@8.0.1: - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} - engines: {node: '>=18.18.0'} - iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -5208,10 +5185,6 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -5251,10 +5224,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} @@ -5800,10 +5769,6 @@ packages: resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} engines: {node: '>=8'} - npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -5934,10 +5899,6 @@ packages: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} - parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} @@ -5977,10 +5938,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -6014,6 +5971,9 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6203,10 +6163,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-ms@9.2.0: - resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} - engines: {node: '>=18'} - prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -6639,8 +6595,8 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - sirv@3.0.1: - resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} slash@1.0.0: @@ -6796,10 +6752,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -7053,10 +7005,6 @@ packages: unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - universal-cookie@4.0.4: resolution: {integrity: sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==} @@ -7072,9 +7020,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-utils@0.2.5: - resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} - engines: {node: '>=18.12.0'} + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} @@ -7185,8 +7133,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-inspect@11.3.2: - resolution: {integrity: sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==} + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} engines: {node: '>=14'} peerDependencies: '@nuxt/kit': '*' @@ -7195,8 +7143,8 @@ packages: '@nuxt/kit': optional: true - vite-plugin-vue-devtools@8.0.0: - resolution: {integrity: sha512-9bWQig8UMu3nPbxX86NJv56aelpFYoBHxB5+pxuQz3pa3Tajc1ezRidj/0dnADA4/UHuVIfwIVYHOvMXYcPshg==} + vite-plugin-vue-devtools@8.0.3: + resolution: {integrity: sha512-yIi3u31xUi28HcLlTpV0BvSLQHgZ2dA8Zqa59kWfIeMdHqbsunt6TCjq4wCNfOcGSju+E7qyHyI09EjRRFMbuQ==} engines: {node: '>=v14.21.3'} peerDependencies: vite: ^6.0.0 || ^7.0.0-0 @@ -7347,10 +7295,10 @@ packages: peerDependencies: vue: ^3.0.0 - vue-router@4.5.0: - resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + vue-router@4.6.3: + resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==} peerDependencies: - vue: ^3.2.0 + vue: ^3.5.0 vue-tsc@2.2.0: resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==} @@ -7564,10 +7512,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} - engines: {node: '>=18'} - zen-observable-ts@0.8.19: resolution: {integrity: sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==} @@ -9989,8 +9933,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true - '@sec-ant/readable-stream@0.4.1': {} - '@sentry-internal/feedback@7.120.3': dependencies: '@sentry/core': 7.120.3 @@ -10128,8 +10070,6 @@ snapshots: '@sindresorhus/is@4.6.0': {} - '@sindresorhus/merge-streams@4.0.0': {} - '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 @@ -10642,7 +10582,7 @@ snapshots: '@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 - sirv: 3.0.1 + sirv: 3.0.2 tinyrainbow: 2.0.0 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/browser@3.2.4)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.37.0)(yaml@2.7.0) ws: 8.18.0 @@ -10780,10 +10720,10 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.7 - '@vue/devtools-core@8.0.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2))': + '@vue/devtools-core@8.0.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2))': dependencies: - '@vue/devtools-kit': 8.0.0 - '@vue/devtools-shared': 8.0.0 + '@vue/devtools-kit': 8.0.3 + '@vue/devtools-shared': 8.0.3 mitt: 3.0.1 nanoid: 5.1.5 pathe: 2.0.3 @@ -10795,20 +10735,20 @@ snapshots: '@vue/devtools-kit@7.7.7': dependencies: '@vue/devtools-shared': 7.7.7 - birpc: 2.5.0 + birpc: 2.7.0 hookable: 5.5.3 mitt: 3.0.1 perfect-debounce: 1.0.0 speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-kit@8.0.0': + '@vue/devtools-kit@8.0.3': dependencies: - '@vue/devtools-shared': 8.0.0 - birpc: 2.5.0 + '@vue/devtools-shared': 8.0.3 + birpc: 2.7.0 hookable: 5.5.3 mitt: 3.0.1 - perfect-debounce: 1.0.0 + perfect-debounce: 2.0.0 speakingurl: 14.0.1 superjson: 2.2.2 @@ -10816,7 +10756,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/devtools-shared@8.0.0': + '@vue/devtools-shared@8.0.3': dependencies: rfdc: 1.4.1 @@ -11218,7 +11158,7 @@ snapshots: binary-extensions@2.3.0: {} - birpc@2.5.0: {} + birpc@2.7.0: {} bl@4.1.0: dependencies: @@ -12462,21 +12402,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@9.6.0: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.6 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.1 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 6.0.0 - pretty-ms: 9.2.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.1 - expect-type@1.2.2: {} express@4.21.2: @@ -12577,10 +12502,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - figures@6.1.0: - dependencies: - is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -12746,11 +12667,6 @@ snapshots: dependencies: pump: 3.0.2 - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.3 @@ -12957,8 +12873,6 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@8.0.1: {} - iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -13123,8 +13037,6 @@ snapshots: is-plain-obj@2.1.0: {} - is-plain-obj@4.1.0: {} - is-potential-custom-element-name@1.0.1: {} is-regex@1.2.1: @@ -13161,8 +13073,6 @@ snapshots: is-unicode-supported@0.1.0: {} - is-unicode-supported@2.1.0: {} - is-url@1.2.4: {} is-weakmap@2.0.2: {} @@ -13693,11 +13603,6 @@ snapshots: dependencies: path-key: 3.1.1 - npm-run-path@6.0.0: - dependencies: - path-key: 4.0.0 - unicorn-magic: 0.3.0 - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -13832,8 +13737,6 @@ snapshots: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 - parse-ms@4.0.0: {} - parse5@7.2.1: dependencies: entities: 4.5.0 @@ -13866,8 +13769,6 @@ snapshots: path-key@3.1.1: {} - path-key@4.0.0: {} - path-parse@1.0.7: {} path-scurry@1.11.1: @@ -13897,6 +13798,8 @@ snapshots: perfect-debounce@1.0.0: {} + perfect-debounce@2.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -14021,10 +13924,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - pretty-ms@9.2.0: - dependencies: - parse-ms: 4.0.0 - prismjs@1.30.0: {} process-nextick-args@2.0.1: {} @@ -14626,7 +14525,7 @@ snapshots: dependencies: semver: 7.7.1 - sirv@3.0.1: + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 @@ -14808,8 +14707,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-final-newline@4.0.0: {} - strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -15117,8 +15014,6 @@ snapshots: unfetch@4.2.0: {} - unicorn-magic@0.3.0: {} - universal-cookie@4.0.4: dependencies: '@types/cookie': 0.3.3 @@ -15130,7 +15025,7 @@ snapshots: unpipe@1.0.0: {} - unplugin-utils@0.2.5: + unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 picomatch: 4.0.3 @@ -15221,7 +15116,7 @@ snapshots: vite-dev-rpc@1.1.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)): dependencies: - birpc: 2.5.0 + birpc: 2.7.0 vite: 7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) vite-hot-client: 2.1.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) @@ -15292,30 +15187,29 @@ snapshots: - tsx - yaml - vite-plugin-inspect@11.3.2(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)): + vite-plugin-inspect@11.3.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)): dependencies: ansis: 4.1.0 debug: 4.4.1(supports-color@8.1.1) error-stack-parser-es: 1.0.5 ohash: 2.0.11 open: 10.2.0 - perfect-debounce: 1.0.0 - sirv: 3.0.1 - unplugin-utils: 0.2.5 + perfect-debounce: 2.0.0 + sirv: 3.0.2 + unplugin-utils: 0.3.1 vite: 7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) vite-dev-rpc: 1.1.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.0.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)): + vite-plugin-vue-devtools@8.0.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)): dependencies: - '@vue/devtools-core': 8.0.0(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)) - '@vue/devtools-kit': 8.0.0 - '@vue/devtools-shared': 8.0.0 - execa: 9.6.0 - sirv: 3.0.1 + '@vue/devtools-core': 8.0.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.2)) + '@vue/devtools-kit': 8.0.3 + '@vue/devtools-shared': 8.0.3 + sirv: 3.0.2 vite: 7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0) - vite-plugin-inspect: 11.3.2(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) + vite-plugin-inspect: 11.3.3(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@types/node@24.2.1)(jiti@1.21.7)(terser@5.37.0)(yaml@2.7.0)) transitivePeerDependencies: - '@nuxt/kit' @@ -15437,7 +15331,7 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.7.2) - vue-router@4.5.0(vue@3.5.13(typescript@5.7.2)): + vue-router@4.6.3(vue@3.5.13(typescript@5.7.2)): dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.7.2) @@ -15662,8 +15556,6 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors@2.1.1: {} - zen-observable-ts@0.8.19: dependencies: tslib: 2.8.1