// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts

/* eslint-disable @typescript-eslint/no-explicit-any */

import { generateUuid } from '@theia/core/lib/common/uuid';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { PluginWorker } from './plugin-worker';
import { getPluginId, DeployedPlugin, HostedPluginServer } from '../../common/plugin-protocol';
import { HostedPluginWatcher } from './hosted-plugin-watcher';
import { ExtensionKind, MAIN_RPC_CONTEXT, PluginManagerExt, UIKind } from '../../common/plugin-api-rpc';
import { setUpPluginApi } from '../../main/browser/main-context';
import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol';
import {
    Disposable, DisposableCollection, isCancelled,
    CommandRegistry, WillExecuteCommandEvent,
    CancellationTokenSource, ProgressService, nls,
    RpcProxy
} from '@theia/core';
import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/common/preferences';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler';
import { getQueryParameters } from '../../main/browser/env-main';
import { getPreferences } from '../../main/browser/preference-registry-main';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { Event, WaitUntilEvent } from '@theia/core/lib/common/event';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry';
import { WillResolveTaskProvider, TaskProviderRegistry, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution';
import { TaskDefinitionRegistry } from '@theia/task/lib/browser/task-definition-registry';
import { WebviewEnvironment } from '../../main/browser/webview/webview-environment';
import { WebviewWidget } from '../../main/browser/webview/webview';
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import URI from '@theia/core/lib/common/uri';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store';
import { FileService, FileSystemProviderActivationEvent } from '@theia/filesystem/lib/browser/file-service';
import { PluginCustomEditorRegistry } from '../../main/browser/custom-editors/plugin-custom-editor-registry';
import { CustomEditorWidget } from '../../main/browser/custom-editors/custom-editor-widget';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageService';
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer';
import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel';
import { NotebookTypeRegistry, NotebookService, NotebookRendererMessagingService } from '@theia/notebook/lib/browser';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import {
    AbstractHostedPluginSupport, PluginContributions, PluginHost,
    ALL_ACTIVATION_EVENT, isConnectionScopedBackendPlugin
} from '../common/hosted-plugin';
import { isRemote } from '@theia/core/lib/browser/browser';
import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';

export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker' | 'onDebugDynamicConfigurations';

export const PluginProgressLocation = 'plugin';

@injectable()
export class HostedPluginSupport extends AbstractHostedPluginSupport<PluginManagerExt, RpcProxy<HostedPluginServer>> {

    protected static ADDITIONAL_ACTIVATION_EVENTS_ENV = 'ADDITIONAL_ACTIVATION_EVENTS';
    protected static BUILTIN_ACTIVATION_EVENTS = [
        '*',
        'onLanguage',
        'onCommand',
        'onDebug',
        'onDebugInitialConfigurations',
        'onDebugResolve',
        'onDebugAdapterProtocolTracker',
        'onDebugDynamicConfigurations',
        'onTaskType',
        'workspaceContains',
        'onView',
        'onUri',
        'onTerminalProfile',
        'onWebviewPanel',
        'onFileSystem',
        'onCustomEditor',
        'onStartupFinished',
        'onAuthenticationRequest',
        'onNotebook',
        'onNotebookSerializer'
    ];

    @inject(HostedPluginWatcher)
    protected readonly watcher: HostedPluginWatcher;

    @inject(PluginContributionHandler)
    protected readonly contributionHandler: PluginContributionHandler;

    @inject(PreferenceProviderProvider)
    protected readonly preferenceProviderProvider: PreferenceProviderProvider;

    @inject(PreferenceServiceImpl)
    protected readonly preferenceServiceImpl: PreferenceServiceImpl;

    @inject(WorkspaceService)
    protected readonly workspaceService: WorkspaceService;

    @inject(NotebookService)
    protected readonly notebookService: NotebookService;

    @inject(NotebookRendererMessagingService)
    protected readonly notebookRendererMessagingService: NotebookRendererMessagingService;

    @inject(CommandRegistry)
    protected readonly commands: CommandRegistry;

    @inject(DebugSessionManager)
    protected readonly debugSessionManager: DebugSessionManager;

    @inject(DebugConfigurationManager)
    protected readonly debugConfigurationManager: DebugConfigurationManager;

    @inject(FileService)
    protected readonly fileService: FileService;

    @inject(FileSearchService)
    protected readonly fileSearchService: FileSearchService;

    @inject(FrontendApplicationStateService)
    protected readonly appState: FrontendApplicationStateService;

    @inject(NotebookTypeRegistry)
    protected readonly notebookTypeRegistry: NotebookTypeRegistry;

    @inject(PluginViewRegistry)
    protected readonly viewRegistry: PluginViewRegistry;

    @inject(TaskProviderRegistry)
    protected readonly taskProviderRegistry: TaskProviderRegistry;

    @inject(TaskResolverRegistry)
    protected readonly taskResolverRegistry: TaskResolverRegistry;

    @inject(TaskDefinitionRegistry)
    protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;

    @inject(ProgressService)
    protected readonly progressService: ProgressService;

    @inject(WebviewEnvironment)
    protected readonly webviewEnvironment: WebviewEnvironment;

    @inject(WidgetManager)
    protected readonly widgets: WidgetManager;

    @inject(TerminalService)
    protected readonly terminalService: TerminalService;

    @inject(JsonSchemaStore)
    protected readonly jsonSchemaStore: JsonSchemaStore;

    @inject(PluginCustomEditorRegistry)
    protected readonly customEditorRegistry: PluginCustomEditorRegistry;

    @inject(ApplicationServer)
    protected readonly applicationServer: ApplicationServer;

    @inject(WorkspaceTrustService)
    protected readonly workspaceTrustService: WorkspaceTrustService;

    constructor() {
        super(generateUuid());
    }

    @postConstruct()
    protected override init(): void {
        super.init();

        this.workspaceService.onWorkspaceChanged(() => this.updateStoragePath());

        const languageService = (StandaloneServices.get(ILanguageService) as LanguageService);
        for (const language of languageService['_requestedBasicLanguages'] as Set<string>) {
            this.activateByLanguage(language);
        }
        languageService.onDidRequestBasicLanguageFeatures(language => this.activateByLanguage(language));
        this.commands.onWillExecuteCommand(event => this.ensureCommandHandlerRegistration(event));
        this.debugSessionManager.onWillStartDebugSession(event => this.ensureDebugActivation(event));
        this.debugSessionManager.onWillResolveDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugResolve', event.debugType));
        this.debugConfigurationManager.onWillProvideDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugInitialConfigurations'));
        // Activate providers of dynamic configurations. When a specific debugType is provided,
        // only activate that type's extension. Otherwise, activate all providers.
        this.debugConfigurationManager.onWillProvideDynamicDebugConfiguration(event => {
            const debugType = 'debugType' in event ? event.debugType : undefined;
            this.ensureDebugActivation(event, 'onDebugDynamicConfigurations', debugType ?? ALL_ACTIVATION_EVENT);
        });
        this.viewRegistry.onDidExpandView(id => this.activateByView(id));
        this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event));
        this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event));
        this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event));
        this.customEditorRegistry.onWillOpenCustomEditor(event => this.activateByCustomEditor(event));
        this.notebookService.onWillOpenNotebook(async event => this.activateByNotebook(event));
        this.notebookRendererMessagingService.onWillActivateRenderer(rendererId => this.activateByNotebookRenderer(rendererId));

        this.widgets.onDidCreateWidget(({ factoryId, widget }) => {
            // note: state restoration of custom editors is handled in `PluginCustomEditorRegistry.init`
            if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) {
                const storeState = widget.storeState.bind(widget);
                const restoreState = widget.restoreState.bind(widget);

                widget.storeState = () => {
                    if (this.webviewRevivers.has(widget.viewType)) {
                        return storeState();
                    }
                    return undefined;
                };

                widget.restoreState = state => {
                    if (state.viewType) {
                        restoreState(state);
                        this.preserveWebview(widget);
                    } else {
                        widget.dispose();
                    }
                };
            }
        });
    }

    protected createTheiaReadyPromise(): Promise<unknown> {
        return Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]);
    }

    protected override runOperation(operation: () => Promise<void>): Promise<void> {
        return this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad());
    }

    protected override afterStart(): void {
        this.watcher.onDidDeploy(() => this.load());
        this.server.onDidOpenConnection(() => this.load());
    }

    // Only load connection-scoped plugins
    protected acceptPlugin(plugin: DeployedPlugin): boolean {
        return isConnectionScopedBackendPlugin(plugin);
    }

    protected override async beforeSyncPlugins(toDisconnect: DisposableCollection): Promise<void> {
        await super.beforeSyncPlugins(toDisconnect);

        toDisconnect.push(Disposable.create(() => this.preserveWebviews()));
        this.server.onDidCloseConnection(() => toDisconnect.dispose());
    }

    protected override async beforeLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
        // Make sure the shell is attached so that registries (commands, menus, views, etc.)
        // are ready to accept contributions. We intentionally do NOT wait for initialized_layout
        // here, because layout restoration may depend on plugin-provided file system providers
        // (e.g. git: scheme for merge editors), and those providers are registered during
        // startPlugins which runs after this point. Waiting for initialized_layout would deadlock.
        await this.appState.reachedState('attached_shell');
        this.workspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust();
    }

    protected override async afterLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
        // Defer view initialization until the layout has been restored: initWidgets/removeStaleWidgets
        // must not run while the ShellLayoutRestorer still holds restored plugin view widgets, otherwise
        // removeStaleWidgets can dispose a restored view container mid-restore and abort the whole
        // layout restoration (see https://github.com/eclipse-theia/theia/issues/17770).
        // Deliberately not awaited: startPlugins runs after this hook and may register file system
        // providers the layout restoration depends on (see beforeLoadContributions).
        this.appState.reachedState('initialized_layout').then(async () => {
            if (toDisconnect.disposed) {
                return;
            }
            await this.viewRegistry.initWidgets();
            // remove restored plugin widgets which were not registered by contributions
            this.viewRegistry.removeStaleWidgets();
        }).catch(console.error);
        this.workspaceTrustService.refreshRestrictedModeIndicator();
    }

    protected handleContributions(plugin: DeployedPlugin): Disposable {
        return this.contributionHandler.handleContributions(this.clientId, plugin);
    }

    protected override handlePluginStarted(manager: PluginManagerExt, plugin: DeployedPlugin): void {
        this.activateByWorkspaceContains(manager, plugin);
    }

    protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise<PluginManagerExt | undefined> {
        let manager = this.managers.get(host);
        if (!manager) {
            const pluginId = getPluginId(hostContributions[0].plugin.metadata.model);
            const rpc = this.initRpc(host, pluginId);
            toDisconnect.push(rpc);

            manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT);
            this.managers.set(host, manager);
            toDisconnect.push(Disposable.create(() => this.managers.delete(host)));

            const [extApi, globalState, workspaceState, webviewResourceRoot, webviewCspSource, defaultShell, jsonValidation] = await Promise.all([
                this.server.getExtPluginAPI(),
                this.pluginServer.getAllStorageValues(undefined),
                this.pluginServer.getAllStorageValues({
                    workspace: this.workspaceService.workspace?.resource.toString(),
                    roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString())
                }),
                this.webviewEnvironment.resourceRoot(host),
                this.webviewEnvironment.cspSource(),
                this.terminalService.getDefaultShell(),
                this.jsonSchemaStore.schemas
            ]);
            if (toDisconnect.disposed) {
                return undefined;
            }

            const isElectron = environment.electron.is();

            const supportedActivationEvents = [...HostedPluginSupport.BUILTIN_ACTIVATION_EVENTS];
            const [additionalActivationEvents, appRoot] = await Promise.all([
                this.envServer.getValue(HostedPluginSupport.ADDITIONAL_ACTIVATION_EVENTS_ENV),
                this.applicationServer.getApplicationRoot()
            ]);
            if (additionalActivationEvents && additionalActivationEvents.value) {
                additionalActivationEvents.value.split(',').forEach(event => supportedActivationEvents.push(event));
            }

            await manager.$init({
                preferences: getPreferences(this.preferenceProviderProvider, this.workspaceService.tryGetRoots()),
                globalState,
                workspaceState,
                env: {
                    queryParams: getQueryParameters(),
                    language: nls.locale || nls.defaultLocale,
                    shell: defaultShell,
                    uiKind: isElectron ? UIKind.Desktop : UIKind.Web,
                    appName: FrontendApplicationConfigProvider.get().applicationName,
                    appHost: isElectron ? 'desktop' : 'web', // TODO: 'web' could be the embedder's name, e.g. 'github.dev'
                    appRoot,
                    appUriScheme: FrontendApplicationConfigProvider.get().electron.uriScheme
                },
                extApi,
                webview: {
                    webviewResourceRoot,
                    webviewCspSource
                },
                jsonValidation,
                pluginKind: isRemote ? ExtensionKind.Workspace : ExtensionKind.UI,
                supportedActivationEvents
            });
            if (toDisconnect.disposed) {
                return undefined;
            }
            this.activationEvents.forEach(event => manager!.$activateByEvent(event));
        }
        return manager;
    }

    protected initRpc(host: PluginHost, pluginId: string): RPCProtocol {
        const rpc = host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(host);
        setUpPluginApi(rpc, this.container);
        this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container));
        return rpc;
    }

    protected createServerRpc(pluginHostId: string): RPCProtocol {

        const channel = new BasicChannel(() => {
            const writer = new Uint8ArrayWriteBuffer();
            writer.onCommit(buffer => {
                this.server.onMessage(pluginHostId, buffer);
            });
            return writer;
        });

        // Create RPC protocol before adding the listener to the watcher to receive the watcher's cached messages after the rpc protocol was created.
        const rpc = new RPCProtocolImpl(channel);

        this.watcher.onPostMessageEvent(received => {
            if (pluginHostId === received.pluginHostId) {
                channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(received.message));
            }
        });

        return rpc;
    }

    protected async updateStoragePath(): Promise<void> {
        const path = await this.getStoragePath();
        for (const manager of this.managers.values()) {
            manager.$updateStoragePath(path);
        }
    }

    protected async getStoragePath(): Promise<string | undefined> {
        const roots = await this.workspaceService.roots;
        return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace?.resource.toString(), roots.map(root => root.resource.toString()));
    }

    protected async getHostGlobalStoragePath(): Promise<string> {
        const configDirUri = await this.envServer.getConfigDirUri();
        const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage');

        // Make sure that folder by the path exists
        if (!await this.fileService.exists(globalStorageFolderUri)) {
            await this.fileService.createFolder(globalStorageFolderUri, { fromUserGesture: false });
        }
        const globalStorageFolderFsPath = await this.fileService.fsPath(globalStorageFolderUri);
        if (!globalStorageFolderFsPath) {
            throw new Error(`Could not resolve the FS path for URI: ${globalStorageFolderUri}`);
        }
        return globalStorageFolderFsPath;
    }

    async activateByViewContainer(viewContainerId: string): Promise<void> {
        await Promise.all(this.viewRegistry.getContainerViews(viewContainerId).map(viewId => this.activateByView(viewId)));
    }

    async activateByView(viewId: string): Promise<void> {
        await this.activateByEvent(`onView:${viewId}`);
    }

    async activateByLanguage(languageId: string): Promise<void> {
        await this.activateByEvent('onLanguage');
        await this.activateByEvent(`onLanguage:${languageId}`);
    }

    async activateByUri(scheme: string, authority: string): Promise<void> {
        await this.activateByEvent(`onUri:${scheme}://${authority}`);
    }

    async activateByCommand(commandId: string): Promise<void> {
        await this.activateByEvent(`onCommand:${commandId}`);
    }

    async activateByTaskType(taskType: string): Promise<void> {
        await this.activateByEvent(`onTaskType:${taskType}`);
    }

    async activateByCustomEditor(viewType: string): Promise<void> {
        await this.activateByEvent(`onCustomEditor:${viewType}`);
    }

    async activateByNotebook(viewType: string): Promise<void> {
        await this.activateByEvent(`onNotebook:${viewType}`);
    }

    async activateByNotebookSerializer(viewType: string): Promise<void> {
        await this.activateByEvent(`onNotebookSerializer:${viewType}`);
    }

    async activateByNotebookRenderer(rendererId: string): Promise<void> {
        await this.activateByEvent(`onRenderer:${rendererId}`);
    }

    activateByFileSystem(event: FileSystemProviderActivationEvent): Promise<void> {
        return this.activateByEvent(`onFileSystem:${event.scheme}`);
    }

    activateByTerminalProfile(profileId: string): Promise<void> {
        return this.activateByEvent(`onTerminalProfile:${profileId}`);
    }

    protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void {
        event.waitUntil((async () => {
            // Wait until plugins are synced so that activation events are recorded
            // and will be replayed when managers start. This does not depend on
            // layout initialization, so it cannot deadlock.
            await this.willStart;
            await this.activateByFileSystem(event);
            if (!this.fileService.hasProvider(event.scheme)) {
                return waitForEvent(Event.filter(this.fileService.onDidChangeFileSystemProviderRegistrations,
                    ({ added, scheme }) => added && scheme === event.scheme), 3000);
            }
        })());
    }

    protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void {
        const activation = this.activateByCommand(event.commandId);
        if (this.commands.getCommand(event.commandId) &&
            (!this.contributionHandler.hasCommand(event.commandId) ||
                this.contributionHandler.hasCommandHandler(event.commandId))) {
            return;
        }
        const waitForCommandHandler = new Deferred<void>();
        const listener = this.contributionHandler.onDidRegisterCommandHandler(id => {
            if (id === event.commandId) {
                listener.dispose();
                waitForCommandHandler.resolve();
            }
        });
        const p = Promise.all([
            activation,
            waitForCommandHandler.promise
        ]);
        p.then(() => listener.dispose(), () => listener.dispose());
        event.waitUntil(p);
    }

    protected ensureTaskActivation(event: WillResolveTaskProvider): void {
        const promises = [this.activateByCommand('workbench.action.tasks.runTask')];
        const taskType = event.taskType;
        if (taskType) {
            if (taskType === ALL_ACTIVATION_EVENT) {
                for (const taskDefinition of this.taskDefinitionRegistry.getAll()) {
                    promises.push(this.activateByTaskType(taskDefinition.taskType));
                }
            } else {
                promises.push(this.activateByTaskType(taskType));
            }
        }

        event.waitUntil(Promise.all(promises));
    }

    protected ensureDebugActivation(event: WaitUntilEvent, activationEvent?: DebugActivationEvent, debugType?: string): void {
        event.waitUntil(this.activateByDebug(activationEvent, debugType));
    }

    async activateByDebug(activationEvent?: DebugActivationEvent, debugType?: string): Promise<void> {
        const promises = [this.activateByEvent('onDebug')];
        if (activationEvent) {
            promises.push(this.activateByEvent(activationEvent));
            if (debugType) {
                promises.push(this.activateByEvent(activationEvent + ':' + debugType));
            }
        }
        await Promise.all(promises);
    }

    protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: DeployedPlugin): Promise<void> {
        const activationEvents = plugin.contributes && plugin.contributes.activationEvents;
        if (!activationEvents) {
            return;
        }
        const paths: string[] = [];
        const includePatterns: string[] = [];
        // should be aligned with https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts#L460-L469
        for (const activationEvent of activationEvents) {
            if (/^workspaceContains:/.test(activationEvent)) {
                const fileNameOrGlob = activationEvent.substring('workspaceContains:'.length);
                if (fileNameOrGlob.indexOf(ALL_ACTIVATION_EVENT) >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
                    includePatterns.push(fileNameOrGlob);
                } else {
                    paths.push(fileNameOrGlob);
                }
            }
        }
        const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.metadata.model.id}`);
        const promises: Promise<boolean>[] = [];
        if (paths.length) {
            promises.push(this.workspaceService.containsSome(paths));
        }
        if (includePatterns.length) {
            const tokenSource = new CancellationTokenSource();
            const searchTimeout = setTimeout(() => {
                tokenSource.cancel();
                // activate eagerly if took to long to search
                activatePlugin();
            }, 7000);
            promises.push((async () => {
                try {
                    const result = await this.fileSearchService.find('', {
                        rootUris: this.workspaceService.tryGetRoots().map(r => r.resource.toString()),
                        includePatterns,
                        limit: 1
                    }, tokenSource.token);
                    return result.length > 0;
                } catch (e) {
                    if (!isCancelled(e)) {
                        console.error(e);
                    }
                    return false;
                } finally {
                    clearTimeout(searchTimeout);
                }
            })());
        }
        if (promises.length && await Promise.all(promises).then(exists => exists.some(v => v))) {
            await activatePlugin();
        }
    }

    protected readonly webviewsToRestore = new Map<string, WebviewWidget>();
    protected readonly webviewRevivers = new Map<string, (webview: WebviewWidget) => Promise<void>>();

    registerWebviewReviver(viewType: string, reviver: (webview: WebviewWidget) => Promise<void>): void {
        if (this.webviewRevivers.has(viewType)) {
            throw new Error(`Reviver for ${viewType} already registered`);
        }
        this.webviewRevivers.set(viewType, reviver);

        if (this.webviewsToRestore.has(viewType)) {
            this.restoreWebview(this.webviewsToRestore.get(viewType) as WebviewWidget);
        }
    }

    unregisterWebviewReviver(viewType: string): void {
        this.webviewRevivers.delete(viewType);
    }

    protected async preserveWebviews(): Promise<void> {
        for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) {
            this.preserveWebview(webview as WebviewWidget);
        }
        for (const webview of this.widgets.getWidgets(CustomEditorWidget.FACTORY_ID)) {
            (webview as CustomEditorWidget).modelRef.dispose();
            if ((webview as any)['closeWithoutSaving']) {
                delete (webview as any)['closeWithoutSaving'];
            }
            this.customEditorRegistry.resolveWidget(webview as CustomEditorWidget);
        }
    }

    protected preserveWebview(webview: WebviewWidget): void {
        if (!this.webviewsToRestore.has(webview.viewType)) {
            this.activateByEvent(`onWebviewPanel:${webview.viewType}`);
            this.webviewsToRestore.set(webview.viewType, webview);
            webview.disposed.connect(() => this.webviewsToRestore.delete(webview.viewType));
        }
    }

    protected async restoreWebview(webview: WebviewWidget): Promise<void> {
        const restore = this.webviewRevivers.get(webview.viewType);
        if (restore) {
            try {
                await restore(webview);
            } catch (e) {
                webview.setHTML(this.getDeserializationFailedContents(`
                An error occurred while restoring '${webview.viewType}' view. Please check logs.
                `));
                console.error('Failed to restore the webview', e);
            }
        }
    }

    protected getDeserializationFailedContents(message: string): string {
        return `<!DOCTYPE html>
        <html>
            <head>
                <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
                <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
            </head>
            <body>${message}</body>
        </html>`;
    }

}
