diff --git a/package-lock.json b/package-lock.json index 218b16d..4f1a1e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@node-projects/css-parser": "^5.2.0", "@node-projects/lean-he-esm": "^3.4.1", "@node-projects/node-html-parser-esm": "^6.4.1", - "@node-projects/web-component-designer": "^0.1.339", + "@node-projects/web-component-designer": "^0.1.340", "@node-projects/web-component-designer-htmlparserservice-base-custom-webcomponent": "^0.1.5", "@node-projects/web-component-designer-htmlparserservice-nodehtmlparser": "^0.1.12", "@node-projects/web-component-designer-stylesheetservice-css-parser": "^0.1.4", @@ -983,9 +983,9 @@ } }, "node_modules/@node-projects/web-component-designer": { - "version": "0.1.339", - "resolved": "https://registry.npmjs.org/@node-projects/web-component-designer/-/web-component-designer-0.1.339.tgz", - "integrity": "sha512-xhD4QKDVYeIVO7aETiA0ywTTI3VvfOYy96bcqg2BFivW0U9QbSE6czosMTyJguPFLJ440Xlg8+R9RB+j5WYDfQ==", + "version": "0.1.340", + "resolved": "https://registry.npmjs.org/@node-projects/web-component-designer/-/web-component-designer-0.1.340.tgz", + "integrity": "sha512-B4ESeRQ+xE6S8O+sSdIvkQ3JnA6IMcCaOy0R5iiokx1nH5NwCR5EOfId1UeQkAuRTySbOo600572H9eU0XpV8A==", "license": "MIT", "dependencies": { "@node-projects/base-custom-webcomponent": ">=0.27.8" @@ -6040,9 +6040,9 @@ } }, "@node-projects/web-component-designer": { - "version": "0.1.339", - "resolved": "https://registry.npmjs.org/@node-projects/web-component-designer/-/web-component-designer-0.1.339.tgz", - "integrity": "sha512-xhD4QKDVYeIVO7aETiA0ywTTI3VvfOYy96bcqg2BFivW0U9QbSE6czosMTyJguPFLJ440Xlg8+R9RB+j5WYDfQ==", + "version": "0.1.340", + "resolved": "https://registry.npmjs.org/@node-projects/web-component-designer/-/web-component-designer-0.1.340.tgz", + "integrity": "sha512-B4ESeRQ+xE6S8O+sSdIvkQ3JnA6IMcCaOy0R5iiokx1nH5NwCR5EOfId1UeQkAuRTySbOo600572H9eU0XpV8A==", "requires": { "@node-projects/base-custom-webcomponent": ">=0.27.8" } diff --git a/package.json b/package.json index 57257d7..1573791 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,20 @@ "customEditorOutline" ], "contributes": { + "submenus": [ + { + "id": "designer.outline.editSubmenu", + "label": "Edit" + }, + { + "id": "designer.outline.modifySubmenu", + "label": "Modify" + }, + { + "id": "designer.outline.viewSubmenu", + "label": "View" + } + ], "menus": { "editor/title": [ { @@ -39,6 +53,114 @@ "when": "true", "group": "navigation" } + ], + "customEditor/outline/toolbar": [ + { + "command": "designer.outline.lock", + "group": "inline@1", + "when": "customEditorOutlineItem =~ /^(?!.*:locked)/" + }, + { + "command": "designer.outline.unlock", + "group": "inline@1", + "when": "customEditorOutlineItem =~ /:locked/" + }, + { + "command": "designer.outline.hideInDesigner", + "group": "inline@2", + "when": "customEditorOutlineItem =~ /^(?!.*:hideDesign)/" + }, + { + "command": "designer.outline.showInDesigner", + "group": "inline@2", + "when": "customEditorOutlineItem =~ /:hideDesign/" + }, + { + "command": "designer.outline.hideAtRuntime", + "group": "inline@3", + "when": "customEditorOutlineItem =~ /^(?!.*:hideRuntime)/" + }, + { + "command": "designer.outline.showAtRuntime", + "group": "inline@3", + "when": "customEditorOutlineItem =~ /:hideRuntime/" + } + ], + "customEditor/outline/context": [ + { + "submenu": "designer.outline.editSubmenu", + "group": "1_edit" + }, + { + "submenu": "designer.outline.modifySubmenu", + "group": "2_modify" + }, + { + "submenu": "designer.outline.viewSubmenu", + "group": "3_view" + }, + { + "command": "designer.outline.expandChildren", + "group": "4_expandCollapse@1" + }, + { + "command": "designer.outline.collapseChildren", + "group": "4_expandCollapse@2" + } + ], + "designer.outline.editSubmenu": [ + { + "command": "designer.outline.copy", + "group": "1_clipboard@1" + }, + { + "command": "designer.outline.cut", + "group": "1_clipboard@2" + }, + { + "command": "designer.outline.paste", + "group": "1_clipboard@3" + }, + { + "command": "designer.outline.delete", + "group": "2_delete@1" + } + ], + "designer.outline.modifySubmenu": [ + { + "command": "designer.outline.rotateLeft", + "group": "1_rotate@1" + }, + { + "command": "designer.outline.rotateRight", + "group": "1_rotate@2" + }, + { + "command": "designer.outline.toFront", + "group": "2_order@1" + }, + { + "command": "designer.outline.moveForward", + "group": "2_order@2" + }, + { + "command": "designer.outline.moveBackward", + "group": "2_order@3" + }, + { + "command": "designer.outline.toBack", + "group": "2_order@4" + } + ], + "designer.outline.viewSubmenu": [ + { + "command": "designer.outline.moveTo", + "group": "1_navigation@1" + }, + { + "command": "designer.outline.jumpTo", + "group": "1_navigation@2" + } ] }, "commands": [ @@ -49,6 +171,106 @@ "light": "resources/light/editor.svg", "dark": "resources/dark/editor.svg" } + }, + { + "command": "designer.outline.lock", + "title": "Lock", + "icon": "$(unlock)" + }, + { + "command": "designer.outline.unlock", + "title": "Unlock", + "icon": "$(lock)" + }, + { + "command": "designer.outline.hideInDesigner", + "title": "Hide in Designer", + "icon": "$(eye)" + }, + { + "command": "designer.outline.showInDesigner", + "title": "Show in Designer", + "icon": "$(eye-closed)" + }, + { + "command": "designer.outline.hideAtRuntime", + "title": "Hide at Runtime", + "icon": "$(eye)" + }, + { + "command": "designer.outline.showAtRuntime", + "title": "Show at Runtime", + "icon": "$(eye-closed)" + }, + { + "command": "designer.outline.copy", + "title": "Copy", + "icon": "$(copy)" + }, + { + "command": "designer.outline.cut", + "title": "Cut", + "icon": "$(scissors)" + }, + { + "command": "designer.outline.paste", + "title": "Paste", + "icon": "$(clippy)" + }, + { + "command": "designer.outline.delete", + "title": "Delete", + "icon": "$(trash)" + }, + { + "command": "designer.outline.rotateLeft", + "title": "Rotate Left", + "icon": "$(debug-reverse-continue)" + }, + { + "command": "designer.outline.rotateRight", + "title": "Rotate Right", + "icon": "$(debug-continue)" + }, + { + "command": "designer.outline.toFront", + "title": "To Front", + "icon": "$(arrow-circle-up)" + }, + { + "command": "designer.outline.moveForward", + "title": "Move Forward", + "icon": "$(arrow-up)" + }, + { + "command": "designer.outline.moveBackward", + "title": "Move Backward", + "icon": "$(arrow-down)" + }, + { + "command": "designer.outline.toBack", + "title": "To Back", + "icon": "$(arrow-circle-down)" + }, + { + "command": "designer.outline.moveTo", + "title": "Move To", + "icon": "$(move)" + }, + { + "command": "designer.outline.jumpTo", + "title": "Jump To", + "icon": "$(search)" + }, + { + "command": "designer.outline.expandChildren", + "title": "Expand Children", + "icon": "$(expand-all)" + }, + { + "command": "designer.outline.collapseChildren", + "title": "Collapse Children", + "icon": "$(collapse-all)" } ], "customEditors": [ @@ -112,7 +334,7 @@ "@node-projects/css-parser": "^5.2.0", "@node-projects/lean-he-esm": "^3.4.1", "@node-projects/node-html-parser-esm": "^6.4.1", - "@node-projects/web-component-designer": "^0.1.339", + "@node-projects/web-component-designer": "^0.1.340", "@node-projects/web-component-designer-htmlparserservice-base-custom-webcomponent": "^0.1.5", "@node-projects/web-component-designer-htmlparserservice-nodehtmlparser": "^0.1.12", "@node-projects/web-component-designer-stylesheetservice-css-parser": "^0.1.4", diff --git a/src/vscode/DesignerOutlineProvider.ts b/src/vscode/DesignerOutlineProvider.ts new file mode 100644 index 0000000..bd632d3 --- /dev/null +++ b/src/vscode/DesignerOutlineProvider.ts @@ -0,0 +1,73 @@ + import * as vscode from 'vscode'; + + export interface SerializedOutlineItem { + id: string; + label: string; + detail?: string; + icon?: string; + contextValue?: string; + children?: SerializedOutlineItem[]; + } + + function convertItems(items: SerializedOutlineItem[]): vscode.CustomEditorOutlineItem[] { + return items.map(item => ({ + id: item.id, + label: item.label, + detail: item.detail, + icon: item.icon ? new vscode.ThemeIcon(item.icon) : undefined, + contextValue: item.contextValue, + children: item.children ? convertItems(item.children) : undefined, + })); + } + + interface ResourceState { + items: vscode.CustomEditorOutlineItem[]; + webview: vscode.Webview; + } + + export class DesignerOutlineProvider implements vscode.CustomEditorOutlineProvider { + private readonly _onDidChangeOutline = new vscode.EventEmitter(); + readonly onDidChangeOutline = this._onDidChangeOutline.event; + + private readonly _onDidChangeActiveItem = new vscode.EventEmitter<{ uri: vscode.Uri; itemId: string | undefined }>(); + readonly onDidChangeActiveItem = this._onDidChangeActiveItem.event; + + private readonly _resources = new Map(); + + setWebview(resource: vscode.Uri, webview: vscode.Webview): void { + const state = this._resources.get(resource.toString()); + if (state) { + state.webview = webview; + } else { + this._resources.set(resource.toString(), { items: [], webview }); + } + } + + removeResource(resource: vscode.Uri): void { + this._resources.delete(resource.toString()); + } + + updateFromWebview(resource: vscode.Uri, serializedItems: SerializedOutlineItem[]): void { + const state = this._resources.get(resource.toString()); + if (state) { + state.items = convertItems(serializedItems); + } + this._onDidChangeOutline.fire(resource); + } + + setActive(resource: vscode.Uri, itemId: string | undefined): void { + this._onDidChangeActiveItem.fire({ uri: resource, itemId }); + } + + provideOutline(resource: vscode.Uri, _token: vscode.CancellationToken): vscode.CustomEditorOutlineItem[] { + return this._resources.get(resource.toString())?.items ?? []; + } + + revealItem(resource: vscode.Uri, itemId: string): void { + this._resources.get(resource.toString())?.webview?.postMessage({ type: 'reveal', id: itemId }); + } + + sendCommand(resource: vscode.Uri, command: string, itemId?: string): void { + this._resources.get(resource.toString())?.webview?.postMessage({ type: 'outlineCommand', command, id: itemId }); + } + } \ No newline at end of file diff --git a/src/vscode/DesignerTextEditor.ts b/src/vscode/DesignerTextEditor.ts index 9fe0944..1547234 100644 --- a/src/vscode/DesignerTextEditor.ts +++ b/src/vscode/DesignerTextEditor.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { DesignerOutlineProvider } from './DesignerOutlineProvider.js'; export function getNonce() { let text = ''; @@ -11,19 +12,21 @@ export function getNonce() { export class DesignerTextEditor implements vscode.CustomTextEditorProvider { - public static register(context: vscode.ExtensionContext): vscode.Disposable { - const provider = new DesignerTextEditor(context); + public static register(context: vscode.ExtensionContext): [vscode.Disposable[], DesignerOutlineProvider] { + let outlineProvider = new DesignerOutlineProvider(); + const provider = new DesignerTextEditor(context, outlineProvider); const providerRegistration = vscode.window.registerCustomEditorProvider(DesignerTextEditor.viewType, provider, { webviewOptions: { retainContextWhenHidden: true } }); - return providerRegistration; + const outlineRegistration = vscode.window.registerCustomEditorOutlineProvider('designer.designerTextEditor', outlineProvider) + return [[providerRegistration, outlineRegistration], outlineProvider]; } private static readonly viewType = 'designer.designerTextEditor'; - constructor(private readonly context: vscode.ExtensionContext) { } + constructor(private readonly context: vscode.ExtensionContext, private readonly outline: DesignerOutlineProvider) { } public async addCustomElementsJsons(webviewPanel: vscode.WebviewPanel) { //TODO: @@ -99,6 +102,7 @@ export class DesignerTextEditor implements vscode.CustomTextEditorProvider { webviewPanel.onDidDispose(() => { changeDocumentSubscription.dispose(); changeTextEditorSelection.dispose(); + this.outline.removeResource(document.uri); }); // Receive message from the webview. @@ -139,8 +143,16 @@ export class DesignerTextEditor implements vscode.CustomTextEditorProvider { } } return; + case 'outlineData': + this.outline.updateFromWebview(document.uri, e.items); + return; + case 'outlineActiveItem': + this.outline.setActive(document.uri, e.id); + return; } }); + + this.outline.setWebview(document.uri, webviewPanel.webview); } /** diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index d7fbf9a..20fd068 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -1,12 +1,52 @@ import * as vscode from 'vscode'; import { DesignerTextEditor } from './DesignerTextEditor.js'; +const outlineCommands: Record = { + 'designer.outline.lock': 'toggleLock', + 'designer.outline.unlock': 'toggleLock', + 'designer.outline.hideInDesigner': 'toggleHideInDesigner', + 'designer.outline.showInDesigner': 'toggleHideInDesigner', + 'designer.outline.hideAtRuntime': 'toggleHideAtRuntime', + 'designer.outline.showAtRuntime': 'toggleHideAtRuntime', + 'designer.outline.copy': 'copy', + 'designer.outline.cut': 'cut', + 'designer.outline.paste': 'paste', + 'designer.outline.delete': 'delete', + 'designer.outline.rotateLeft': 'rotateLeft', + 'designer.outline.rotateRight': 'rotateRight', + 'designer.outline.toFront': 'toFront', + 'designer.outline.moveForward': 'moveForward', + 'designer.outline.moveBackward': 'moveBackward', + 'designer.outline.toBack': 'toBack', + 'designer.outline.moveTo': 'moveTo', + 'designer.outline.jumpTo': 'jumpTo', +}; + export function activate(context: vscode.ExtensionContext) { - //context.subscriptions.push( - // vscode.window.registerWebviewViewProvider(ColorsViewProvider.viewType, provider)); - context.subscriptions.push(DesignerTextEditor.register(context)); + const [registrations, outlineProvider] = DesignerTextEditor.register(context); + context.subscriptions.push(...registrations); vscode.commands.registerCommand('designer.openInDesignerTextEditor', (uri: vscode.Uri) => { vscode.commands.executeCommand('vscode.openWith', uri, 'designer.designerTextEditor'); }); + + for (const [cmd, action] of Object.entries(outlineCommands)) { + context.subscriptions.push( + vscode.commands.registerCommand(cmd, (item?: { id?: string; uri?: vscode.Uri }) => { + if (item?.uri) { + outlineProvider.sendCommand(item.uri, action, item.id); + } + }) + ); + } + + // Expand/collapse operate directly on the VS Code outline tree view + context.subscriptions.push( + vscode.commands.registerCommand('designer.outline.expandChildren', () => { + vscode.commands.executeCommand('list.expandRecursively'); + }), + vscode.commands.registerCommand('designer.outline.collapseChildren', () => { + vscode.commands.executeCommand('list.collapseAll'); + }) + ); } diff --git a/src/vscode/vscode.proposed.customEditorOutline.d.ts b/src/vscode/vscode.proposed.customEditorOutline.d.ts new file mode 100644 index 0000000..ce4eeca --- /dev/null +++ b/src/vscode/vscode.proposed.customEditorOutline.d.ts @@ -0,0 +1,23 @@ +declare module 'vscode' { + + export interface CustomEditorOutlineItem { + readonly id: string; + readonly label: string; + readonly detail?: string; + readonly tooltip?: string; + readonly icon?: ThemeIcon; + readonly contextValue?: string; + readonly children?: CustomEditorOutlineItem[]; + } + + export interface CustomEditorOutlineProvider { + readonly onDidChangeOutline: Event; + readonly onDidChangeActiveItem: Event; + provideOutline(token: CancellationToken): ProviderResult; + revealItem(itemId: string): void; + } + + export namespace window { + export function registerCustomEditorOutlineProvider(viewType: string, provider: CustomEditorOutlineProvider): Disposable; + } +} diff --git a/src/webview/designer.ts b/src/webview/designer.ts index f66d8ac..06269f5 100644 --- a/src/webview/designer.ts +++ b/src/webview/designer.ts @@ -11,11 +11,102 @@ if (!window.CSSContainerRule) window.CSSContainerRule = class { } import { DomHelper } from '@node-projects/base-custom-webcomponent'; -import { DesignerView, IDesignItem, PaletteView, PreDefinedElementsService, PropertyGrid, WebcomponentManifestElementsService, WebcomponentManifestPropertiesService } from '@node-projects/web-component-designer'; +import { CommandType, DesignerView, IDesignItem, NodeType, PaletteView, PreDefinedElementsService, PropertyGrid, WebcomponentManifestElementsService, WebcomponentManifestPropertiesService } from '@node-projects/web-component-designer'; import createDefaultServiceContainer from '@node-projects/web-component-designer/dist/elements/services/DefaultServiceBootstrap.js'; import { DesignerHtmlParserAndWriterService } from './DesignerHtmlParserAndWriterService.js'; import { CssParserStylesheetService } from '@node-projects/web-component-designer-stylesheetservice-css-parser'; +// --- Outline tree serialization --- + +interface SerializedOutlineItem { + id: string; + label: string; + detail?: string; + icon?: string; + contextValue?: string; + children?: SerializedOutlineItem[]; +} + +let nextOutlineId = 0; +const elementToOutlineId = new WeakMap(); +const outlineIdToDesignItem = new Map(); + +function getOutlineId(item: IDesignItem): string { + let id = elementToOutlineId.get(item.element); + if (!id) { + id = `el-${nextOutlineId++}`; + elementToOutlineId.set(item.element, id); + } + return id; +} + +function buildOutlineTree(item: IDesignItem): SerializedOutlineItem | null { + if (item.isEmptyTextNode) return null; + + const id = getOutlineId(item); + outlineIdToDesignItem.set(id, item); + + const children: SerializedOutlineItem[] = []; + if (item.hasChildren) { + for (const child of item.children()) { + const c = buildOutlineTree(child); + if (c) children.push(c); + } + } + + let label: string; + let icon: string; + let detail: string | undefined; + + if (item.nodeType === NodeType.TextNode) { + label = '#text'; + detail = item.content?.substring(0, 60); + icon = 'symbol-string'; + } else if (item.nodeType === NodeType.Comment) { + label = '#comment'; + detail = item.content?.substring(0, 60); + icon = 'comment'; + } else { + label = item.name; + if (item.id) detail = '#' + item.id; + icon = item.isRootItem ? 'layout' : 'symbol-class'; + } + + let contextValue = 'element'; + if (item.lockAtDesignTime) contextValue += ':locked'; + if (item.hideAtDesignTime) contextValue += ':hideDesign'; + if (item.hideAtRunTime) contextValue += ':hideRuntime'; + + return { + id, + label, + detail, + icon, + contextValue, + children: children.length > 0 ? children : undefined, + }; +} + +function sendOutlineData(rootItem: IDesignItem) { + outlineIdToDesignItem.clear(); + const tree = buildOutlineTree(rootItem); + vscode.postMessage({ + type: 'outlineData', + items: tree ? [tree] : [] + }); +} + +function sendActiveOutlineItem(selectedElements: IDesignItem[]) { + const primary = selectedElements.length > 0 ? selectedElements[0] : undefined; + const id = primary ? elementToOutlineId.get(primary.element) : undefined; + vscode.postMessage({ + type: 'outlineActiveItem', + id: id ?? undefined + }); +} + +// --- End outline helpers --- + await window.customElements.whenDefined("node-projects-designer-view"); const designerView = document.querySelector("node-projects-designer-view"); const propertyGrid = document.getElementById("propertyGrid"); @@ -62,6 +153,8 @@ async function parseHTML(html: string) { } let parsing = true; +let revealFromOutline = false; + window.addEventListener('message', async event => { const message = event.data; switch (message.type) { @@ -70,6 +163,7 @@ window.addEventListener('message', async event => { designerHtmlParserService.filename = message.filename; await parseHTML(message.text); parsing = false; + sendOutlineData(designerView.designerCanvas.rootDesignItem); break; case 'changeSelection': const pos = message.position; @@ -89,15 +183,126 @@ window.addEventListener('message', async event => { } paletteView.loadControls(serviceContainer, serviceContainer.elementsServices); break; + case 'reveal': { + const designItem = outlineIdToDesignItem.get(message.id); + if (designItem) { + revealFromOutline = true; + designerView.instanceServiceContainer.selectionService.setSelectedElements([designItem]); + setTimeout(() => { revealFromOutline = false; }, 50); + } + break; + } + case 'outlineCommand': { + handleOutlineCommand(message.command, message.id); + break; + } } }); +function handleOutlineCommand(command: string, outlineItemId?: string) { + let selectedElements: IDesignItem[]; + if (outlineItemId) { + const item = outlineIdToDesignItem.get(outlineItemId); + if (!item) return; + selectedElements = [item]; + } else { + selectedElements = [...designerView.instanceServiceContainer.selectionService.selectedElements]; + } + if (selectedElements.length === 0) return; + + const modelCommandService = designerView.serviceContainer.modelCommandService; + switch (command) { + case 'toggleLock': + for (const item of selectedElements) { + item.lockAtDesignTime = !item.lockAtDesignTime; + } + sendOutlineData(designerView.designerCanvas.rootDesignItem); + break; + case 'toggleHideInDesigner': + for (const item of selectedElements) { + item.hideAtDesignTime = !item.hideAtDesignTime; + } + sendOutlineData(designerView.designerCanvas.rootDesignItem); + break; + case 'toggleHideAtRuntime': + for (const item of selectedElements) { + item.hideAtRunTime = !item.hideAtRunTime; + } + sendOutlineData(designerView.designerCanvas.rootDesignItem); + break; + case 'copy': + designerView.executeCommand({ type: CommandType.copy }); + break; + case 'cut': + designerView.executeCommand({ type: CommandType.cut }); + break; + case 'paste': + designerView.executeCommand({ type: CommandType.paste }); + break; + case 'delete': + designerView.executeCommand({ type: CommandType.delete }); + break; + case 'rotateLeft': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.rotateCounterClockwise }, selectedElements); + break; + case 'rotateRight': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.rotateClockwise }, selectedElements); + break; + case 'toFront': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveToFront }, selectedElements); + break; + case 'moveForward': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveForward }, selectedElements); + break; + case 'moveBackward': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveBackward }, selectedElements); + break; + case 'toBack': + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveToBack }, selectedElements); + break; + case 'moveTo': { + const item = selectedElements[0]; + const coord = designerView.designerCanvas.getNormalizedElementCoordinates(item.element); + designerView.designerCanvas.zoomPoint( + { x: coord.x + coord.width / 2, y: coord.y + coord.height / 2 }, + designerView.designerCanvas.zoomFactor + ); + break; + } + case 'jumpTo': { + const item = selectedElements[0]; + const offset = 10; + const coord = designerView.designerCanvas.getNormalizedElementCoordinates(item.element); + const startPoint = { x: coord.x - offset, y: coord.y - offset }; + const endPoint = { x: coord.x + coord.width + offset, y: coord.y + coord.height + offset }; + const rect = { + x: Math.min(startPoint.x, endPoint.x), + y: Math.min(startPoint.y, endPoint.y), + width: Math.abs(startPoint.x - endPoint.x), + height: Math.abs(startPoint.y - endPoint.y), + }; + const zFactorWidth = designerView.designerCanvas.outerRect.width / rect.width; + const zFactorHeight = designerView.designerCanvas.outerRect.height / rect.height; + const zoomFactor = Math.min(zFactorWidth, zFactorHeight); + designerView.designerCanvas.zoomPoint( + { x: coord.x + coord.width / 2, y: coord.y + coord.height / 2 }, + zoomFactor + ); + break; + } + } +} + designerView.instanceServiceContainer.selectionService.onSelectionChanged.on(() => { let primarySelection = designerView.instanceServiceContainer.selectionService.primarySelection; if (primarySelection) { const selectionPosition = designerView.instanceServiceContainer.designItemDocumentPositionService.getPosition(primarySelection); vscode.postMessage({ type: 'setSelection', position: selectionPosition }); } + // Sync selection to outline (skip if the selection came from the outline itself) + if (!revealFromOutline) { + sendActiveOutlineItem([...designerView.instanceServiceContainer.selectionService.selectedElements]); + } }); /*designerView.instanceServiceContainer.stylesheetService.stylesheetChanged.on((event) => { console.log(event); @@ -112,6 +317,8 @@ designerView.designerCanvas.onContentChanged.on(() => { } code = designerHtmlParserService.write(code, css); vscode.postMessage({ type: 'updateDocument', code: code }); + // Rebuild outline tree after content changes + sendOutlineData(designerView.designerCanvas.rootDesignItem); } })