Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
feat(recommendations): Provide a plug-in for recommendations
Browse files Browse the repository at this point in the history
Devfile without plug-ins: provide featured plug-ins and ask to reload workspace
With plug-ins: suggest it
When opening file: Inform that some plug-ins may exist

Change-Id: I5d35f5329f44504e4e6fcad3ba198aa932d50252
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
  • Loading branch information
benoitf committed Mar 2, 2021
1 parent 294121b commit 84fa2c9
Show file tree
Hide file tree
Showing 69 changed files with 6,717 additions and 3 deletions.
1 change: 1 addition & 0 deletions che-theia-init-sources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ sources:
- plugins/ssh-plugin
- plugins/telemetry-plugin
- plugins/github-auth-plugin
- plugins/recommendations-plugin
checkoutTo: master
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

import { WorkspaceService, WorkspaceSettings } from '@eclipse-che/theia-remote-api/lib/common/workspace-service';

import { CheWorkspaceMain } from '../common/che-protocol';
import { ConfirmDialog } from '@theia/core/lib/browser';
import { MessageService } from '@theia/core';
import { RestartWorkspaceOptions } from '@eclipse-che/plugin';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { WorkspaceService } from '@eclipse-che/theia-remote-api/lib/common/workspace-service';
import { che as cheApi } from '@eclipse-che/api';
import { interfaces } from 'inversify';

Expand Down Expand Up @@ -49,6 +50,9 @@ export class CheWorkspaceMainImpl implements CheWorkspaceMain {
}
);
}
async $getSettings(): Promise<WorkspaceSettings> {
return this.workspaceService.getWorkspaceSettings();
}

async $update(workspaceId: string, workspace: cheApi.workspace.Workspace): Promise<cheApi.workspace.Workspace> {
return await this.workspaceService.updateWorkspace(workspaceId, workspace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface CheWorkspaceMain {
// start(workspaceId: string, environmentName: string): Promise<any>;
// startTemporary(config: WorkspaceConfig): Promise<any>;
// stop(workspaceId: string): Promise<any>;
// getSettings(): Promise<WorkspaceSettings>;
$getSettings(): Promise<che.KeyValue>;
$restartWorkspace(machineToken: string, restartWorkspaceOptions?: che.RestartWorkspaceOptions): Promise<boolean>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class CheWorkspaceImpl implements CheWorkspace {
}

getSettings(): Promise<che.KeyValue> {
throw new Error('Method not implemented.');
return this.workspaceMain.$getSettings();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
4 changes: 4 additions & 0 deletions plugins/recommendations-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
lib/
node_modules/
*.theia
coverage
9 changes: 9 additions & 0 deletions plugins/recommendations-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# recommendations-plugin
recommendations-plugin providing recommendations for plug-ins to use.

When opening a workspace from a devfile, the plug-in will scan files and detect file extensions.
If there is no plug-in for the given file extensions, it will add default featured plug-ins from the plug-in registry and prompt the user to restart the workspace.

This feature can be turned off by setting `extensions.ignoreRecommendations` to true in devfile attributes.

This plug-in can suggest plug-ins that can be useful for a given set of languages when opening files. This featured is disabled by default for now and can be enabled using `extensions.openFileRecommendations`
20 changes: 20 additions & 0 deletions plugins/recommendations-plugin/__mocks__/@eclipse-che/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */

/**
* Mock of @theia/plugin module
* @author Florent Benoit
*/
const che: any = {};
che.workspace = {};
module.exports = che;
34 changes: 34 additions & 0 deletions plugins/recommendations-plugin/__mocks__/@theia/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */

/**
* Mock of @theia/plugin module
* @author Florent Benoit
*/
const theia: any = {};
const outputChannel =
{
appendLine: jest.fn(),
}
theia.window = {};
theia.window.createOutputChannel = jest.fn();
theia.window.createOutputChannel.mockReturnValue(outputChannel);
theia.plugins = {};
theia.plugins.all = [];
theia.window.showInformationMessage = jest.fn();
theia.workspace = {
workspaceFolders: undefined,
onDidOpenTextDocument: jest.fn(),
};
theia.plugins.getPlugin = jest.fn();
module.exports = theia;
42 changes: 42 additions & 0 deletions plugins/recommendations-plugin/__mocks__/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/
/* eslint-disable @typescript-eslint/no-explicit-any */

const axios: any = jest.createMockFromModule('axios');

// map between URL and content
const myContent: Map<string, any> = new Map();
const myErrors: Map<string, any> = new Map();

function __setContent(url: string, content: string): void {
myContent.set(url, content);
}

function __setError(url: string, error: any): void {
myErrors.set(url, error);
}

function get(url: string): any {
if (myErrors.has(url)) {
throw myErrors.get(url);
}

return Promise.resolve({ data: myContent.get(url) });
}
function __clearMock(): void {
myContent.clear();
myErrors.clear();
}

axios.get = jest.fn(get);
axios.__setContent = __setContent;
axios.__setError = __setError;
axios.__clearMock = __clearMock;
module.exports = axios;
41 changes: 41 additions & 0 deletions plugins/recommendations-plugin/__mocks__/globby.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

const globby: any = jest.requireActual('globby');

let customError: any | undefined = undefined;
let multipleEnd = false;

function __setStreamError(error: string): void {
customError = error;
}

function __setStreamEnd(): void {
multipleEnd = true;
}

globby.__setStreamError = __setStreamError;
globby.__setStreamEnd = __setStreamEnd;

const originalStream = globby.stream;
globby.stream = (pattern: any, options?: any) => {
const result = originalStream(pattern, options);
if (customError) {
result.emit('error', customError);
}
if (multipleEnd) {
result.emit('end');
}
return result;
};

module.exports = globby;
86 changes: 86 additions & 0 deletions plugins/recommendations-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"name": "@eclipse-che/recommendations-plugin",
"publisher": "Eclipse-Che",
"keywords": [
"theia-plugin"
],
"version": "0.0.1",
"license": "EPL-2.0",
"files": [
"src"
],
"extensionDependencies": [
"Eclipse Che.@eclipse-che/workspace-plugin"
],
"dependencies": {
"@eclipse-che/plugin": "0.0.1",
"@theia/plugin": "next",
"axios": "^0.21.0",
"globby": "^11.0.1",
"inversify": "^5.0.1",
"kind-of": "^6.0.3",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@types/jest": "^26",
"@theia/plugin-packager": "latest",
"@types/fs-extra": "^9.0.3",
"eslint-plugin-header": "^3.1.0",
"ts-jest": "26.4.3",
"jest": "^26.6.3",
"prettier": "^2.1.2",
"prettier-plugin-import-sort": "^0.0.6"
},
"activationEvents": [
"*"
],
"scripts": {
"prepare": "yarn run clean && yarn run build && yarn test",
"clean": "rimraf lib",
"watch": "tsc -watch",
"format": "if-env SKIP_FORMAT=true && echo 'skip format check' || prettier --check '{src,tests}/**/*.ts' package.json",
"format:fix": "prettier --write '{src,tests}/**/*.ts' package.json",
"lint": "if-env SKIP_LINT=true && echo 'skip lint check' || eslint --cache=true --no-error-on-unmatched-pattern=true '{src,tests}/**/*.ts'",
"lint:fix": "eslint --fix --cache=true --no-error-on-unmatched-pattern=true \"{src,tests}/**/*.{ts,tsx}\"",
"compile": "tsc",
"build": "concurrently -n \"format,lint,compile\" -c \"red,green,blue\" \"yarn format\" \"yarn lint\" \"yarn compile\" && theia-plugin pack",
"test": "jest --forceExit",
"test-watch": "jest --watchAll"
},
"engines": {
"theiaPlugin": "next"
},
"theiaPlugin": {
"backend": "lib/plugin.js"
},
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.ts"
],
"testEnvironment": "node",
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"coverageDirectory": "./coverage",
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"modulePathIgnorePatterns": [
"<rootDir>/lib"
],
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
]
}
}
18 changes: 18 additions & 0 deletions plugins/recommendations-plugin/src/analyzer/analyzer-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/
import { ContainerModule, interfaces } from 'inversify';

