Skip to content

Commit

Permalink
[plugin] fix eclipse-theia#3972: basic support of snippets
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
Signed-off-by: Bogdan Stolojan <petre.stolojan@arm.com>
  • Loading branch information
akosyakov authored and Bogdan Stolojan committed Jan 21, 2019
1 parent 62774cf commit a3734c1
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 15 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
"--no-app-auto-install"
],
"env": {
"NODE_ENV": "development"
"NODE_ENV": "development",
"THEIA_DEFAULT_PLUGINS": "local-dir:plugins"
},
"sourceMaps": true,
"outFiles": [
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"clean": "theia clean && rimraf errorShots",
"build": "theia build --mode development",
"watch": "yarn build --watch",
"start": "theia start",
"start": "export THEIA_DEFAULT_PLUGINS=local-dir:../../plugins && theia start",
"start:debug": "yarn start --log-level=debug",
"test": "wdio wdio.conf.js",
"test-non-headless": "wdio wdio-non-headless.conf.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@
import { injectable, inject } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider';

@injectable()
export class MonacoFrontendApplicationContribution implements FrontendApplicationContribution {

@inject(ThemeService)
protected readonly themeService: ThemeService;

@inject(MonacoSnippetSuggestProvider)
protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider;

async initialize() {
const currentTheme = this.themeService.getCurrentTheme();
this.changeTheme(currentTheme.editorTheme);
this.themeService.onThemeChange(event => this.changeTheme(event.newTheme.editorTheme));

monaco.suggest.setSnippetSuggestSupport(this.snippetSuggestProvider);
}

protected changeTheme(editorTheme: string | undefined) {
Expand Down
14 changes: 8 additions & 6 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import '../../src/browser/style/index.css';
import '../../src/browser/style/symbol-sprite.svg';
import '../../src/browser/style/symbol-icons.css';

import { ContainerModule, decorate, injectable } from 'inversify';
import { MenuContribution, CommandContribution } from '@theia/core/lib/common';
import { QuickOpenService, FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser';
Expand Down Expand Up @@ -43,17 +47,15 @@ import MonacoTextmateModuleBinder from './textmate/monaco-textmate-frontend-bind
import { MonacoSemanticHighlightingService } from './monaco-semantic-highlighting-service';
import { SemanticHighlightingService } from '@theia/editor/lib/browser/semantic-highlight/semantic-highlighting-service';
import { MonacoBulkEditService } from './monaco-bulk-edit-service';
import { MonacoOutlineDecorator } from './monaco-outline-decorator';
import { OutlineTreeDecorator } from '@theia/outline-view/lib/browser/outline-decorator-service';
import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider';

decorate(injectable(), MonacoToProtocolConverter);
decorate(injectable(), ProtocolToMonacoConverter);

import '../../src/browser/style/index.css';
import '../../src/browser/style/symbol-sprite.svg';
import '../../src/browser/style/symbol-icons.css';
import { MonacoOutlineDecorator } from './monaco-outline-decorator';
import { OutlineTreeDecorator } from '@theia/outline-view/lib/browser/outline-decorator-service';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoSnippetSuggestProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).to(MonacoFrontendApplicationContribution).inSingletonScope();

bind(MonacoToProtocolConverter).toSelf().inSingletonScope();
Expand Down
84 changes: 84 additions & 0 deletions packages/monaco/src/browser/monaco-snippet-suggest-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/********************************************************************************
* 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
********************************************************************************/
import { injectable } from 'inversify';

@injectable()
export class MonacoSnippetSuggestProvider implements monaco.modes.ISuggestSupport {

protected readonly snippets = new Map<string, MonacoSnippetSuggestion[]>();

push(...snippets: Snippet[]): void {
for (const snippet of snippets) {
for (const scope of snippet.scopes) {
const languageSnippets = this.snippets.get(scope) || [];
languageSnippets.push(new MonacoSnippetSuggestion(snippet));
this.snippets.set(scope, languageSnippets);
}
}
}

async provideCompletionItems(model: monaco.editor.ITextModel): Promise<monaco.modes.ISuggestResult> {
const languageId = model.getModeId(); // TODO: look up a language id at the position
const suggestions = this.snippets.get(languageId) || [];
return { suggestions };
}

resolveCompletionItem(_: monaco.editor.ITextModel, __: monaco.Position, item: monaco.modes.ISuggestion): monaco.modes.ISuggestion {
return item instanceof MonacoSnippetSuggestion ? item.resolve() : item;
}

}

export interface Snippet {
readonly scopes: string[]
readonly name: string
readonly prefix: string
readonly description: string
readonly body: string
readonly source: string
}

export class MonacoSnippetSuggestion implements monaco.modes.ISuggestion {

readonly label: string;
readonly detail: string;
readonly sortText: string;
readonly noAutoAccept = true;
readonly type: 'snippet' = 'snippet';
readonly snippetType: 'textmate' = 'textmate';

insertText: string;
documentation?: monaco.IMarkdownString;

constructor(protected readonly snippet: Snippet) {
this.label = snippet.prefix;
this.detail = `${snippet.description || snippet.name} (${snippet.source})"`;
this.insertText = snippet.body;
this.sortText = `z-${snippet.prefix}`;
}

protected resolved = false;
resolve(): MonacoSnippetSuggestion {
if (!this.resolved) {
const codeSnippet = new monaco.snippetParser.SnippetParser().parse(this.snippet.body).toString();
this.insertText = codeSnippet;
this.documentation = { value: '```\n' + codeSnippet + '```' };
this.resolved = true;
}
return this;
}

}
2 changes: 2 additions & 0 deletions packages/monaco/src/typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,8 @@ declare module monaco.suggest {
token?: monaco.CancellationToken
): Promise<ISuggestionItem[]>;

export function setSnippetSuggestSupport(support: monaco.modes.ISuggestSupport): monaco.modes.ISuggestSupport;

}

declare module monaco.suggestController {
Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CharacterPair, CommentRule, PluginAPIFactory, Plugin } from '../api/plu
import { PreferenceSchema } from '@theia/core/lib/browser/preferences';
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
import { Snippet } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider';

export const hostedServicePath = '/services/hostedPlugin';

Expand Down Expand Up @@ -62,6 +63,7 @@ export interface PluginPackageContribution {
menus?: { [location: string]: PluginPackageMenu[] };
keybindings?: PluginPackageKeybinding[];
debuggers?: PluginPackageDebuggersContribution[];
snippets: PluginPackageSnippetsContribution[];
}

export interface PluginPackageViewContainer {
Expand Down Expand Up @@ -100,6 +102,26 @@ export interface ScopeMap {
[scopeName: string]: string;
}

export interface PluginPackageSnippetsContribution {
language?: string;
path?: string;
}

export interface JsonSerializedSnippets {
[name: string]: JsonSerializedSnippet | { [name: string]: JsonSerializedSnippet };
}
export interface JsonSerializedSnippet {
body: string | string[];
scope: string;
prefix: string;
description: string;
}
export namespace JsonSerializedSnippet {
export function is(obj: Object | undefined): obj is JsonSerializedSnippet {
return typeof obj === 'object' && 'body' in obj && 'prefix' in obj;
}
}

export interface PlatformSpecificAdapterContribution {
program?: string;
args?: string[];
Expand Down Expand Up @@ -337,6 +359,7 @@ export interface PluginContribution {
menus?: { [location: string]: Menu[] };
keybindings?: Keybinding[];
debuggers?: DebuggerContribution[];
snippets?: Snippet[];
}

export interface GrammarsContribution {
Expand Down
82 changes: 76 additions & 6 deletions packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ import {
Menu,
PluginPackageMenu,
PluginPackageDebuggersContribution,
DebuggerContribution
DebuggerContribution,
JsonSerializedSnippets,
JsonSerializedSnippet
} from '../../../common/plugin-protocol';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -48,6 +50,7 @@ import { CharacterPair } from '../../../api/plugin-api';
import * as jsoncparser from 'jsonc-parser';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import { deepClone } from '@theia/core/lib/common/objects';
import { Snippet } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider';

namespace nls {
export function localize(key: string, _default: string) {
Expand Down Expand Up @@ -168,9 +171,79 @@ export class TheiaPluginScanner implements PluginScanner {
contributions.debuggers = debuggers;
}

contributions.snippets = this.readSnippets(rawPlugin);
return contributions;
}

protected readSnippets(pck: PluginPackage): Snippet[] | undefined {
if (!pck.contributes || !pck.contributes.snippets) {
return undefined;
}
const result: Snippet[] = [];
for (const contribution of pck.contributes.snippets) {
if (contribution.path) {
// TODO: load files lazy on snippet suggest
const snippets = this.readJson<JsonSerializedSnippets>(path.join(pck.packagePath, contribution.path));
this.parseSnippets(snippets, (name, snippet) => {
let { prefix, body, description } = snippet;
if (Array.isArray(body)) {
body = body.join('\n');
}
if (typeof prefix !== 'string' || typeof body !== 'string') {
return;
}
const scopes: string[] = [];
if (contribution.language) {
scopes.push(contribution.language);
} else if (typeof snippet.scope === 'string') {
for (const rawScope of snippet.scope.split(',')) {
const scope = rawScope.trim();
if (scope) {
scopes.push(scope);
}
}
}
const source = pck.displayName || pck.name;
result.push({
scopes,
name,
prefix,
description,
body,
source
});
});
}
}
return result;
}
protected parseSnippets(snippets: JsonSerializedSnippets | undefined, accept: (name: string, snippet: JsonSerializedSnippet) => void): void {
if (typeof snippets === 'object') {
// tslint:disable-next-line:forin
for (const name in snippets) {
const scopeOrTemplate = snippets[name];
if (JsonSerializedSnippet.is(scopeOrTemplate)) {
accept(name, scopeOrTemplate);
} else {
this.parseSnippets(scopeOrTemplate, accept);
}
}
}
}

protected readJson<T>(filePath: string): T | undefined {
const content = this.readFileSync(filePath);
return content ? jsoncparser.parse(content, undefined, { disallowComments: false }) : undefined;
}
protected readFileSync(filePath: string): string {
try {
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
} catch (e) {
console.error(e);
return '';
}
}

// tslint:disable-next-line:no-any
private readConfiguration(rawConfiguration: any, pluginPath: string): any {
return {
Expand Down Expand Up @@ -244,11 +317,8 @@ export class TheiaPluginScanner implements PluginScanner {
mimetypes: rawLang.mimetypes
};
if (rawLang.configuration) {
const conf = fs.readFileSync(path.resolve(pluginPath, rawLang.configuration), 'utf8');
if (conf) {
const strippedContent = jsoncparser.stripComments(conf);
const rawConfiguration: PluginPackageLanguageContributionConfiguration = jsoncparser.parse(strippedContent);

const rawConfiguration = this.readJson<PluginPackageLanguageContributionConfiguration>(path.resolve(pluginPath, rawLang.configuration));
if (rawConfiguration) {
const configuration: LanguageConfiguration = {
brackets: rawConfiguration.brackets,
comments: rawConfiguration.comments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '..
import { PreferenceSchemaProvider } from '@theia/core/lib/browser';
import { PreferenceSchema } from '@theia/core/lib/browser/preferences';
import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler';
import { MonacoSnippetSuggestProvider } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider';

@injectable()
export class PluginContributionHandler {
Expand All @@ -47,6 +48,9 @@ export class PluginContributionHandler {
@inject(KeybindingsContributionPointHandler)
private readonly keybindingsContributionHandler: KeybindingsContributionPointHandler;

@inject(MonacoSnippetSuggestProvider)
protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider;

handleContributions(contributions: PluginContribution): void {
if (contributions.configuration) {
this.updateConfigurationSchema(contributions.configuration);
Expand Down Expand Up @@ -131,6 +135,9 @@ export class PluginContributionHandler {

this.menusContributionHandler.handle(contributions);
this.keybindingsContributionHandler.handle(contributions);
if (contributions.snippets) {
this.snippetSuggestProvider.push(...contributions.snippets);
}
}

private updateConfigurationSchema(schema: PreferenceSchema): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export class LocalDirectoryPluginDeployerResolver implements PluginDeployerResol
return;
}
// remove prefix
const dirPath = localDirSetting.substring('local-dir'.length + 1);
let dirPath = localDirSetting.substring('local-dir'.length + 1);
if (!path.isAbsolute(dirPath)) {
dirPath = path.resolve(process.cwd(), dirPath);
}

// check directory exists
if (!fs.existsSync(dirPath)) {
Expand Down

0 comments on commit a3734c1

Please sign in to comment.