ui element selection (#246643)

* ui element selection window hack

* add background

* target simple browser

* revert to non-simple browser attempt

* some saucy stuff

* saucy cleanup

* some additions:

* add better button, better listening, even saucier

* move to css and also make sure not to block elements during screenshot

* it's even saucier now

* remove browser id lookup

* fix merge conflicts and clean up

* make timeout 3 seconds

* some cleanup

* remove computed css

* use built in button instead

* address many comments :)
This commit is contained in:
Justin Chen
2025-04-25 11:19:54 -07:00
committed by GitHub
parent 6bbb824f0f
commit e3734c0794
18 changed files with 732 additions and 18 deletions
@@ -95,6 +95,8 @@ onceDocumentLoaded(() => {
// Try to bust the cache for the iframe // Try to bust the cache for the iframe
// There does not appear to be any way to reliably do this except modifying the url // There does not appear to be any way to reliably do this except modifying the url
const existing = new URLSearchParams(location.search);
url.searchParams.append('id', existing.get('id')!);
url.searchParams.append('vscodeBrowserReqId', Date.now().toString()); url.searchParams.append('vscodeBrowserReqId', Date.now().toString());
iframe.src = url.toString(); iframe.src = url.toString();
+9 -1
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../base/common/buffer.js'; import { VSBuffer } from '../../../base/common/buffer.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { Event } from '../../../base/common/event.js'; import { Event } from '../../../base/common/event.js';
import { URI } from '../../../base/common/uri.js'; import { URI } from '../../../base/common/uri.js';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from '../../../base/parts/sandbox/common/electronTypes.js'; import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from '../../../base/parts/sandbox/common/electronTypes.js';
@@ -38,6 +39,12 @@ export interface INativeHostOptions {
readonly targetWindowId?: number; readonly targetWindowId?: number;
} }
export interface IElementData {
readonly outerHTML: string;
readonly computedStyle: string;
readonly bounds: IRectangle;
}
export interface ICommonNativeHostService { export interface ICommonNativeHostService {
readonly _serviceBrand: undefined; readonly _serviceBrand: undefined;
@@ -148,7 +155,8 @@ export interface ICommonNativeHostService {
hasWSLFeatureInstalled(): Promise<boolean>; hasWSLFeatureInstalled(): Promise<boolean>;
// Screenshots // Screenshots
getScreenshot(): Promise<VSBuffer | undefined>; getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined>;
getElementData(offsetX: number, offsetY: number, token: CancellationToken): Promise<IElementData | undefined>;
// Process // Process
getProcessId(): Promise<number | undefined>; getProcessId(): Promise<number | undefined>;
@@ -28,7 +28,7 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ
import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js';
import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js';
import { ILogService } from '../../log/common/log.js'; import { ILogService } from '../../log/common/log.js';
import { ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js'; import { ICommonNativeHostService, IElementData, INativeHostOptions, IOSProperties, IOSStatistics } from '../common/native.js';
import { IProductService } from '../../product/common/productService.js'; import { IProductService } from '../../product/common/productService.js';
import { IPartsSplash } from '../../theme/common/themeService.js'; import { IPartsSplash } from '../../theme/common/themeService.js';
import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js';
@@ -48,11 +48,18 @@ import { IConfigurationService } from '../../configuration/common/configuration.
import { IProxyAuthService } from './auth.js'; import { IProxyAuthService } from './auth.js';
import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js';
import { randomPath } from '../../../base/common/extpath.js'; import { randomPath } from '../../../base/common/extpath.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { } export interface INativeHostMainService extends AddFirstParameterToFunctions<ICommonNativeHostService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
export const INativeHostMainService = createDecorator<INativeHostMainService>('nativeHostMainService'); export const INativeHostMainService = createDecorator<INativeHostMainService>('nativeHostMainService');
interface NodeDataResponse {
outerHTML: string;
computedStyle: string;
bounds: IRectangle;
}
export class NativeHostMainService extends Disposable implements INativeHostMainService { export class NativeHostMainService extends Disposable implements INativeHostMainService {
declare readonly _serviceBrand: undefined; declare readonly _serviceBrand: undefined;
@@ -742,14 +749,302 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
//#region Screenshots //#region Screenshots
async getScreenshot(windowId: number | undefined, options?: INativeHostOptions): Promise<VSBuffer | undefined> { async getScreenshot(windowId: number | undefined, rect?: IRectangle, options?: INativeHostOptions): Promise<VSBuffer | undefined> {
const window = this.windowById(options?.targetWindowId, windowId); const window = this.windowById(options?.targetWindowId, windowId);
const captured = await window?.win?.webContents.capturePage(); const captured = await window?.win?.webContents.capturePage(rect);
const buf = captured?.toJPEG(95); const buf = captured?.toJPEG(95);
return buf && VSBuffer.wrap(buf); return buf && VSBuffer.wrap(buf);
} }
async getElementData(windowId: number | undefined, offsetX: number = 0, offsetY: number = 0, token: CancellationToken): Promise<IElementData | undefined> {
const window = this.windowById(windowId, windowId);
if (!window?.win) {
return undefined;
}
// Find the simple browser webview
const allWebContents = webContents.getAllWebContents();
const simpleBrowserWebview = allWebContents.find(webContent => webContent.getTitle().includes('Simple Browser'));
if (!simpleBrowserWebview) {
return undefined;
}
const debuggers = simpleBrowserWebview.debugger;
debuggers.attach();
const { targetInfos } = await debuggers.sendCommand('Target.getTargets');
let resultId: string | undefined = undefined;
let target: typeof targetInfos[number] | undefined = undefined;
let targetSessionId: number | undefined = undefined;
try {
// find parent id and extract id
const matchingTarget = targetInfos.find((targetInfo: { url: string }) => {
const url = new URL(targetInfo.url);
return url.searchParams.get('parentId') === window?.id.toString();
});
if (matchingTarget) {
const url = new URL(matchingTarget.url);
resultId = url.searchParams.get('id')!;
}
// use id to grab simple browser target
if (resultId) {
target = targetInfos.find((targetInfo: { url: string }) => {
const url = new URL(targetInfo.url);
return url.searchParams.get('id') === resultId && url.searchParams.get('vscodeBrowserReqId')!;
});
}
const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', {
targetId: target.targetId,
flatten: true,
});
targetSessionId = sessionId;
await debuggers.sendCommand('DOM.enable', {}, sessionId);
await debuggers.sendCommand('CSS.enable', {}, sessionId);
await debuggers.sendCommand('Overlay.enable', {}, sessionId);
await debuggers.sendCommand('Debugger.enable', {}, sessionId);
await debuggers.sendCommand('Runtime.enable', {}, sessionId);
await debuggers.sendCommand('Runtime.evaluate', {
expression: `(function() {
const style = document.createElement('style');
style.id = '__pseudoBlocker__';
style.textContent = '*::before, *::after { pointer-events: none !important; }';
document.head.appendChild(style);
})();`,
}, sessionId);
// slightly changed default CDP debugger inspect colors
await debuggers.sendCommand('Overlay.setInspectMode', {
mode: 'searchForNode',
highlightConfig: {
showInfo: true,
showRulers: false,
showStyles: true,
showAccessibilityInfo: true,
showExtensionLines: false,
contrastAlgorithm: 'aa',
contentColor: { r: 173, g: 216, b: 255, a: 0.8 },
paddingColor: { r: 150, g: 200, b: 255, a: 0.5 },
borderColor: { r: 120, g: 180, b: 255, a: 0.7 },
marginColor: { r: 200, g: 220, b: 255, a: 0.4 },
eventTargetColor: { r: 130, g: 160, b: 255, a: 0.8 },
shapeColor: { r: 130, g: 160, b: 255, a: 0.8 },
shapeMarginColor: { r: 130, g: 160, b: 255, a: 0.5 },
gridHighlightConfig: {
rowGapColor: { r: 140, g: 190, b: 255, a: 0.3 },
rowHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
columnGapColor: { r: 140, g: 190, b: 255, a: 0.3 },
columnHatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
rowLineColor: { r: 120, g: 180, b: 255 },
columnLineColor: { r: 120, g: 180, b: 255 },
rowLineDash: true,
columnLineDash: true
},
flexContainerHighlightConfig: {
containerBorder: {
color: { r: 120, g: 180, b: 255 },
pattern: 'solid'
},
itemSeparator: {
color: { r: 140, g: 190, b: 255 },
pattern: 'solid'
},
lineSeparator: {
color: { r: 140, g: 190, b: 255 },
pattern: 'solid'
},
mainDistributedSpace: {
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
},
crossDistributedSpace: {
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
},
rowGapSpace: {
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
},
columnGapSpace: {
hatchColor: { r: 140, g: 190, b: 255, a: 0.7 },
fillColor: { r: 140, g: 190, b: 255, a: 0.4 }
}
},
flexItemHighlightConfig: {
baseSizeBox: {
hatchColor: { r: 130, g: 170, b: 255, a: 0.6 }
},
baseSizeBorder: {
color: { r: 120, g: 180, b: 255 },
pattern: 'solid'
},
flexibilityArrow: {
color: { r: 130, g: 190, b: 255 }
}
},
},
}, sessionId);
} catch (e) {
debuggers.detach();
throw new Error('No target found', e);
}
if (!targetSessionId) {
debuggers.detach();
throw new Error('No target session id found');
}
const nodeData = await this.getNodeData(targetSessionId, debuggers, window.win);
debuggers.detach();
const zoomFactor = simpleBrowserWebview.getZoomFactor();
const scaledBounds = {
x: (nodeData.bounds.x + offsetX) * zoomFactor,
y: (nodeData.bounds.y + offsetY) * zoomFactor,
width: nodeData.bounds.width * zoomFactor,
height: nodeData.bounds.height * zoomFactor
};
return { outerHTML: nodeData.outerHTML, computedStyle: nodeData.computedStyle, bounds: scaledBounds };
}
async getNodeData(sessionId: number, debuggers: any, window: BrowserWindow): Promise<NodeDataResponse> {
return new Promise((resolve, reject) => {
const onMessage = async (event: any, method: string, params: { backendNodeId: number }) => {
if (method === 'Overlay.inspectNodeRequested') {
await debuggers.sendCommand('Runtime.evaluate', {
expression: `(() => {
const style = document.getElementById('__pseudoBlocker__');
if (style) style.remove();
})();`,
}, sessionId);
this._register(debuggers.off('message', onMessage));
const backendNodeId = params?.backendNodeId;
if (!backendNodeId) {
throw new Error('Missing backendNodeId in inspectNodeRequested event');
}
try {
await debuggers.sendCommand('DOM.getDocument', {}, sessionId);
const { nodeIds } = await debuggers.sendCommand('DOM.pushNodesByBackendIdsToFrontend', { backendNodeIds: [backendNodeId] }, sessionId);
if (!nodeIds || nodeIds.length === 0) {
throw new Error('Failed to get node IDs.');
}
const nodeId = nodeIds[0];
const { model } = await debuggers.sendCommand('DOM.getBoxModel', { nodeId }, sessionId);
if (!model) {
throw new Error('Failed to get box model.');
}
const margin = model.margin;
const x = margin[0];
const y = margin[1] + 32.4 + 35; // 32.4 is height of the title bar, 35 is height of the tab bar
const width = margin[2] - margin[0];
const height = margin[5] - margin[1];
const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId);
if (!matched) {
throw new Error('Failed to get matched css.');
}
const formatted = this.formatMatchedStyles(matched);
const { outerHTML } = await debuggers.sendCommand('DOM.getOuterHTML', { nodeId }, sessionId);
if (!outerHTML) {
throw new Error('Failed to get outerHTML.');
}
resolve({
outerHTML,
computedStyle: formatted,
bounds: { x, y, width, height }
});
} catch (err) {
debuggers.detach();
reject(err);
}
}
};
window.webContents.on('ipc-message', async (event, channel) => {
if (channel === 'vscode:cancelElementSelection') {
this._register(debuggers.off('message', onMessage));
if (debuggers.isAttached()) {
debuggers.detach();
}
}
});
this._register(debuggers.on('message', onMessage));
});
}
formatMatchedStyles(matched: any): string {
const lines: string[] = [];
// inline
if (matched.inlineStyle?.cssProperties?.length) {
lines.push('/* Inline style */');
lines.push('element {');
for (const prop of matched.inlineStyle.cssProperties) {
if (prop.name && prop.value) {
lines.push(` ${prop.name}: ${prop.value};`);
}
}
lines.push('}\n');
}
// matched
if (matched.matchedCSSRules?.length) {
for (const ruleEntry of matched.matchedCSSRules) {
const rule = ruleEntry.rule;
const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', ');
lines.push(`/* Matched Rule from ${rule.origin} */`);
lines.push(`${selectors} {`);
for (const prop of rule.style.cssProperties) {
if (prop.name && prop.value) {
lines.push(` ${prop.name}: ${prop.value};`);
}
}
lines.push('}\n');
}
}
// inherited rules
if (matched.inherited?.length) {
let level = 1;
for (const inherited of matched.inherited) {
const rules = inherited.matchedCSSRules || [];
for (const ruleEntry of rules) {
const rule = ruleEntry.rule;
const selectors = rule.selectorList.selectors.map((s: any) => s.text).join(', ');
lines.push(`/* Inherited from ancestor level ${level} (${rule.origin}) */`);
lines.push(`${selectors} {`);
for (const prop of rule.style.cssProperties) {
if (prop.name && prop.value) {
lines.push(` ${prop.name}: ${prop.value};`);
}
}
lines.push('}\n');
}
level++;
}
}
return '\n' + lines.join('\n');
}
//#endregion //#endregion
@@ -488,7 +488,6 @@ export class AttachContextAction extends Action2 {
const fileService = accessor.get(IFileService); const fileService = accessor.get(IFileService);
const textModelService = accessor.get(ITextModelService); const textModelService = accessor.get(ITextModelService);
const quickInputService = accessor.get(IQuickInputService); const quickInputService = accessor.get(IQuickInputService);
const toAttach: IChatRequestVariableEntry[] = []; const toAttach: IChatRequestVariableEntry[] = [];
for (const pick of picks) { for (const pick of picks) {
@@ -630,6 +629,7 @@ export class AttachContextAction extends Action2 {
fullName: pick.label, fullName: pick.label,
value: resizedImage, value: resizedImage,
kind: 'image', kind: 'image',
references: [{ reference: pick.resource, kind: 'reference' }]
}); });
} }
} else { } else {
@@ -80,6 +80,7 @@ import { ChatEditingEditorAccessibility } from './chatEditing/chatEditingEditorA
import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js'; import { registerChatEditorActions } from './chatEditing/chatEditingEditorActions.js';
import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js'; import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js';
import { ChatEditingEditorOverlay } from './chatEditing/chatEditingEditorOverlay.js'; import { ChatEditingEditorOverlay } from './chatEditing/chatEditingEditorOverlay.js';
import { SimpleBrowserOverlay } from './chatEditing/simpleBrowserEditorOverlay.js';
import { ChatEditingService } from './chatEditing/chatEditingServiceImpl.js'; import { ChatEditingService } from './chatEditing/chatEditingServiceImpl.js';
import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditing/notebook/chatEditingNotebookFileSystemProvider.js'; import { ChatEditingNotebookFileSystemProviderContrib } from './chatEditing/notebook/chatEditingNotebookFileSystemProvider.js';
import { ChatEditor, IChatEditorOptions } from './chatEditor.js'; import { ChatEditor, IChatEditorOptions } from './chatEditor.js';
@@ -640,6 +641,7 @@ registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribu
registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEditorContextKeys, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore);
@@ -25,7 +25,7 @@ import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener
import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js'; import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js';
import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js'; import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js';
import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, INotebookOutputVariableEntry, OmittedState } from '../common/chatModel.js'; import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, OmittedState } from '../common/chatModel.js';
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';
import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js';
import { KeyCode } from '../../../../base/common/keyCodes.js'; import { KeyCode } from '../../../../base/common/keyCodes.js';
@@ -36,6 +36,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet
import { INotebookService } from '../../notebook/common/notebookService.js'; import { INotebookService } from '../../notebook/common/notebookService.js';
import { CellUri } from '../../notebook/common/notebookCommon.js'; import { CellUri } from '../../notebook/common/notebookCommon.js';
import { ThemeIcon } from '../../../../base/common/themables.js'; import { ThemeIcon } from '../../../../base/common/themables.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
abstract class AbstractChatAttachmentWidget extends Disposable { abstract class AbstractChatAttachmentWidget extends Disposable {
public readonly element: HTMLElement; public readonly element: HTMLElement;
@@ -542,3 +543,41 @@ export class NotebookCellOutputChatAttachmentWidget extends AbstractChatAttachme
} }
} }
export class ElementChatAttachmentWidget extends AbstractChatAttachmentWidget {
constructor(
attachment: IElementVariableEntry,
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
shouldFocusClearButton: boolean,
container: HTMLElement,
contextResourceLabels: ResourceLabels,
hoverDelegate: IHoverDelegate,
@ICommandService commandService: ICommandService,
@IOpenerService openerService: IOpenerService,
@IEditorService editorService: IEditorService,
) {
super(attachment, shouldFocusClearButton, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);
const ariaLabel = localize('chat.elementAttachment', "Attached element, {0}", attachment.name);
this.element.ariaLabel = ariaLabel;
this.element.style.position = 'relative';
this.element.style.cursor = 'pointer';
const attachmentLabel = attachment.name;
const withIcon = attachment.icon?.id ? `$(${attachment.icon.id})\u00A0${attachmentLabel}` : attachmentLabel;
this.label.setLabel(withIcon, undefined, { title: localize('chat.clickToViewContents', "Click to view the contents of: {0}", attachmentLabel) });
this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => {
const content = attachment.value?.toString() || '';
await editorService.openEditor({
resource: undefined,
contents: content,
options: {
pinned: true
}
});
}));
this.attachClearButton();
}
}
@@ -285,7 +285,7 @@ export class ChatDragAndDrop extends Themable {
const resource = URI.parse(url); const resource = URI.parse(url);
if (IMAGE_DATA_REGEX.test(url)) { if (IMAGE_DATA_REGEX.test(url)) {
return { data: await convertStringToUInt8Array(url), name: createDisplayName(), resource }; return { data: convertStringToUInt8Array(url), name: createDisplayName(), resource };
} }
if (URL_REGEX.test(url)) { if (URL_REGEX.test(url)) {
@@ -0,0 +1,295 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import '../media/simpleBrowserOverlay.css';
import { combinedDisposable, DisposableMap, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
import { autorun, derivedOpts, observableFromEvent, observableSignalFromEvent } from '../../../../../base/common/observable.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { localize } from '../../../../../nls.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { EditorGroupView } from '../../../../browser/parts/editor/editorGroupView.js';
import { Event } from '../../../../../base/common/event.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';
import { isEqual } from '../../../../../base/common/resources.js';
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { IHostService } from '../../../../services/host/browser/host.js';
import { IChatWidgetService, showChatView } from '../chat.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { Button } from '../../../../../base/browser/ui/button/button.js';
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
import { addDisposableListener } from '../../../../../base/browser/dom.js';
class SimpleBrowserOverlayWidget {
private readonly _domNode: HTMLElement;
private readonly _showStore = new DisposableStore();
constructor(
private readonly _editor: IEditorGroup,
private readonly _container: HTMLElement,
@IHostService private readonly _hostService: IHostService,
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
@IViewsService private readonly _viewService: IViewsService,
) {
this._domNode = document.createElement('div');
this._domNode.className = 'element-selection-message';
const message = document.createElement('span');
const startSelectionMessage = localize('elementSelectionMessage', 'Add UI element to chat.');
message.textContent = startSelectionMessage;
this._domNode.appendChild(message);
let cts: CancellationTokenSource;
const selectButton = new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('selectAnElement', 'Click to select an element.') });
const cancelButton = new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('cancelSelection', 'Click to cancel selection.') });
selectButton.element.className = 'element-selection-start';
selectButton.label = localize('startSelection', 'Start Selection');
cancelButton.element.className = 'element-selection-cancel';
cancelButton.label = localize('cancel', 'Cancel');
this.hideElement(cancelButton.element);
this._showStore.add(addDisposableListener(selectButton.element, 'click', async () => {
cts = new CancellationTokenSource();
this._editor.focus();
// start selection
message.textContent = localize('elementSelectionInProgress', 'Selection in progress...');
this.hideElement(selectButton.element);
this.showElement(cancelButton.element);
await this.addElementToChat(cts);
// stop selection
this.hideElement(cancelButton.element);
message.textContent = localize('elementSelectionComplete', 'Element added to chat.');
// wait 3 seconds before showing the start selection button again
setTimeout(() => {
message.textContent = startSelectionMessage;
this.showElement(selectButton.element);
}, 3000);
}));
this._showStore.add(addDisposableListener(cancelButton.element, 'click', () => {
cts.cancel();
this.hideElement(cancelButton.element);
message.textContent = localize('elementCancelMessage', 'Selection canceled');
setTimeout(() => {
message.textContent = startSelectionMessage;
this.showElement(selectButton.element);
}, 3000);
}));
}
hideElement(element: HTMLElement) {
element.classList.add('hidden');
}
showElement(element: HTMLElement) {
element.classList.remove('hidden');
}
async addElementToChat(cts: CancellationTokenSource) {
const rect = this._container.getBoundingClientRect();
const elementData = await this._hostService.getElementData(rect.x, rect.y, cts.token);
if (!elementData) {
throw new Error('Element data not found');
}
const bounds = elementData.bounds;
// remove container so we don't block anything on screenshot
this._domNode.style.display = 'none';
// Wait 1 extra frame to make sure overlay is gone
await new Promise(resolve => setTimeout(resolve, 100));
const screenshot = await this._hostService.getScreenshot(bounds);
if (!screenshot) {
throw new Error('Screenshot failed');
}
this._domNode.style.display = '';
const widget = this._chatWidgetService.lastFocusedWidget ?? await showChatView(this._viewService);
widget?.attachmentModel?.addContext({
id: 'element-' + Date.now(),
name: this.getDisplayNameFromOuterHTML(elementData.outerHTML),
fullName: this.getDisplayNameFromOuterHTML(elementData.outerHTML),
value: elementData.outerHTML + elementData.computedStyle,
kind: 'element',
icon: ThemeIcon.fromId(Codicon.layout.id),
}, {
id: 'element-screenshot-' + Date.now(),
name: 'Element Screenshot',
fullName: 'Element Screenshot',
kind: 'image',
value: screenshot.buffer
});
}
getDisplayNameFromOuterHTML(outerHTML: string): string {
const firstElementMatch = outerHTML.match(/^<(\w+)([^>]*?)>/);
if (!firstElementMatch) {
throw new Error('No outer element found');
}
const tagName = firstElementMatch[1];
const idMatch = firstElementMatch[2].match(/\s+id\s*=\s*["']([^"']+)["']/i);
const id = idMatch ? `#${idMatch[1]}` : '';
const classMatch = firstElementMatch[2].match(/\s+class\s*=\s*["']([^"']+)["']/i);
const className = classMatch ? `.${classMatch[1].replace(/\s+/g, '.')}` : '';
return `${tagName}${id}${className}`;
}
dispose() {
this.hide();
this._showStore.dispose();
}
getDomNode(): HTMLElement {
return this._domNode;
}
hide() {
this._showStore.clear();
}
}
class SimpleBrowserOverlayController {
private readonly _store = new DisposableStore();
private readonly _domNode = document.createElement('div');
constructor(
container: HTMLElement,
group: IEditorGroup,
@IInstantiationService instaService: IInstantiationService,
) {
this._domNode.classList.add('chat-editing-editor-overlay');
this._domNode.style.position = 'absolute';
this._domNode.style.bottom = `5px`;
this._domNode.style.right = `5px`;
this._domNode.style.zIndex = `100`;
const widget = instaService.createInstance(SimpleBrowserOverlayWidget, group, container);
this._domNode.appendChild(widget.getDomNode());
this._store.add(toDisposable(() => this._domNode.remove()));
this._store.add(widget);
const show = () => {
if (!container.contains(this._domNode)) {
container.appendChild(this._domNode);
}
};
const hide = () => {
if (container.contains(this._domNode)) {
widget.hide();
this._domNode.remove();
}
};
const activeEditorSignal = observableSignalFromEvent(this, Event.any(group.onDidActiveEditorChange, group.onDidModelChange));
const activeUriObs = derivedOpts({ equalsFn: isEqual }, r => {
activeEditorSignal.read(r); // signal
const editor = group.activeEditorPane;
if (editor?.input.editorId === 'mainThreadWebview-simpleBrowser.view') {
const uri = EditorResourceAccessor.getOriginalUri(editor?.input, { supportSideBySide: SideBySideEditor.PRIMARY });
return uri;
}
return undefined;
});
this._store.add(autorun(r => {
const data = activeUriObs.read(r);
if (!data) {
hide();
return;
}
// widget.show();
show();
}));
}
dispose(): void {
this._store.dispose();
}
}
export class SimpleBrowserOverlay implements IWorkbenchContribution {
static readonly ID = 'chat.simpleBrowser.overlay';
private readonly _store = new DisposableStore();
constructor(
@IEditorGroupsService editorGroupsService: IEditorGroupsService,
@IInstantiationService instantiationService: IInstantiationService,
) {
const editorGroups = observableFromEvent(
this,
Event.any(editorGroupsService.onDidAddGroup, editorGroupsService.onDidRemoveGroup),
() => editorGroupsService.groups
);
const overlayWidgets = new DisposableMap<IEditorGroup>();
this._store.add(autorun(r => {
const toDelete = new Set(overlayWidgets.keys());
const groups = editorGroups.read(r);
for (const group of groups) {
if (!(group instanceof EditorGroupView)) {
// TODO@jrieken better with https://github.com/microsoft/vscode/tree/ben/layout-group-container
continue;
}
toDelete.delete(group); // we keep the widget for this group!
if (!overlayWidgets.has(group)) {
const scopedInstaService = instantiationService.createChild(
new ServiceCollection([IContextKeyService, group.scopedContextKeyService])
);
const container = group.element;
const ctrl = scopedInstaService.createInstance(SimpleBrowserOverlayController, container, group);
overlayWidgets.set(group, combinedDisposable(ctrl, scopedInstaService));
}
}
for (const group of toDelete) {
overlayWidgets.deleteAndDispose(group);
}
}));
}
dispose(): void {
this._store.dispose();
}
}
@@ -71,7 +71,7 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd
import { IChatAgentService } from '../common/chatAgents.js'; import { IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatContextKeys } from '../common/chatContextKeys.js';
import { IChatEditingSession } from '../common/chatEditingService.js'; import { IChatEditingSession } from '../common/chatEditingService.js';
import { IChatRequestVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry } from '../common/chatModel.js';
import { IChatFollowup, IChatService } from '../common/chatService.js'; import { IChatFollowup, IChatService } from '../common/chatService.js';
import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatVariablesService } from '../common/chatVariables.js';
import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js';
@@ -85,7 +85,7 @@ import { PromptInstructionsAttachmentsCollectionWidget } from './attachments/pro
import { IChatWidget } from './chat.js'; import { IChatWidget } from './chat.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js';
import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js'; import { toChatVariable } from './chatAttachmentModel/chatPromptAttachmentsCollection.js';
import { DefaultChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget } from './chatAttachmentWidgets.js'; import { DefaultChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, ElementChatAttachmentWidget } from './chatAttachmentWidgets.js';
import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js';
import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js';
import { ChatDragAndDrop } from './chatDragAndDrop.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js';
@@ -1196,6 +1196,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate);
} else if (isImageVariableEntry(attachment)) { } else if (isImageVariableEntry(attachment)) {
attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate);
} else if (isElementVariableEntry(attachment)) {
attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate);
} else if (isPasteVariableEntry(attachment)) { } else if (isPasteVariableEntry(attachment)) {
attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate); attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, shouldFocusClearButton, container, this._contextResourceLabels, hoverDelegate);
} else { } else {
@@ -1713,6 +1713,9 @@ have to be updated for changes to the rules above, or to support more deeply nes
display: block; display: block;
max-height: 350px; max-height: 350px;
max-width: 100%; max-width: 100%;
min-width: 200px;
min-height: 200px;
} }
.chat-attached-context-hover .chat-attached-context-url { .chat-attached-context-hover .chat-attached-context-url {
@@ -1757,6 +1760,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 2px; border-radius: 2px;
object-fit: cover;
} }
.chat-attached-context-attachment .chat-attached-context-custom-text { .chat-attached-context-attachment .chat-attached-context-custom-text {
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.element-selection-message {
position: absolute;
bottom: 10px;
right: 10px;
padding: 8px 10px;
background: var(--vscode-notifications-background);
color: var(--vscode-notifications-foreground);
border-radius: 4px;
font-size: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
gap: 8px;
width: max-content
}
.element-selection-cancel,
.element-selection-start {
padding: 5px 10px;
width: fit-content;
}
.element-selection-message .hidden {
display: none;
}
@@ -196,9 +196,13 @@ export interface IDiagnosticVariableEntry extends IBaseChatRequestVariableEntry,
readonly kind: 'diagnostic'; readonly kind: 'diagnostic';
} }
export interface IElementVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'element';
}
export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry
| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry
| IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry; | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry;
export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {
return obj.kind === 'implicit'; return obj.kind === 'implicit';
@@ -216,6 +220,10 @@ export function isNotebookOutputVariableEntry(obj: IChatRequestVariableEntry): o
return obj.kind === 'notebookOutput'; return obj.kind === 'notebookOutput';
} }
export function isElementVariableEntry(obj: IChatRequestVariableEntry): obj is IElementVariableEntry {
return obj.kind === 'element';
}
export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry { export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {
return obj.kind === 'diagnostic'; return obj.kind === 'diagnostic';
} }
@@ -419,6 +419,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
// The extensionId and purpose in the URL are used for filtering in js-debug: // The extensionId and purpose in the URL are used for filtering in js-debug:
const params: { [key: string]: string } = { const params: { [key: string]: string } = {
id: this.id, id: this.id,
parentId: targetWindow.vscodeWindowId.toString(),
origin: this.origin, origin: this.origin,
swVersion: String(this._expectedServiceWorkerVersion), swVersion: String(this._expectedServiceWorkerVersion),
extensionId: extension?.id.value ?? '', extensionId: extension?.id.value ?? '',
@@ -42,6 +42,7 @@ import { isIOS, isMacintosh } from '../../../../base/common/platform.js';
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import { URI } from '../../../../base/common/uri.js'; import { URI } from '../../../../base/common/uri.js';
import { VSBuffer } from '../../../../base/common/buffer.js'; import { VSBuffer } from '../../../../base/common/buffer.js';
import { IElementData } from '../../../../platform/native/common/native.js';
enum HostShutdownReason { enum HostShutdownReason {
@@ -97,6 +98,7 @@ export class BrowserHostService extends Disposable implements IHostService {
this.registerListeners(); this.registerListeners();
} }
private registerListeners(): void { private registerListeners(): void {
// Veto shutdown depending on `window.confirmBeforeClose` setting // Veto shutdown depending on `window.confirmBeforeClose` setting
@@ -650,6 +652,14 @@ export class BrowserHostService extends Disposable implements IHostService {
} }
} }
async getElementData(): Promise<IElementData | undefined> {
return undefined;
}
async getBrowserId(): Promise<string | undefined> {
return undefined;
}
//#endregion //#endregion
//#region Native Handle //#region Native Handle
@@ -4,8 +4,10 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../base/common/buffer.js'; import { VSBuffer } from '../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Event } from '../../../../base/common/event.js'; import { Event } from '../../../../base/common/event.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IElementData } from '../../../../platform/native/common/native.js';
import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js';
export const IHostService = createDecorator<IHostService>('hostService'); export const IHostService = createDecorator<IHostService>('hostService');
@@ -128,7 +130,9 @@ export interface IHostService {
/** /**
* Captures a screenshot. * Captures a screenshot.
*/ */
getScreenshot(): Promise<VSBuffer | undefined>; getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined>;
getElementData(offsetX: number, offsetY: number, token: CancellationToken): Promise<IElementData | undefined>;
//#endregion //#endregion
@@ -5,7 +5,7 @@
import { Emitter, Event } from '../../../../base/common/event.js'; import { Emitter, Event } from '../../../../base/common/event.js';
import { IHostService } from '../browser/host.js'; import { IHostService } from '../browser/host.js';
import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IElementData, INativeHostService } from '../../../../platform/native/common/native.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
@@ -18,6 +18,8 @@ import { disposableWindowInterval, getActiveDocument, getWindowId, getWindowsCou
import { memoize } from '../../../../base/common/decorators.js'; import { memoize } from '../../../../base/common/decorators.js';
import { isAuxiliaryWindow } from '../../../../base/browser/window.js'; import { isAuxiliaryWindow } from '../../../../base/browser/window.js';
import { VSBuffer } from '../../../../base/common/buffer.js'; import { VSBuffer } from '../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ipcRenderer } from '../../../../base/parts/sandbox/electron-sandbox/globals.js';
class WorkbenchNativeHostService extends NativeHostService { class WorkbenchNativeHostService extends NativeHostService {
@@ -193,8 +195,17 @@ class WorkbenchHostService extends Disposable implements IHostService {
//#region Screenshots //#region Screenshots
getScreenshot(): Promise<VSBuffer | undefined> { getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined> {
return this.nativeHostService.getScreenshot(); return this.nativeHostService.getScreenshot(rect);
}
async getElementData(offsetX: number, offsetY: number, token: CancellationToken): Promise<IElementData | undefined> {
const disposable = token.onCancellationRequested(() => {
ipcRenderer.send('vscode:cancelElementSelection');
});
const elementData = this.nativeHostService.getElementData(offsetX, offsetY, token);
elementData.finally(() => disposable.dispose());
return elementData;
} }
//#endregion //#endregion
@@ -32,7 +32,7 @@ import { ILanguageService } from '../../../editor/common/languages/language.js';
import { IHistoryService } from '../../services/history/common/history.js'; import { IHistoryService } from '../../services/history/common/history.js';
import { IInstantiationService, ServiceIdentifier } from '../../../platform/instantiation/common/instantiation.js'; import { IInstantiationService, ServiceIdentifier } from '../../../platform/instantiation/common/instantiation.js';
import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js'; import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js';
import { MenuBarVisibility, IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions } from '../../../platform/window/common/window.js'; import { MenuBarVisibility, IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IRectangle } from '../../../platform/window/common/window.js';
import { TestWorkspace } from '../../../platform/workspace/test/common/testWorkspace.js'; import { TestWorkspace } from '../../../platform/workspace/test/common/testWorkspace.js';
import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js';
@@ -183,6 +183,7 @@ import { IHoverService } from '../../../platform/hover/browser/hover.js';
import { NullHoverService } from '../../../platform/hover/test/browser/nullHoverService.js'; import { NullHoverService } from '../../../platform/hover/test/browser/nullHoverService.js';
import { IActionViewItemService, NullActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js'; import { IActionViewItemService, NullActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js';
import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js';
import { IElementData } from '../../../platform/native/common/native.js';
export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined);
@@ -1577,7 +1578,8 @@ export class TestHostService implements IHostService {
async toggleFullScreen(): Promise<void> { } async toggleFullScreen(): Promise<void> { }
async getScreenshot(): Promise<VSBuffer | undefined> { return undefined; } async getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined> { return undefined; }
async getElementData(offsetX: number, offsetY: number, token: CancellationToken): Promise<IElementData | undefined> { return undefined; }
async getNativeWindowHandle(_windowId: number): Promise<VSBuffer | undefined> { return undefined; } async getNativeWindowHandle(_windowId: number): Promise<VSBuffer | undefined> { return undefined; }
@@ -6,7 +6,7 @@
import { Event } from '../../../base/common/event.js'; import { Event } from '../../../base/common/event.js';
import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestEncodingOracle, TestEnvironmentService, TestFileDialogService, TestFilesConfigurationService, TestFileService, TestLifecycleService, TestTextFileService } from '../browser/workbenchTestServices.js'; import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestEncodingOracle, TestEnvironmentService, TestFileDialogService, TestFilesConfigurationService, TestFileService, TestLifecycleService, TestTextFileService } from '../browser/workbenchTestServices.js';
import { ISharedProcessService } from '../../../platform/ipc/electron-sandbox/services.js'; import { ISharedProcessService } from '../../../platform/ipc/electron-sandbox/services.js';
import { INativeHostService, INativeHostOptions, IOSProperties, IOSStatistics } from '../../../platform/native/common/native.js'; import { INativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IElementData } from '../../../platform/native/common/native.js';
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../base/common/buffer.js';
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
import { URI } from '../../../base/common/uri.js'; import { URI } from '../../../base/common/uri.js';
@@ -166,7 +166,8 @@ export class TestNativeHostService implements INativeHostService {
async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise<boolean> { return false; } async hasClipboard(format: string, type?: 'selection' | 'clipboard' | undefined): Promise<boolean> { return false; }
async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> { return undefined; } async windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined> { return undefined; }
async profileRenderer(): Promise<any> { throw new Error(); } async profileRenderer(): Promise<any> { throw new Error(); }
async getScreenshot(): Promise<VSBuffer | undefined> { return undefined; } async getScreenshot(rect?: IRectangle): Promise<VSBuffer | undefined> { return undefined; }
async getElementData(offsetX: number, offsetY: number, token: CancellationToken): Promise<IElementData | undefined> { return undefined; }
} }
export class TestExtensionTipsService extends AbstractNativeExtensionTipsService { export class TestExtensionTipsService extends AbstractNativeExtensionTipsService {