diff --git a/src/vs/base/browser/membrane/membranePortManager.ts b/src/vs/base/browser/membrane/membranePortManager.ts new file mode 100644 index 0000000000000..bb43f1361383d --- /dev/null +++ b/src/vs/base/browser/membrane/membranePortManager.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +interface IMembraneWindow extends Window { + vscodeToGazePort?: MessagePort; +} + +declare const window: IMembraneWindow; + +/** + * Manages direct communication from VSCode to Gaze, bypassing the extension. + * Used for low-latency updates like: + * - Editor metrics (scroll, view zones, line positions) + * - Dialogs + * - Notifications + */ +export class GazePortManager { + private static port: MessagePort | null = null; + private static responseHandlers = new Map void>(); + + static setResponseHandler(messageType: string, handler: (response: unknown) => void): void { + GazePortManager.responseHandlers.set(messageType, handler); + } + + static ensureInitialized(): void { + if (GazePortManager.port) { + return; + } + + GazePortManager.port = window.vscodeToGazePort || null; + if (window.vscodeToGazePort) { + delete window.vscodeToGazePort; + } + + if (!GazePortManager.port) { + console.warn('GazePortManager: vscodeToGazePort not available'); + return; + } + + GazePortManager.port.onmessage = (event) => { + try { + const handler = GazePortManager.responseHandlers.get(event.data.messageType); + handler?.(event.data); + } catch (error) { + console.error(`Error handling ${event.data.messageType}:`, error); + } + }; + } + + static sendMessage(messageType: string, data: object): void { + try { + GazePortManager.ensureInitialized(); + if (!GazePortManager.port) { + return; + } + GazePortManager.port.postMessage({ messageType, ...data }); + } catch (error) { + console.error(`Error sending ${messageType}:`, error); + } + } +} diff --git a/src/vs/base/browser/ui/dialog/membraneDialog.ts b/src/vs/base/browser/ui/dialog/membraneDialog.ts index 7ef9b4d00b2cf..6973fcd1d1f41 100644 --- a/src/vs/base/browser/ui/dialog/membraneDialog.ts +++ b/src/vs/base/browser/ui/dialog/membraneDialog.ts @@ -7,7 +7,7 @@ import { IDialogOptions, IDialogResult } from './dialog.js'; import { Disposable } from '../../../common/lifecycle.js'; import { generateUuid } from '../../../common/uuid.js'; // import { mainWindow } from '../../../browser/window.js'; -import { MembranePortManager } from '../../../../base/browser/ui/dialog/membranePortManager.js'; +import { GazePortManager } from '../../membrane/membranePortManager.js'; export interface MembraneDialogMessage { type: 'confirm' | 'prompt' | 'info' | 'warn' | 'error' | 'input'; @@ -64,7 +64,7 @@ export class MembraneDialog extends Disposable { this.dialogId = generateUuid(); // Register this instance as the dialog response handler - MembranePortManager.setDialogResponseHandler((response: unknown) => { + GazePortManager.setResponseHandler('membraneDialogResponse', (response: unknown) => { this.handleDialogResponse(response as MembraneDialogResponse); }); } @@ -89,8 +89,9 @@ export class MembraneDialog extends Disposable { }; - // Send via MessagePort using shared port manager - MembranePortManager.sendMessage('membraneDialog', dialogMessage); + // Send via MessagePort using port manager + GazePortManager.sendMessage('membraneDialog', dialogMessage); + // Set up timeout to prevent hanging dialogs setTimeout(() => { @@ -149,7 +150,7 @@ export class MembraneDialog extends Disposable { // Send update to Gaze if dialog is currently showing if (MembraneDialog.pendingResponses.has(this.dialogId)) { - MembranePortManager.sendMessage('membraneDialogUpdate', { + GazePortManager.sendMessage('membraneDialogUpdate', { id: this.dialogId, message: message }); diff --git a/src/vs/base/browser/ui/dialog/membranePortManager.ts b/src/vs/base/browser/ui/dialog/membranePortManager.ts deleted file mode 100644 index 5a1b653758b4a..0000000000000 --- a/src/vs/base/browser/ui/dialog/membranePortManager.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -interface IMembraneWindow extends Window { - dialogsToGazePort?: MessagePort; -} - -declare const window: IMembraneWindow; - -export class MembranePortManager { - private static dialogsPort: MessagePort | null = null; - private static notificationResponseHandler: ((response: unknown) => void) | null = null; - private static dialogResponseHandler: ((response: unknown) => void) | null = null; - - static setNotificationResponseHandler(handler: (response: unknown) => void): void { - MembranePortManager.notificationResponseHandler = handler; - } - - static setDialogResponseHandler(handler: (response: unknown) => void): void { - MembranePortManager.dialogResponseHandler = handler; - } - - static ensureInitialized(): void { - if (MembranePortManager.dialogsPort) { - return; // Already initialized - } - - MembranePortManager.dialogsPort = window.dialogsToGazePort || null; - if (window.dialogsToGazePort) { - delete window.dialogsToGazePort; - } - - // Check if the port was actually available - if (!MembranePortManager.dialogsPort) { - console.warn('MembranePortManager: dialogsToGazePort not available. Dialogs and notifications may not work.'); - return; - } - - // Set up listener for dialog and notification responses - MembranePortManager.dialogsPort.onmessage = (event) => { - try { - if (event.data.messageType === 'membraneDialogResponse') { - MembranePortManager.dialogResponseHandler?.(event.data); - } else if (event.data.messageType === 'membraneNotificationResponse') { - MembranePortManager.notificationResponseHandler?.(event.data); - } - } catch (error) { - console.error(`Error handling ${event.data.messageType} message:`, error); - } - }; - - } - - static sendMessage(messageType: string, data: object): void { - try { - MembranePortManager.ensureInitialized(); - if (!MembranePortManager.dialogsPort) { - console.warn(`MembranePortManager: Cannot send ${messageType} message - dialogs port not available`); - return; - } - const message = { - messageType, - ...data - }; - MembranePortManager.dialogsPort.postMessage(message); - } catch (error) { - console.error(`Error sending ${messageType} message:`, error); - } - } - -} \ No newline at end of file diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index e471da5a50413..a6ecb2026204f 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -1,4 +1,3 @@ - /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -24,11 +23,8 @@ declare const window: Window & { SENTRY_CAPTURE_EXCEPTION?: (error: Error) => void; extensionToGazePort?: MessagePort; }; -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] }; @@ -124,6 +120,7 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; { id: 'membrane.getLaunchParams', handler: () => { + // eslint-disable-next-line no-restricted-syntax const meta = mainWindow.document.querySelector( 'meta[name="membrane-launch-params"]', ) as HTMLMetaElement; @@ -155,119 +152,8 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; 'window.commandCenter': false, // Hide command center }; + // eslint-disable-next-line no-restricted-syntax const domElement = window.vscodeTargetContainer || document.body; create(domElement, config); - - CommandsRegistry.registerCommand( - 'membrane.setViewZones', - async ( - accessor: ServicesAccessor, - args: { - uri: string; - zones: Array<{ - afterLineNumber: number; - heightInPx: number; - lines: string[]; - styled?: boolean; - }>; - }, - ) => { - 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; - } - - const modelUri = model.uri; - if ( - modelUri.scheme !== targetUri.scheme || - modelUri.path !== targetUri.path - ) { - return; - } - - // Track zones per-editor (use a WeakMap or store on editor instance) - const existingZoneIds: string[] = (activeControl as any).__membraneViewZones || []; - - activeControl.changeViewZones((accessor) => { - // Remove ALL existing zones first - for (const zoneId of existingZoneIds) { - accessor.removeZone(zoneId); - } - - // Add all new zones - const newZoneIds: string[] = []; - for (const zone of args.zones) { - const container = document.createElement('div'); - - if (zone.styled) { - 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); - `; - - 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); - }); - } - - 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.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, - contentLeft: activeControl.getLayoutInfo().contentLeft, - }; - }, -); - - - })(); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 61a9771bb7d02..9ed16c159c655 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3406,7 +3406,8 @@ class EditorMinimap extends BaseEditorOption(Extensions.Configuration).registerConfigurat 'breadcrumbs.enabled': { description: localize('enabled', "Enable/disable navigation breadcrumbs."), type: 'boolean', - default: true + // MEMBRANE + default: false }, 'breadcrumbs.filePath': { description: localize('filepath', "Controls whether and how file paths are shown in the breadcrumbs view."), diff --git a/src/vs/workbench/browser/parts/notifications/membraneNotificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/membraneNotificationsToasts.ts index 9f7524274d11b..2f9ac032df5a9 100644 --- a/src/vs/workbench/browser/parts/notifications/membraneNotificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/membraneNotificationsToasts.ts @@ -13,7 +13,7 @@ import { NotificationsFilter, NotificationPriority, Severity } from '../../../.. import { IntervalCounter } from '../../../../base/common/async.js'; import { NotificationsToastsVisibleContext } from '../../../common/contextkeys.js'; import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { MembranePortManager } from '../../../../base/browser/ui/dialog/membranePortManager.js'; +import { GazePortManager } from '../../../../base/browser/membrane/membranePortManager.js'; declare global { interface Window { @@ -58,9 +58,9 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica this.registerListeners(); // Initialize port manager to set up notification response listener - MembranePortManager.ensureInitialized(); + GazePortManager.ensureInitialized(); // Register this instance as the notification response handler - MembranePortManager.setNotificationResponseHandler((response: unknown) => { + GazePortManager.setResponseHandler('membraneNotificationResponse', (response: unknown) => { this.handleNotificationAction(response as MembraneNotificationActionResponse); }); } @@ -164,9 +164,8 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica }; // Send notification via MembranePortManager - - MembranePortManager.ensureInitialized(); - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.ensureInitialized(); + GazePortManager.sendMessage('membraneNotification', { type: 'toast', id: `notification-${notificationId}`, notification: notificationData @@ -199,7 +198,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica this.activeNotifications.delete(item.id); // Notify Gaze to hide the notification via MembranePortManager - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'hide', id: `notification-${item.id}` }); @@ -227,7 +226,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica focus(): boolean { // For keyboard navigation send focus request to Gaze via MembranePortManager if (this.activeNotifications.size > 0) { - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'focus', target: 'first' }); @@ -238,7 +237,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica focusNext(): boolean { if (this.activeNotifications.size > 0) { - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'focus', target: 'next' }); @@ -249,7 +248,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica focusPrevious(): boolean { if (this.activeNotifications.size > 0) { - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'focus', target: 'previous' }); @@ -260,7 +259,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica focusFirst(): boolean { if (this.activeNotifications.size > 0) { - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'focus', target: 'first' }); @@ -271,7 +270,7 @@ export class MembraneNotificationsToasts extends Disposable implements INotifica focusLast(): boolean { if (this.activeNotifications.size > 0) { - MembranePortManager.sendMessage('membraneNotification', { + GazePortManager.sendMessage('membraneNotification', { type: 'focus', target: 'last' }); diff --git a/src/vs/workbench/contrib/membrane/browser/membraneEditorDecorations.contribution.ts b/src/vs/workbench/contrib/membrane/browser/membraneEditorDecorations.contribution.ts new file mode 100644 index 0000000000000..f7427294113a2 --- /dev/null +++ b/src/vs/workbench/contrib/membrane/browser/membraneEditorDecorations.contribution.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { isCodeEditor, ICodeEditor, IViewZoneChangeAccessor } from '../../../../editor/browser/editorBrowser.js'; +import { IModelDeltaDecoration, OverviewRulerLane } from '../../../../editor/common/model.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { mainWindow } from '../../../../base/browser/window.js'; +import { URI } from '../../../../base/common/uri.js'; + +interface IMembraneViewZone { + afterLine: number; + lines: string[]; + styled?: boolean; +} + +interface IMembraneHighlight { + startLine: number; + endLine: number; +} + +interface IFileDecorations { + viewZones: IMembraneViewZone[]; + highlights: IMembraneHighlight[]; +} + +// View zones are editor-specific (can't persist across editor instances) +interface IEditorWithMembraneDecorations extends ICodeEditor { + __membraneViewZones?: string[]; +} + +// Highlights are model-specific (persist when switching tabs) +interface IModelWithMembraneDecorations { + __membraneHighlights?: string[]; +} + +export class MembraneEditorDecorationsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.membraneEditorDecorations'; + + constructor( + @IEditorService private readonly editorService: IEditorService, + ) { + super(); + this._setupFileDecorationsListener(); + } + + + private _setupFileDecorationsListener(): void { + const handler = ((event: CustomEvent) => { + const { uri, decorations } = event.detail as { uri: string; decorations: IFileDecorations }; + const parsedUri = URI.parse(uri); + + const activeControl = this.editorService.activeTextEditorControl; + if (activeControl && isCodeEditor(activeControl)) { + this._applyFileDecorations(activeControl, parsedUri, decorations); + } + }) as EventListener; + + mainWindow.addEventListener('membrane:setFileDecorations', handler); + this._register({ dispose: () => mainWindow.removeEventListener('membrane:setFileDecorations', handler) }); + } + + private _applyFileDecorations(editor: ICodeEditor, targetUri: URI, decorations: IFileDecorations): void { + const model = editor.getModel(); + if (!model || model.uri.scheme !== targetUri.scheme || model.uri.path !== targetUri.path) { + return; + } + + this._applyViewZones(editor, targetUri, decorations.viewZones); + + this._applyHighlights(editor, targetUri, decorations.highlights); + } + + private _applyViewZones(editor: ICodeEditor, targetUri: URI, zones: IMembraneViewZone[]): void { + const model = editor.getModel(); + if (!model || model.uri.scheme !== targetUri.scheme || model.uri.path !== targetUri.path) { + return; + } + + const editorWithDecorations = editor as IEditorWithMembraneDecorations; + const existingZoneIds: string[] = editorWithDecorations.__membraneViewZones || []; + + editor.changeViewZones((zoneAccessor: IViewZoneChangeAccessor) => { + // Remove existing zones + for (const zoneId of existingZoneIds) { + zoneAccessor.removeZone(zoneId); + } + + // Add new zones + const newZoneIds: string[] = []; + for (const zone of zones) { + const zoneId = zoneAccessor.addZone({ + afterLineNumber: zone.afterLine, + domNode: this._createViewZoneNode(zone), + suppressMouseDown: false, + }); + newZoneIds.push(zoneId); + } + editorWithDecorations.__membraneViewZones = newZoneIds; + }); + } + + private _applyHighlights(editor: ICodeEditor, targetUri: URI, highlights: IMembraneHighlight[]): void { + const model = editor.getModel(); + if (!model || model.uri.scheme !== targetUri.scheme || model.uri.path !== targetUri.path) { + return; + } + + // Store decoration IDs on the model (persists across editor switches) + const modelWithDecorations = model as unknown as IModelWithMembraneDecorations; + const existingDecorationIds: string[] = modelWithDecorations.__membraneHighlights || []; + + const lineCount = model.getLineCount(); + + // Use diff editor's inserted line background (already themed) + const newDecorations: IModelDeltaDecoration[] = highlights + .filter(h => h.startLine >= 1 && h.endLine <= lineCount && h.startLine <= h.endLine) + .map(h => ({ + range: new Range(h.startLine, 1, h.endLine, model.getLineMaxColumn(h.endLine)), + options: { + description: 'membrane-added-line', + isWholeLine: true, + className: 'line-insert', + overviewRuler: { + color: { id: 'diffEditor.insertedLineBackground' }, + position: OverviewRulerLane.Full, + }, + }, + })); + + const newIds = model.deltaDecorations(existingDecorationIds, newDecorations); + modelWithDecorations.__membraneHighlights = newIds; + } + + + private _createViewZoneNode(zone: IMembraneViewZone): HTMLElement { + const container = document.createElement('div'); + + if (zone.styled) { + // Styled view zone (e.g., deleted lines in diff) + 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); + `; + zone.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;`; + lineDiv.textContent = line; + container.appendChild(lineDiv); + }); + } + // Unstyled zones are just empty space (for codelens buttons) + + return container; + } +} + +registerWorkbenchContribution2( + MembraneEditorDecorationsContribution.ID, + MembraneEditorDecorationsContribution, + WorkbenchPhase.AfterRestored +); diff --git a/src/vs/workbench/contrib/membrane/browser/membraneEditorMetrics.contribution.ts b/src/vs/workbench/contrib/membrane/browser/membraneEditorMetrics.contribution.ts new file mode 100644 index 0000000000000..9af4b8a735bf7 --- /dev/null +++ b/src/vs/workbench/contrib/membrane/browser/membraneEditorMetrics.contribution.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { isCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { mainWindow } from '../../../../base/browser/window.js'; + +export class MembraneEditorMetricsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.membraneEditorMetrics'; + + private readonly _currentEditorDisposables = this._register(new DisposableStore()); + + constructor( + @IEditorService private readonly editorService: IEditorService, + ) { + super(); + this._setupMetricsListener(); + this._register(this.editorService.onDidActiveEditorChange(() => this._setupMetricsListener())); + } + + private _sendMetricsSync(metrics: object | null): void { + if (!metrics) { + return; + } + mainWindow.dispatchEvent(new CustomEvent('membrane:editorMetricsSync', { + detail: { messageType: 'editorMetricsUpdate', ...metrics } + })); + } + + private _setupMetricsListener(): void { + this._currentEditorDisposables.clear(); + + const activeControl = this.editorService.activeTextEditorControl; + if (!activeControl || !isCodeEditor(activeControl)) { + return; + } + + const sendMetrics = () => { + const metrics = this._gatherMetrics(activeControl); + this._sendMetricsSync(metrics); + }; + + sendMetrics(); + + this._currentEditorDisposables.add(activeControl.onDidScrollChange(sendMetrics)); + this._currentEditorDisposables.add(activeControl.onDidChangeViewZones(sendMetrics)); + this._currentEditorDisposables.add(activeControl.onDidLayoutChange(sendMetrics)); + this._currentEditorDisposables.add(activeControl.onDidChangeHiddenAreas(sendMetrics)); + this._currentEditorDisposables.add(activeControl.onDidChangeConfiguration(sendMetrics)); + } + + private _gatherMetrics(editor: ICodeEditor) { + const model = editor.getModel(); + if (!model) { + return null; + } + + const visibleRanges = editor.getVisibleRanges(); + const firstVisibleLine = visibleRanges[0]?.startLineNumber ?? 1; + const lastVisibleLine = visibleRanges[visibleRanges.length - 1]?.endLineNumber ?? 1; + const layoutInfo = editor.getLayoutInfo(); + const lineCount = model.getLineCount(); + const startLine = Math.max(1, firstVisibleLine - 20); + const endLine = Math.min(lineCount, lastVisibleLine + 20); + + const linePositions: Array<{ line: number; top: number; bottom: number }> = []; + for (let line = startLine; line <= endLine; line++) { + linePositions.push({ + line, + top: editor.getTopForLineNumber(line, true), + bottom: editor.getBottomForLineNumber(line), + }); + } + + return { + uri: model.uri.toString(), + scrollTop: editor.getScrollTop(), + scrollLeft: editor.getScrollLeft(), + firstVisibleLine, + firstLineTop: editor.getTopForLineNumber(firstVisibleLine, true), + lastVisibleLine, + contentLeft: layoutInfo.contentLeft, + contentWidth: layoutInfo.contentWidth, + linePositions, + }; + } +} + +registerWorkbenchContribution2( + MembraneEditorMetricsContribution.ID, + MembraneEditorMetricsContribution, + WorkbenchPhase.AfterRestored +); diff --git a/src/vs/workbench/workbench.web.main.internal.ts b/src/vs/workbench/workbench.web.main.internal.ts index 6fe55156455db..140e325f6427d 100644 --- a/src/vs/workbench/workbench.web.main.internal.ts +++ b/src/vs/workbench/workbench.web.main.internal.ts @@ -123,6 +123,12 @@ registerSingleton(ISharedWebContentExtractorService, NullSharedWebContentExtract //#region --- workbench contributions +// MEMBRANE Editor Metrics +import './contrib/membrane/browser/membraneEditorMetrics.contribution.js'; + +// MEMBRANE Editor Decorations (view zones + highlights) +import './contrib/membrane/browser/membraneEditorDecorations.contribution.js'; + // Logs import './contrib/logs/browser/logs.contribution.js';