diff --git a/packages/core/src/browser/theming.ts b/packages/core/src/browser/theming.ts index 72949d198c16f..4f9b321a9196d 100644 --- a/packages/core/src/browser/theming.ts +++ b/packages/core/src/browser/theming.ts @@ -17,6 +17,7 @@ import { injectable, inject } from 'inversify'; import { CommandRegistry, CommandContribution, CommandHandler, Command } from '../common/command'; import { Emitter, Event } from '../common/event'; +import { Disposable } from '../common/disposable'; import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from './quick-open/quick-open-model'; import { QuickOpenService } from './quick-open/quick-open-service'; import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; @@ -63,10 +64,18 @@ export class ThemeService { global[ThemeServiceSymbol] = this; } - register(...themes: Theme[]): void { + register(...themes: Theme[]): Disposable { for (const theme of themes) { this.themes[theme.id] = theme; } + return Disposable.create(() => { + for (const theme of themes) { + delete this.themes[theme.id]; + } + if (this.activeTheme && !this.themes[this.activeTheme.id]) { + this.startupTheme(); + } + }); } getThemes(): Theme[] { diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index 91f1967f016a4..b181a935a0251 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -62,12 +62,15 @@ import { MimeService } from '@theia/core/lib/browser/mime-service'; import { MonacoEditorServices } from './monaco-editor'; import { MonacoColorRegistry } from './monaco-color-registry'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; +import { MonacoThemingService } from './monaco-theming-service'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); decorate(injectable(), monaco.contextKeyService.ContextKeyService); export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(MonacoThemingService).toSelf().inSingletonScope(); + bind(MonacoContextKeyService).toSelf().inSingletonScope(); rebind(ContextKeyService).toService(MonacoContextKeyService); diff --git a/packages/monaco/src/browser/monaco-theming-service.ts b/packages/monaco/src/browser/monaco-theming-service.ts new file mode 100644 index 0000000000000..1c5126d6a1efe --- /dev/null +++ b/packages/monaco/src/browser/monaco-theming-service.ts @@ -0,0 +1,121 @@ +/******************************************************************************** + * Copyright (C) 2019 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 + ********************************************************************************/ + +// tslint:disable:no-any + +import { injectable, inject } from 'inversify'; +import * as jsoncparser from 'jsonc-parser'; +import { ThemeService, BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; +import URI from '@theia/core/lib/common/uri'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; +import { MonacoThemeRegistry } from './textmate/monaco-theme-registry'; + +export interface MonacoTheme { + id?: string; + label?: string; + uiTheme?: 'vs' | 'vs-dark' | 'hc-black'; + description?: string; + uri: string; +} + +@injectable() +export class MonacoThemingService { + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + // tslint:disable-next-line:no-any + register(theme: MonacoTheme, pendingIncludes: { [uri: string]: Promise } = {}): Disposable { + const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); + this.doRegister(theme, pendingIncludes, toDispose); + return toDispose; + } + + protected async doRegister(theme: MonacoTheme, + pendingIncludes: { [uri: string]: Promise }, + toDispose: DisposableCollection + ): Promise { + try { + if (new URI(theme.uri).path.ext !== '.json') { + console.error('Unknown theme file: ' + theme.uri); + return; + } + const includes = {}; + const json = await this.loadTheme(theme.uri, includes, pendingIncludes, toDispose); + if (toDispose.disposed) { + return; + } + const uiTheme = theme.uiTheme || 'vs-dark'; + const label = theme.label || new URI(theme.uri).path.base; + const id = theme.id || label; + const cssSelector = this.toCssSelector(id); + const editorTheme = MonacoThemeRegistry.SINGLETON.register(json, includes, cssSelector, uiTheme).name!; + const type = uiTheme === 'vs' ? 'light' : uiTheme === 'vs-dark' ? 'dark' : 'hc'; + const builtInTheme = uiTheme === 'vs' ? BuiltinThemeProvider.lightCss : BuiltinThemeProvider.darkCss; + toDispose.push(ThemeService.get().register({ + type, + id, + label, + description: theme.description, + editorTheme, + activate(): void { + builtInTheme.use(); + }, + deactivate(): void { + builtInTheme.unuse(); + } + })); + } catch (e) { + console.error('Failed to load theme from ' + theme.uri, e); + } + } + + protected async loadTheme( + uri: string, + includes: { [include: string]: any }, + pendingIncludes: { [uri: string]: Promise }, + toDispose: DisposableCollection + ): Promise { + // tslint:enabled:no-any + const { content } = await this.fileSystem.resolveContent(uri); + if (toDispose.disposed) { + return undefined; + } + const json = jsoncparser.parse(content, undefined, { disallowComments: false }); + if (json.include) { + const includeUri = new URI(uri).parent.resolve(json.include).toString(); + if (!pendingIncludes[includeUri]) { + pendingIncludes[includeUri] = this.loadTheme(includeUri, includes, pendingIncludes, toDispose); + } + includes[json.include] = await pendingIncludes[includeUri]; + if (toDispose.disposed) { + return; + } + } + return json; + } + + /* remove all characters that are not allowed in css */ + protected toCssSelector(str: string): string { + str = str.replace(/[^\-a-zA-Z0-9]/g, '-'); + if (str.charAt(0).match(/[0-9\-]/)) { + str = '-' + str; + } + return str; + } + +} diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index c2acff87e9300..92dd58630e081 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -76,6 +76,7 @@ export interface PluginPackageContribution { keybindings?: PluginPackageKeybinding | PluginPackageKeybinding[]; debuggers?: PluginPackageDebuggersContribution[]; snippets: PluginPackageSnippetsContribution[]; + themes?: PluginThemeContribution[]; taskDefinitions?: PluginTaskDefinitionContribution[]; problemMatchers?: PluginProblemMatcherContribution[]; problemPatterns?: PluginProblemPatternContribution[]; @@ -134,6 +135,14 @@ export interface PluginPackageSnippetsContribution { path?: string; } +export interface PluginThemeContribution { + id?: string; + label?: string; + description?: string; + path?: string; + uiTheme?: 'vs' | 'vs-dark' | 'hc-black'; +} + export interface PlatformSpecificAdapterContribution { program?: string; args?: string[]; @@ -408,6 +417,7 @@ export interface PluginContribution { keybindings?: Keybinding[]; debuggers?: DebuggerContribution[]; snippets?: SnippetContribution[]; + themes?: ThemeContribution[]; taskDefinitions?: TaskDefinition[]; problemMatchers?: ProblemMatcherContribution[]; problemPatterns?: ProblemPatternContribution[]; @@ -419,6 +429,14 @@ export interface SnippetContribution { language?: string } +export interface ThemeContribution { + id?: string; + label?: string; + description?: string; + uri: string; + uiTheme?: 'vs' | 'vs-dark' | 'hc-black'; +} + export interface GrammarsContribution { format: 'json' | 'plist'; language?: string; diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 6c906383d04e5..22d4368f5e7d1 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -43,7 +43,8 @@ import { SnippetContribution, PluginPackageCommand, PluginCommand, - IconUrl + IconUrl, + ThemeContribution } from '../../../common/plugin-protocol'; import * as fs from 'fs'; import * as path from 'path'; @@ -271,6 +272,12 @@ export class TheiaPluginScanner implements PluginScanner { } catch (err) { console.error(`Could not read '${rawPlugin.name}' contribution 'snippets'.`, rawPlugin.contributes!.snippets, err); } + + try { + contributions.themes = this.readThemes(rawPlugin); + } catch (err) { + console.error(`Could not read '${rawPlugin.name}' contribution 'themes'.`, rawPlugin.contributes.themes, err); + } return contributions; } @@ -293,6 +300,25 @@ export class TheiaPluginScanner implements PluginScanner { return PluginPackage.toPluginUrl(pck, relativePath); } + protected readThemes(pck: PluginPackage): ThemeContribution[] | undefined { + if (!pck.contributes || !pck.contributes.themes) { + return undefined; + } + const result: ThemeContribution[] = []; + for (const contribution of pck.contributes.themes) { + if (contribution.path) { + result.push({ + id: contribution.id, + uri: FileUri.create(path.join(pck.packagePath, contribution.path)).toString(), + description: contribution.description, + label: contribution.label, + uiTheme: contribution.uiTheme + }); + } + } + return result; + } + protected readSnippets(pck: PluginPackage): SnippetContribution[] | undefined { if (!pck.contributes || !pck.contributes.snippets) { return undefined; diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index ba8a0b8af8353..5d5be3c99a948 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -31,6 +31,7 @@ import { Emitter } from '@theia/core/lib/common/event'; import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser'; import { PluginDebugService } from './debug/plugin-debug-service'; import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater'; +import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; @injectable() export class PluginContributionHandler { @@ -79,6 +80,9 @@ export class PluginContributionHandler { @inject(DebugSchemaUpdater) protected readonly debugSchema: DebugSchemaUpdater; + @inject(MonacoThemingService) + protected readonly monacoThemingService: MonacoThemingService; + protected readonly commandHandlers = new Map(); protected readonly onDidRegisterCommandHandlerEmitter = new Emitter(); @@ -230,6 +234,13 @@ export class PluginContributionHandler { } } + if (contributions.themes && contributions.themes.length) { + const includes = {}; + for (const theme of contributions.themes) { + pushContribution(`themes.${theme.uri}`, () => this.monacoThemingService.register(theme, includes)); + } + } + if (contributions.taskDefinitions) { for (const taskDefinition of contributions.taskDefinitions) { pushContribution(`taskDefinitions.${taskDefinition.taskType}`, @@ -473,4 +484,5 @@ export class PluginContributionHandler { } } } + } diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts index 42c7c5a3419a0..b6561344ee095 100644 --- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts +++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts @@ -16,7 +16,7 @@ import { injectable } from 'inversify'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { ThemeService, Theme, BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; +import { ThemeService, Theme } from '@theia/core/lib/browser/theming'; import { IconUrl } from '../../common/plugin-protocol'; import { Reference, SyncReferenceCollection } from '@theia/core/lib/common/reference'; @@ -110,7 +110,7 @@ export class PluginSharedStyle { background-position: 2px; width: ${size}px; height: ${size}px; - background: no-repeat url("${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl}"); + background: no-repeat url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}"); background-size: ${size}px; `)); return { diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 35a3e4cdaffcc..1e7a84167c5d1 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -38,7 +38,6 @@ import { open, OpenerService } from '@theia/core/lib/browser/opener-service'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { Schemes } from '../../../common/uri-components'; import { PluginSharedStyle } from '../plugin-shared-style'; -import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; import { WebviewThemeDataProvider } from './webview-theme-data-provider'; import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; @@ -352,7 +351,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { const iconClass = `webview-${this.identifier.id}-file-icon`; this.toDisposeOnIcon.push(this.sharedStyle.insertRule( `.theia-webview-icon.${iconClass}::before`, - theme => `background-image: url(${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl});` + theme => `background-image: url(${theme.type === 'light' ? lightIconUrl : darkIconUrl});` )); this.title.iconClass = `theia-webview-icon ${iconClass}`; } else {