diff --git a/editor/src/components/custom-code/code-file.ts b/editor/src/components/custom-code/code-file.ts index c658747bb47a..2c808e9876e8 100644 --- a/editor/src/components/custom-code/code-file.ts +++ b/editor/src/components/custom-code/code-file.ts @@ -53,6 +53,7 @@ import { getProjectFileByFilePath } from '../assets' import type { EditorDispatch } from '../editor/action-types' import { StylingOptions } from 'utopia-api' import type { Emphasis, Focus, Icon, Styling } from 'utopia-api' +import type { Bounds } from 'utopia-vscode-common' type ModuleExportTypes = { [name: string]: ExportType } @@ -227,14 +228,17 @@ export function isDefaultComponentDescriptor( export interface ComponentDescriptorFromDescriptorFile { type: 'DESCRIPTOR_FILE' sourceDescriptorFile: string + bounds: Bounds | null } export function componentDescriptorFromDescriptorFile( sourceDescriptorFile: string, + bounds: Bounds | null, ): ComponentDescriptorFromDescriptorFile { return { type: 'DESCRIPTOR_FILE', sourceDescriptorFile: sourceDescriptorFile, + bounds: bounds, } } diff --git a/editor/src/components/editor/action-types.ts b/editor/src/components/editor/action-types.ts index a50f7c246861..2929cab3cc6c 100644 --- a/editor/src/components/editor/action-types.ts +++ b/editor/src/components/editor/action-types.ts @@ -81,6 +81,7 @@ import type { MapLike } from 'typescript' import type { CommentFilterMode } from '../inspector/sections/comment-section' import type { Collaborator } from '../../core/shared/multiplayer' import type { PageTemplate } from '../canvas/remix/remix-utils' +import type { Bounds } from 'utopia-vscode-common' export { isLoggedIn, loggedInUser, notLoggedIn } from '../../common/user' export type { LoginState, UserDetails } from '../../common/user' @@ -644,6 +645,7 @@ export interface OpenCodeEditorFile { action: 'OPEN_CODE_EDITOR_FILE' filename: string forceShowCodeEditor: boolean + bounds: Bounds | null } export interface CloseDesignerFile { diff --git a/editor/src/components/editor/actions/action-creators.ts b/editor/src/components/editor/actions/action-creators.ts index 219231d9b2f4..65994460fbc7 100644 --- a/editor/src/components/editor/actions/action-creators.ts +++ b/editor/src/components/editor/actions/action-creators.ts @@ -265,6 +265,7 @@ import type { SetHuggingParentToFixed } from '../../canvas/canvas-strategies/str import type { CommentFilterMode } from '../../inspector/sections/comment-section' import type { Collaborator } from '../../../core/shared/multiplayer' import type { PageTemplate } from '../../canvas/remix/remix-utils' +import type { Bounds } from 'utopia-vscode-common' export function clearSelection(): EditorAction { return { @@ -1055,11 +1056,13 @@ export function addFolder(parentPath: string, fileName: string): AddFolder { export function openCodeEditorFile( filename: string, forceShowCodeEditor: boolean, + bounds: Bounds | null = null, ): OpenCodeEditorFile { return { action: 'OPEN_CODE_EDITOR_FILE', filename: filename, forceShowCodeEditor: forceShowCodeEditor, + bounds: bounds, } } diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 14c2163a97fb..a4b1b6df8112 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -427,7 +427,7 @@ import { import { loadStoredState } from '../stored-state' import { applyMigrations } from './migrations/migrations' -import { defaultConfig } from 'utopia-vscode-common' +import { boundsInFile, defaultConfig } from 'utopia-vscode-common' import { reorderElement } from '../../../components/canvas/commands/reorder-element-command' import type { BuiltInDependencies } from '../../../core/es-modules/package-manager/built-in-dependencies-list' import { fetchNodeModules } from '../../../core/es-modules/package-manager/fetch-packages' @@ -593,7 +593,7 @@ import { getPrintAndReparseCodeResult, } from '../../../core/workers/parser-printer/parser-printer-worker' import { isSteganographyEnabled } from '../../../core/shared/stegano-text' -import type { ParsedTextFileWithPath } from '../../../core/property-controls/property-controls-local' +import type { TextFileContentsWithPath } from '../../../core/property-controls/property-controls-local' import { updatePropertyControlsOnDescriptorFileDelete, isComponentDescriptorFile, @@ -3795,7 +3795,7 @@ export const UPDATE_FNS = { }, OPEN_CODE_EDITOR_FILE: (action: OpenCodeEditorFile, editor: EditorModel): EditorModel => { // Side effect. - sendOpenFileMessage(action.filename) + sendOpenFileMessage(action.filename, action.bounds) if (action.forceShowCodeEditor) { return { ...editor, @@ -6073,11 +6073,11 @@ export const UPDATE_FNS = { ): EditorModel => { const evaluator = createModuleEvaluator(state) - const filesToUpdate: ParsedTextFileWithPath[] = [] + const filesToUpdate: TextFileContentsWithPath[] = [] for (const filePath of action.paths) { const file = getProjectFileByFilePath(state.projectContents, filePath) if (file != null && file.type === 'TEXT_FILE') { - filesToUpdate.push({ path: filePath, file: file.fileContents.parsed }) + filesToUpdate.push({ path: filePath, file: file.fileContents }) } } diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index a88058efe598..edacb0a066e1 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -548,7 +548,7 @@ import type { import { fontSettings } from '../../inspector/common/css-utils' import type { ElementPaste, ProjectListing } from '../action-types' import { projectListing } from '../action-types' -import type { UtopiaVSCodeConfig } from 'utopia-vscode-common' +import type { Bounds, UtopiaVSCodeConfig } from 'utopia-vscode-common' import type { MouseButtonsPressed } from '../../../utils/mouse' import { assertNever } from '../../../core/shared/utils' import type { @@ -3129,6 +3129,23 @@ export const HighlightBoundsKeepDeepEquality: KeepDeepEqualityCall = combine4EqualityCalls( + (bounds) => bounds.startLine, + NumberKeepDeepEquality, + (bounds) => bounds.startCol, + NumberKeepDeepEquality, + (bounds) => bounds.endLine, + NumberKeepDeepEquality, + (bounds) => bounds.endCol, + NumberKeepDeepEquality, + (startLine, startCol, endLine, endCol) => ({ + startLine, + startCol, + endLine, + endCol, + }), +) + export const HighlightBoundsForUidsKeepDeepEquality: KeepDeepEqualityCall = objectDeepEquality(HighlightBoundsKeepDeepEquality) @@ -3484,12 +3501,15 @@ const PreferredChildComponentDescriptorKeepDeepEquality: KeepDeepEqualityCall = - combine2EqualityCalls( + combine3EqualityCalls( (descriptor) => descriptor.type, StringKeepDeepEquality, (descriptor) => descriptor.sourceDescriptorFile, StringKeepDeepEquality, - componentDescriptorFromDescriptorFile, + (descriptor) => descriptor.bounds, + nullableDeepEquality(BoundsKeepDeepEquality), + (_, sourceDescriptorFile, lineNumber) => + componentDescriptorFromDescriptorFile(sourceDescriptorFile, lineNumber), ) export function ComponentDescriptorSourceKeepDeepEquality(): KeepDeepEqualityCall { diff --git a/editor/src/components/inspector/sections/component-section/component-section.tsx b/editor/src/components/inspector/sections/component-section/component-section.tsx index e6a61639c46c..682b5e0243d5 100644 --- a/editor/src/components/inspector/sections/component-section/component-section.tsx +++ b/editor/src/components/inspector/sections/component-section/component-section.tsx @@ -1469,9 +1469,7 @@ export const ComponentSectionInner = React.memo((props: ComponentSectionProps) = ) const descriptorFile = - registeredComponent?.source.type === 'DESCRIPTOR_FILE' - ? registeredComponent?.source.sourceDescriptorFile - : null + registeredComponent?.source.type === 'DESCRIPTOR_FILE' ? registeredComponent.source : null if (registeredComponent?.label == null) { return { @@ -1493,7 +1491,13 @@ export const ComponentSectionInner = React.memo((props: ComponentSectionProps) = const openDescriptorFile = React.useCallback(() => { if (componentData?.descriptorFile != null) { - dispatch([openCodeEditorFile(componentData?.descriptorFile, true)]) + dispatch([ + openCodeEditorFile( + componentData.descriptorFile.sourceDescriptorFile, + true, + componentData.descriptorFile.bounds, + ), + ]) } }, [dispatch, componentData?.descriptorFile]) diff --git a/editor/src/core/property-controls/component-descriptor-parser.ts b/editor/src/core/property-controls/component-descriptor-parser.ts new file mode 100644 index 000000000000..f6551afd86de --- /dev/null +++ b/editor/src/core/property-controls/component-descriptor-parser.ts @@ -0,0 +1,89 @@ +import * as BabelParser from '@babel/parser' +import traverse from '@babel/traverse' +import type { Bounds } from 'utopia-vscode-common' +import type { TextFileContentsWithPath } from './property-controls-local' + +export type ComponentBoundsByModule = { + [moduleName: string]: { [componentName: string]: Bounds } +} + +export function generateComponentBounds( + descriptorFile: TextFileContentsWithPath, +): ComponentBoundsByModule { + const ast = BabelParser.parse(descriptorFile.file.code, { + sourceType: 'module', + plugins: ['jsx'], + }) + + const componentBoundsByModule: ComponentBoundsByModule = {} + + // Store variable declarations + const variableDeclarations: Record = {} + // Traverse the AST to collect variable declarations + traverse(ast, { + VariableDeclarator(path) { + const { id, init } = path.node + if (id.type === 'Identifier' && init != null) { + variableDeclarations[id.name] = init + } + }, + }) + + // Function to resolve a variable to its object value + function resolveVariable(node: any): any { + if (node.type === 'Identifier' && variableDeclarations[node.name] != null) { + return variableDeclarations[node.name] + } + return node + } + + // Traverse the AST to find the default export and process the Components object + traverse(ast, { + ExportDefaultDeclaration(path) { + const declaration = path.node.declaration + if (declaration.type !== 'Identifier') { + return + } + + const variableName = declaration.name + const componentsNode = variableDeclarations[variableName] + if (componentsNode?.type !== 'ObjectExpression') { + return + } + + componentsNode.properties.forEach((module: any) => { + if (module.type !== 'ObjectProperty' || module.key.type !== 'StringLiteral') { + return + } + + const moduleName = module.key.value + if (componentBoundsByModule[moduleName] == null) { + componentBoundsByModule[moduleName] = {} + } + + const moduleValue = resolveVariable(module.value) + if (moduleValue.type !== 'ObjectExpression') { + return + } + + moduleValue.properties.forEach((component: any) => { + if (component.type !== 'ObjectProperty' || component.key.type !== 'Identifier') { + return + } + + const componentName = component.key.name + const { loc } = component + if (loc != null) { + componentBoundsByModule[moduleName][componentName] = { + startLine: loc.start.line, + startCol: loc.start.column, + endLine: loc.end.line, + endCol: loc.end.column, + } + } + }) + }) + }, + }) + return componentBoundsByModule +} diff --git a/editor/src/core/property-controls/property-controls-local.spec.tsx b/editor/src/core/property-controls/property-controls-local.spec.tsx index 381d7d97fcf2..f337b0da4261 100644 --- a/editor/src/core/property-controls/property-controls-local.spec.tsx +++ b/editor/src/core/property-controls/property-controls-local.spec.tsx @@ -141,6 +141,12 @@ describe('registered property controls', () => { }, }, "source": Object { + "bounds": Object { + "endCol": 5, + "endLine": 42, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components.utopia.js", "type": "DESCRIPTOR_FILE", }, @@ -287,6 +293,7 @@ describe('registered property controls', () => { }, }, "source": Object { + "bounds": null, "sourceDescriptorFile": "/utopia/components.utopia.js", "type": "DESCRIPTOR_FILE", }, @@ -370,6 +377,12 @@ describe('registered property controls', () => { }, }, "source": Object { + "bounds": Object { + "endCol": 5, + "endLine": 16, + "startCol": 4, + "startLine": 6, + }, "sourceDescriptorFile": "/utopia/components.utopia.js", "type": "DESCRIPTOR_FILE", }, @@ -997,6 +1010,12 @@ describe('registered property controls', () => { }, }, "source": Object { + "bounds": Object { + "endCol": 5, + "endLine": 32, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components.utopia.js", "type": "DESCRIPTOR_FILE", }, @@ -1911,6 +1930,12 @@ describe('registered property controls', () => { }, }, "source": Object { + "bounds": Object { + "endCol": 5, + "endLine": 34, + "startCol": 4, + "startLine": 6, + }, "sourceDescriptorFile": "/utopia/components.utopia.js", "type": "DESCRIPTOR_FILE", }, @@ -2662,6 +2687,12 @@ describe('Lifecycle management of registering components', () => { expect(renderResult.getEditorState().editor.propertyControlsInfo['/src/card']['Card'].source) .toMatchInlineSnapshot(` Object { + "bounds": Object { + "endCol": 5, + "endLine": 12, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components1.utopia.js", "type": "DESCRIPTOR_FILE", } @@ -2673,6 +2704,12 @@ describe('Lifecycle management of registering components', () => { expect(renderResult.getEditorState().editor.propertyControlsInfo['/src/card']['Card'].source) .toMatchInlineSnapshot(` Object { + "bounds": Object { + "endCol": 5, + "endLine": 12, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components2.utopia.js", "type": "DESCRIPTOR_FILE", } @@ -2708,6 +2745,12 @@ describe('Lifecycle management of registering components', () => { expect(renderResult.getEditorState().editor.propertyControlsInfo['/src/card']['Card'].source) .toMatchInlineSnapshot(` Object { + "bounds": Object { + "endCol": 5, + "endLine": 12, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components1.utopia.js", "type": "DESCRIPTOR_FILE", } @@ -2753,6 +2796,12 @@ describe('Lifecycle management of registering components', () => { expect(renderResult.getEditorState().editor.propertyControlsInfo['/src/card']['Card'].source) .toMatchInlineSnapshot(` Object { + "bounds": Object { + "endCol": 5, + "endLine": 12, + "startCol": 4, + "startLine": 5, + }, "sourceDescriptorFile": "/utopia/components1.utopia.js", "type": "DESCRIPTOR_FILE", } diff --git a/editor/src/core/property-controls/property-controls-local.ts b/editor/src/core/property-controls/property-controls-local.ts index 340f7cf073b3..26c7f1324af6 100644 --- a/editor/src/core/property-controls/property-controls-local.ts +++ b/editor/src/core/property-controls/property-controls-local.ts @@ -77,7 +77,12 @@ import { sequenceEither, } from '../shared/either' import { assertNever } from '../shared/utils' -import type { Imports, ParsedTextFile } from '../shared/project-file-types' +import type { + Imports, + ParsedTextFile, + TextFile, + TextFileContents, +} from '../shared/project-file-types' import { importAlias, importDetails, @@ -117,7 +122,7 @@ import type { ScriptLine } from '../../third-party/react-error-overlay/utils/sta import { intrinsicHTMLElementNamesAsStrings } from '../shared/dom-utils' import { valueOrArrayToArray } from '../shared/array-utils' import { optionalMap } from '../shared/optional-utils' -import type { RenderContext } from 'src/components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils' +import { generateComponentBounds } from './component-descriptor-parser' const exportedNameSymbol = Symbol('__utopia__exportedName') const moduleNameSymbol = Symbol('__utopia__moduleName') @@ -346,27 +351,31 @@ function isComponentRegistrationValid( } async function getComponentDescriptorPromisesFromParseResult( - descriptorFile: ParsedTextFileWithPath, + descriptorFile: TextFileContentsWithPath, workers: UtopiaTsWorkers, evaluator: ModuleEvaluator, ): Promise { - if (descriptorFile.file.type === 'UNPARSED') { + if (descriptorFile.file.parsed.type === 'UNPARSED') { return { descriptors: [], errors: [{ type: 'file-unparsed' }] } } - if (descriptorFile.file.type === 'PARSE_FAILURE') { + if (descriptorFile.file.parsed.type === 'PARSE_FAILURE') { return { descriptors: [], errors: [ - { type: 'file-parse-failure', parseErrorMessages: descriptorFile.file.errorMessages }, + { + type: 'file-parse-failure', + parseErrorMessages: descriptorFile.file.parsed.errorMessages, + }, ], } } - const exportDefaultIdentifier = descriptorFile.file.exportsDetail.find(isExportDefault) + const exportDefaultIdentifier = descriptorFile.file.parsed.exportsDetail.find(isExportDefault) if (exportDefaultIdentifier?.name == null) { return { descriptors: [], errors: [{ type: 'no-export-default' }] } } + const componentBoundsByModule = generateComponentBounds(descriptorFile) try { const evaluatedFile = evaluator(descriptorFile.path) @@ -412,7 +421,10 @@ async function getComponentDescriptorPromisesFromParseResult( componentName, moduleName, workers, - componentDescriptorFromDescriptorFile(descriptorFile.path), + componentDescriptorFromDescriptorFile( + descriptorFile.path, + componentBoundsByModule[moduleName][componentName] ?? null, + ), ) switch (componentDescriptor.type) { @@ -522,14 +534,14 @@ function errorsFromComponentRegistration( }) } -export interface ParsedTextFileWithPath { - file: ParsedTextFile +export interface TextFileContentsWithPath { + file: TextFileContents path: string } export async function maybeUpdatePropertyControls( previousPropertyControlsInfo: PropertyControlsInfo, - filesToUpdate: ParsedTextFileWithPath[], + filesToUpdate: TextFileContentsWithPath[], workers: UtopiaTsWorkers, dispatch: EditorDispatch, evaluator: ModuleEvaluator, diff --git a/editor/src/core/property-controls/property-controls-utils.spec.ts b/editor/src/core/property-controls/property-controls-utils.spec.ts index 8c92402e72e8..6e45db318b47 100644 --- a/editor/src/core/property-controls/property-controls-utils.spec.ts +++ b/editor/src/core/property-controls/property-controls-utils.spec.ts @@ -106,7 +106,7 @@ export const App = (props) => { supportsChildren: true, preferredChildComponents: [], variants: [], - source: componentDescriptorFromDescriptorFile('/components.utopia.js'), + source: componentDescriptorFromDescriptorFile('/components.utopia.js', null), focus: 'default', inspector: { type: 'hidden' }, emphasis: 'regular', diff --git a/editor/src/core/vscode/vscode-bridge.ts b/editor/src/core/vscode/vscode-bridge.ts index d8a2fb266c91..51cacfae677c 100644 --- a/editor/src/core/vscode/vscode-bridge.ts +++ b/editor/src/core/vscode/vscode-bridge.ts @@ -6,6 +6,7 @@ import type { SelectedElementChanged, UpdateDecorationsMessage, ForceNavigation, + Bounds, } from 'utopia-vscode-common' import { boundsInFile, @@ -177,8 +178,8 @@ export function sendMessage(message: FromUtopiaToVSCodeMessage) { vscodeIFrame?.postMessage(message, { targetOrigin: '*' }) } -export function sendOpenFileMessage(filePath: string) { - sendMessage(toVSCodeExtensionMessage(openFileMessage(filePath))) +export function sendOpenFileMessage(filePath: string, bounds: Bounds | null) { + sendMessage(toVSCodeExtensionMessage(openFileMessage(filePath, bounds))) } export function sendSetFollowSelectionEnabledMessage(enabled: boolean) { diff --git a/utopia-vscode-common/src/messages.ts b/utopia-vscode-common/src/messages.ts index 67e8fefacd4f..7d5528c07287 100644 --- a/utopia-vscode-common/src/messages.ts +++ b/utopia-vscode-common/src/messages.ts @@ -3,12 +3,14 @@ import type { UtopiaVSCodeConfig } from './utopia-vscode-config' export interface OpenFileMessage { type: 'OPEN_FILE' filePath: string + bounds: Bounds | null } -export function openFileMessage(filePath: string): OpenFileMessage { +export function openFileMessage(filePath: string, bounds: Bounds | null): OpenFileMessage { return { type: 'OPEN_FILE', filePath: filePath, + bounds: bounds, } } diff --git a/utopia-vscode-common/src/vscode-communication.ts b/utopia-vscode-common/src/vscode-communication.ts index 9945d568e489..d836a93b90f6 100644 --- a/utopia-vscode-common/src/vscode-communication.ts +++ b/utopia-vscode-common/src/vscode-communication.ts @@ -138,7 +138,7 @@ async function initIndexedDBBridge( await sendMessage(getUtopiaVSCodeConfig()) watchForChanges() if (openFilePath != null) { - await sendMessage(openFileMessage(openFilePath)) + await sendMessage(openFileMessage(openFilePath, null)) } else { window.top?.postMessage(fromVSCodeExtensionMessage(clearLoadingScreen()), '*') } diff --git a/utopia-vscode-extension/src/extension.ts b/utopia-vscode-extension/src/extension.ts index 549225e6a405..e64fe6b70836 100644 --- a/utopia-vscode-extension/src/extension.ts +++ b/utopia-vscode-extension/src/extension.ts @@ -338,7 +338,11 @@ function initMessaging(context: vscode.ExtensionContext, workspaceRootUri: vscod function handleMessage(message: ToVSCodeMessage): void { switch (message.type) { case 'OPEN_FILE': - openFile(vscode.Uri.joinPath(workspaceRootUri, message.filePath)) + if (message.bounds != null) { + revealRangeIfPossible(workspaceRootUri, { ...message.bounds, filePath: message.filePath }) + } else { + openFile(vscode.Uri.joinPath(workspaceRootUri, message.filePath)) + } break case 'UPDATE_DECORATIONS': currentDecorations = message.decorations