From b705635741750e05abb6ee801ac80bc35e055de8 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 25 Mar 2026 22:24:06 +0100 Subject: [PATCH 1/5] implementation of the new outline provider --- package.json | 180 +++++++++++++++++- src/vscode/DesignerOutlineProvider.ts | 57 ++++++ src/vscode/DesignerTextEditor.ts | 19 +- src/vscode/extension.ts | 34 +++- .../vscode.proposed.customEditorOutline.d.ts | 23 +++ src/webview/designer.ts | 148 +++++++++++++- 6 files changed, 452 insertions(+), 9 deletions(-) create mode 100644 src/vscode/DesignerOutlineProvider.ts create mode 100644 src/vscode/vscode.proposed.customEditorOutline.d.ts diff --git a/package.json b/package.json index 57257d7..9d32ba6 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,99 @@ "when": "true", "group": "navigation" } + ], + "customEditor/outline/toolbar": [ + { + "command": "designer.outline.toggleLock", + "group": "inline", + "when": "true" + }, + { + "command": "designer.outline.toggleHideInDesigner", + "group": "inline", + "when": "true" + }, + { + "command": "designer.outline.toggleHideAtRuntime", + "group": "inline", + "when": "true" + } + ], + "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 +156,77 @@ "light": "resources/light/editor.svg", "dark": "resources/dark/editor.svg" } + }, + { + "command": "designer.outline.toggleLock", + "title": "Toggle Lock", + "icon": "$(lock)" + }, + { + "command": "designer.outline.toggleHideInDesigner", + "title": "Toggle Hide in Designer", + "icon": "$(eye)" + }, + { + "command": "designer.outline.toggleHideAtRuntime", + "title": "Toggle Hide at Runtime", + "icon": "$(eye-closed)" + }, + { + "command": "designer.outline.copy", + "title": "Copy" + }, + { + "command": "designer.outline.cut", + "title": "Cut" + }, + { + "command": "designer.outline.paste", + "title": "Paste" + }, + { + "command": "designer.outline.delete", + "title": "Delete" + }, + { + "command": "designer.outline.rotateLeft", + "title": "Rotate Left" + }, + { + "command": "designer.outline.rotateRight", + "title": "Rotate Right" + }, + { + "command": "designer.outline.toFront", + "title": "To Front" + }, + { + "command": "designer.outline.moveForward", + "title": "Move Forward" + }, + { + "command": "designer.outline.moveBackward", + "title": "Move Backward" + }, + { + "command": "designer.outline.toBack", + "title": "To Back" + }, + { + "command": "designer.outline.moveTo", + "title": "Move To" + }, + { + "command": "designer.outline.jumpTo", + "title": "Jump To" + }, + { + "command": "designer.outline.expandChildren", + "title": "Expand Children" + }, + { + "command": "designer.outline.collapseChildren", + "title": "Collapse Children" } ], "customEditors": [ @@ -119,4 +297,4 @@ "es-module-shims": "^2.8.0", "typescript": "^5.8.3" } -} +} \ No newline at end of file diff --git a/src/vscode/DesignerOutlineProvider.ts b/src/vscode/DesignerOutlineProvider.ts new file mode 100644 index 0000000..46ac96c --- /dev/null +++ b/src/vscode/DesignerOutlineProvider.ts @@ -0,0 +1,57 @@ +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, + })); +} + +export class DesignerOutlineProvider implements vscode.CustomEditorOutlineProvider { + private readonly _onDidChangeOutline = new vscode.EventEmitter(); + readonly onDidChangeOutline = this._onDidChangeOutline.event; + + private readonly _onDidChangeActiveItem = new vscode.EventEmitter(); + readonly onDidChangeActiveItem = this._onDidChangeActiveItem.event; + + private _items: vscode.CustomEditorOutlineItem[] = []; + private _webview: vscode.Webview | undefined; + + setWebview(webview: vscode.Webview): void { + this._webview = webview; + } + + updateFromWebview(serializedItems: SerializedOutlineItem[]): void { + this._items = convertItems(serializedItems); + this._onDidChangeOutline.fire(); + } + + setActive(itemId: string | undefined): void { + this._onDidChangeActiveItem.fire(itemId); + } + + provideOutline(_token: vscode.CancellationToken): vscode.CustomEditorOutlineItem[] { + return this._items; + } + + revealItem(itemId: string): void { + this._webview?.postMessage({ type: 'reveal', id: itemId }); + } + + sendCommand(command: string): void { + this._webview?.postMessage({ type: 'outlineCommand', command }); + } +} diff --git a/src/vscode/DesignerTextEditor.ts b/src/vscode/DesignerTextEditor.ts index 9fe0944..2cd99cc 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: @@ -139,8 +142,16 @@ export class DesignerTextEditor implements vscode.CustomTextEditorProvider { } } return; + case 'outlineData': + this.outline.updateFromWebview(e.items); + return; + case 'outlineActiveItem': + this.outline.setActive(e.id); + return; } }); + + this.outline.setWebview(webviewPanel.webview); } /** diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index d7fbf9a..a006d2a 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -1,12 +1,40 @@ import * as vscode from 'vscode'; import { DesignerTextEditor } from './DesignerTextEditor.js'; +const outlineCommands = [ + 'designer.outline.toggleLock', + 'designer.outline.toggleHideInDesigner', + 'designer.outline.toggleHideAtRuntime', + 'designer.outline.copy', + 'designer.outline.cut', + 'designer.outline.paste', + 'designer.outline.delete', + 'designer.outline.rotateLeft', + 'designer.outline.rotateRight', + 'designer.outline.toFront', + 'designer.outline.moveForward', + 'designer.outline.moveBackward', + 'designer.outline.toBack', + 'designer.outline.moveTo', + 'designer.outline.jumpTo', + 'designer.outline.expandChildren', + 'designer.outline.collapseChildren', +]; + 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 of outlineCommands) { + context.subscriptions.push( + vscode.commands.registerCommand(cmd, () => { + const shortName = cmd.replace('designer.outline.', ''); + outlineProvider.sendCommand(shortName); + }) + ); + } } 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..b980673 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 { 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,65 @@ 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); + break; + } } }); +function handleOutlineCommand(command: string) { + const selectedElements = [...designerView.instanceServiceContainer.selectionService.selectedElements]; + if (selectedElements.length === 0) return; + + 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; + // Context menu commands are dispatched as custom events + // so the host application can handle them + default: + window.dispatchEvent(new CustomEvent('designer-outline-command', { + detail: { command, selectedElements } + })); + 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 +256,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); } }) From ef9a176ea2f733405d4515629449d89b2e401bf1 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 25 Mar 2026 22:39:44 +0100 Subject: [PATCH 2/5] work on customOutlineView --- package.json | 42 +++++++++++++++++--------- src/vscode/extension.ts | 12 ++++++-- src/webview/designer.ts | 67 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 9d32ba6..9118d36 100644 --- a/package.json +++ b/package.json @@ -174,59 +174,73 @@ }, { "command": "designer.outline.copy", - "title": "Copy" + "title": "Copy", + "icon": "$(copy)" }, { "command": "designer.outline.cut", - "title": "Cut" + "title": "Cut", + "icon": "$(scissors)" }, { "command": "designer.outline.paste", - "title": "Paste" + "title": "Paste", + "icon": "$(clippy)" }, { "command": "designer.outline.delete", - "title": "Delete" + "title": "Delete", + "icon": "$(trash)" }, { "command": "designer.outline.rotateLeft", - "title": "Rotate Left" + "title": "Rotate Left", + "icon": "$(debug-reverse-continue)" }, { "command": "designer.outline.rotateRight", - "title": "Rotate Right" + "title": "Rotate Right", + "icon": "$(debug-continue)" }, { "command": "designer.outline.toFront", - "title": "To Front" + "title": "To Front", + "icon": "$(arrow-circle-up)" }, { "command": "designer.outline.moveForward", - "title": "Move Forward" + "title": "Move Forward", + "icon": "$(arrow-up)" }, { "command": "designer.outline.moveBackward", - "title": "Move Backward" + "title": "Move Backward", + "icon": "$(arrow-down)" }, { "command": "designer.outline.toBack", - "title": "To Back" + "title": "To Back", + "icon": "$(arrow-circle-down)" }, { "command": "designer.outline.moveTo", - "title": "Move To" + "title": "Move To", + "icon": "$(move)" }, { "command": "designer.outline.jumpTo", - "title": "Jump To" + "title": "Jump To", + "icon": "$(search)" }, { "command": "designer.outline.expandChildren", - "title": "Expand Children" + "title": "Expand Children", + "icon": "$(expand-all)" }, { "command": "designer.outline.collapseChildren", - "title": "Collapse Children" + "title": "Collapse Children", + "icon": "$(collapse-all)" } ], "customEditors": [ diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index a006d2a..cbe9f7f 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -17,8 +17,6 @@ const outlineCommands = [ 'designer.outline.toBack', 'designer.outline.moveTo', 'designer.outline.jumpTo', - 'designer.outline.expandChildren', - 'designer.outline.collapseChildren', ]; export function activate(context: vscode.ExtensionContext) { @@ -37,4 +35,14 @@ export function activate(context: vscode.ExtensionContext) { }) ); } + + // 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/webview/designer.ts b/src/webview/designer.ts index b980673..2666bf7 100644 --- a/src/webview/designer.ts +++ b/src/webview/designer.ts @@ -11,7 +11,7 @@ if (!window.CSSContainerRule) window.CSSContainerRule = class { } import { DomHelper } from '@node-projects/base-custom-webcomponent'; -import { DesignerView, IDesignItem, NodeType, 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'; @@ -222,13 +222,66 @@ function handleOutlineCommand(command: string) { } sendOutlineData(designerView.designerCanvas.rootDesignItem); break; - // Context menu commands are dispatched as custom events - // so the host application can handle them - default: - window.dispatchEvent(new CustomEvent('designer-outline-command', { - detail: { command, selectedElements } - })); + 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': + designerView.executeCommand({ type: CommandType.rotateCounterClockwise }); + break; + case 'rotateRight': + designerView.executeCommand({ type: CommandType.rotateClockwise }); + break; + case 'toFront': + designerView.executeCommand({ type: CommandType.moveToFront }); + break; + case 'moveForward': + designerView.executeCommand({ type: CommandType.moveForward }); + break; + case 'moveBackward': + designerView.executeCommand({ type: CommandType.moveBackward }); + break; + case 'toBack': + designerView.executeCommand({ type: CommandType.moveToBack }); + 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; + } } } From a48975875d925af86734455b81755256c1098bd1 Mon Sep 17 00:00:00 2001 From: jkuehner Date: Thu, 26 Mar 2026 10:54:03 +0100 Subject: [PATCH 3/5] fix outline view items commands on non selected --- package-lock.json | 14 +++++++------- package.json | 4 ++-- src/vscode/DesignerOutlineProvider.ts | 4 ++-- src/vscode/extension.ts | 4 ++-- src/webview/designer.ts | 26 +++++++++++++++++--------- 5 files changed, 30 insertions(+), 22 deletions(-) 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 9118d36..ee9d660 100644 --- a/package.json +++ b/package.json @@ -304,11 +304,11 @@ "@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", "es-module-shims": "^2.8.0", "typescript": "^5.8.3" } -} \ No newline at end of file +} diff --git a/src/vscode/DesignerOutlineProvider.ts b/src/vscode/DesignerOutlineProvider.ts index 46ac96c..bd154e4 100644 --- a/src/vscode/DesignerOutlineProvider.ts +++ b/src/vscode/DesignerOutlineProvider.ts @@ -51,7 +51,7 @@ export class DesignerOutlineProvider implements vscode.CustomEditorOutlineProvid this._webview?.postMessage({ type: 'reveal', id: itemId }); } - sendCommand(command: string): void { - this._webview?.postMessage({ type: 'outlineCommand', command }); + sendCommand(command: string, itemId?: string): void { + this._webview?.postMessage({ type: 'outlineCommand', command, id: itemId }); } } diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index cbe9f7f..1658e56 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -29,9 +29,9 @@ export function activate(context: vscode.ExtensionContext) { for (const cmd of outlineCommands) { context.subscriptions.push( - vscode.commands.registerCommand(cmd, () => { + vscode.commands.registerCommand(cmd, (item?: { id?: string }) => { const shortName = cmd.replace('designer.outline.', ''); - outlineProvider.sendCommand(shortName); + outlineProvider.sendCommand(shortName, item?.id); }) ); } diff --git a/src/webview/designer.ts b/src/webview/designer.ts index 2666bf7..06269f5 100644 --- a/src/webview/designer.ts +++ b/src/webview/designer.ts @@ -193,16 +193,24 @@ window.addEventListener('message', async event => { break; } case 'outlineCommand': { - handleOutlineCommand(message.command); + handleOutlineCommand(message.command, message.id); break; } } }); -function handleOutlineCommand(command: string) { - const selectedElements = [...designerView.instanceServiceContainer.selectionService.selectedElements]; +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) { @@ -235,22 +243,22 @@ function handleOutlineCommand(command: string) { designerView.executeCommand({ type: CommandType.delete }); break; case 'rotateLeft': - designerView.executeCommand({ type: CommandType.rotateCounterClockwise }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.rotateCounterClockwise }, selectedElements); break; case 'rotateRight': - designerView.executeCommand({ type: CommandType.rotateClockwise }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.rotateClockwise }, selectedElements); break; case 'toFront': - designerView.executeCommand({ type: CommandType.moveToFront }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveToFront }, selectedElements); break; case 'moveForward': - designerView.executeCommand({ type: CommandType.moveForward }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveForward }, selectedElements); break; case 'moveBackward': - designerView.executeCommand({ type: CommandType.moveBackward }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveBackward }, selectedElements); break; case 'toBack': - designerView.executeCommand({ type: CommandType.moveToBack }); + modelCommandService.executeCommand(designerView.designerCanvas, { type: CommandType.moveToBack }, selectedElements); break; case 'moveTo': { const item = selectedElements[0]; From 9db5b610e70f519d25cb67c7af21a5471ecf934a Mon Sep 17 00:00:00 2001 From: jkuehner Date: Thu, 26 Mar 2026 11:20:11 +0100 Subject: [PATCH 4/5] fixes outline clicks --- package.json | 60 ++++++++++++++++++++++++++++++----------- src/vscode/extension.ts | 42 +++++++++++++++-------------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index ee9d660..1573791 100644 --- a/package.json +++ b/package.json @@ -56,19 +56,34 @@ ], "customEditor/outline/toolbar": [ { - "command": "designer.outline.toggleLock", - "group": "inline", - "when": "true" + "command": "designer.outline.lock", + "group": "inline@1", + "when": "customEditorOutlineItem =~ /^(?!.*:locked)/" }, { - "command": "designer.outline.toggleHideInDesigner", - "group": "inline", - "when": "true" + "command": "designer.outline.unlock", + "group": "inline@1", + "when": "customEditorOutlineItem =~ /:locked/" }, { - "command": "designer.outline.toggleHideAtRuntime", - "group": "inline", - "when": "true" + "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": [ @@ -158,18 +173,33 @@ } }, { - "command": "designer.outline.toggleLock", - "title": "Toggle Lock", + "command": "designer.outline.lock", + "title": "Lock", + "icon": "$(unlock)" + }, + { + "command": "designer.outline.unlock", + "title": "Unlock", "icon": "$(lock)" }, { - "command": "designer.outline.toggleHideInDesigner", - "title": "Toggle Hide in Designer", + "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.toggleHideAtRuntime", - "title": "Toggle Hide at Runtime", + "command": "designer.outline.showAtRuntime", + "title": "Show at Runtime", "icon": "$(eye-closed)" }, { diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index 1658e56..73fa77c 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -1,23 +1,26 @@ import * as vscode from 'vscode'; import { DesignerTextEditor } from './DesignerTextEditor.js'; -const outlineCommands = [ - 'designer.outline.toggleLock', - 'designer.outline.toggleHideInDesigner', - 'designer.outline.toggleHideAtRuntime', - 'designer.outline.copy', - 'designer.outline.cut', - 'designer.outline.paste', - 'designer.outline.delete', - 'designer.outline.rotateLeft', - 'designer.outline.rotateRight', - 'designer.outline.toFront', - 'designer.outline.moveForward', - 'designer.outline.moveBackward', - 'designer.outline.toBack', - 'designer.outline.moveTo', - 'designer.outline.jumpTo', -]; +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) { const [registrations, outlineProvider] = DesignerTextEditor.register(context); @@ -27,11 +30,10 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.executeCommand('vscode.openWith', uri, 'designer.designerTextEditor'); }); - for (const cmd of outlineCommands) { + for (const [cmd, action] of Object.entries(outlineCommands)) { context.subscriptions.push( vscode.commands.registerCommand(cmd, (item?: { id?: string }) => { - const shortName = cmd.replace('designer.outline.', ''); - outlineProvider.sendCommand(shortName, item?.id); + outlineProvider.sendCommand(action, item?.id); }) ); } From 0c2f60211a5aef03d1ba9e464e2a2a9d5ace3f0e Mon Sep 17 00:00:00 2001 From: jkuehner Date: Thu, 26 Mar 2026 11:57:20 +0100 Subject: [PATCH 5/5] update cause of impl changes --- src/vscode/DesignerOutlineProvider.ts | 114 +++++++++++++++----------- src/vscode/DesignerTextEditor.ts | 7 +- src/vscode/extension.ts | 6 +- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/src/vscode/DesignerOutlineProvider.ts b/src/vscode/DesignerOutlineProvider.ts index bd154e4..bd632d3 100644 --- a/src/vscode/DesignerOutlineProvider.ts +++ b/src/vscode/DesignerOutlineProvider.ts @@ -1,57 +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, - })); -} - -export class DesignerOutlineProvider implements vscode.CustomEditorOutlineProvider { - private readonly _onDidChangeOutline = new vscode.EventEmitter(); - readonly onDidChangeOutline = this._onDidChangeOutline.event; - - private readonly _onDidChangeActiveItem = new vscode.EventEmitter(); - readonly onDidChangeActiveItem = this._onDidChangeActiveItem.event; - - private _items: vscode.CustomEditorOutlineItem[] = []; - private _webview: vscode.Webview | undefined; - - setWebview(webview: vscode.Webview): void { - this._webview = webview; - } + import * as vscode from 'vscode'; - updateFromWebview(serializedItems: SerializedOutlineItem[]): void { - this._items = convertItems(serializedItems); - this._onDidChangeOutline.fire(); + export interface SerializedOutlineItem { + id: string; + label: string; + detail?: string; + icon?: string; + contextValue?: string; + children?: SerializedOutlineItem[]; } - setActive(itemId: string | undefined): void { - this._onDidChangeActiveItem.fire(itemId); + 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, + })); } - provideOutline(_token: vscode.CancellationToken): vscode.CustomEditorOutlineItem[] { - return this._items; + interface ResourceState { + items: vscode.CustomEditorOutlineItem[]; + webview: vscode.Webview; } - revealItem(itemId: string): void { - this._webview?.postMessage({ type: 'reveal', id: itemId }); - } + export class DesignerOutlineProvider implements vscode.CustomEditorOutlineProvider { + private readonly _onDidChangeOutline = new vscode.EventEmitter(); + readonly onDidChangeOutline = this._onDidChangeOutline.event; - sendCommand(command: string, itemId?: string): void { - this._webview?.postMessage({ type: 'outlineCommand', command, id: itemId }); - } -} + 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 2cd99cc..1547234 100644 --- a/src/vscode/DesignerTextEditor.ts +++ b/src/vscode/DesignerTextEditor.ts @@ -102,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. @@ -143,15 +144,15 @@ export class DesignerTextEditor implements vscode.CustomTextEditorProvider { } return; case 'outlineData': - this.outline.updateFromWebview(e.items); + this.outline.updateFromWebview(document.uri, e.items); return; case 'outlineActiveItem': - this.outline.setActive(e.id); + this.outline.setActive(document.uri, e.id); return; } }); - this.outline.setWebview(webviewPanel.webview); + this.outline.setWebview(document.uri, webviewPanel.webview); } /** diff --git a/src/vscode/extension.ts b/src/vscode/extension.ts index 73fa77c..20fd068 100644 --- a/src/vscode/extension.ts +++ b/src/vscode/extension.ts @@ -32,8 +32,10 @@ export function activate(context: vscode.ExtensionContext) { for (const [cmd, action] of Object.entries(outlineCommands)) { context.subscriptions.push( - vscode.commands.registerCommand(cmd, (item?: { id?: string }) => { - outlineProvider.sendCommand(action, item?.id); + vscode.commands.registerCommand(cmd, (item?: { id?: string; uri?: vscode.Uri }) => { + if (item?.uri) { + outlineProvider.sendCommand(item.uri, action, item.id); + } }) ); }