Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/features/browserTabManagementFeatures.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize, localize2 } from '../../../../../nls.js';6import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';7import { ServicesAccessor, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';8import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';9import { KeyMod, KeyCode } from '../../../../../base/common/keyCodes.js';10import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';11import { IEditorGroupsService, GroupsOrder } from '../../../../services/editor/common/editorGroupsService.js';12import { EditorsOrder, GroupIdentifier } from '../../../../common/editor.js';13import { IQuickInputService, IQuickInputButton, IQuickPickItem, IQuickPickSeparator, QuickInputButtonLocation, IQuickPick } from '../../../../../platform/quickinput/common/quickInput.js';14import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';15import { URI } from '../../../../../base/common/uri.js';16import { Codicon } from '../../../../../base/common/codicons.js';17import { ThemeIcon } from '../../../../../base/common/themables.js';18import { BrowserViewUri } from '../../../../../platform/browserView/common/browserViewUri.js';19import { generateUuid } from '../../../../../base/common/uuid.js';20import { BrowserEditorInput } from '../../common/browserEditorInput.js';21import { BROWSER_EDITOR_ACTIVE, BrowserActionCategory, BrowserActionGroup } from '../browserViewActions.js';22import { logBrowserOpen } from '../../../../../platform/browserView/common/browserViewTelemetry.js';23import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';24import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';25import { BrowserViewCommandId } from '../../../../../platform/browserView/common/browserView.js';26import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';27import { IBrowserViewModel, IBrowserViewWorkbenchService } from '../../common/browserView.js';28import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../../platform/configuration/common/configurationRegistry.js';29import { workbenchConfigurationNodeBase } from '../../../../common/configuration.js';30import { IExternalOpener, IOpenerService } from '../../../../../platform/opener/common/opener.js';31import { isLocalhostAuthority } from '../../../../../platform/url/common/trustedDomains.js';32import { IConfigurationService, isConfigured } from '../../../../../platform/configuration/common/configuration.js';33import { ICommandService } from '../../../../../platform/commands/common/commands.js';34import { CancellationToken } from '../../../../../base/common/cancellation.js';35import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js';36import { Registry } from '../../../../../platform/registry/common/platform.js';37import { match } from '../../../../../base/common/glob.js';38import { $, addDisposableListener, EventType } from '../../../../../base/browser/dom.js';39import { BrowserEditor, BrowserEditorContribution, IBrowserEditorWidgetContribution } from '../browserEditor.js';40import { IHoverService } from '../../../../../platform/hover/browser/hover.js';41import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';42import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';43import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';44import { disposableTimeout } from '../../../../../base/common/async.js';45import { MarkdownString } from '../../../../../base/common/htmlContent.js';46import { IsSessionsWindowContext } from '../../../../common/contextkeys.js';4748const CONTEXT_BROWSER_EDITOR_OPEN = new RawContextKey<boolean>('browserEditorOpen', false, localize('browser.editorOpen', "Whether any browser editor is currently open"));4950interface IBrowserQuickPickItem extends IQuickPickItem {51groupId: GroupIdentifier;52editor: BrowserEditorInput;53}5455const closeButtonItem: IQuickInputButton = {56iconClass: ThemeIcon.asClassName(Codicon.close),57tooltip: localize('browser.closeTab', "Close")58};5960const closeAllButtonItem: IQuickInputButton = {61iconClass: ThemeIcon.asClassName(Codicon.closeAll),62tooltip: localize('browser.closeAllTabs', "Close All"),63location: QuickInputButtonLocation.Inline64};656667/**68* Manages a quick pick that lists all open browser tabs grouped by editor group,69* with close buttons, live updates, and an always-visible "New Integrated Browser Tab" entry.70*/71class BrowserTabQuickPick extends Disposable {7273private readonly _quickPick: IQuickPick<IBrowserQuickPickItem, { useSeparators: true }>;74private readonly _itemListeners = this._register(new DisposableStore());7576private readonly _openNewTabPick: IBrowserQuickPickItem = {77groupId: -1,78editor: undefined!,79label: localize('browser.openNewTab', "New Integrated Browser Tab"),80iconClass: ThemeIcon.asClassName(Codicon.add),81alwaysShow: true,82};8384constructor(85@IEditorService private readonly _editorService: IEditorService,86@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,87@IQuickInputService quickInputService: IQuickInputService,88@ITelemetryService telemetryService: ITelemetryService,89@IBrowserViewWorkbenchService private readonly _browserViewService: IBrowserViewWorkbenchService,90) {91super();9293this._quickPick = this._register(quickInputService.createQuickPick<IBrowserQuickPickItem>({ useSeparators: true }));94this._quickPick.placeholder = localize('browser.quickOpenPlaceholder', "Select a browser tab");95this._quickPick.matchOnDescription = true;96this._quickPick.sortByLabel = false;97this._quickPick.buttons = [closeAllButtonItem];9899this._register(this._quickPick.onDidTriggerItemButton(async ({ item }) => {100item.editor?.dispose(true);101}));102103this._register(this._quickPick.onDidTriggerButton(async () => {104for (const editor of this._browserViewService.getKnownBrowserViews().values()) {105editor.dispose(true);106}107}));108109this._register(this._quickPick.onDidAccept(async () => {110const [selected] = this._quickPick.selectedItems;111if (!selected) {112return;113}114if (selected === this._openNewTabPick) {115logBrowserOpen(telemetryService, 'quickOpenWithoutUrl');116this._quickPick.hide();117await this._editorService.openEditor({118resource: BrowserViewUri.forId(generateUuid()),119});120} else {121await this._editorService.openEditor(selected.editor, selected.groupId);122}123}));124125this._register(this._quickPick.onDidHide(() => this.dispose()));126}127128show(): void {129this._buildItems();130131// Pre-select the currently active browser editor132const activeEditor = this._editorService.activeEditor;133if (activeEditor instanceof BrowserEditorInput) {134const activePick = (this._quickPick.items as readonly (IBrowserQuickPickItem | IQuickPickSeparator)[])135.find((item): item is IBrowserQuickPickItem => item.type !== 'separator' && item.editor === activeEditor);136if (activePick) {137this._quickPick.activeItems = [activePick];138}139}140141this._quickPick.show();142}143144private _buildItems(): void {145this._itemListeners.clear();146147// Remember which editor was active so we can restore selection148const activeEditor = this._quickPick.activeItems[0]?.editor;149150const picks: (IBrowserQuickPickItem | IQuickPickSeparator)[] = [];151const groups = this._editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE);152153const groupsWithBrowserEditors = groups154.map(group => ({ group, browserEditors: group.editors.filter((e): e is BrowserEditorInput => e instanceof BrowserEditorInput) }))155.filter(({ browserEditors }) => browserEditors.length > 0);156157// Track which view IDs appear in at least one editor group158const viewsInGroups = new Set<string>();159for (const { browserEditors } of groupsWithBrowserEditors) {160for (const editor of browserEditors) {161viewsInGroups.add(editor.id);162}163}164165// Background views: known but not open in any editor group166const backgroundEditors = [...this._browserViewService.getKnownBrowserViews().values()].filter(e => !viewsInGroups.has(e.id));167const backgroundLabel = localize('browser.backgroundGroup', "Background");168169// Build sections: each editor group + optional background170type Section = { label: string; ariaLabel: string; groupId: number; editors: BrowserEditorInput[]; isPinned?: (e: BrowserEditorInput) => boolean };171const sections: Section[] = groupsWithBrowserEditors.map(({ group, browserEditors }) => ({172label: group.label,173ariaLabel: group.ariaLabel,174groupId: group.id,175editors: browserEditors,176isPinned: e => group.isPinned(e),177}));178if (backgroundEditors.length > 0) {179sections.push({ label: backgroundLabel, ariaLabel: backgroundLabel, groupId: ACTIVE_GROUP, editors: backgroundEditors });180}181for (const { group } of groupsWithBrowserEditors) {182this._itemListeners.add(group.onDidModelChange(() => this._buildItems()));183}184this._itemListeners.add(this._browserViewService.onDidChangeBrowserViews(() => this._buildItems()));185186const hasMultipleSections = sections.length > 1;187let newActivePick: IBrowserQuickPickItem | undefined;188189for (const section of sections) {190if (hasMultipleSections) {191picks.push({ type: 'separator', label: section.label });192}193for (const editor of section.editors) {194const icon = editor.getIcon();195const description = editor.getDescription();196const nameAndDescription = description ? `${editor.getName()} ${description}` : editor.getName();197const pick: IBrowserQuickPickItem = {198groupId: section.groupId,199editor,200label: editor.getName(),201ariaLabel: hasMultipleSections202? localize('browserEntryAriaLabelWithGroup', "{0}, {1}", nameAndDescription, section.ariaLabel)203: nameAndDescription,204description,205buttons: [closeButtonItem],206italic: section.isPinned ? !section.isPinned(editor) : undefined,207};208if (icon instanceof URI) {209pick.iconPath = { dark: icon };210} else if (icon) {211pick.iconClass = ThemeIcon.asClassName(icon);212}213picks.push(pick);214215if (editor === activeEditor) {216newActivePick = pick;217}218219this._itemListeners.add(editor.onDidChangeLabel(() => this._buildItems()));220}221}222223picks.push({ type: 'separator' });224picks.push(this._openNewTabPick);225226this._quickPick.keepScrollPosition = true;227this._quickPick.items = picks;228if (newActivePick) {229this._quickPick.activeItems = [newActivePick];230}231}232}233234class QuickOpenBrowserAction extends Action2 {235constructor() {236super({237id: BrowserViewCommandId.QuickOpen,238title: localize2('browser.quickOpenAction', "Quick Open Browser Tab..."),239icon: Codicon.globe,240category: BrowserActionCategory,241f1: true,242keybinding: {243weight: KeybindingWeight.WorkbenchContrib,244// Note: on Linux this conflicts with the "toggle block comment" keybinding.245// it's not as problem at the moment becase oh the `when`, but worth noting for the future.246primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA,247when: BROWSER_EDITOR_ACTIVE248},249});250}251252run(accessor: ServicesAccessor): void {253const picker = accessor.get(IInstantiationService).createInstance(BrowserTabQuickPick);254picker.show();255}256}257258interface IOpenBrowserOptions {259url?: string;260openToSide?: boolean;261262/**263* If set, the first existing tab with a URL matching this glob pattern will be reused / focused instead of opening a new tab.264*265* This is used by Live Preview extension to reuse tabs, especially after reload / restart.266*/267reuseUrlFilter?: string;268}269270class OpenIntegratedBrowserAction extends Action2 {271constructor() {272super({273id: BrowserViewCommandId.Open,274title: localize2('browser.openAction', "Open Integrated Browser"),275category: BrowserActionCategory,276icon: Codicon.globe,277f1: true,278});279}280281async run(accessor: ServicesAccessor, urlOrOptions?: string | IOpenBrowserOptions): Promise<void> {282const editorService = accessor.get(IEditorService);283const telemetryService = accessor.get(ITelemetryService);284const browserViewService = accessor.get(IBrowserViewWorkbenchService);285286// Parse arguments287const options = typeof urlOrOptions === 'string' ? { url: urlOrOptions } : (urlOrOptions ?? {});288const resource = BrowserViewUri.forId(generateUuid());289const group = options.openToSide ? SIDE_GROUP : ACTIVE_GROUP;290291if (options.reuseUrlFilter) {292const filterUri = URI.parse(options.reuseUrlFilter);293const matchingEditor = [...browserViewService.getKnownBrowserViews().values()].find((e) => {294const editorUri = URI.parse(e.url || '');295// URIs default to putting "file" scheme. Check that the scheme is really in the filter.296if (filterUri.scheme && options.reuseUrlFilter!.startsWith(`${filterUri.scheme}:`) && filterUri.scheme !== editorUri.scheme) {297return false;298}299if (filterUri.authority && !match(filterUri.authority, editorUri.authority)) {300return false;301}302if (filterUri.path && !match(filterUri.path, editorUri.path)) {303return false;304}305if (filterUri.query) {306const filterParams = new URLSearchParams(filterUri.query);307const editorParams = new URLSearchParams(editorUri.query);308if (![...filterParams].every(([key, value]) => match(value, editorParams.get(key) ?? ''))) {309return false;310}311}312313return true;314});315if (matchingEditor) {316if (options.url) {317matchingEditor.navigate(options.url);318}319await editorService.openEditor(matchingEditor);320return;321}322}323324logBrowserOpen(telemetryService, options.url ? 'commandWithUrl' : 'commandWithoutUrl');325326const editorPane = await editorService.openEditor({ resource, options: { viewState: { url: options.url } } }, group);327328// Lock the group when opening to the side329if (options.openToSide && editorPane?.group) {330editorPane.group.lock(true);331}332}333}334335class NewTabAction extends Action2 {336constructor() {337super({338id: BrowserViewCommandId.NewTab,339title: localize2('browser.newTabAction', "New Tab"),340category: BrowserActionCategory,341f1: true,342precondition: BROWSER_EDITOR_ACTIVE,343menu: {344id: MenuId.BrowserActionsToolbar,345group: BrowserActionGroup.Tabs,346order: 1,347},348// When already in a browser, Ctrl/Cmd + T opens a new tab349keybinding: {350weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over search actions351primary: KeyMod.CtrlCmd | KeyCode.KeyT,352}353});354}355356async run(accessor: ServicesAccessor, _browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {357const editorService = accessor.get(IEditorService);358const telemetryService = accessor.get(ITelemetryService);359const resource = BrowserViewUri.forId(generateUuid());360361logBrowserOpen(telemetryService, 'newTabCommand');362363await editorService.openEditor({ resource });364}365}366367class CloseAllBrowserTabsAction extends Action2 {368constructor() {369super({370id: BrowserViewCommandId.CloseAll,371title: localize2('browser.closeAll', "Close All Browser Tabs"),372category: BrowserActionCategory,373f1: true,374precondition: CONTEXT_BROWSER_EDITOR_OPEN,375});376}377378async run(accessor: ServicesAccessor): Promise<void> {379const editorGroupsService = accessor.get(IEditorGroupsService);380for (const group of editorGroupsService.getGroups(GroupsOrder.GRID_APPEARANCE)) {381const browserEditors = group.getEditors(EditorsOrder.SEQUENTIAL).filter((e): e is BrowserEditorInput => e instanceof BrowserEditorInput);382if (browserEditors.length > 0) {383await group.closeEditors(browserEditors);384}385}386}387}388389class CloseAllBrowserTabsInGroupAction extends Action2 {390constructor() {391super({392id: BrowserViewCommandId.CloseAllInGroup,393title: localize2('browser.closeAllInGroup', "Close All Browser Tabs in Group"),394category: BrowserActionCategory,395f1: true,396precondition: BROWSER_EDITOR_ACTIVE,397});398}399400async run(accessor: ServicesAccessor): Promise<void> {401const editorGroupsService = accessor.get(IEditorGroupsService);402const editorService = accessor.get(IEditorService);403const group = editorGroupsService.getGroup(editorService.activeEditorPane?.group?.id ?? editorGroupsService.activeGroup.id);404if (!group) {405return;406}407const browserEditors = group.getEditors(EditorsOrder.SEQUENTIAL).filter((e): e is BrowserEditorInput => e instanceof BrowserEditorInput);408if (browserEditors.length > 0) {409await group.closeEditors(browserEditors);410}411}412}413414class OpenOrListBrowsersAction extends Action2 {415constructor() {416super({417id: BrowserViewCommandId.OpenOrList,418title: localize2('browser.openOrListAction', "Browser"),419icon: Codicon.globe,420f1: false,421keybinding: {422weight: KeybindingWeight.WorkbenchContrib,423primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Slash,424},425menu: {426id: MenuId.TitleBar,427group: 'navigation',428order: 10,429when: ContextKeyExpr.and(430ContextKeyExpr.equals('config.workbench.browser.showInTitleBar', false).negate(),431ContextKeyExpr.or(432CONTEXT_BROWSER_EDITOR_OPEN,433// This is a hack to work around `true` just testing for truthiness of the key. It works since `1 == true` in JS.434ContextKeyExpr.equals('config.workbench.browser.showInTitleBar', 1)435)436),437}438});439}440441async run(accessor: ServicesAccessor): Promise<void> {442const browserViewService = accessor.get(IBrowserViewWorkbenchService);443const commandService = accessor.get(ICommandService);444445const hasOpenBrowserEditor = browserViewService.getKnownBrowserViews().size > 0;446447if (hasOpenBrowserEditor) {448await commandService.executeCommand(BrowserViewCommandId.QuickOpen);449return;450}451452await commandService.executeCommand(BrowserViewCommandId.Open);453}454}455456// Register in View menu457MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {458group: '4_auxbar',459command: {460id: BrowserViewCommandId.OpenOrList,461title: localize({ key: 'miOpenBrowser', comment: ['&& denotes a mnemonic'] }, "&&Browser")462},463order: 2464});465466// Register as "Close All Browser Tabs" action in editor title menu to align with the regular "Close All" action467MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: BrowserViewCommandId.CloseAllInGroup, title: localize('browser.closeAllInGroupShort', "Close All Browser Tabs") }, group: '1_close', order: 55, when: BROWSER_EDITOR_ACTIVE });468469registerAction2(QuickOpenBrowserAction);470registerAction2(OpenIntegratedBrowserAction);471registerAction2(OpenOrListBrowsersAction);472registerAction2(NewTabAction);473registerAction2(CloseAllBrowserTabsAction);474registerAction2(CloseAllBrowserTabsInGroupAction);475476registerAction2(class ToggleBrowserTitleBarButton extends ToggleTitleBarConfigAction {477constructor() {478super('workbench.browser.showInTitleBar', localize('toggle.browser', 'Integrated Browser'), localize('toggle.browserDescription', "Toggle visibility of the Integrated Browser button in title bar"), 8);479}480});481482/**483* Tracks whether any browser editor is open across all editor groups and484* keeps the `browserEditorOpen` context key in sync.485*/486class BrowserEditorOpenContextKeyContribution extends Disposable implements IWorkbenchContribution {487static readonly ID = 'workbench.contrib.browserEditorOpenContextKey';488489constructor(490@IContextKeyService contextKeyService: IContextKeyService,491@IBrowserViewWorkbenchService browserViewService: IBrowserViewWorkbenchService,492) {493super();494495const contextKey = CONTEXT_BROWSER_EDITOR_OPEN.bindTo(contextKeyService);496const update = () => contextKey.set(browserViewService.getKnownBrowserViews().size > 0);497498update();499this._register(browserViewService.onDidChangeBrowserViews(() => update()));500}501}502503registerWorkbenchContribution2(BrowserEditorOpenContextKeyContribution.ID, BrowserEditorOpenContextKeyContribution, WorkbenchPhase.AfterRestored);504505/**506* Opens localhost URLs in the Integrated Browser when the setting is enabled.507*/508class LocalhostLinkOpenerContribution extends Disposable implements IWorkbenchContribution, IExternalOpener {509static readonly ID = 'workbench.contrib.localhostLinkOpener';510511constructor(512@IOpenerService openerService: IOpenerService,513@IConfigurationService private readonly configurationService: IConfigurationService,514@IEditorService private readonly editorService: IEditorService,515@ITelemetryService private readonly telemetryService: ITelemetryService516) {517super();518519this._register(openerService.registerExternalOpener(this));520}521522async openExternal(href: string, _ctx: { sourceUri: URI; preferredOpenerId?: string }, _token: CancellationToken): Promise<boolean> {523if (!this.configurationService.getValue<boolean>('workbench.browser.openLocalhostLinks')) {524return false;525}526527try {528const parsed = new URL(href);529if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {530return false;531}532if (!isLocalhostAuthority(parsed.host)) {533return false;534}535} catch {536return false;537}538539logBrowserOpen(this.telemetryService, 'localhostLinkOpener');540541// Check whether the setting was explicitly set by the user or is still at its default value.542// When it is a default, tag the viewState so that the hint pill can be shown.543const isDefaultLinkOpen = !isConfigured(this.configurationService.inspect('workbench.browser.openLocalhostLinks'));544545const browserUri = BrowserViewUri.forId(generateUuid());546await this.editorService.openEditor({ resource: browserUri, options: { pinned: true, viewState: { url: href, isDefaultLinkOpen } } });547return true;548}549}550551registerWorkbenchContribution2(LocalhostLinkOpenerContribution.ID, LocalhostLinkOpenerContribution, WorkbenchPhase.BlockStartup);552553// ---- Link opened hint pill (URL bar widget) --------------------------------554555const LOCALHOST_HINT_DISMISSED_KEY = 'workbench.browser.linkOpenedHintDismissed';556557/**558* A small pill shown in the URL bar that informs the user their link was opened559* in the Integrated Browser by default. Clicking it shows a tooltip560* with an explanation and options to open settings or dismiss permanently.561*/562class LinkOpenedHintPill extends BrowserEditorContribution {563564private readonly _pill: HTMLElement;565private readonly _attentionTimeout = this._register(new MutableDisposable());566567constructor(568editor: BrowserEditor,569@IHoverService private readonly hoverService: IHoverService,570@IStorageService private readonly storageService: IStorageService,571@IPreferencesService private readonly preferencesService: IPreferencesService,572@IContextKeyService private readonly contextKeyService: IContextKeyService573) {574super(editor);575576this._pill = $('.browser-link-opened-hint-pill');577this._pill.tabIndex = 0;578this._pill.role = 'button';579this._pill.ariaLabel = localize('browser.linkOpenedHint.ariaLabel', "This link opened in the integrated browser");580this._pill.ariaHidden = 'true';581582const icon = $('span');583icon.className = ThemeIcon.asClassName(Codicon.info);584const label = $('span');585label.textContent = localize('browser.linkOpenedHint.label', "Link opened here");586587this._pill.appendChild(icon);588this._pill.appendChild(label);589590const hoverOptions = () => ({591content: new MarkdownString(localize('browser.linkOpenedHint.detail', "**Integrated Browser**\n\nLocalhost links automatically open in the integrated browser.")),592actions: [593{594label: localize('browser.linkOpenedHint.openSettings', "Open Settings"),595commandId: 'workbench.action.openSettings',596iconClass: ThemeIcon.asClassName(Codicon.settingsGear),597run: () => {598this.preferencesService.openUserSettings({ query: 'workbench.browser.openLocalhostLinks' });599}600},601{602label: localize('browser.linkOpenedHint.dismiss', "Don't Show Again"),603commandId: '',604run: () => {605this._dismiss();606}607}608],609position: { hoverPosition: HoverPosition.BELOW }610});611612this._register(this.hoverService.setupDelayedHover(this._pill, hoverOptions, { setupKeyboardEvents: true }));613this._register(addDisposableListener(this._pill, EventType.CLICK, () => {614this.hoverService.showInstantHover({ ...hoverOptions(), target: this._pill, persistence: { sticky: true } }, true);615}));616}617618override get urlBarWidgets(): readonly IBrowserEditorWidgetContribution[] {619return [{ element: this._pill, order: 100 }];620}621622protected override subscribeToModel(_model: IBrowserViewModel, _store: DisposableStore, isNew: boolean): void {623if (IsSessionsWindowContext.getValue(this.contextKeyService)) {624this._setVisible(false);625return;626}627628const input = this.editor.input;629if (input instanceof BrowserEditorInput && input.isDefaultLinkOpen) {630const dismissed = this.storageService.getBoolean(LOCALHOST_HINT_DISMISSED_KEY, StorageScope.APPLICATION, false);631this._setVisible(!dismissed);632if (!dismissed && isNew) {633this._callAttention();634}635} else {636this._setVisible(false);637}638}639640override clear(): void {641this._attentionTimeout.clear();642this._setVisible(false);643}644645private _setVisible(visible: boolean): void {646if (!visible) {647this._attentionTimeout.clear();648this._pill.classList.remove('attention');649}650this._pill.classList.toggle('visible', visible);651this._pill.ariaHidden = visible ? 'false' : 'true';652}653654private _callAttention(): void {655this._attentionTimeout.clear();656this._pill.classList.remove('attention');657// Start collapsed (icon only), expand after 300ms, then collapse back after another 2s658this._attentionTimeout.value = disposableTimeout(() => {659this._pill.classList.add('attention');660this._attentionTimeout.value = disposableTimeout(() => {661this._pill.classList.remove('attention');662}, 2000);663}, 300);664}665666private _dismiss(): void {667this.storageService.store(LOCALHOST_HINT_DISMISSED_KEY, true, StorageScope.APPLICATION, StorageTarget.USER);668this._setVisible(false);669}670}671672BrowserEditor.registerContribution(LinkOpenedHintPill);673674Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({675...workbenchConfigurationNodeBase,676properties: {677'workbench.browser.showInTitleBar': {678type: ['boolean', 'string'],679enum: [true, false, 'whenOpen'],680enumDescriptions: [681localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'browser.showInTitleBar.true' }, 'The button is always shown in the title bar.'),682localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'browser.showInTitleBar.false' }, 'The button is never shown in the title bar.'),683localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'browser.showInTitleBar.whenOpen' }, 'The button is shown in the title bar when a browser editor is open.')684],685default: 'whenOpen',686experiment: { mode: 'startup' },687description: localize(688{ comment: ['This is the description for a setting.'], key: 'browser.showInTitleBar' },689'Controls whether the Integrated Browser button is shown in the title bar.'690)691},692'workbench.browser.openLocalhostLinks': {693type: 'boolean',694default: false,695experiment: { mode: 'startup' },696markdownDescription: localize(697{ comment: ['This is the description for a setting.'], key: 'browser.openLocalhostLinks' },698'When enabled, localhost links from the terminal, chat, and other sources will open in the Integrated Browser instead of the system browser.'699)700}701}702});703704705