/********************************************************************************
 * Copyright (C) 2017 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 { injectable , inject , named } from 'inversify' ; import { Widget } from '@phosphor/widgets' ; import { FrontendApplication } from '../frontend-application' ; import { WidgetManager , WidgetConstructionOptions } from '../widget-manager' ; import { StorageService } from '../storage-service' ; import { ILogger } from '../../common/logger' ; import { CommandContribution , CommandRegistry , Command } from '../../common/command' ; import { ThemeService } from '../theming' ; import { ContributionProvider } from '../../common/contribution-provider' ; import { MaybePromise } from '../../common/types' ; import { ApplicationShell , applicationShellLayoutVersion , ApplicationShellLayoutVersion } from './application-shell' ; import { CommonCommands } from '../common-frontend-contribution' ; import { WindowService } from '../window/window-service' ; /**
 * A contract for widgets that want to store and restore their inner state, between sessions.
 */ export interface StatefulWidget { /**
     * Called on unload to store the inner state. Returns 'undefined' if the widget cannot be stored.
     */ storeState ( ) : object | undefined ; /**
     * Called when the widget got created by the storage service
     */ restoreState ( oldState ) : void ; } export namespace StatefulWidget { export function is ( arg ) { return arg !== undefined && typeof arg [ 'storeState' ] === 'function' && typeof arg [ 'restoreState' ] === 'function' ; } } export interface WidgetDescription { constructionOptions : WidgetConstructionOptions ; innerWidgetState ? : string | object ; } export interface ApplicationShellLayoutMigrationContext { /**
     * A resolved version of a current layout.
     */ layoutVersion : number ; /**
     * A layout to be inflated.
     */ layout : ApplicationShell . LayoutData ; /**
     * A parent widget is to be inflated. `undefined` if the application shell
     */ parent ? : Widget ; } export interface ApplicationShellLayoutMigrationError extends Error { code : 'ApplicationShellLayoutMigrationError' ; } export namespace ApplicationShellLayoutMigrationError { const code = 'ApplicationShellLayoutMigrationError' ; export function create ( message ? ) { return Object . assign ( new Error ( `Could not migrate layout to version ${ applicationShellLayoutVersion }.` + ( message ? '\n' + message : '' ) ) , { code } ) ; } export function is ( error ) { return ! ! error && 'code' in error && error [ 'code' ] === code ; } } export const ApplicationShellLayoutMigration = Symbol ( 'ApplicationShellLayoutMigration' ) ; export interface ApplicationShellLayoutMigration { /**
     * A target migration version.
     */ readonly layoutVersion : ApplicationShellLayoutVersion ; throws onWillInflateLayout ? ( context ) : MaybePromise < void > ; returns throws onWillInflateWidget ? ( desc , context ) : MaybePromise < WidgetDescription | undefined > ; } export const RESET_LAYOUT = Command . toLocalizedCommand ( { id : 'reset.layout' , category : CommonCommands . VIEW_CATEGORY , label : 'Reset Workbench Layout' } , 'theia/core/resetWorkbenchLayout' , CommonCommands . VIEW_CATEGORY_KEY ) ; @ injectable ( ) export class ShellLayoutRestorer implements CommandContribution { protected storageKey = 'layout' ; protected shouldStoreLayout : boolean = true ; @ inject ( ContributionProvider ) @ named ( ApplicationShellLayoutMigration ) protected readonly migrations : ContributionProvider < ApplicationShellLayoutMigration > ; @ inject ( WindowService ) protected readonly windowService : WindowService ; constructor ( @ inject ( WidgetManager ) protected widgetManager , @ inject ( ILogger ) protected logger , @ inject ( StorageService ) protected storageService ) { } registerCommands ( commands ) { commands . registerCommand ( RESET_LAYOUT , { execute : async ( ) => this . resetLayout ( ) } ) ; } protected async resetLayout ( ) { if ( await this . windowService . isSafeToShutDown ( ) ) { this . logger . info ( '>>> Resetting layout...' ) ; this . shouldStoreLayout = false ; this . storageService . setData ( this . storageKey , undefined ) ; ThemeService . get ( ) . reset ( ) ; this . logger . info ( '<<< The layout has been successfully reset.' ) ; this . windowService . reload ( ) ; } } storeLayout ( app ) { if ( this . shouldStoreLayout ) { try { this . logger . info ( '>>> Storing the layout...' ) ; const layoutData = app . shell . getLayoutData ( ) ; const serializedLayoutData = this . deflate ( layoutData ) ; this . storageService . setData ( this . storageKey , serializedLayoutData ) ; this . logger . info ( '<<< The layout has been successfully stored.' ) ; } catch ( error ) { this . storageService . setData ( this . storageKey , undefined ) ; this . logger . error ( 'Error during serialization of layout data' , error ) ; } } } async restoreLayout ( app ) { this . logger . info ( '>>> Restoring the layout state...' ) ; const serializedLayoutData = await this . storageService . getData < string > ( this . storageKey ) ; if ( serializedLayoutData === undefined ) { this . logger . info ( '<<< Nothing to restore.' ) ; return false ; } const layoutData = await this . inflate ( serializedLayoutData ) ; await app . shell . setLayoutData ( layoutData ) ; this . logger . info ( '<<< The layout has been successfully restored.' ) ; return true ; } protected isWidgetProperty ( propertyName ) { return propertyName === 'widget' ; } protected isWidgetsProperty ( propertyName ) { return propertyName === 'widgets' ; } /**
     * Turns the layout data to a string representation.
     */ protected deflate ( data ) { return JSON . stringify ( data , ( property , value ) => { if ( this . isWidgetProperty ( property ) ) { const description = this . convertToDescription ( value as Widget ) ; return description ; } else if ( this . isWidgetsProperty ( property ) ) { const descriptions = [ ] ; for ( const widget of ( value as Widget [ ] ) ) { const description = this . convertToDescription ( widget ) ; if ( description ) { descriptions . push ( description ) ; } } return descriptions ; } return value ; } ) ; } private convertToDescription ( widget ) { const desc = this . widgetManager . getDescription ( widget ) ; if ( desc ) { if ( StatefulWidget . is ( widget ) ) { const innerState = widget . storeState ( ) ; return innerState ? { constructionOptions : desc , innerWidgetState : this . deflate ( innerState ) } : undefined ; } else { return { constructionOptions : desc , innerWidgetState : undefined } ; } } } /**
     * Creates the layout data from its string representation.
     */ protected async inflate ( layoutData ) { const parseContext = new ShellLayoutRestorer . ParseContext ( ) ; const layout = this . parse < ApplicationShell . LayoutData > ( layoutData , parseContext ) ; let layoutVersion ; try { layoutVersion = 'version' in layout && Number ( layout . version ) ; } catch { } if ( typeof layoutVersion !== 'number' || Number . isNaN ( layoutVersion ) ) { throw new Error ( 'could not resolve a layout version' ) ; } if ( layoutVersion !== applicationShellLayoutVersion ) { if ( layoutVersion < applicationShellLayoutVersion ) { console . warn ( `Layout version ${ layoutVersion } is behind current layout version ${ applicationShellLayoutVersion }, trying to migrate...` ) ; } else { console . warn ( `Layout version ${ layoutVersion } is ahead current layout version ${ applicationShellLayoutVersion }, trying to load anyway...` ) ; } console . info ( `Please use '${ RESET_LAYOUT . label }' command if the layout looks bogus.` ) ; } const migrations = this . migrations . getContributions ( ) . filter ( m => m . layoutVersion > layoutVersion && m . layoutVersion <= applicationShellLayoutVersion ) . sort ( ( m , m2 ) => m . layoutVersion - m2 . layoutVersion ) ; if ( migrations . length ) { console . info ( `Found ${ migrations . length } migrations from layout version ${ layoutVersion } to version ${ applicationShellLayoutVersion }, migrating...` ) ; } const context = { layout , layoutVersion , migrations } ; await this . fireWillInflateLayout ( context ) ; await parseContext . inflate ( context ) ; return layout ; } protected async fireWillInflateLayout ( context ) { for ( const migration of context . migrations ) { if ( migration . onWillInflateLayout ) { await migration . onWillInflateLayout ( context ) ; } } } protected parse < T > ( layoutData , parseContext ) { return JSON . parse ( layoutData , ( property , value ) => { if ( this . isWidgetsProperty ( property ) ) { const widgets = parseContext . filteredArray ( ) ; const descs = ( value as WidgetDescription [ ] ) ; for ( let i = 0 ; i < descs . length ; i ++ ) { parseContext . push ( async ( context ) => { widgets [ i ] = await this . convertToWidget ( descs [ i ] , context ) ; } ) ; } return widgets ; } else if ( value && typeof value === 'object' && ! Array . isArray ( value ) ) { const copy = { } ; for ( const p in value ) { if ( this . isWidgetProperty ( p ) ) { parseContext . push ( async ( context ) => { copy [ p ] = await this . convertToWidget ( value [ p ] , context ) ; } ) ; } else { copy [ p ] = value [ p ] ; } } return copy ; } return value ; } ) ; } protected async fireWillInflateWidget ( desc , context ) { for ( const migration of context . migrations ) { if ( migration . onWillInflateWidget ) { const migrated = await migration . onWillInflateWidget ( desc , context ) ; if ( migrated ) { if ( migrated . innerWidgetState && typeof migrated . innerWidgetState !== 'string' ) { migrated . innerWidgetState = JSON . stringify ( migrated . innerWidgetState ) ; } desc = migrated ; } } } return desc ; } protected async convertToWidget ( desc , context ) { if ( ! desc . constructionOptions ) { return undefined ; } try { desc = await this . fireWillInflateWidget ( desc , context ) ; const widget = await this . widgetManager . getOrCreateWidget ( desc . constructionOptions . factoryId , desc . constructionOptions . options ) ; if ( StatefulWidget . is ( widget ) && desc . innerWidgetState !== undefined ) { try { let oldState ; if ( typeof desc . innerWidgetState === 'string' ) { const parseContext = new ShellLayoutRestorer . ParseContext ( ) ; oldState = this . parse ( desc . innerWidgetState , parseContext ) ; await parseContext . inflate ( { ... context , parent : widget } ) ; } else { oldState = desc . innerWidgetState ; } widget . restoreState ( oldState ) ; } catch ( e ) { if ( ApplicationShellLayoutMigrationError . is ( e ) ) { throw e ; } this . logger . warn ( `Couldn't restore widget state for ${ widget . id }. Error: ${ e } ` ) ; } } if ( widget . isDisposed ) { return undefined ; } return widget ; } catch ( e ) { if ( ApplicationShellLayoutMigrationError . is ( e ) ) { throw e ; } this . logger . warn ( `Couldn't restore widget for ${ desc . constructionOptions . factoryId }. Error: ${ e } ` ) ; return undefined ; } } } export namespace ShellLayoutRestorer { export class ParseContext { protected readonly toInflate : Inflate [ ] = [ ] ; protected readonly toFilter : Widgets [ ] = [ ] ; /**
         * Returns an array, which will be filtered from undefined elements
         * after resolving promises, that create widgets.
         */ filteredArray ( ) { const array = [ ] ; this . toFilter . push ( array ) ; return array ; } push ( toInflate ) { this . toInflate . push ( toInflate ) ; } async inflate ( context ) { const pending = [ ] ; while ( this . toInflate . length ) { pending . push ( this . toInflate . pop ( ) ! ( context ) ) ; } await Promise . all ( pending ) ; if ( this . toFilter . length ) { this . toFilter . forEach ( array => { for ( let i = 0 ; i < array . length ; i ++ ) { if ( array [ i ] === undefined ) { array . splice ( i -- , 1 ) ; } } } ) ; } } } export type Widgets = ( Widget | undefined ) [ ] ; export type Inflate = ( context : InflateContext ) => Promise < void > ; export interface InflateContext extends ApplicationShellLayoutMigrationContext { readonly migrations : ApplicationShellLayoutMigration [ ] ; } }