import { VSCodeCurrentExtensions } from './vscode-current-extensions';

const analyzerModule = new ContainerModule((bind: interfaces.Bind) => {
bind(VSCodeCurrentExtensions).toSelf().inSingletonScope();
});

export { analyzerModule };
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/
export interface LanguageInformation {
id: string;
fileExtensions: string[];
extensions: string[];
workspaceContains: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**********************************************************************
* Copyright (c) 2021 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/
import * as theia from '@theia/plugin';

import { LanguageInformation } from './language-information';
import { VSCodeExtensionsInstalledLanguages as VSCodeCurrentExtensionsLanguages } from './vscode-extensions-installed-languages';
import { injectable } from 'inversify';

/**
* Allow to grab information on VS Code extensions that are installed.
*/
@injectable()
export class VSCodeCurrentExtensions {
async analyze(): Promise<VSCodeCurrentExtensionsLanguages> {
// Map between file extension and language Id
const languagesByFileExtensions = new Map<string, string[]>();

// Map between a language Id and the extension's Ids
const vscodeExtensionByLanguageId = new Map<string, string[]>();

theia.plugins.all.forEach(plugin => {
// populate map between a file extension and the language Id
const contributes = plugin.packageJSON.contributes || { languages: [] };
const languages: LanguageInformation[] = contributes.languages || [];
languages.forEach(language => {
const languageId = language.id;
if (languageId) {
const fileExtensions = language.extensions || [];
fileExtensions.forEach(fileExtension => {
let existingLanguageIds = languagesByFileExtensions.get(fileExtension);
if (!existingLanguageIds) {
existingLanguageIds = [];
languagesByFileExtensions.set(fileExtension, existingLanguageIds);
}
if (!existingLanguageIds.includes(languageId)) {
existingLanguageIds.push(languageId);
}
});
}
});

// populate map between a language Id and a plug-in's Id
const activationEvents: string[] = plugin.packageJSON.activationEvents || [];
activationEvents.forEach(activationEvent => {
if (activationEvent.startsWith('onLanguage:')) {
const languageId = activationEvent.substring('onLanguage:'.length);
let existingPlugins = vscodeExtensionByLanguageId.get(languageId);
if (!existingPlugins) {
existingPlugins = [];
vscodeExtensionByLanguageId.set(languageId, existingPlugins);
}
if (!existingPlugins.includes(plugin.id)) {
existingPlugins.push(plugin.id);
}
}
});
});

return { languagesByFileExtensions, vscodeExtensionByLanguageId };
}
}
Loading

0 comments on commit 84fa2c9

Please sign in to comment.