From 57080fc14771452392020e83c897013c4c1149d2 Mon Sep 17 00:00:00 2001 From: Thomas Seeley Date: Wed, 24 Dec 2025 15:35:18 -0500 Subject: [PATCH 1/4] support view zone management --- src/vs/code/browser/workbench/workbench.ts | 146 ++++++++++++++++++--- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 779f7506bc6ce..5b260eb24ab67 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -1,3 +1,4 @@ + /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -23,6 +24,11 @@ declare const window: Window & { SENTRY_CAPTURE_EXCEPTION?: (error: Error) => void; extensionToGazePort?: MessagePort; }; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { mainWindow } from 'vs/base/browser/window'; type Writeable = { -readonly [P in keyof T]: T[P] }; @@ -45,7 +51,6 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; delete window.extensionToGazePort; } - const isHttps = window.location.protocol === 'https:'; const isDev = window.location.hostname === 'localhost'; const extensionUrl = { @@ -57,17 +62,15 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; config.additionalBuiltinExtensions = [URI.revive(extensionUrl)]; config.workspaceProvider = { - // IMPORTANT: this filename must match the filename used in `memfs.ts`. - // TODO: Somehow use product.json to configure that globally workspace: { workspaceUri: URI.parse('memfs:/membrane.code-workspace') }, payload: { - 'skipReleaseNotes': 'true', - 'skipWelcome': 'true', + skipReleaseNotes: 'true', + skipWelcome: 'true', }, trusted: true, open: async ( _workspace: IWorkspace, - _options?: { reuse?: boolean; payload?: object } + _options?: { reuse?: boolean; payload?: object }, ) => { return true; }, @@ -76,20 +79,17 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; config.secretStorageProvider = SecretStorageProvider.getInstance(); config.commands = [ - // Used to refresh the page from the extension when a new version of the IDE is known to exist. { id: 'membrane.refreshPage', handler: () => window.location.reload() }, - // Invoked when the navigator finishes loading { - id: 'membrane.completeInitialization', handler: () => window.completeInitialization?.() + id: 'membrane.completeInitialization', + handler: () => window.completeInitialization?.(), }, - // For product tour, emit an event to advance to the next step { id: 'membrane.advanceTour', handler: (...args: unknown[]) => { const cmdArgs = args[0] as { trigger: string }; window.dispatchEvent(new Event(`tour:${cmdArgs.trigger}`)); } }, - // For extension panels to bubble up errors { id: 'membrane.reportError', handler: (...args: unknown[]) => { @@ -106,7 +106,7 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; window.SENTRY_REPORT_ISSUE?.({ source: cmdArgs.source, message: cmdArgs.message, - context: cmdArgs.context + context: cmdArgs.context, }); }, }, @@ -124,8 +124,9 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; { id: 'membrane.getLaunchParams', handler: () => { - // eslint-disable-next-line no-restricted-syntax - const meta = document.querySelector('meta[name="membrane-launch-params"]') as HTMLMetaElement; + const meta = mainWindow.document.querySelector( + 'meta[name="membrane-launch-params"]', + ) as HTMLMetaElement; return meta?.content ?? ''; }, }, @@ -157,4 +158,119 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; // eslint-disable-next-line no-restricted-syntax const domElement = window.vscodeTargetContainer || document.body; create(domElement, config); -})(); \ No newline at end of file +})(); + const domElement = (window as any).vscodeTargetContainer || mainWindow.document.body; + create(domElement, config); + + // Register Monaco commands for review decorations + // These allow the extension to add/remove view zones (e.g., showing removed lines during code review) + CommandsRegistry.registerCommand( + 'membrane.addMonacoViewZone', + async ( + accessor: ServicesAccessor, + args: { + uri: string; + afterLineNumber: number; + heightInPx: number; + lines: string[]; + }, + ) => { + const editorService = accessor.get(IEditorService); + const targetUri = URI.parse(args.uri); + + const activeControl = editorService.activeTextEditorControl; + if (!activeControl || !isCodeEditor(activeControl)) { + return null; + } + + const model = activeControl.getModel(); + if (!model) { + return null; + } + + // Verify we're in the correct file + const modelUri = model.uri; + if ( + modelUri.scheme !== targetUri.scheme || + modelUri.path !== targetUri.path + ) { + return null; + } + + let zoneId: string | null = null; + + activeControl.changeViewZones((accessor) => { + const container = document.createElement('div'); + container.style.cssText = ` + position: relative; + background: rgba(255, 0, 0, 0.15); + border-left: 1px solid rgba(255, 0, 0, 0.4); + font-family: var(--monaco-monospace-font, 'Menlo', 'Monaco', 'Courier New', monospace); + font-size: 12px; + line-height: 18px; + padding: 0; + color: rgba(255, 100, 100, 0.9); + `; + + args.lines.forEach((line: string, idx: number) => { + const lineDiv = document.createElement('div'); + lineDiv.style.cssText = ` + position: absolute; + top: ${idx * 18}px; + left: 0; + right: 0; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + `; + + lineDiv.textContent = line; + container.appendChild(lineDiv); + }); + + zoneId = accessor.addZone({ + afterLineNumber: args.afterLineNumber, + heightInPx: args.heightInPx, + domNode: container, + suppressMouseDown: false, + }); + }); + + return { zoneId }; + }, + ); + + CommandsRegistry.registerCommand( + 'membrane.removeMonacoViewZone', + async ( + accessor: ServicesAccessor, + args: { uri: string; zoneId: string }, + ) => { + const editorService = accessor.get(IEditorService); + const targetUri = URI.parse(args.uri); + + const activeControl = editorService.activeTextEditorControl; + if (!activeControl || !isCodeEditor(activeControl)) { + return; + } + + const model = activeControl.getModel(); + if (!model) { + return; + } + + // Verify we're in the correct file + const modelUri = model.uri; + if ( + modelUri.scheme !== targetUri.scheme || + modelUri.path !== targetUri.path + ) { + return; + } + + activeControl.changeViewZones((accessor) => { + accessor.removeZone(args.zoneId); + }); + }, + ); +})(); From 3f012f91d1a94bb9bbda38bb730cb997afd35287 Mon Sep 17 00:00:00 2001 From: Thomas Seeley Date: Fri, 9 Jan 2026 14:37:23 -0500 Subject: [PATCH 2/4] view zone and scroll info commands --- src/vs/code/browser/workbench/workbench.ts | 190 ++++++++++----------- 1 file changed, 93 insertions(+), 97 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 5b260eb24ab67..ca11923b985f9 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -162,115 +162,111 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; const domElement = (window as any).vscodeTargetContainer || mainWindow.document.body; create(domElement, config); - // Register Monaco commands for review decorations - // These allow the extension to add/remove view zones (e.g., showing removed lines during code review) + // NOTE: Registering these commands here ... CommandsRegistry.registerCommand( - 'membrane.addMonacoViewZone', - async ( - accessor: ServicesAccessor, - args: { - uri: string; - afterLineNumber: number; - heightInPx: number; - lines: string[]; - }, - ) => { - const editorService = accessor.get(IEditorService); - const targetUri = URI.parse(args.uri); + 'membrane.setViewZones', + async ( + accessor: ServicesAccessor, + args: { + uri: string; + zones: Array<{ + afterLineNumber: number; + heightInPx: number; + lines: string[]; + }>; + }, + ) => { + const editorService = accessor.get(IEditorService); + const targetUri = URI.parse(args.uri); - const activeControl = editorService.activeTextEditorControl; - if (!activeControl || !isCodeEditor(activeControl)) { - return null; - } + const activeControl = editorService.activeTextEditorControl; + if (!activeControl || !isCodeEditor(activeControl)) { + return; + } - const model = activeControl.getModel(); - if (!model) { - return null; - } + const model = activeControl.getModel(); + if (!model) { + return; + } - // Verify we're in the correct file - const modelUri = model.uri; - if ( - modelUri.scheme !== targetUri.scheme || - modelUri.path !== targetUri.path - ) { - return null; - } + const modelUri = model.uri; + if ( + modelUri.scheme !== targetUri.scheme || + modelUri.path !== targetUri.path + ) { + return; + } - let zoneId: string | null = null; + // Track zones per-editor (use a WeakMap or store on editor instance) + const existingZoneIds: string[] = (activeControl as any).__membraneViewZones || []; - activeControl.changeViewZones((accessor) => { - const container = document.createElement('div'); - container.style.cssText = ` - position: relative; - background: rgba(255, 0, 0, 0.15); - border-left: 1px solid rgba(255, 0, 0, 0.4); - font-family: var(--monaco-monospace-font, 'Menlo', 'Monaco', 'Courier New', monospace); - font-size: 12px; - line-height: 18px; - padding: 0; - color: rgba(255, 100, 100, 0.9); - `; + activeControl.changeViewZones((accessor) => { + // Remove ALL existing zones first + for (const zoneId of existingZoneIds) { + accessor.removeZone(zoneId); + } - args.lines.forEach((line: string, idx: number) => { - const lineDiv = document.createElement('div'); - lineDiv.style.cssText = ` - position: absolute; - top: ${idx * 18}px; - left: 0; - right: 0; - white-space: pre; - overflow: hidden; - text-overflow: ellipsis; - `; + // Add all new zones + const newZoneIds: string[] = []; + for (const zone of args.zones) { + const container = document.createElement('div'); + container.style.cssText = ` + position: relative; + background: rgba(255, 0, 0, 0.15); + border-left: 1px solid rgba(255, 0, 0, 0.4); + font-family: var(--monaco-monospace-font); + font-size: 12px; + line-height: 18px; + color: rgba(255, 100, 100, 0.9); + `; - lineDiv.textContent = line; - container.appendChild(lineDiv); - }); + zone.lines.forEach((line, idx) => { + const lineDiv = document.createElement('div'); + lineDiv.style.cssText = ` + position: absolute; + top: ${idx * 18}px; + left: 0; + right: 0; + white-space: pre; + overflow: hidden; + `; + lineDiv.textContent = line; + container.appendChild(lineDiv); + }); - zoneId = accessor.addZone({ - afterLineNumber: args.afterLineNumber, - heightInPx: args.heightInPx, - domNode: container, - suppressMouseDown: false, - }); - }); - - return { zoneId }; - }, - ); + const zoneId = accessor.addZone({ + afterLineNumber: zone.afterLineNumber, + heightInPx: zone.heightInPx, + domNode: container, + suppressMouseDown: false, + }); + newZoneIds.push(zoneId); + } + // Store for next call + (activeControl as any).__membraneViewZones = newZoneIds; + }); + }, +); + CommandsRegistry.registerCommand( - 'membrane.removeMonacoViewZone', - async ( - accessor: ServicesAccessor, - args: { uri: string; zoneId: string }, - ) => { - const editorService = accessor.get(IEditorService); - const targetUri = URI.parse(args.uri); + 'membrane.getEditorScrollInfo', + (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const activeControl = editorService.activeTextEditorControl; + if (!activeControl || !isCodeEditor(activeControl)) { + return null; + } + const visibleRanges = activeControl.getVisibleRanges(); + const firstVisibleLine = visibleRanges[0]?.startLineNumber ?? 1; + return { + scrollTop: activeControl.getScrollTop(), + firstLineTop: activeControl.getTopForLineNumber(firstVisibleLine), + firstVisibleLine: firstVisibleLine, + }; + }, +); - const activeControl = editorService.activeTextEditorControl; - if (!activeControl || !isCodeEditor(activeControl)) { - return; - } - const model = activeControl.getModel(); - if (!model) { - return; - } - // Verify we're in the correct file - const modelUri = model.uri; - if ( - modelUri.scheme !== targetUri.scheme || - modelUri.path !== targetUri.path - ) { - return; - } - - activeControl.changeViewZones((accessor) => { - accessor.removeZone(args.zoneId); - }); - }, - ); })(); From d9b463a923cb274e5c2adbb3aa5bb69535401638 Mon Sep 17 00:00:00 2001 From: Thomas Seeley Date: Fri, 9 Jan 2026 16:46:00 -0500 Subject: [PATCH 3/4] fix imports and syntax error --- src/vs/code/browser/workbench/workbench.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index ca11923b985f9..9e1d6942f4b7d 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -24,11 +24,11 @@ declare const window: Window & { SENTRY_CAPTURE_EXCEPTION?: (error: Error) => void; extensionToGazePort?: MessagePort; }; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { mainWindow } from 'vs/base/browser/window'; +import { CommandsRegistry } from '../../../platform/commands/common/commands.js'; +import { IEditorService } from '../../../workbench/services/editor/common/editorService.js'; +import { isCodeEditor } from '../../../editor/browser/editorBrowser.js'; +import type { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; +import { mainWindow } from '../../../base/browser/window.js'; type Writeable = { -readonly [P in keyof T]: T[P] }; @@ -158,9 +158,6 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; // eslint-disable-next-line no-restricted-syntax const domElement = window.vscodeTargetContainer || document.body; create(domElement, config); -})(); - const domElement = (window as any).vscodeTargetContainer || mainWindow.document.body; - create(domElement, config); // NOTE: Registering these commands here ... CommandsRegistry.registerCommand( From 219e3ca35a067f0bef27d610e6ecf20ce5e29b98 Mon Sep 17 00:00:00 2001 From: Thomas Seeley <104278845+iamseeley@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:37:24 -0500 Subject: [PATCH 4/4] add styled arg to control if a view zone is styled or not, add contentLeft for codelens button positioning to scrollInfo command (#85) --- src/vs/code/browser/workbench/workbench.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 9e1d6942f4b7d..e471da5a50413 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -159,7 +159,6 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; const domElement = window.vscodeTargetContainer || document.body; create(domElement, config); - // NOTE: Registering these commands here ... CommandsRegistry.registerCommand( 'membrane.setViewZones', async ( @@ -170,6 +169,7 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; afterLineNumber: number; heightInPx: number; lines: string[]; + styled?: boolean; }>; }, ) => { @@ -206,7 +206,9 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; // Add all new zones const newZoneIds: string[] = []; for (const zone of args.zones) { - const container = document.createElement('div'); + const container = document.createElement('div'); + + if (zone.styled) { container.style.cssText = ` position: relative; background: rgba(255, 0, 0, 0.15); @@ -230,6 +232,7 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; lineDiv.textContent = line; container.appendChild(lineDiv); }); + } const zoneId = accessor.addZone({ afterLineNumber: zone.afterLineNumber, @@ -260,6 +263,7 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; scrollTop: activeControl.getScrollTop(), firstLineTop: activeControl.getTopForLineNumber(firstVisibleLine), firstVisibleLine: firstVisibleLine, + contentLeft: activeControl.getLayoutInfo().contentLeft, }; }, );