Skip to content

Commit

Permalink
[plugin/theming] #4831: load themes from json files
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Nov 1, 2019
1 parent aee7427 commit 85cf502
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 6 deletions.
11 changes: 10 additions & 1 deletion packages/core/src/browser/theming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[] {
Expand Down
3 changes: 3 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
121 changes: 121 additions & 0 deletions packages/monaco/src/browser/monaco-theming-service.ts
Original file line number Diff line number Diff line change
@@ -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<any> } = {}): 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<any> },
toDispose: DisposableCollection
): Promise<void> {
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<any> },
toDispose: DisposableCollection
): Promise<any> {
// 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;
}

}
18 changes: 18 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface PluginPackageContribution {
keybindings?: PluginPackageKeybinding | PluginPackageKeybinding[];
debuggers?: PluginPackageDebuggersContribution[];
snippets: PluginPackageSnippetsContribution[];
themes?: PluginThemeContribution[];
taskDefinitions?: PluginTaskDefinitionContribution[];
problemMatchers?: PluginProblemMatcherContribution[];
problemPatterns?: PluginProblemPatternContribution[];
Expand Down Expand Up @@ -133,6 +134,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[];
Expand Down Expand Up @@ -407,6 +416,7 @@ export interface PluginContribution {
keybindings?: Keybinding[];
debuggers?: DebuggerContribution[];
snippets?: SnippetContribution[];
themes?: ThemeContribution[];
taskDefinitions?: TaskDefinition[];
problemMatchers?: ProblemMatcherContribution[];
problemPatterns?: ProblemPatternContribution[];
Expand All @@ -418,6 +428,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;
Expand Down
28 changes: 27 additions & 1 deletion packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -79,6 +80,9 @@ export class PluginContributionHandler {
@inject(DebugSchemaUpdater)
protected readonly debugSchema: DebugSchemaUpdater;

@inject(MonacoThemingService)
protected readonly monacoThemingService: MonacoThemingService;

protected readonly commandHandlers = new Map<string, CommandHandler['execute'] | undefined>();

protected readonly onDidRegisterCommandHandlerEmitter = new Emitter<string>();
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -473,4 +484,5 @@ export class PluginContributionHandler {
}
}
}

}
4 changes: 2 additions & 2 deletions packages/plugin-ext/src/main/browser/plugin-shared-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -111,7 +111,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 {
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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';

// tslint:disable:no-any
Expand Down Expand Up @@ -246,7 +245,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
const iconClass = `${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 {
Expand Down

0 comments on commit 85cf502

Please sign in to comment.