// ***************************************************************************** // 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 // ***************************************************************************** import { injectable, inject, named } from '@theia/core/shared/inversify'; import { ITokenTypeMap, IEmbeddedLanguagesMap } from 'vscode-textmate'; import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarDefinition } from '@theia/monaco/lib/browser/textmate'; import { MenusContributionPointHandler } from './menus/menus-contribution-handler'; import { PluginViewRegistry } from './view/plugin-view-registry'; import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry'; import { PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin, GrammarsContribution, EnterAction, OnEnterRule, RegExpOptions, IconContribution, PluginPackage } from '../../common'; import { DefaultUriLabelProviderContribution, LabelProviderContribution, } from '@theia/core/lib/browser'; import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler'; import { MonacoSnippetSuggestProvider } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider'; import { PluginSharedStyle } from './plugin-shared-style'; import { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser'; import { NotebookRendererRegistry, NotebookTypeRegistry } from '@theia/notebook/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'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { PluginIconService } from './plugin-icon-service'; import { PluginIconThemeService } from './plugin-icon-theme-service'; import { ContributionProvider, isObject, OVERRIDE_PROPERTY_PATTERN, PreferenceSchemaService } from '@theia/core/lib/common'; import * as monaco from '@theia/monaco-editor-core'; import { ContributedTerminalProfileStore, TerminalProfileStore } from '@theia/terminal/lib/browser/terminal-profile-service'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { PluginTerminalRegistry } from './plugin-terminal-registry'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { LanguageService } from '@theia/core/lib/browser/language-service'; import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; import { JSONObject, JSONValue } from '@theia/core/shared/@lumino/coreutils'; // The enum export is missing from `vscode-textmate@9.2.0` const enum StandardTokenType { Other = 0, Comment = 1, String = 2, RegEx = 3 } @injectable() export class PluginContributionHandler { private injections = new Map(); @inject(TextmateRegistry) private readonly grammarsRegistry: TextmateRegistry; @inject(PluginViewRegistry) private readonly viewRegistry: PluginViewRegistry; @inject(PluginCustomEditorRegistry) private readonly customEditorRegistry: PluginCustomEditorRegistry; @inject(MenusContributionPointHandler) private readonly menusContributionHandler: MenusContributionPointHandler; @inject(PreferenceSchemaService) private readonly preferenceSchemaProvider: PreferenceSchemaService; @inject(MonacoTextmateService) private readonly monacoTextmateService: MonacoTextmateService; @inject(KeybindingsContributionPointHandler) private readonly keybindingsContributionHandler: KeybindingsContributionPointHandler; @inject(MonacoSnippetSuggestProvider) protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider; @inject(CommandRegistry) protected readonly commands: CommandRegistry; @inject(LanguageService) protected readonly languageService: LanguageService; @inject(PluginSharedStyle) protected readonly style: PluginSharedStyle; @inject(TaskDefinitionRegistry) protected readonly taskDefinitionRegistry: TaskDefinitionRegistry; @inject(ProblemMatcherRegistry) protected readonly problemMatcherRegistry: ProblemMatcherRegistry; @inject(ProblemPatternRegistry) protected readonly problemPatternRegistry: ProblemPatternRegistry; @inject(PluginDebugService) protected readonly debugService: PluginDebugService; @inject(DebugSchemaUpdater) protected readonly debugSchema: DebugSchemaUpdater; @inject(MonacoThemingService) protected readonly monacoThemingService: MonacoThemingService; @inject(ColorRegistry) protected readonly colors: ColorRegistry; @inject(PluginIconService) protected readonly iconService: PluginIconService; @inject(PluginIconThemeService) protected readonly iconThemeService: PluginIconThemeService; @inject(TerminalService) protected readonly terminalService: TerminalService; @inject(PluginTerminalRegistry) protected readonly pluginTerminalRegistry: PluginTerminalRegistry; @inject(ContributedTerminalProfileStore) protected readonly contributedProfileStore: TerminalProfileStore; @inject(NotebookTypeRegistry) protected readonly notebookTypeRegistry: NotebookTypeRegistry; @inject(NotebookRendererRegistry) protected readonly notebookRendererRegistry: NotebookRendererRegistry; @inject(ContributionProvider) @named(LabelProviderContribution) protected readonly contributionProvider: ContributionProvider; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; protected readonly commandHandlers = new Map(); protected readonly onDidRegisterCommandHandlerEmitter = new Emitter(); readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event; /** * Always synchronous in order to simplify handling disconnections. * @throws never, loading of each contribution should handle errors * in order to avoid preventing loading of other contributions or extensions */ handleContributions(clientId: string, plugin: DeployedPlugin): Disposable { const contributions = plugin.contributes; if (!contributions) { return Disposable.NULL; } const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); /* eslint-disable @typescript-eslint/no-explicit-any */ const logError = (message: string, ...args: any[]) => console.error(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args); const logWarning = (message: string, ...args: any[]) => console.warn(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args); const pushContribution = (id: string, contribute: () => Disposable) => { if (toDispose.disposed) { return; } try { toDispose.push(contribute()); } catch (e) { logError(`Failed to load '${id}' contribution.`, e); } }; const configuration = contributions.configuration; if (configuration) { for (const config of configuration) { pushContribution('configuration', () => this.preferenceSchemaProvider.addSchema(config)); } } const configurationDefaults = contributions.configurationDefaults; if (configurationDefaults) { pushContribution('configurationDefaults', () => this.updateDefaultOverridesSchema(configurationDefaults)); } const languages = contributions.languages; if (languages && languages.length) { for (const lang of languages) { // it is not possible to unregister a language monaco.languages.register({ id: lang.id, aliases: lang.aliases, extensions: lang.extensions, filenamePatterns: lang.filenamePatterns, filenames: lang.filenames, firstLine: lang.firstLine, mimetypes: lang.mimetypes }); if (lang.icon) { const languageIcon = this.style.toFileIconClass(lang.icon); pushContribution(`language.${lang.id}.icon`, () => languageIcon); pushContribution(`language.${lang.id}.iconRegistration`, () => this.languageService.registerIcon(lang.id, languageIcon.object.iconClass)); } const langConfiguration = lang.configuration; if (langConfiguration) { const comments = langConfiguration.comments; pushContribution(`language.${lang.id}.configuration`, () => monaco.languages.setLanguageConfiguration(lang.id, { wordPattern: this.createRegex(langConfiguration.wordPattern), autoClosingPairs: langConfiguration.autoClosingPairs, brackets: langConfiguration.brackets, // @monaco-uplift: Monaco doesn't support LineCommentRule yet, extract the string comments: comments ? { lineComment: comments.lineComment && typeof comments.lineComment === 'object' ? comments.lineComment.comment : comments.lineComment, blockComment: comments.blockComment, } : undefined, folding: this.convertFolding(langConfiguration.folding), surroundingPairs: langConfiguration.surroundingPairs, indentationRules: this.convertIndentationRules(langConfiguration.indentationRules), onEnterRules: this.convertOnEnterRules(langConfiguration.onEnterRules), })); } } } const grammars = contributions.grammars; if (grammars && grammars.length) { const grammarsWithLanguage: GrammarsContribution[] = []; for (const grammar of grammars) { if (grammar.injectTo) { for (const injectScope of grammar.injectTo) { pushContribution(`grammar.injectTo.${injectScope}`, () => { const injections = this.injections.get(injectScope) || []; injections.push(grammar.scope); this.injections.set(injectScope, injections); return Disposable.create(() => { const index = injections.indexOf(grammar.scope); if (index !== -1) { injections.splice(index, 1); } }); }); } } if (grammar.language) { // processing is deferred. grammarsWithLanguage.push(grammar); } pushContribution(`grammar.textmate.scope.${grammar.scope}`, () => this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, { async getGrammarDefinition(): Promise { return { format: grammar.format, content: grammar.grammar || '', location: grammar.grammarLocation }; }, getInjections: (scopeName: string) => this.injections.get(scopeName)! })); } // load grammars on next tick to await registration of languages from all plugins in current tick // see https://github.com/eclipse-theia/theia/issues/6907#issuecomment-578600243 setTimeout(() => { for (const grammar of grammarsWithLanguage) { const language = grammar.language!; pushContribution(`grammar.language.${language}.scope`, () => this.grammarsRegistry.mapLanguageIdToTextmateGrammar(language, grammar.scope)); pushContribution(`grammar.language.${language}.configuration`, () => this.grammarsRegistry.registerGrammarConfiguration(language, { embeddedLanguages: this.convertEmbeddedLanguages(grammar.embeddedLanguages, logWarning), tokenTypes: this.convertTokenTypes(grammar.tokenTypes), balancedBracketSelectors: grammar.balancedBracketScopes ?? ['*'], unbalancedBracketSelectors: grammar.balancedBracketScopes, })); } // activate grammars only once everything else is loaded. // see https://github.com/eclipse-theia/theia-cpp-extensions/issues/100#issuecomment-610643866 setTimeout(() => { for (const grammar of grammarsWithLanguage) { const language = grammar.language!; pushContribution(`grammar.language.${language}.activation`, () => this.monacoTextmateService.activateLanguage(language) ); } }); }); } pushContribution('commands', () => this.registerCommands(contributions)); pushContribution('menus', () => this.menusContributionHandler.handle(plugin)); pushContribution('keybindings', () => this.keybindingsContributionHandler.handle(contributions)); if (contributions.customEditors) { for (const customEditor of contributions.customEditors) { pushContribution(`customEditors.${customEditor.viewType}`, () => this.customEditorRegistry.registerCustomEditor(customEditor, plugin) ); } } if (contributions.viewsContainers) { for (const location in contributions.viewsContainers) { if (contributions.viewsContainers!.hasOwnProperty(location)) { for (const viewContainer of contributions.viewsContainers[location]) { pushContribution(`viewContainers.${viewContainer.id}`, () => this.viewRegistry.registerViewContainer(location, viewContainer) ); } } } } if (contributions.views) { // eslint-disable-next-line guard-for-in for (const location in contributions.views) { for (const view of contributions.views[location]) { pushContribution(`views.${view.id}`, () => this.viewRegistry.registerView(location, view) ); } } } if (contributions.viewsWelcome) { for (const [index, viewWelcome] of contributions.viewsWelcome.entries()) { pushContribution(`viewsWelcome.${viewWelcome.view}.${index}`, () => this.viewRegistry.registerViewWelcome(viewWelcome) ); } } if (contributions.snippets) { for (const snippet of contributions.snippets) { pushContribution(`snippets.${snippet.uri}`, () => this.snippetSuggestProvider.fromURI(snippet.uri, { language: snippet.language, source: snippet.source })); } } if (contributions.themes && contributions.themes.length) { const pending = {}; for (const theme of contributions.themes) { pushContribution(`themes.${theme.uri}`, () => this.monacoThemingService.register(theme, pending)); } } if (contributions.iconThemes && contributions.iconThemes.length) { for (const iconTheme of contributions.iconThemes) { pushContribution(`iconThemes.${iconTheme.uri}`, () => this.iconThemeService.register(iconTheme, plugin)); } } if (contributions.icons && contributions.icons.length) { for (const icon of contributions.icons) { const defaultIcon = icon.defaults; let key: string; if (IconContribution.isIconDefinition(defaultIcon)) { key = defaultIcon.location; } else { key = defaultIcon.id; } pushContribution(`icons.${key}`, () => this.iconService.register(icon, plugin)); } } const colors = contributions.colors; if (colors) { pushContribution('colors', () => this.colors.register(...colors)); } if (contributions.taskDefinitions) { for (const taskDefinition of contributions.taskDefinitions) { pushContribution(`taskDefinitions.${taskDefinition.taskType}`, () => this.taskDefinitionRegistry.register(taskDefinition) ); } } if (contributions.problemPatterns) { for (const problemPattern of contributions.problemPatterns) { pushContribution(`problemPatterns.${problemPattern.name || problemPattern.regexp}`, () => this.problemPatternRegistry.register(problemPattern) ); } } if (contributions.problemMatchers) { for (const problemMatcher of contributions.problemMatchers) { pushContribution(`problemMatchers.${problemMatcher.label}`, () => this.problemMatcherRegistry.register(problemMatcher) ); } } if (contributions.debuggers && contributions.debuggers.length) { toDispose.push(Disposable.create(() => this.debugSchema.update())); for (const contribution of contributions.debuggers) { pushContribution(`debuggers.${contribution.type}`, () => this.debugService.registerDebugger(contribution) ); } this.debugSchema.update(); } // Register dynamic debug configuration types discovered from activation events. // This allows the debug dropdown to show provider types before the extension has activated. if (contributions.activationEvents) { for (const event of contributions.activationEvents) { if (event.startsWith('onDebugDynamicConfigurations:')) { // Explicit type declaration: onDebugDynamicConfigurations:python // Try to find a matching debugger to get the label const debugType = event.slice('onDebugDynamicConfigurations:'.length); const debuggerContrib = contributions.debuggers?.find(d => d.type === debugType); const label = debuggerContrib?.label ?? debugType; pushContribution(`dynamicDebugType.${debugType}`, () => this.debugService.registerDynamicDebugConfigurationType(debugType, label) ); } else if (event === 'onDebugDynamicConfigurations' && contributions.debuggers?.length) { // Generic event - register all unique debugger types from this extension const registeredTypes = new Set(); for (const contrib of contributions.debuggers) { if (!registeredTypes.has(contrib.type)) { registeredTypes.add(contrib.type); const label = contrib.label ?? contrib.type; pushContribution(`dynamicDebugType.${contrib.type}`, () => this.debugService.registerDynamicDebugConfigurationType(contrib.type, label) ); } } } } } if (contributions.resourceLabelFormatters) { for (const formatter of contributions.resourceLabelFormatters) { for (const contribution of this.contributionProvider.getContributions()) { if (contribution instanceof DefaultUriLabelProviderContribution) { pushContribution(`resourceLabelFormatters.${formatter.scheme}`, () => contribution.registerFormatter(formatter) ); } } } } const self = this; if (contributions.terminalProfiles) { for (const profile of contributions.terminalProfiles) { pushContribution(`terminalProfiles.${profile.id}`, () => { this.contributedProfileStore.registerTerminalProfile(profile.title, { async start(): Promise { const terminalId = await self.pluginTerminalRegistry.start(profile.id); const result = self.terminalService.getById(terminalId); if (!result) { throw new Error(`Error starting terminal from profile ${profile.id}`); } return result; } }); return Disposable.create(() => { this.contributedProfileStore.unregisterTerminalProfile(profile.id); }); }); } } if (contributions.notebooks) { for (const notebook of contributions.notebooks) { pushContribution(`notebook.${notebook.type}`, () => this.notebookTypeRegistry.registerNotebookType(notebook, plugin.metadata.model.displayName) ); } } if (contributions.notebookRenderer) { for (const renderer of contributions.notebookRenderer) { pushContribution(`notebookRenderer.${renderer.id}`, () => this.notebookRendererRegistry.registerNotebookRenderer(renderer, PluginPackage.toPluginUrl(plugin.metadata.model, '')) ); } } if (contributions.notebookPreload) { for (const preload of contributions.notebookPreload) { pushContribution(`notebookPreloads.${preload.type}:${preload.entrypoint}`, () => this.notebookRendererRegistry.registerStaticNotebookPreload(preload.type, preload.entrypoint, PluginPackage.toPluginUrl(plugin.metadata.model, '')) ); } } return toDispose; } protected registerCommands(contribution: PluginContribution): Disposable { if (!contribution.commands) { return Disposable.NULL; } const toDispose = new DisposableCollection(); for (const { iconUrl, themeIcon, command, category, shortTitle, title, originalTitle, enablement } of contribution.commands) { const reference = iconUrl && this.style.toIconClass(iconUrl); const icon = themeIcon && ThemeIcon.fromString(themeIcon); let iconClass; if (reference) { toDispose.push(reference); iconClass = reference.object.iconClass; } else if (icon) { iconClass = ThemeIcon.asClassName(icon); } toDispose.push(this.registerCommand({ id: command, category, shortTitle, label: title, originalLabel: originalTitle, iconClass }, enablement)); } return toDispose; } registerCommand(command: Command, enablement?: string): Disposable { if (this.hasCommand(command.id)) { console.warn(`command '${command.id}' already registered`); return Disposable.NULL; } const commandHandler: CommandHandler = { execute: async (...args) => { const handler = this.commandHandlers.get(command.id); if (!handler) { throw new Error(`command '${command.id}' not found`); } return handler(...args); }, // Always enabled - a command can be executed programmatically or via the commands palette. isEnabled: () => { if (enablement) { return this.contextKeyService.match(enablement); } return true; }, // Visibility rules are defined via the `menus` contribution point. isVisible(): boolean { return true; } }; if (enablement) { const contextKeys = this.contextKeyService.parseKeys(enablement); if (contextKeys && contextKeys.size > 0) { commandHandler.onDidChangeEnabled = (listener: () => void) => this.contextKeyService.onDidChange(e => { if (e.affects(contextKeys)) { listener(); } }); } } const toDispose = new DisposableCollection(); if (this.commands.getCommand(command.id)) { // overriding built-in command, i.e. `type` by the VSCodeVim extension toDispose.push(this.commands.registerHandler(command.id, commandHandler)); } else { toDispose.push(this.commands.registerCommand(command, commandHandler)); } this.commandHandlers.set(command.id, undefined); toDispose.push(Disposable.create(() => this.commandHandlers.delete(command.id))); return toDispose; } registerCommandHandler(id: string, execute: CommandHandler['execute']): Disposable { if (this.hasCommandHandler(id)) { console.warn(`command handler '${id}' already registered`); return Disposable.NULL; } this.commandHandlers.set(id, execute); this.onDidRegisterCommandHandlerEmitter.fire(id); return Disposable.create(() => this.commandHandlers.set(id, undefined)); } hasCommand(id: string): boolean { return this.commandHandlers.has(id); } hasCommandHandler(id: string): boolean { return !!this.commandHandlers.get(id); } protected updateDefaultOverridesSchema(configurationDefaults: JSONObject): Disposable { const disposables = new DisposableCollection(); // eslint-disable-next-line guard-for-in for (const key in configurationDefaults) { const defaultValue = configurationDefaults[key]; const match = key.match(OVERRIDE_PROPERTY_PATTERN); if (match && isObject(defaultValue)) { for (const [propertyName, value] of Object.entries(defaultValue)) { disposables.push(this.preferenceSchemaProvider.registerOverride(propertyName, match[1], value as JSONValue)); } } else { // regular configuration override disposables.push(this.preferenceSchemaProvider.registerOverride(key, undefined, defaultValue)); } } return disposables; } private createRegex(value: string | RegExpOptions | undefined): RegExp | undefined { if (typeof value === 'string') { return new RegExp(value, ''); } if (typeof value == 'undefined') { return undefined; } return new RegExp(value.pattern, value.flags); } private convertIndentationRules(rules?: IndentationRules): monaco.languages.IndentationRule | undefined { if (!rules) { return undefined; } return { decreaseIndentPattern: this.createRegex(rules.decreaseIndentPattern)!, increaseIndentPattern: this.createRegex(rules.increaseIndentPattern)!, indentNextLinePattern: this.createRegex(rules.indentNextLinePattern), unIndentedLinePattern: this.createRegex(rules.unIndentedLinePattern) }; } private convertFolding(folding?: FoldingRules): monaco.languages.FoldingRules | undefined { if (!folding) { return undefined; } const result: monaco.languages.FoldingRules = { offSide: folding.offSide }; if (folding.markers) { result.markers = { end: this.createRegex(folding.markers.end)!, start: this.createRegex(folding.markers.start)! }; } return result; } private convertTokenTypes(tokenTypes?: ScopeMap): ITokenTypeMap | undefined { if (typeof tokenTypes === 'undefined' || tokenTypes === null) { return undefined; } const result = Object.create(null); const scopes = Object.keys(tokenTypes); const len = scopes.length; for (let i = 0; i < len; i++) { const scope = scopes[i]; const tokenType = tokenTypes[scope]; switch (tokenType) { case 'string': result[scope] = StandardTokenType.String; break; case 'other': result[scope] = StandardTokenType.Other; break; case 'comment': result[scope] = StandardTokenType.Comment; break; } } return result; } private convertEmbeddedLanguages(languages: ScopeMap | undefined, logWarning: (warning: string) => void): IEmbeddedLanguagesMap | undefined { if (typeof languages === 'undefined' || languages === null) { return undefined; } const result = Object.create(null); const scopes = Object.keys(languages); const len = scopes.length; for (let i = 0; i < len; i++) { const scope = scopes[i]; const langId = languages[scope]; result[scope] = getEncodedLanguageId(langId); if (!result[scope]) { logWarning(`Language for '${scope}' not found.`); } } return result; } private convertOnEnterRules(onEnterRules?: OnEnterRule[]): monaco.languages.OnEnterRule[] | undefined { if (!onEnterRules) { return undefined; } const result: monaco.languages.OnEnterRule[] = []; for (const onEnterRule of onEnterRules) { const rule: monaco.languages.OnEnterRule = { beforeText: this.createRegex(onEnterRule.beforeText)!, afterText: this.createRegex(onEnterRule.afterText), previousLineText: this.createRegex(onEnterRule.previousLineText), action: this.createEnterAction(onEnterRule.action), }; result.push(rule); } return result; } private createEnterAction(action: EnterAction): monaco.languages.EnterAction { let indentAction: monaco.languages.IndentAction; switch (action.indent) { case 'indent': indentAction = monaco.languages.IndentAction.Indent; break; case 'indentOutdent': indentAction = monaco.languages.IndentAction.IndentOutdent; break; case 'outdent': indentAction = monaco.languages.IndentAction.Outdent; break; default: indentAction = monaco.languages.IndentAction.None; break; } return { indentAction, appendText: action.appendText, removeText: action.removeText }; } }