/********************************************************************************
 * Copyright (C) 2018 TypeFox 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 WITH Classpath-exception-2.0
 ********************************************************************************/ import { Widget } from '@theia/core/shared/@phosphor/widgets' ; import { injectable , inject , optional } from '@theia/core/shared/inversify' ; import URI from '@theia/core/lib/common/uri' ; import { MaybePromise } from '@theia/core/lib/common/types' ; import { codicon , QuickInputService } from '@theia/core/lib/browser' ; import { ApplicationShell } from '@theia/core/lib/browser/shell' ; import { Command , CommandContribution , CommandRegistry } from '@theia/core/lib/common/command' ; import { MenuContribution , MenuModelRegistry } from '@theia/core/lib/common/menu' ; import { TabBarToolbarContribution , TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar' ; import { NavigatableWidget , NavigatableWidgetOpenHandler } from '@theia/core/lib/browser/navigatable' ; import { open , OpenerService } from '@theia/core/lib/browser/opener-service' ; import { LabelProvider } from '@theia/core/lib/browser/label-provider' ; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application' ; import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler' ; import { MiniBrowserService } from '../common/mini-browser-service' ; import { MiniBrowser , MiniBrowserProps } from './mini-browser' ; import { LocationMapperService } from './location-mapper-service' ; import { nls } from '@theia/core/lib/common/nls' ; export namespace MiniBrowserCommands { export const PREVIEW_CATEGORY = 'Preview' ; export const PREVIEW_CATEGORY_KEY = nls . getDefaultKey ( PREVIEW_CATEGORY ) ; export const PREVIEW = Command . toLocalizedCommand ( { id : 'mini-browser.preview' , label : 'Open Preview' , iconClass : codicon ( 'open-preview' ) } , 'vscode.markdown-language-features/package/markdown.preview.title' ) ; export const OPEN_SOURCE = { id : 'mini-browser.open.source' , iconClass : codicon ( 'go-to-file' ) } ; export const OPEN_URL = Command . toDefaultLocalizedCommand ( { id : 'mini-browser.openUrl' , category : PREVIEW_CATEGORY , label : 'Open URL' } ) ; } /**
 * Further options for opening a new `Mini Browser` widget.
 */ export interface MiniBrowserOpenerOptions extends WidgetOpenerOptions , MiniBrowserProps { /**
     * Controls how the mini-browser widget should be opened.
     * - `source`: editable source.
     * - `preview`: rendered content of the source.
     */ openFor ? : 'source' | 'preview' ; } @ injectable ( ) export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler < MiniBrowser > implements FrontendApplicationContribution , CommandContribution , MenuContribution , TabBarToolbarContribution { static PREVIEW_URI = new URI ( ) . withScheme ( '__minibrowser__preview__' ) ; /**
     * Instead of going to the backend with each file URI to ask whether it can handle the current file or not,
     * we have this map of extension and priority pairs that we populate at application startup.
     * The real advantage of this approach is the following: [Phosphor cannot run async code when invoking `isEnabled`/`isVisible`
     * for the command handlers](https://github.com/eclipse-theia/theia/issues/1958#issuecomment-392829371)
     * so the menu item would be always visible for the user even if the file type cannot be handled eventually.
     * Hopefully, we could get rid of this hack once we have migrated the existing Phosphor code to [React](https://github.com/eclipse-theia/theia/issues/1915).
     */ protected readonly supportedExtensions : Map < string , number > = new Map ( ) ; readonly id = MiniBrowser . ID ; readonly label = nls . localize ( MiniBrowserCommands . PREVIEW_CATEGORY_KEY , MiniBrowserCommands . PREVIEW_CATEGORY ) ; @ inject ( OpenerService ) protected readonly openerService : OpenerService ; @ inject ( LabelProvider ) protected readonly labelProvider : LabelProvider ; @ inject ( QuickInputService ) @ optional ( ) protected readonly quickInputService : QuickInputService ; @ inject ( MiniBrowserService ) protected readonly miniBrowserService : MiniBrowserService ; @ inject ( LocationMapperService ) protected readonly locationMapperService : LocationMapperService ; onStart ( ) { this . miniBrowserService . supportedFileExtensions ( ) . then ( entries => { entries . forEach ( entry => { const { extension , priority } = entry ; this . supportedExtensions . set ( extension , priority ) ; } ) ; } ) ; } canHandle ( uri , options ? ) { const extension = uri . toString ( ) . split ( '.' ) . pop ( ) ; if ( ! extension ) { return 0 ; } if ( options ?. openFor === 'source' ) { return - 100 ; } else if ( options ?. openFor === 'preview' ) { return 200 ; } else { return this . supportedExtensions . get ( extension . toLocaleLowerCase ( ) ) || 0 ; } } async open ( uri , options ? ) { const widget = await super . open ( uri , options ) ; const area = this . shell . getAreaFor ( widget ) ; if ( area === 'right' || area === 'left' ) { const panelLayout = area === 'right' ? this . shell . getLayoutData ( ) . rightPanel : this . shell . getLayoutData ( ) . leftPanel ; const minSize = this . shell . mainPanel . node . offsetWidth / 2 ; if ( panelLayout && panelLayout . size && panelLayout . size <= minSize ) { requestAnimationFrame ( ( ) => this . shell . resize ( minSize , area ) ) ; } } return widget ; } protected async getOrCreateWidget ( uri , options ? ) { const props = await this . options ( uri , options ) ; const widget = await super . getOrCreateWidget ( uri , props ) ; widget . setProps ( props ) ; return widget ; } protected async options ( uri ? , options ? ) { let result = await this . defaultOptions ( ) ; if ( uri ) { const startPage = uri . toString ( true ) ; const name = this . labelProvider . getName ( uri ) ; const iconClass = `${ this . labelProvider . getIcon ( uri ) } file-icon` ; const resetBackground = await this . resetBackground ( uri ) ; result = { ... result , startPage , name , iconClass , toolbar : 'hide' , resetBackground } ; } if ( options ) { result = { ... result , ... options } ; } return result ; } protected resetBackground ( uri ) { const { scheme } = uri ; const uriStr = uri . toString ( ) ; return scheme === 'http' || scheme === 'https' || ( scheme === 'file' && ( uriStr . endsWith ( 'html' ) || uriStr . endsWith ( '.htm' ) ) ) ; } protected async defaultOptions ( ) { return { mode : 'activate' , widgetOptions : { area : 'main' } , sandbox : MiniBrowserProps . SandboxOptions . DEFAULT , toolbar : 'show' } ; } registerCommands ( commands ) { commands . registerCommand ( MiniBrowserCommands . PREVIEW , { execute : widget => this . preview ( widget ) , isEnabled : widget => this . canPreviewWidget ( widget ) , isVisible : widget => this . canPreviewWidget ( widget ) } ) ; commands . registerCommand ( MiniBrowserCommands . OPEN_SOURCE , { execute : widget => this . openSource ( widget ) , isEnabled : widget => ! ! this . getSourceUri ( widget ) , isVisible : widget => ! ! this . getSourceUri ( widget ) } ) ; commands . registerCommand ( MiniBrowserCommands . OPEN_URL , { execute : ( arg ? ) => this . openUrl ( arg ) } ) ; } registerMenus ( menus ) { menus . registerMenuAction ( [ 'editor_context_menu' , 'navigation' ] , { commandId : MiniBrowserCommands . PREVIEW . id } ) ; } registerToolbarItems ( toolbar ) { toolbar . registerItem ( { id : MiniBrowserCommands . PREVIEW . id , command : MiniBrowserCommands . PREVIEW . id , tooltip : nls . localize ( 'vscode.markdown-language-features/package/markdown.previewSide.title' , 'Open Preview to the Side' ) } ) ; toolbar . registerItem ( { id : MiniBrowserCommands . OPEN_SOURCE . id , command : MiniBrowserCommands . OPEN_SOURCE . id , tooltip : nls . localize ( 'vscode.markdown-language-features/package/markdown.showSource.title' , 'Open Source' ) } ) ; } protected canPreviewWidget ( widget ? ) { const uri = this . getUriToPreview ( widget ) ; return ! ! uri && ! ! this . canHandle ( uri ) ; } protected getUriToPreview ( widget ? ) { const current = this . getWidgetToPreview ( widget ) ; return current && current . getResourceUri ( ) ; } protected getWidgetToPreview ( widget ? ) { const current = widget ? widget : this . shell . currentWidget ; return ! ( current instanceof MiniBrowser ) && NavigatableWidget . is ( current ) && current || undefined ; } protected async preview ( widget ? ) { const ref = this . getWidgetToPreview ( widget ) ; if ( ! ref ) { return ; } const uri = ref . getResourceUri ( ) ; if ( ! uri ) { return ; } await this . open ( uri , { mode : 'reveal' , widgetOptions : { ref , mode : 'open-to-right' } , openFor : 'preview' } ) ; } protected async openSource ( ref ? ) { const uri = this . getSourceUri ( ref ) ; if ( uri ) { await open ( this . openerService , uri , { widgetOptions : { ref , mode : 'tab-after' } , openFor : 'source' } ) ; } } protected getSourceUri ( ref ? ) { const uri = ref instanceof MiniBrowser && ref . getResourceUri ( ) || undefined ; if ( ! uri || uri . scheme === 'http' || uri . scheme === 'https' || uri . isEqual ( MiniBrowserOpenHandler . PREVIEW_URI ) ) { return undefined ; } return uri ; } protected async openUrl ( arg ? ) { const url = arg ? arg : await this . quickInputService ?. input ( { prompt : nls . localizeByDefault ( 'URL to open' ) , placeHolder : nls . localize ( 'theia/mini-browser/typeUrl' , 'Type a URL' ) } ) ; if ( url ) { await this . openPreview ( url ) ; } } async openPreview ( startPage ) { const props = await this . getOpenPreviewProps ( await this . locationMapperService . map ( startPage ) ) ; return this . open ( MiniBrowserOpenHandler . PREVIEW_URI , props ) ; } protected async getOpenPreviewProps ( startPage ) { const resetBackground = await this . resetBackground ( new URI ( startPage ) ) ; return { name : nls . localize ( MiniBrowserCommands . PREVIEW_CATEGORY_KEY , MiniBrowserCommands . PREVIEW_CATEGORY ) , startPage , toolbar : 'read-only' , widgetOptions : { area : 'right' } , resetBackground , iconClass : codicon ( 'preview' ) , openFor : 'preview' } ; } }