deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,46 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - OPEN VSX REGISTRY EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/vsx-registry` extension provides integration with the Open VSX Registry.
### Configuration
The extension connects to the public Open VSX Registry hosted on `http://open-vsx.org/`.
One can host own instance of a [registry](https://github.com/eclipse/openvsx#eclipse-open-vsx)
and configure `VSX_REGISTRY_URL` environment variable to use it.
### Using multiple registries
It is possible to target multiple registries by specifying a CLI argument when
running the backend: `--ovsx-router-config=<path>` where `path` must point to
a json defining an `OVSXRouterConfig` object.
See `@theia/ovsx-client`'s documentation to read more about `OVSXRouterClient`
and its `OVSXRouterConfig` configuration.
## Additional Information
- [API documentation for `@theia/vsx-registry`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_vsx-registry.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,65 @@
{
"name": "@theia/vsx-registry",
"version": "1.68.0",
"description": "Theia - VSX Registry",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/navigator": "1.68.0",
"@theia/ovsx-client": "1.68.0",
"@theia/plugin-ext": "1.68.0",
"@theia/plugin-ext-vscode": "1.68.0",
"@theia/preferences": "1.68.0",
"@theia/workspace": "1.68.0",
"limiter": "^2.1.0",
"luxon": "^2.4.0",
"p-debounce": "^2.1.0",
"semver": "^7.5.4",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"theia-extension"
],
"theiaExtensions": [
{
"frontend": "lib/common/vsx-registry-common-module",
"backend": "lib/common/vsx-registry-common-module"
},
{
"frontend": "lib/browser/vsx-registry-frontend-module",
"backend": "lib/node/vsx-registry-backend-module"
}
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0",
"@types/luxon": "^2.3.2"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,98 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 {
FolderPreferenceProvider,
FolderPreferenceProviderFactory,
FolderPreferenceProviderFolder,
} from '@theia/preferences/lib/browser';
import { Container, injectable, interfaces } from '@theia/core/shared/inversify';
import { extensionsConfigurationSchema } from './recommended-extensions-json-schema';
import {
WorkspaceFilePreferenceProvider,
WorkspaceFilePreferenceProviderFactory,
WorkspaceFilePreferenceProviderOptions
} from '@theia/preferences/lib/browser/workspace-file-preference-provider';
import { SectionPreferenceProviderUri, SectionPreferenceProviderSection } from '@theia/preferences/lib/common/section-preference-provider';
import { UserPreferenceProvider, UserPreferenceProviderFactory } from '@theia/preferences/lib/common/user-preference-provider';
import { bindFactory } from '@theia/core';
/**
* The overrides in this file are required because the base preference providers assume that a
* section name (extensions) will not be used as a prefix (extensions.ignoreRecommendations).
*/
@injectable()
export class FolderPreferenceProviderWithExtensions extends FolderPreferenceProvider {
protected override getPath(preferenceName: string): string[] | undefined {
const path = super.getPath(preferenceName);
if (this.section !== 'extensions' || !path?.length) {
return path;
}
const isExtensionsField = path[0] in extensionsConfigurationSchema.properties!;
if (isExtensionsField) {
return path;
}
return undefined;
}
}
@injectable()
export class UserPreferenceProviderWithExtensions extends UserPreferenceProvider {
protected override getPath(preferenceName: string): string[] | undefined {
const path = super.getPath(preferenceName);
if (this.section !== 'extensions' || !path?.length) {
return path;
}
const isExtensionsField = path[0] in extensionsConfigurationSchema.properties!;
if (isExtensionsField) {
return path;
}
return undefined;
}
}
@injectable()
export class WorkspaceFilePreferenceProviderWithExtensions extends WorkspaceFilePreferenceProvider {
protected override belongsInSection(firstSegment: string, remainder: string): boolean {
if (firstSegment === 'extensions') {
return remainder in extensionsConfigurationSchema.properties!;
}
return this.configurations.isSectionName(firstSegment);
}
}
export function bindPreferenceProviderOverrides(bind: interfaces.Bind, unbind: interfaces.Unbind): void {
unbind(UserPreferenceProviderFactory);
unbind(FolderPreferenceProviderFactory);
unbind(WorkspaceFilePreferenceProviderFactory);
bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProviderWithExtensions, SectionPreferenceProviderUri, SectionPreferenceProviderSection);
bindFactory(
bind,
FolderPreferenceProviderFactory,
FolderPreferenceProviderWithExtensions,
SectionPreferenceProviderUri,
SectionPreferenceProviderSection,
FolderPreferenceProviderFolder,
);
bind(WorkspaceFilePreferenceProviderFactory).toFactory(ctx => (options: WorkspaceFilePreferenceProviderOptions) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
child.bind(WorkspaceFilePreferenceProvider).to(WorkspaceFilePreferenceProviderWithExtensions);
child.bind(WorkspaceFilePreferenceProviderOptions).toConstantValue(options);
return child.get(WorkspaceFilePreferenceProvider);
});
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { JsonSchemaContribution, JsonSchemaDataStore, JsonSchemaRegisterContext } from '@theia/core/lib/browser/json-schema-store';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { extensionsSchemaID } from '../../common/recommended-extensions-preference-contribution';
export const extensionsConfigurationSchema: IJSONSchema = {
$id: extensionsSchemaID,
default: { recommendations: [] },
type: 'object',
properties: {
recommendations: {
title: 'A list of extensions recommended for users of this workspace. Should use the form "<publisher>.<extension name>"',
type: 'array',
items: {
type: 'string',
pattern: '^\\w[\\w-]+\\.\\w[\\w-]+$',
patternErrorMessage: "Expected format '${publisher}.${name}'. Example: 'eclipse.theia'."
},
default: [],
},
unwantedRecommendations: {
title: 'A list of extensions recommended by default that should not be recommended to users of this workspace. Should use the form "<publisher>.<extension name>"',
type: 'array',
items: {
type: 'string',
pattern: '^\\w[\\w-]+\\.\\w[\\w-]+$',
patternErrorMessage: "Expected format '${publisher}.${name}'. Example: 'eclipse.theia'."
},
default: [],
}
},
allowComments: true,
allowTrailingCommas: true,
};
@injectable()
export class ExtensionSchemaContribution implements JsonSchemaContribution {
protected readonly uri = new URI(extensionsSchemaID);
@inject(JsonSchemaDataStore) protected readonly schemaStore: JsonSchemaDataStore;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@postConstruct()
protected init(): void {
this.schemaStore.setSchema(this.uri, extensionsConfigurationSchema);
}
registerSchemas(context: JsonSchemaRegisterContext): void {
context.registerSchema({
fileMatch: ['extensions.json'],
url: this.uri.toString(),
});
this.workspaceService.updateSchema('extensions', { $ref: this.uri.toString() });
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg fill="#F6F6F6" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><g fill="#F6F6F6"><path clip-rule="evenodd" d="m3 3h10v4h-6v6 2 6h6v4h-10zm18 12v6h-6v4h10v-10zm4-2v-10h-10v4h3v-1h4v4h-1v3z" fill-rule="evenodd"/><path d="m9 9h10v10h-10z"/></g></svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -0,0 +1,421 @@
/********************************************************************************
* Copyright (C) 2020 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-only WITH Classpath-exception-2.0
********************************************************************************/
:root {
--theia-vsx-extension-icon-size: calc(var(--theia-ui-icon-font-size) * 3);
--theia-vsx-extension-editor-icon-size: calc(var(--theia-vsx-extension-icon-size) * 3);
}
.vsx-search-container {
display: flex;
align-items: center;
width: 100%;
background: var(--theia-input-background);
border-style: solid;
border-width: var(--theia-border-width);
border-color: var(--theia-input-background);
border-radius: 2px;
}
.vsx-search-container:focus-within {
border-color: var(--theia-focusBorder);
}
.vsx-search-container .option-buttons {
height: 23px;
display: flex;
align-items: center;
align-self: flex-start;
background-color: none;
margin: 2px;
}
.vsx-search-container .option {
margin: 0 1px;
display: inline-block;
box-sizing: border-box;
user-select: none;
background-repeat: no-repeat;
background-position: center;
border: var(--theia-border-width) solid transparent;
opacity: 0.7;
cursor: pointer;
}
.vsx-search-container .option.enabled {
color: var(--theia-inputOption-activeForeground);
border: var(--theia-border-width) var(--theia-inputOption-activeBorder) solid;
background-color: var(--theia-inputOption-activeBackground);
opacity: 1;
}
.vsx-search-container .option:hover {
opacity: 1;
}
.theia-vsx-extensions {
height: 100%;
}
.theia-vsx-extension,
.theia-vsx-extensions-view-container .part>.body {
min-height: calc(var(--theia-content-line-height) * 3);
}
.theia-vsx-extensions-search-bar {
display: flex;
align-content: center;
padding: var(--theia-ui-padding)
max(var(--theia-scrollbar-width), var(--theia-ui-padding))
var(--theia-ui-padding)
calc(var(--theia-ui-padding) * 3);
}
.theia-vsx-extensions-search-bar .theia-input {
overflow: hidden;
flex: 1;
background: transparent;
}
.theia-vsx-extensions-search-bar .theia-input:focus {
border: none;
outline: none;
}
.theia-vsx-extension {
display: flex;
flex-direction: row;
line-height: calc(var(--theia-content-line-height) * 17 / 22);
align-items: center;
}
.theia-vsx-extension-icon {
height: var(--theia-vsx-extension-icon-size);
width: var(--theia-vsx-extension-icon-size);
align-self: center;
padding-right: calc(var(--theia-ui-padding) * 2.5);
flex-shrink: 0;
object-fit: contain;
}
.theia-vsx-extension-icon.placeholder {
background-size: var(--theia-vsx-extension-icon-size);
background-repeat: no-repeat;
background-image: url("defaultIcon.png");
}
.theia-vsx-extension-content {
display: flex;
flex-direction: column;
width: calc(100% - var(--theia-vsx-extension-icon-size) - var(--theia-ui-padding) * 2.5);
}
.theia-vsx-extension-content .title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
white-space: nowrap;
}
.theia-vsx-extension-content .title .name {
font-weight: bold;
}
.theia-vsx-extension-content .disabled {
font-weight: bold;
}
.theia-vsx-extension-content .title .version,
.theia-vsx-extension-content .title .stat {
opacity: 0.85;
font-size: 80%;
}
.theia-vsx-extension-content .title .stat .codicon {
font-size: 110%;
}
.theia-vsx-extension-content .title .stat .download-count,
.theia-vsx-extension-content .title .stat .average-rating {
display: inline-flex;
align-items: center;
}
.theia-vsx-extension-content .title .stat .average-rating>i {
color: #ff8e00;
}
.theia-vsx-extension-editor .download-count>i,
.theia-vsx-extension-content .title .stat .average-rating>i,
.theia-vsx-extension-content .title .stat .download-count>i {
padding-right: calc(var(--theia-ui-padding) / 2);
}
.theia-vsx-extension-content .title .stat .average-rating,
.theia-vsx-extension-content .title .stat .download-count {
padding-left: var(--theia-ui-padding);
}
.theia-vsx-extension-description {
padding-right: calc(var(--theia-ui-padding) * 2);
}
.theia-vsx-extension-publisher {
font-weight: 600;
font-size: 90%;
}
.theia-vsx-extension-action-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
white-space: nowrap;
}
.theia-vsx-extension-action-bar .codicon-verified-filled {
color: var(--theia-extensionIcon-verifiedForeground);
margin-right: 2px;
}
.theia-vsx-extension-publisher-container {
display: flex;
}
.theia-vsx-extension-action-bar .action {
font-size: 90%;
min-width: auto !important;
padding: 2px var(--theia-ui-padding) !important;
margin-top: 2px;
vertical-align: middle;
}
/* Editor Section */
.theia-vsx-extension-editor {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
padding-top: 0;
position: relative;
}
.theia-vsx-extension-editor .header {
display: flex;
padding: calc(var(--theia-ui-padding) * 3)
max(var(--theia-ui-padding), var(--theia-scrollbar-width))
calc(var(--theia-ui-padding) * 3)
calc(var(--theia-ui-padding) * 3);
flex-shrink: 0;
border-bottom: 1px solid hsla(0, 0%, 50%, 0.5);
width: 100%;
background: var(--theia-editor-background);
}
.theia-vsx-extension-editor .scroll-container {
position: relative;
padding-top: 0;
max-width: 100%;
width: 100%;
}
.theia-vsx-extension-editor .body {
flex: 1;
padding: calc(var(--theia-ui-padding) * 2);
padding-top: 0;
max-width: 1000px;
margin: 0 auto;
line-height: 22px;
}
.theia-vsx-extension-editor .body h1 {
padding-bottom: var(--theia-ui-padding);
border-bottom: 1px solid hsla(0, 0%, 50%, 0.5);
margin-top: calc(var(--theia-ui-padding) * 5);
}
.theia-vsx-extension-editor .body a {
text-decoration: none;
}
.theia-vsx-extension-editor .body a:hover {
text-decoration: underline;
}
.theia-vsx-extension-editor .body table {
border-collapse: collapse;
}
.theia-vsx-extension-editor .body table>thead>tr>th {
text-align: left;
border-bottom: 1px solid var(--theia-extensionEditor-tableHeadBorder);
}
.theia-vsx-extension-editor .body table>thead>tr>th,
.theia-vsx-extension-editor .body table>thead>tr>td,
.theia-vsx-extension-editor .body table>tbody>tr>th,
.theia-vsx-extension-editor .body table>tbody>tr>td {
padding: 5px 10px;
}
.theia-vsx-extension-editor .body table>tbody>tr+tr>td {
border-top: 1px solid var(--theia-extensionEditor-tableCellBorder);
}
.theia-vsx-extension-editor .scroll-container .body pre {
white-space: normal;
}
.theia-vsx-extension-editor .body img {
max-width: 100%;
}
.theia-vsx-extension-editor .header .icon-container {
height: var(--theia-vsx-extension-editor-icon-size);
width: var(--theia-vsx-extension-editor-icon-size);
align-self: center;
padding-right: calc(var(--theia-ui-padding) * 2.5);
flex-shrink: 0;
object-fit: contain;
}
.theia-vsx-extension-editor .header .icon-container.placeholder {
background-size: var(--theia-vsx-extension-editor-icon-size);
background-repeat: no-repeat;
background-image: url("defaultIcon.png");
}
.theia-vsx-extension-editor .header .details {
overflow: hidden;
user-select: text;
-webkit-user-select: text;
}
.theia-vsx-extension-editor .header .details .title,
.theia-vsx-extension-editor .header .details .subtitle {
display: flex;
align-items: center;
}
.theia-vsx-extension-editor .header .details .title .name {
flex: 0;
font-size: calc(var(--theia-ui-font-size1) * 2);
font-weight: 600;
white-space: nowrap;
cursor: pointer;
}
.theia-vsx-extension-editor .header .details .title .identifier {
margin-left: calc(var(--theia-ui-padding) * 5 / 3);
opacity: 0.6;
background: hsla(0, 0%, 68%, 0.31);
user-select: text;
-webkit-user-select: text;
white-space: nowrap;
}
.theia-vsx-extension-editor .header .details .title .preview {
background: #d63f26;
}
.vs .theia-vsx-extension-editor .header .details .title .preview {
color: white;
}
.theia-vsx-extension-editor .header .details .title .identifier,
.theia-vsx-extension-editor .header .details .title .preview,
.theia-vsx-extension-editor .header .details .title .builtin {
line-height: var(--theia-code-line-height);
}
.theia-vsx-extension-editor .header .details .title .identifier,
.theia-vsx-extension-editor .header .details .title .preview {
padding: calc(var(--theia-ui-padding) * 2 / 3);
padding-top: 0px;
padding-bottom: 0px;
border-radius: calc(var(--theia-ui-padding) * 2 / 3);
}
.theia-vsx-extension-editor .header .details .title .preview,
.theia-vsx-extension-editor .header .details .title .builtin {
font-size: var(--theia-ui-font-size0);
font-style: italic;
margin-left: calc(var(--theia-ui-padding) * 5 / 3);
}
.theia-vsx-extension-editor .header .details .subtitle {
padding-top: var(--theia-ui-padding);
white-space: nowrap;
}
.theia-vsx-extension-editor .header .details .subtitle>span {
display: flex;
align-items: center;
cursor: pointer;
line-height: var(--theia-content-line-height);
height: var(--theia-content-line-height);
}
.theia-vsx-extension-editor .header .details .subtitle>span:not(:first-child):not(:empty) {
border-left: 1px solid hsla(0, 0%, 50%, 0.7);
padding-left: calc(var(--theia-ui-padding) * 2);
margin-left: calc(var(--theia-ui-padding) * 2);
font-weight: 500;
}
.theia-vsx-extension-editor .header .details .subtitle .publisher {
font-size: var(--theia-ui-font-size3);
}
.theia-vsx-extension-editor .header .details .subtitle .publisher .namespace-access,
.theia-vsx-extension-editor .header .details .subtitle .download-count::before {
padding-right: var(--theia-ui-padding);
}
.theia-vsx-extension-editor .header .details .subtitle .average-rating>i {
color: #ff8e00;
}
.theia-vsx-extension-editor .header .details .subtitle .average-rating>i:not(:first-child) {
padding-left: calc(var(--theia-ui-padding) / 2);
}
.theia-vsx-extension-editor .header .details .description {
margin-top: calc(var(--theia-ui-padding) * 5 / 3);
}
.theia-vsx-extension-editor .action {
font-weight: 600;
margin-top: calc(var(--theia-ui-padding) * 5 / 3);
margin-left: 0px;
padding: 1px var(--theia-ui-padding);
vertical-align: middle;
}
/** Theming */
.theia-vsx-extension-editor .action.prominent,
.theia-vsx-extension-action-bar .action.prominent {
color: var(--theia-extensionButton-prominentForeground);
background-color: var(--theia-extensionButton-prominentBackground);
}
.theia-vsx-extension-editor .action.prominent:hover,
.theia-vsx-extension-action-bar .action.prominent:hover {
background-color: var(--theia-extensionButton-prominentHoverBackground);
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2024 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ArgumentProcessor } from '@theia/plugin-ext/lib/common/commands';
import { VSXExtension } from './vsx-extension';
@injectable()
export class VsxExtensionArgumentProcessor implements ArgumentProcessor {
processArgument(arg: unknown): unknown {
if (arg instanceof VSXExtension) {
return arg.id;
}
return arg;
}
}

View File

@@ -0,0 +1,76 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 { nls } from '@theia/core/lib/common/nls';
import { codicon } from '@theia/core/lib/browser';
import { Command } from '@theia/core/lib/common';
export namespace VSXExtensionsCommands {
const EXTENSIONS_CATEGORY = 'Extensions';
export const CLEAR_ALL = Command.toDefaultLocalizedCommand({
id: 'vsxExtensions.clearAll',
category: EXTENSIONS_CATEGORY,
label: 'Clear Search Results',
iconClass: codicon('clear-all')
});
export const INSTALL_FROM_VSIX: Command & { dialogLabel: string } = {
id: 'vsxExtensions.installFromVSIX',
category: nls.localizeByDefault(EXTENSIONS_CATEGORY),
originalCategory: EXTENSIONS_CATEGORY,
originalLabel: 'Install from VSIX...',
label: nls.localizeByDefault('Install from VSIX') + '...',
dialogLabel: nls.localizeByDefault('Install from VSIX')
};
export const INSTALL_VSIX_FILE: Command = Command.toDefaultLocalizedCommand({
id: 'vsxExtensions.installVSIX',
label: 'Install Extension VSIX',
category: EXTENSIONS_CATEGORY,
});
export const INSTALL_ANOTHER_VERSION: Command = {
id: 'vsxExtensions.installAnotherVersion'
};
export const DISABLE: Command = {
id: 'vsxExtensions.disable'
};
export const ENABLE: Command = {
id: 'vsxExtensions.enable'
};
export const COPY: Command = {
id: 'vsxExtensions.copy'
};
export const COPY_EXTENSION_ID: Command = {
id: 'vsxExtensions.copyExtensionId'
};
export const SHOW_BUILTINS = Command.toDefaultLocalizedCommand({
id: 'vsxExtension.showBuiltins',
label: 'Show Built-in Extensions',
category: EXTENSIONS_CATEGORY,
});
export const SHOW_INSTALLED = Command.toLocalizedCommand({
id: 'vsxExtension.showInstalled',
label: 'Show Installed Extensions',
category: EXTENSIONS_CATEGORY,
}, 'theia/vsx-registry/showInstalled');
export const SHOW_RECOMMENDATIONS = Command.toDefaultLocalizedCommand({
id: 'vsxExtension.showRecommendations',
label: 'Show Recommended Extensions',
category: EXTENSIONS_CATEGORY,
});
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { WidgetOpenHandler } from '@theia/core/lib/browser';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { VSXExtensionEditor } from './vsx-extension-editor';
@injectable()
export class VSXExtensionEditorManager extends WidgetOpenHandler<VSXExtensionEditor> {
readonly id = VSXExtensionEditor.ID;
canHandle(uri: URI): number {
const id = VSCodeExtensionUri.toId(uri);
return !!id ? 500 : 0;
}
protected createWidgetOptions(uri: URI): { id: string } {
const id = VSCodeExtensionUri.toId(uri);
if (!id) {
throw new Error('Invalid URI: ' + uri.toString());
}
return id;
}
}

View File

@@ -0,0 +1,96 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { ReactWidget, Message, Widget, codicon } from '@theia/core/lib/browser';
import { VSXExtension, VSXExtensionEditorComponent } from './vsx-extension';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class VSXExtensionEditor extends ReactWidget {
static ID = 'vsx-extension-editor';
@inject(VSXExtension)
protected readonly extension: VSXExtension;
@inject(VSXExtensionsModel)
protected readonly model: VSXExtensionsModel;
protected readonly deferredScrollContainer = new Deferred<HTMLElement>();
@postConstruct()
protected init(): void {
this.addClass('theia-vsx-extension-editor');
this.id = VSXExtensionEditor.ID + ':' + this.extension.id;
this.title.closable = true;
this.updateTitle();
this.title.iconClass = codicon('list-selection');
this.node.tabIndex = -1;
this.update();
this.toDispose.push(this.model.onDidChange(() => this.update()));
}
override getScrollContainer(): Promise<HTMLElement> {
return this.deferredScrollContainer.promise;
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.updateTitle();
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.update();
}
protected updateTitle(): void {
const label = nls.localizeByDefault('Extension: {0}', (this.extension.displayName || this.extension.name));
this.title.label = label;
this.title.caption = label;
}
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.update();
};
protected resolveScrollContainer = (element: VSXExtensionEditorComponent | null) => {
if (!element) {
this.deferredScrollContainer.reject(new Error('element is null'));
} else if (!element.scrollContainer) {
this.deferredScrollContainer.reject(new Error('element.scrollContainer is undefined'));
} else {
this.deferredScrollContainer.resolve(element.scrollContainer);
}
};
protected render(): React.ReactNode {
return <VSXExtensionEditorComponent
ref={this.resolveScrollContainer}
extension={this.extension}
/>;
}
}

View File

@@ -0,0 +1,765 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { TreeElement, TreeElementNode } from '@theia/core/lib/browser/source-tree';
import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { PluginServer, DeployedPlugin, PluginIdentifiers, PluginDeployOptions } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { CommandRegistry, MenuPath, nls } from '@theia/core/lib/common';
import { codicon, ConfirmDialog, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
import { VSXExtensionsModel } from './vsx-extensions-model';
export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];
export namespace VSXExtensionsContextMenu {
export const INSTALL = [...EXTENSIONS_CONTEXT_MENU, '1_install'];
export const DISABLE = [...EXTENSIONS_CONTEXT_MENU, '2_disable'];
export const ENABLE = [...EXTENSIONS_CONTEXT_MENU, '2_enable'];
export const COPY = [...EXTENSIONS_CONTEXT_MENU, '3_copy'];
export const CONTRIBUTION = [...EXTENSIONS_CONTEXT_MENU, '4_contribution'];
}
@injectable()
export class VSXExtensionData {
readonly version?: string;
readonly iconUrl?: string;
readonly publisher?: string;
readonly name?: string;
readonly displayName?: string;
readonly description?: string;
readonly averageRating?: number;
readonly downloadCount?: number;
readonly downloadUrl?: string;
readonly readmeUrl?: string;
readonly licenseUrl?: string;
readonly repository?: string;
readonly license?: string;
readonly readme?: string;
readonly preview?: boolean;
readonly verified?: boolean;
readonly namespaceAccess?: VSXExtensionNamespaceAccess;
readonly publishedBy?: VSXUser;
static KEYS: Set<(keyof VSXExtensionData)> = new Set([
'version',
'iconUrl',
'publisher',
'name',
'displayName',
'description',
'averageRating',
'downloadCount',
'downloadUrl',
'readmeUrl',
'licenseUrl',
'repository',
'license',
'readme',
'preview',
'verified',
'namespaceAccess',
'publishedBy'
]);
}
@injectable()
export class VSXExtensionOptions {
readonly id: string;
readonly version?: string;
readonly model: VSXExtensionsModel;
}
export const VSXExtensionFactory = Symbol('VSXExtensionFactory');
export type VSXExtensionFactory = (options: VSXExtensionOptions) => VSXExtension;
@injectable()
export class VSXExtension implements VSXExtensionData, TreeElement {
/**
* Ensure the version string begins with `'v'`.
*/
static formatVersion(version: string | undefined): string | undefined {
if (version && !version.startsWith('v')) {
return `v${version}`;
}
return version;
}
@inject(VSXExtensionOptions)
protected readonly options: VSXExtensionOptions;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(HostedPluginSupport)
protected readonly pluginSupport: HostedPluginSupport;
@inject(PluginServer)
protected readonly pluginServer: PluginServer;
@inject(ProgressService)
protected readonly progressService: ProgressService;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(VSXEnvironment)
readonly environment: VSXEnvironment;
@inject(VSXExtensionsSearchModel)
readonly search: VSXExtensionsSearchModel;
@inject(HoverService)
protected readonly hoverService: HoverService;
@inject(WindowService)
readonly windowService: WindowService;
@inject(CommandRegistry)
readonly commandRegistry: CommandRegistry;
protected readonly data: Partial<VSXExtensionData> = {};
protected registryUri: Promise<string>;
@postConstruct()
protected postConstruct(): void {
this.registryUri = this.environment.getRegistryUri();
}
get uri(): URI {
return VSCodeExtensionUri.fromId(this.id);
}
get id(): string {
return this.options.id;
}
get installedVersion(): string | undefined {
return this.plugin?.metadata.model.version || this.options.version;
}
get model(): VSXExtensionsModel {
return this.options.model;
}
get visible(): boolean {
return true;
}
get plugin(): DeployedPlugin | undefined {
return this.pluginSupport.getPlugin(this.id as PluginIdentifiers.UnversionedId);
}
get installed(): boolean {
return !!this.version && this.model
.isInstalledAtSpecificVersion(PluginIdentifiers.idAndVersionToVersionedId({ id: this.id as PluginIdentifiers.UnversionedId, version: this.version }));
}
get uninstalled(): boolean {
return !!this.version && this.model.isUninstalled(PluginIdentifiers.idAndVersionToVersionedId({ id: this.id as PluginIdentifiers.UnversionedId, version: this.version }));
}
get deployed(): boolean {
return !!this.version && this.model.isDeployed(PluginIdentifiers.idAndVersionToVersionedId({ id: this.id as PluginIdentifiers.UnversionedId, version: this.version }));
}
get disabled(): boolean {
return this.model.isDisabled(this.id);
}
get builtin(): boolean {
return this.model.isBuiltIn(this.id);
}
update(data: Partial<VSXExtensionData>): void {
for (const key of VSXExtensionData.KEYS) {
if (key in data) {
Object.assign(this.data, { [key]: data[key] });
}
}
}
reloadWindow(): void {
this.windowService.reload();
}
protected getData<K extends keyof VSXExtensionData>(key: K): VSXExtensionData[K] {
const model = this.plugin?.metadata.model;
if (model && key in model) {
return model[key as keyof typeof model] as VSXExtensionData[K];
}
return this.data[key];
}
get iconUrl(): string | undefined {
const plugin = this.plugin;
const iconUrl = plugin && plugin.metadata.model.iconUrl;
if (iconUrl) {
return new Endpoint({ path: iconUrl }).getRestUrl().toString();
}
return this.data['iconUrl'];
}
get publisher(): string | undefined {
return this.getData('publisher');
}
get name(): string | undefined {
return this.getData('name');
}
get displayName(): string | undefined {
return this.getData('displayName') || this.name;
}
get description(): string | undefined {
return this.getData('description');
}
get version(): string | undefined {
return this.getData('version');
}
get averageRating(): number | undefined {
return this.getData('averageRating');
}
get downloadCount(): number | undefined {
return this.getData('downloadCount');
}
get downloadUrl(): string | undefined {
return this.getData('downloadUrl');
}
get readmeUrl(): string | undefined {
const plugin = this.plugin;
const readmeUrl = plugin && plugin.metadata.model.readmeUrl;
if (readmeUrl) {
return new Endpoint({ path: readmeUrl }).getRestUrl().toString();
}
return this.data['readmeUrl'];
}
get licenseUrl(): string | undefined {
let licenseUrl = this.data['licenseUrl'];
if (licenseUrl) {
return licenseUrl;
} else {
const plugin = this.plugin;
licenseUrl = plugin && plugin.metadata.model.licenseUrl;
if (licenseUrl) {
return new Endpoint({ path: licenseUrl }).getRestUrl().toString();
}
}
}
get repository(): string | undefined {
return this.getData('repository');
}
get license(): string | undefined {
return this.getData('license');
}
get readme(): string | undefined {
return this.getData('readme');
}
get preview(): boolean | undefined {
return this.getData('preview');
}
get verified(): boolean | undefined {
return this.getData('verified');
}
get namespaceAccess(): VSXExtensionNamespaceAccess | undefined {
return this.getData('namespaceAccess');
}
get publishedBy(): VSXUser | undefined {
return this.getData('publishedBy');
}
get tooltip(): string {
let md = `__${this.displayName}__ ${VSXExtension.formatVersion(this.version)}\n\n${this.description}\n_____\n\n${nls.localizeByDefault('Publisher: {0}', this.publisher)}`;
if (this.license) {
md += ` \r${nls.localize('theia/vsx-registry/license', 'License: {0}', this.license)}`;
}
if (this.downloadCount) {
md += ` \r${nls.localize('theia/vsx-registry/downloadCount', 'Download count: {0}', downloadCompactFormatter.format(this.downloadCount))}`;
}
if (this.averageRating) {
md += ` \r${getAverageRatingTitle(this.averageRating)}`;
}
return md;
}
protected _currentTaskName: string | undefined;
get currentTask(): string | undefined {
return this._currentTaskName;
}
protected _currentTask: Promise<void> | undefined;
protected runTask(name: string, task: () => Promise<void>): Promise<void> {
if (this._currentTask) {
return Promise.reject('busy');
}
this._currentTaskName = name;
this._currentTask = task();
this._currentTask.finally(() => {
this._currentTask = undefined;
this._currentTaskName = undefined;
});
return this._currentTask;
}
async install(options?: PluginDeployOptions): Promise<void> {
if (!this.verified) {
const choice = await new ConfirmDialog({
title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation?'),
msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName)
}).open();
if (choice) {
await this.doInstall(options);
}
} else {
await this.doInstall(options);
}
}
async uninstall(): Promise<void> {
const { id, installedVersion } = this;
if (id && installedVersion) {
await this.runTask(nls.localizeByDefault('Uninstalling'),
async () => await this.progressService.withProgress(
nls.localizeByDefault('Uninstalling {0}...', this.id), 'extensions',
() => this.pluginServer.uninstall(PluginIdentifiers.idAndVersionToVersionedId({ id: (id as PluginIdentifiers.UnversionedId), version: installedVersion }))
)
);
}
}
async disable(): Promise<void> {
const { id, installedVersion } = this;
if (id && installedVersion) {
await this.runTask(nls.localize('vsx.disabling', 'Disabling'), async () => {
await this.progressService.withProgress(
nls.localize('vsx.disabling.extensions', 'Disabling {0}...', this.id), 'extensions',
() => this.pluginServer.disablePlugin(id as PluginIdentifiers.UnversionedId)
);
});
}
}
async enable(): Promise<void> {
const { id, installedVersion } = this;
if (id && installedVersion) {
await this.runTask(nls.localize('vsx.enabling', 'Enabling'), async () => {
await this.progressService.withProgress(
nls.localize('vsx.enabling.extension', 'Enabling {0}...', this.id), 'extensions',
() => this.pluginServer.enablePlugin(id as PluginIdentifiers.UnversionedId)
);
});
}
}
protected async doInstall(options?: PluginDeployOptions): Promise<void> {
await this.runTask(nls.localizeByDefault('Installing'),
() => this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
this.pluginServer.install(this.uri.toString(), undefined, options)
));
}
handleContextMenu(e: React.MouseEvent<HTMLElement, MouseEvent>): void {
e.preventDefault();
this.contextMenuRenderer.render({
menuPath: EXTENSIONS_CONTEXT_MENU,
anchor: {
x: e.clientX,
y: e.clientY,
},
args: [this],
context: e.currentTarget
});
}
/**
* Get the registry link for the given extension.
* @param path the url path.
* @returns the registry link for the given extension at the path.
*/
async getRegistryLink(path = ''): Promise<URI> {
const registryUri = new URI(await this.registryUri);
if (this.downloadUrl) {
const downloadUri = new URI(this.downloadUrl);
if (downloadUri.authority !== registryUri.authority) {
throw new Error('cannot generate a valid URL');
}
}
return registryUri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
}
async serialize(): Promise<string> {
const serializedExtension: string[] = [];
serializedExtension.push(`Name: ${this.displayName}`);
serializedExtension.push(`Id: ${this.id}`);
serializedExtension.push(`Description: ${this.description}`);
serializedExtension.push(`Version: ${this.version}`);
serializedExtension.push(`Publisher: ${this.publisher}`);
if (this.downloadUrl !== undefined) {
const registryLink = await this.getRegistryLink();
serializedExtension.push(`Open VSX Link: ${registryLink.toString()}`);
};
return serializedExtension.join('\n');
}
async open(options: OpenerOptions = { mode: 'reveal' }): Promise<void> {
await this.doOpen(this.uri, options);
}
async doOpen(uri: URI, options?: OpenerOptions): Promise<void> {
await open(this.openerService, uri, options);
}
render(host: TreeWidget): React.ReactNode {
return <VSXExtensionComponent extension={this} host={host} hoverService={this.hoverService} />;
}
}
export abstract class AbstractVSXExtensionComponent<Props extends AbstractVSXExtensionComponent.Props = AbstractVSXExtensionComponent.Props> extends React.Component<Props> {
readonly install = async (event?: React.MouseEvent) => {
event?.stopPropagation();
this.forceUpdate();
try {
const pending = this.props.extension.install();
this.forceUpdate();
await pending;
} finally {
this.forceUpdate();
}
};
readonly uninstall = async (event?: React.MouseEvent) => {
event?.stopPropagation();
try {
const pending = this.props.extension.uninstall();
this.forceUpdate();
await pending;
} finally {
this.forceUpdate();
}
};
readonly reloadWindow = (event?: React.MouseEvent) => {
event?.stopPropagation();
this.props.extension.reloadWindow();
};
protected readonly manage = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
this.props.extension.handleContextMenu(e);
};
protected renderAction(host?: TreeWidget): React.ReactNode {
const { builtin, currentTask, disabled, uninstalled, installed, deployed } = this.props.extension;
const isFocused = (host?.model.getFocusedNode() as TreeElementNode)?.element === this.props.extension;
const tabIndex = (!host || isFocused) ? 0 : undefined;
const inactive = disabled || uninstalled || !installed;
const outOfSync = (installed && uninstalled) || deployed === inactive;
if (currentTask) {
return <button className="theia-button action prominent theia-mod-disabled">{currentTask}</button>;
}
return <div>
{
outOfSync && <button className="theia-button action" onClick={this.reloadWindow}>{nls.localizeByDefault('Reload Window')}</button>
}
{
!builtin && installed && !uninstalled && <button className="theia-button action" onClick={this.uninstall}>{nls.localizeByDefault('Uninstall')}</button>
}
{
!builtin && !installed && <button className="theia-button prominent action" onClick={this.install}>{nls.localizeByDefault('Install')}</button>
}
<div className="codicon codicon-settings-gear action" tabIndex={tabIndex} onClick={this.manage}></div>
</div>;
}
}
export namespace AbstractVSXExtensionComponent {
export interface Props {
extension: VSXExtension;
}
}
const downloadFormatter = new Intl.NumberFormat();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const downloadCompactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' } as any);
const averageRatingFormatter = (averageRating: number): number => Math.round(averageRating * 2) / 2;
const getAverageRatingTitle = (averageRating: number): string =>
nls.localizeByDefault('Average rating: {0} out of 5', averageRatingFormatter(averageRating));
export namespace VSXExtensionComponent {
export interface Props extends AbstractVSXExtensionComponent.Props {
host: TreeWidget;
hoverService: HoverService;
}
}
export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = VSXExtensionComponent.Props> extends AbstractVSXExtensionComponent<Props> {
override render(): React.ReactNode {
const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified, disabled, installed } = this.props.extension;
return <div
className='theia-vsx-extension noselect'
onMouseEnter={event => {
this.props.hoverService.requestHover({
content: new MarkdownStringImpl(tooltip),
target: event.currentTarget,
position: 'right'
});
}}
onMouseUp={event => {
if (event.button === 2) {
this.manage(event);
}
}}
>
{iconUrl ?
<img className='theia-vsx-extension-icon' src={iconUrl} /> :
<div className='theia-vsx-extension-icon placeholder' />}
<div className='theia-vsx-extension-content'>
<div className='title'>
<div className='noWrapInfo'>
<span className='name'>{displayName}</span>&nbsp;
<span className='version'>{VSXExtension.formatVersion(version)}&nbsp;
</span>{disabled && installed && <span className='disabled'>({nls.localizeByDefault('disabled')})</span>}
</div>
<div className='stat'>
{!!downloadCount && <span className='download-count'><i className={codicon('cloud-download')} />{downloadCompactFormatter.format(downloadCount)}</span>}
{!!averageRating && <span className='average-rating'><i className={codicon('star-full')} />{averageRatingFormatter(averageRating)}</span>}
</div>
</div>
<div className='noWrapInfo theia-vsx-extension-description'>{description}</div>
<div className='theia-vsx-extension-action-bar'>
<div className='theia-vsx-extension-publisher-container'>
{verified === true ? (
<i className={codicon('verified-filled')} />
) : verified === false ? (
<i className={codicon('verified')} />
) : (
<i className={codicon('question')} />
)}
<span className='noWrapInfo theia-vsx-extension-publisher'>{publisher}</span>
</div>
{this.renderAction(this.props.host)}
</div>
</div>
</div >;
}
}
export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent {
protected header: HTMLElement | undefined;
protected body: HTMLElement | undefined;
protected _scrollContainer: HTMLElement | undefined;
get scrollContainer(): HTMLElement | undefined {
return this._scrollContainer;
}
override render(): React.ReactNode {
const {
builtin, preview, id, iconUrl, publisher, displayName, description, version,
averageRating, downloadCount, repository, license, readme
} = this.props.extension;
const sanitizedReadme = !!readme ? DOMPurify.sanitize(readme) : undefined;
return <React.Fragment>
<div className='header' ref={ref => this.header = (ref || undefined)}>
{iconUrl ?
<img className='icon-container' src={iconUrl} /> :
<div className='icon-container placeholder' />}
<div className='details'>
<div className='title'>
<span title='Extension name' className='name' onClick={this.openExtension}>{displayName}</span>
<span title='Extension identifier' className='identifier'>{id}</span>
{preview && <span className='preview'>Preview</span>}
{builtin && <span className='builtin'>Built-in</span>}
</div>
<div className='subtitle'>
<span title='Publisher name' className='publisher' onClick={this.searchPublisher}>
{this.renderNamespaceAccess()}
{publisher}
</span>
{!!downloadCount && <span className='download-count' onClick={this.openExtension}>
<i className={codicon('cloud-download')} />{downloadFormatter.format(downloadCount)}</span>}
{
averageRating !== undefined &&
<span className='average-rating' title={getAverageRatingTitle(averageRating)} onClick={this.openAverageRating}>{this.renderStars()}</span>
}
{repository && <span className='repository' onClick={this.openRepository}>Repository</span>}
{license && <span className='license' onClick={this.openLicense}>{license}</span>}
{version && <span className='version'>{VSXExtension.formatVersion(version)}</span>}
</div>
<div className='description noWrapInfo'>{description}</div>
{this.renderAction()}
</div>
</div>
{
sanitizedReadme &&
<div className='scroll-container'
ref={ref => this._scrollContainer = (ref || undefined)}>
<div className='body'
ref={ref => this.body = (ref || undefined)}
onClick={this.openLink}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizedReadme }}
/>
</div>
}
</React.Fragment >;
}
protected renderNamespaceAccess(): React.ReactNode {
const { publisher, namespaceAccess, publishedBy } = this.props.extension;
if (namespaceAccess === undefined) {
return undefined;
}
let tooltip = publishedBy ? ` Published by "${publishedBy.loginName}".` : '';
let icon;
if (namespaceAccess === 'public') {
icon = 'globe';
tooltip = `Everyone can publish to "${publisher}" namespace.` + tooltip;
} else {
icon = 'shield';
tooltip = `Only verified owners can publish to "${publisher}" namespace.` + tooltip;
}
return <i className={`${codicon(icon)} namespace-access`} title={tooltip} onClick={this.openPublishedBy} />;
}
protected renderStars(): React.ReactNode {
const rating = this.props.extension.averageRating || 0;
const renderStarAt = (position: number) => position <= rating ?
<i className={codicon('star-full')} /> :
position > rating && position - rating < 1 ?
<i className={codicon('star-half')} /> :
<i className={codicon('star-empty')} />;
return <React.Fragment>
{renderStarAt(1)}{renderStarAt(2)}{renderStarAt(3)}{renderStarAt(4)}{renderStarAt(5)}
</React.Fragment>;
}
// TODO replace with webview
readonly openLink = (event: React.MouseEvent) => {
if (!this.body) {
return;
}
const target = event.nativeEvent.target;
if (!(target instanceof HTMLElement)) {
return;
}
let node = target;
while (node.tagName.toLowerCase() !== 'a') {
if (node === this.body) {
return;
}
if (!(node.parentElement instanceof HTMLElement)) {
return;
}
node = node.parentElement;
}
const href = node.getAttribute('href');
if (href && !href.startsWith('#')) {
event.preventDefault();
this.props.extension.doOpen(new URI(href));
}
};
readonly openExtension = async (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
const uri = await extension.getRegistryLink();
extension.doOpen(uri);
};
readonly searchPublisher = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
if (extension.publisher) {
extension.search.query = extension.publisher;
}
};
readonly openPublishedBy = async (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
const homepage = extension.publishedBy && extension.publishedBy.homepage;
if (homepage) {
extension.doOpen(new URI(homepage));
}
};
readonly openAverageRating = async (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
const uri = await extension.getRegistryLink('reviews');
extension.doOpen(uri);
};
readonly openRepository = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
if (extension.repository) {
extension.doOpen(new URI(extension.repository));
}
};
readonly openLicense = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const extension = this.props.extension;
const licenseUrl = extension.licenseUrl;
if (licenseUrl) {
extension.doOpen(new URI(licenseUrl));
}
};
}

View File

@@ -0,0 +1,398 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
import { CommonMenus, LabelProvider, QuickInputService, QuickPickItem } from '@theia/core/lib/browser';
import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { MenuModelRegistry, MessageService, SelectionService, nls } from '@theia/core/lib/common';
import { Color } from '@theia/core/lib/common/color';
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
import URI from '@theia/core/lib/common/uri';
import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser';
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { OVSXApiFilterProvider, VSXExtensionRaw } from '@theia/ovsx-client';
import { VscodeCommands } from '@theia/plugin-ext-vscode/lib/browser/plugin-vscode-commands-contribution';
import { DateTime } from 'luxon';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { IGNORE_RECOMMENDATIONS_ID } from '../common/recommended-extensions-preference-contribution';
import { VSXExtension, VSXExtensionsContextMenu } from './vsx-extension';
import { VSXExtensionsCommands } from './vsx-extension-commands';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { BUILTIN_QUERY, INSTALLED_QUERY, RECOMMENDED_QUERY } from './vsx-extensions-search-model';
import { VSXExtensionsViewContainer } from './vsx-extensions-view-container';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import debounce = require('@theia/core/shared/lodash.debounce');
export namespace VSXCommands {
export const TOGGLE_EXTENSIONS: Command = {
id: 'vsxExtensions.toggle',
};
}
@injectable()
export class VSXExtensionsContribution extends AbstractViewContribution<VSXExtensionsViewContainer> implements ColorContribution, FrontendApplicationContribution {
@inject(VSXExtensionsModel) protected model: VSXExtensionsModel;
@inject(CommandRegistry) protected commandRegistry: CommandRegistry;
@inject(FileDialogService) protected fileDialogService: FileDialogService;
@inject(MessageService) protected messageService: MessageService;
@inject(LabelProvider) protected labelProvider: LabelProvider;
@inject(ClipboardService) protected clipboardService: ClipboardService;
@inject(PreferenceService) protected preferenceService: PreferenceService;
@inject(OVSXClientProvider) protected clientProvider: OVSXClientProvider;
@inject(OVSXApiFilterProvider) protected vsxApiFilter: OVSXApiFilterProvider;
@inject(ApplicationServer) protected applicationServer: ApplicationServer;
@inject(QuickInputService) protected quickInput: QuickInputService;
@inject(SelectionService) protected readonly selectionService: SelectionService;
constructor() {
super({
widgetId: VSXExtensionsViewContainer.ID,
widgetName: VSXExtensionsViewContainer.LABEL,
defaultWidgetOptions: {
area: 'left',
rank: 500
},
toggleCommandId: VSXCommands.TOGGLE_EXTENSIONS.id,
toggleKeybinding: 'ctrlcmd+shift+x'
});
}
@postConstruct()
protected init(): void {
const oneShotDisposable = this.model.onDidChange(debounce(() => {
this.showRecommendedToast();
oneShotDisposable.dispose();
}, 5000, { trailing: true }));
}
async initializeLayout(app: FrontendApplication): Promise<void> {
await this.openView({ activate: false });
}
override registerCommands(commands: CommandRegistry): void {
super.registerCommands(commands);
commands.registerCommand(VSXExtensionsCommands.CLEAR_ALL, {
execute: () => this.model.search.query = '',
isEnabled: () => !!this.model.search.query,
isVisible: () => true,
});
commands.registerCommand(VSXExtensionsCommands.INSTALL_FROM_VSIX, {
execute: () => this.installFromVSIX()
});
commands.registerCommand(VSXExtensionsCommands.INSTALL_VSIX_FILE,
UriAwareCommandHandler.MonoSelect(this.selectionService, {
execute: fileURI => this.installVsixFile(fileURI),
isEnabled: fileURI => fileURI.scheme === 'file' && fileURI.path.ext === '.vsix'
})
);
commands.registerCommand(VSXExtensionsCommands.INSTALL_ANOTHER_VERSION, {
// Check downloadUrl to ensure we have an idea of where to look for other versions.
isEnabled: (extension: VSXExtension) => !extension.builtin && !!extension.downloadUrl,
execute: async (extension: VSXExtension) => this.installAnotherVersion(extension),
});
commands.registerCommand(VSXExtensionsCommands.DISABLE, {
isVisible: (extension: VSXExtension) => extension.installed && !extension.disabled,
isEnabled: (extension: VSXExtension) => extension.installed && !extension.disabled,
execute: async (extension: VSXExtension) => extension.disable(),
});
commands.registerCommand(VSXExtensionsCommands.ENABLE, {
isVisible: (extension: VSXExtension) => extension.installed && extension.disabled,
isEnabled: (extension: VSXExtension) => extension.installed && extension.disabled,
execute: async (extension: VSXExtension) => extension.enable(),
});
commands.registerCommand(VSXExtensionsCommands.COPY, {
execute: (extension: VSXExtension) => this.copy(extension)
});
commands.registerCommand(VSXExtensionsCommands.COPY_EXTENSION_ID, {
execute: (extension: VSXExtension) => this.copyExtensionId(extension)
});
commands.registerCommand(VSXExtensionsCommands.SHOW_BUILTINS, {
execute: () => this.showBuiltinExtensions()
});
commands.registerCommand(VSXExtensionsCommands.SHOW_INSTALLED, {
execute: () => this.showInstalledExtensions()
});
commands.registerCommand(VSXExtensionsCommands.SHOW_RECOMMENDATIONS, {
execute: () => this.showRecommendedExtensions()
});
}
override registerMenus(menus: MenuModelRegistry): void {
super.registerMenus(menus);
menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
commandId: VSXCommands.TOGGLE_EXTENSIONS.id,
label: nls.localizeByDefault('Extensions'),
order: 'a20'
});
menus.registerMenuAction(VSXExtensionsContextMenu.COPY, {
commandId: VSXExtensionsCommands.COPY.id,
label: nls.localizeByDefault('Copy'),
order: '0'
});
menus.registerMenuAction(VSXExtensionsContextMenu.COPY, {
commandId: VSXExtensionsCommands.COPY_EXTENSION_ID.id,
label: nls.localizeByDefault('Copy Extension ID'),
order: '1'
});
menus.registerMenuAction(VSXExtensionsContextMenu.DISABLE, {
commandId: VSXExtensionsCommands.DISABLE.id,
label: nls.localizeByDefault('Disable')
});
menus.registerMenuAction(VSXExtensionsContextMenu.ENABLE, {
commandId: VSXExtensionsCommands.ENABLE.id,
label: nls.localizeByDefault('Enable')
});
menus.registerMenuAction(VSXExtensionsContextMenu.INSTALL, {
commandId: VSXExtensionsCommands.INSTALL_ANOTHER_VERSION.id,
label: nls.localizeByDefault('Install Specific Version...'),
});
menus.registerMenuAction(NAVIGATOR_CONTEXT_MENU, {
commandId: VSXExtensionsCommands.INSTALL_VSIX_FILE.id,
label: VSXExtensionsCommands.INSTALL_VSIX_FILE.label,
when: 'resourceScheme == file && resourceExtname == .vsix'
});
}
registerColors(colors: ColorRegistry): void {
// VS Code colors should be aligned with https://code.visualstudio.com/api/references/theme-color#extensions
colors.register(
{
id: 'extensionButton.prominentBackground', defaults: {
dark: '#327e36',
light: '#327e36'
}, description: 'Button background color for actions extension that stand out (e.g. install button).'
},
{
id: 'extensionButton.prominentForeground', defaults: {
dark: Color.white,
light: Color.white
}, description: 'Button foreground color for actions extension that stand out (e.g. install button).'
},
{
id: 'extensionButton.prominentHoverBackground', defaults: {
dark: '#28632b',
light: '#28632b'
}, description: 'Button background hover color for actions extension that stand out (e.g. install button).'
},
{
id: 'extensionEditor.tableHeadBorder', defaults: {
dark: Color.transparent('#ffffff', 0.7),
light: Color.transparent('#000000', 0.7),
hcDark: Color.white,
hcLight: Color.black
}, description: 'Border color for the table head row of the extension editor view'
},
{
id: 'extensionEditor.tableCellBorder', defaults: {
dark: Color.transparent('#ffffff', 0.2),
light: Color.transparent('#000000', 0.2),
hcDark: Color.white,
hcLight: Color.black
}, description: 'Border color for a table row of the extension editor view'
},
{
id: 'extensionIcon.verifiedForeground', defaults: {
dark: '#40a6ff',
light: '#40a6ff'
}, description: 'The icon color for extension verified publisher.'
},
);
}
/**
* Installs a local .vsix file after prompting the `Open File` dialog. Resolves to the URI of the file.
*/
protected async installFromVSIX(): Promise<void> {
const props: OpenFileDialogProps = {
title: VSXExtensionsCommands.INSTALL_FROM_VSIX.dialogLabel,
openLabel: nls.localizeByDefault('Install from VSIX'),
filters: { 'VSIX Extensions (*.vsix)': ['vsix'] },
canSelectMany: false,
canSelectFiles: true
};
const extensionUri = await this.fileDialogService.showOpenDialog(props);
if (extensionUri) {
if (extensionUri.path.ext === '.vsix') {
await this.installVsixFile(extensionUri);
} else {
this.messageService.error(nls.localize('theia/vsx-registry/invalidVSIX', 'The selected file is not a valid "*.vsix" plugin.'));
}
}
}
/**
* Installs a local vs-code extension file.
* The implementation doesn't check if the file is a valid VSIX file, or the URI has a *.vsix extension.
* The caller should ensure the file is a valid VSIX file.
*
* @param fileURI the URI of the file to install.
*/
protected async installVsixFile(fileURI: URI): Promise<void> {
const extensionName = this.labelProvider.getName(fileURI);
try {
await this.commandRegistry.executeCommand(VscodeCommands.INSTALL_EXTENSION_FROM_ID_OR_URI.id, fileURI);
this.messageService.info(nls.localizeByDefault('Completed installing extension.', extensionName));
} catch (e) {
this.messageService.error(nls.localize('theia/vsx-registry/failedInstallingVSIX', 'Failed to install {0} from VSIX.', extensionName));
console.warn(e);
}
}
/**
* Given an extension, displays a quick pick of other compatible versions and installs the selected version.
*
* @param extension a VSX extension.
*/
protected async installAnotherVersion(extension: VSXExtension): Promise<void> {
const extensionId = extension.id;
const currentVersion = extension.version;
const client = await this.clientProvider();
const filter = await this.vsxApiFilter();
const targetPlatform = await this.applicationServer.getApplicationPlatform();
const { extensions } = await client.query({ extensionId, includeAllVersions: true });
const latestCompatible = await filter.findLatestCompatibleExtension({
extensionId,
includeAllVersions: true,
targetPlatform
});
let compatibleExtensions: VSXExtensionRaw[] = [];
let activeItem = undefined;
if (latestCompatible) {
compatibleExtensions = extensions.slice(extensions.findIndex(ext => ext.version === latestCompatible.version));
}
const items: QuickPickItem[] = compatibleExtensions.map(ext => {
const item = {
label: ext.version,
description: DateTime.fromISO(ext.timestamp).toRelative({ locale: nls.locale }) ?? ''
};
if (currentVersion === ext.version) {
item.description += ` (${nls.localizeByDefault('Current')})`;
activeItem = item;
}
return item;
});
const selectedItem = await this.quickInput.showQuickPick(items, {
placeholder: nls.localizeByDefault('Select Version to Install'),
runIfSingle: false,
activeItem
});
if (selectedItem) {
const selectedExtension = this.model.getExtension(extensionId);
if (selectedExtension) {
await this.updateVersion(selectedExtension, selectedItem.label);
}
}
}
protected async copy(extension: VSXExtension): Promise<void> {
this.clipboardService.writeText(await extension.serialize());
}
protected copyExtensionId(extension: VSXExtension): void {
this.clipboardService.writeText(extension.id);
}
/**
* Updates an extension to a specific version.
*
* @param extension the extension to update.
* @param updateToVersion the version to update to.
* @param revertToVersion the version to revert to (in case of failure).
*/
protected async updateVersion(extension: VSXExtension, updateToVersion: string): Promise<void> {
try {
await extension.install({ version: updateToVersion, ignoreOtherVersions: true });
} catch {
this.messageService.warn(nls.localize('theia/vsx-registry/vsx-extensions-contribution/update-version-version-error', 'Failed to install version {0} of {1}.',
updateToVersion, extension.displayName));
return;
}
try {
if (extension.version !== updateToVersion) {
await extension.uninstall();
}
} catch {
this.messageService.warn(nls.localize('theia/vsx-registry/vsx-extensions-contribution/update-version-uninstall-error', 'Error while removing the extension: {0}.',
extension.displayName));
}
}
protected async showRecommendedToast(): Promise<void> {
if (!this.preferenceService.get(IGNORE_RECOMMENDATIONS_ID, false)) {
const recommended = new Set<string>();
for (const recommendation of this.model.recommended) {
if (!this.model.isInstalled(recommendation)) {
recommended.add(recommendation);
}
}
if (recommended.size) {
const install = nls.localizeByDefault('Install');
const showRecommendations = nls.localizeByDefault('Show Recommendations');
const neverAskAgain = nls.localizeByDefault('Never ask me again');
const userResponse = await this.messageService.info(
nls.localize('theia/vsx-registry/recommendedExtensions', 'Do you want to install the recommended extensions for this repository?'),
install,
showRecommendations,
neverAskAgain
);
if (userResponse === install) {
for (const recommendation of recommended) {
this.model.getExtension(recommendation)?.install();
}
} else if (userResponse === showRecommendations) {
await this.showRecommendedExtensions();
} else if (userResponse === neverAskAgain) {
await this.preferenceService.set(IGNORE_RECOMMENDATIONS_ID, true, PreferenceScope.Workspace);
}
}
}
}
protected async showBuiltinExtensions(): Promise<void> {
await this.openView({ activate: true });
this.model.search.query = BUILTIN_QUERY;
}
protected async showInstalledExtensions(): Promise<void> {
await this.openView({ activate: true });
this.model.search.query = INSTALLED_QUERY;
}
protected async showRecommendedExtensions(): Promise<void> {
await this.openView({ activate: true });
this.model.search.query = RECOMMENDED_QUERY;
}
}

View File

@@ -0,0 +1,532 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import debounce from 'p-debounce';
import * as markdownit from '@theia/core/shared/markdown-it';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { VSXExtension, VSXExtensionFactory } from './vsx-extension';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { PreferenceInspection, PreferenceInspectionScope, PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { RecommendedExtensions } from '../common/recommended-extensions-preference-contribution';
import URI from '@theia/core/lib/common/uri';
import { OVSXClient, VSXAllVersions, VSXExtensionRaw, VSXResponseError, VSXSearchEntry, VSXSearchOptions, VSXTargetPlatform } from '@theia/ovsx-client/lib/ovsx-types';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { RequestContext, RequestService } from '@theia/core/shared/@theia/request';
import { OVSXApiFilterProvider } from '@theia/ovsx-client';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { HostedPluginServer, PluginIdentifiers, PluginType } from '@theia/plugin-ext';
import { HostedPluginWatcher } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin-watcher';
@injectable()
export class VSXExtensionsModel {
protected initialized: Promise<void>;
/**
* Single source for all extensions
*/
protected readonly extensions = new Map<string, VSXExtension>();
protected readonly onDidChangeEmitter = new Emitter<void>();
protected disabled = new Set<PluginIdentifiers.UnversionedId>();
protected uninstalled = new Set<PluginIdentifiers.VersionedId>();
protected deployed = new Set<PluginIdentifiers.VersionedId>();
protected _versionedInstalled = new Set<PluginIdentifiers.VersionedId>();
protected _unversionedInstalled = new Set<PluginIdentifiers.UnversionedId>();
protected _recommended = new Set<string>();
protected _searchResult = new Set<string>();
protected builtins = new Set<PluginIdentifiers.UnversionedId>();
protected _searchError?: string;
protected searchCancellationTokenSource = new CancellationTokenSource();
protected updateSearchResult = debounce(async () => {
const { token } = this.resetSearchCancellationTokenSource();
await this.doUpdateSearchResult({ query: this.search.query, includeAllVersions: true }, token);
}, 500);
@inject(OVSXClientProvider)
protected clientProvider: OVSXClientProvider;
@inject(HostedPluginSupport)
protected readonly pluginSupport: HostedPluginSupport;
@inject(HostedPluginWatcher)
protected pluginWatcher: HostedPluginWatcher;
@inject(HostedPluginServer)
protected readonly pluginServer: HostedPluginServer;
@inject(VSXExtensionFactory)
protected readonly extensionFactory: VSXExtensionFactory;
@inject(ProgressService)
protected readonly progressService: ProgressService;
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(VSXExtensionsSearchModel)
readonly search: VSXExtensionsSearchModel;
@inject(RequestService)
protected request: RequestService;
@inject(OVSXApiFilterProvider)
protected vsxApiFilter: OVSXApiFilterProvider;
@inject(ApplicationServer)
protected readonly applicationServer: ApplicationServer;
@postConstruct()
protected init(): void {
this.initialized = this.doInit().catch(console.error);
}
protected async doInit(): Promise<void> {
await Promise.all([
this.initInstalled(),
this.initSearchResult(),
this.initRecommended(),
]);
}
get onDidChange(): Event<void> {
return this.onDidChangeEmitter.event;
}
get installed(): IterableIterator<string> {
return this._versionedInstalled.values();
}
get searchError(): string | undefined {
return this._searchError;
}
get searchResult(): IterableIterator<string> {
return this._searchResult.values();
}
get recommended(): IterableIterator<string> {
return this._recommended.values();
}
setOnlyShowVerifiedExtensions(bool: boolean): void {
if (this.preferences.get('extensions.onlyShowVerifiedExtensions') !== bool) {
this.preferences.updateValue('extensions.onlyShowVerifiedExtensions', bool);
}
this.updateSearchResult();
}
isBuiltIn(id: string): boolean {
return this.builtins.has(id as PluginIdentifiers.UnversionedId);
}
/**
* @param id should be a ${@link PluginIdentifiers.VersionedId}
* @returns `true` if the specific version queried installed
*/
isInstalledAtSpecificVersion(id: string): boolean {
return this._versionedInstalled.has(id as PluginIdentifiers.VersionedId);
}
/**
* @param id should be an unversioned Identifier
* @returns `true` if any version of the plugin is installed
*/
isInstalled(id: string): boolean {
return this._unversionedInstalled.has(id as PluginIdentifiers.UnversionedId);
}
isUninstalled(id: string): boolean {
return this.uninstalled.has(id as PluginIdentifiers.VersionedId);
}
isDeployed(id: string): boolean {
return this.deployed.has(id as PluginIdentifiers.VersionedId);
}
isDisabled(id: string): boolean {
return this.disabled.has(id as PluginIdentifiers.UnversionedId);
}
getExtension(id: string): VSXExtension | undefined {
return this.extensions.get(id);
}
resolve(id: string): Promise<VSXExtension> {
return this.doChange(async () => {
await this.initialized;
const extension = await this.refresh(id);
if (!extension) {
throw new Error(`Failed to resolve ${id} extension.`);
}
if (extension.readme === undefined && extension.readmeUrl) {
try {
const rawReadme = RequestContext.asText(
await this.request.request({ url: extension.readmeUrl })
);
const readme = this.compileReadme(rawReadme);
extension.update({ readme });
} catch (e) {
if (!VSXResponseError.is(e) || e.statusCode !== 404) {
console.error(`[${id}]: failed to compile readme, reason:`, e);
}
}
}
return extension;
});
}
protected async initInstalled(): Promise<void> {
await this.pluginSupport.willStart;
try {
await this.updateInstalled();
} catch (e) {
console.error(e);
}
this.pluginWatcher.onDidDeploy(() => {
this.updateInstalled();
});
}
protected async initSearchResult(): Promise<void> {
this.search.onDidChangeQuery(() => this.updateSearchResult());
try {
await this.updateSearchResult();
} catch (e) {
console.error(e);
}
}
protected async initRecommended(): Promise<void> {
this.preferences.onPreferenceChanged(change => {
if (change.preferenceName === 'extensions') {
this.updateRecommended();
}
});
await this.preferences.ready;
try {
await this.updateRecommended();
} catch (e) {
console.error(e);
}
}
protected resetSearchCancellationTokenSource(): CancellationTokenSource {
this.searchCancellationTokenSource.cancel();
return this.searchCancellationTokenSource = new CancellationTokenSource();
}
protected setExtension(id: string, version?: string): VSXExtension {
let extension = this.extensions.get(id);
if (!extension) {
extension = this.extensionFactory({ id, version, model: this });
this.extensions.set(id, extension);
}
return extension;
}
protected doChange<T>(task: () => Promise<T>): Promise<T>;
protected doChange<T>(task: () => Promise<T>, token: CancellationToken): Promise<T | undefined>;
protected doChange<T>(task: () => Promise<T>, token: CancellationToken = CancellationToken.None): Promise<T | undefined> {
return this.progressService.withProgress('', 'extensions', async () => {
if (token && token.isCancellationRequested) {
return;
}
const result = await task();
if (token && token.isCancellationRequested) {
return;
}
this.onDidChangeEmitter.fire();
return result;
});
}
protected doUpdateSearchResult(param: VSXSearchOptions, token: CancellationToken): Promise<void> {
return this.doChange(async () => {
this._searchResult = new Set<string>();
if (!param.query) {
return;
}
const client = await this.clientProvider();
const filter = await this.vsxApiFilter();
try {
const result = await client.search(param);
if (token.isCancellationRequested) {
return;
}
for (const data of result.extensions) {
const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase();
const allVersions = filter.getLatestCompatibleVersion(data);
if (!allVersions) {
continue;
}
if (this.preferences.get('extensions.onlyShowVerifiedExtensions')) {
this.fetchVerifiedStatus(id, client, allVersions).then(verified => {
this.doChange(() => {
this.addExtensions(data, id, allVersions, !!verified);
return Promise.resolve();
});
});
} else {
this.addExtensions(data, id, allVersions);
this.fetchVerifiedStatus(id, client, allVersions).then(verified => {
this.doChange(() => {
let extension = this.getExtension(id);
extension = this.setExtension(id);
extension.update(Object.assign({
verified: verified
}));
return Promise.resolve();
});
});
}
}
} catch (error) {
this._searchError = error?.message || String(error);
}
}, token);
}
protected async fetchVerifiedStatus(id: string, client: OVSXClient, allVersions: VSXAllVersions): Promise<boolean | undefined> {
try {
const res = await client.query({ extensionId: id, extensionVersion: allVersions.version, includeAllVersions: true });
const extension = res.extensions?.[0];
let verified = extension?.verified;
if (!verified && extension?.publishedBy.loginName === 'open-vsx') {
verified = true;
}
return verified;
} catch (error) {
console.error(error);
return false;
}
}
protected addExtensions(data: VSXSearchEntry, id: string, allVersions: VSXAllVersions, verified?: boolean): void {
if (!this.preferences.get('extensions.onlyShowVerifiedExtensions') || verified) {
const extension = this.setExtension(id);
extension.update(Object.assign(data, {
publisher: data.namespace,
downloadUrl: data.files.download,
iconUrl: data.files.icon,
readmeUrl: data.files.readme,
licenseUrl: data.files.license,
version: allVersions.version,
verified: verified
}));
this._searchResult.add(id);
}
}
protected async updateInstalled(): Promise<void> {
const [deployed, uninstalled, disabled, currInstalled] = await Promise.all([
this.pluginServer.getDeployedPluginIds(),
this.pluginServer.getUninstalledPluginIds(),
this.pluginServer.getDisabledPluginIds(),
this.pluginServer.getInstalledPluginIds()
]);
this.uninstalled = new Set();
uninstalled.forEach(id => this.uninstalled.add(id));
this.disabled = new Set(disabled);
this.deployed = new Set();
deployed.forEach(id => this.deployed.add(id));
const prevInstalled = this._versionedInstalled;
const installedVersioned = new Set<PluginIdentifiers.VersionedId>();
return this.doChange(async () => {
const refreshing = [];
for (const versionedId of currInstalled) {
installedVersioned.add(versionedId);
const idAndVersion = PluginIdentifiers.idAndVersionFromVersionedId(versionedId);
if (idAndVersion) {
this._versionedInstalled.delete(versionedId);
this.setExtension(idAndVersion.id, idAndVersion.version);
refreshing.push(this.refresh(idAndVersion.id, idAndVersion.version));
}
}
for (const id of this._versionedInstalled) {
const extension = this.getExtension(id);
if (!extension) { continue; }
refreshing.push(this.refresh(id, extension.version));
}
await Promise.all(refreshing);
const installed = new Set([...prevInstalled, ...currInstalled]);
const installedSorted = Array.from(installed).sort((a, b) => this.compareExtensions(a, b));
this._versionedInstalled = new Set(installedSorted);
this._unversionedInstalled = new Set(installedSorted.map(PluginIdentifiers.toUnversioned));
const missingIds = new Set<PluginIdentifiers.VersionedId>();
for (const id of installedVersioned) {
const unversionedId = PluginIdentifiers.unversionedFromVersioned(id);
const plugin = this.pluginSupport.getPlugin(unversionedId);
if (plugin) {
if (plugin.type === PluginType.System) {
this.builtins.add(unversionedId);
} else {
this.builtins.delete(unversionedId);
}
} else {
missingIds.add(id);
}
}
const missing = await this.pluginServer.getDeployedPlugins([...missingIds.values()]);
for (const plugin of missing) {
if (plugin.type === PluginType.System) {
this.builtins.add(PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model));
} else {
this.builtins.delete(PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model));
}
}
});
}
protected updateRecommended(): Promise<Array<VSXExtension | undefined>> {
return this.doChange<Array<VSXExtension | undefined>>(async () => {
const allRecommendations = new Set<string>();
const allUnwantedRecommendations = new Set<string>();
const updateRecommendationsForScope = (scope: PreferenceInspectionScope, root?: URI) => {
const { recommendations, unwantedRecommendations } = this.getRecommendationsForScope(scope, root);
recommendations.forEach(recommendation => allRecommendations.add(recommendation.toLowerCase()));
unwantedRecommendations.forEach(unwantedRecommendation => allUnwantedRecommendations.add(unwantedRecommendation));
};
updateRecommendationsForScope('defaultValue'); // In case there are application-default recommendations.
const roots = await this.workspaceService.roots;
for (const root of roots) {
updateRecommendationsForScope('workspaceFolderValue', root.resource);
}
if (this.workspaceService.saved) {
updateRecommendationsForScope('workspaceValue');
}
const recommendedSorted = new Set(Array.from(allRecommendations).sort((a, b) => this.compareExtensions(a, b)));
allUnwantedRecommendations.forEach(unwantedRecommendation => recommendedSorted.delete(unwantedRecommendation));
this._recommended = recommendedSorted;
return Promise.all(Array.from(recommendedSorted, plugin => this.refresh(plugin)));
});
}
protected getRecommendationsForScope(scope: PreferenceInspectionScope, root?: URI): Required<RecommendedExtensions> {
const inspection: PreferenceInspection<Required<RecommendedExtensions>> | undefined =
this.preferences.inspect<Required<RecommendedExtensions>>('extensions', root?.toString());
const configuredValue = inspection ? inspection[scope] : undefined;
return {
recommendations: configuredValue?.recommendations ?? [],
unwantedRecommendations: configuredValue?.unwantedRecommendations ?? [],
};
}
protected compileReadme(readmeMarkdown: string): string {
const readmeHtml = markdownit({ html: true }).render(readmeMarkdown);
return DOMPurify.sanitize(readmeHtml);
}
protected async refresh(id: string, version?: string): Promise<VSXExtension | undefined> {
try {
let extension = this.getExtension(id);
if (!this.shouldRefresh(extension)) {
return extension;
}
const filter = await this.vsxApiFilter();
const targetPlatform = await this.applicationServer.getApplicationPlatform() as VSXTargetPlatform;
let data: VSXExtensionRaw | undefined;
if (version === undefined) {
data = await filter.findLatestCompatibleExtension({
extensionId: id,
includeAllVersions: true,
targetPlatform
});
} else {
data = await filter.findLatestCompatibleExtension({
extensionId: id,
extensionVersion: version,
includeAllVersions: true,
targetPlatform
});
}
if (!data || data.error) {
return this.onDidFailRefresh(id, data?.error ?? 'No data found');
}
if (!data.verified) {
if (data.publishedBy.loginName === 'open-vsx') {
data.verified = true;
}
}
extension = this.setExtension(id);
extension.update(Object.assign(data, {
publisher: data.namespace,
downloadUrl: data.files.download,
iconUrl: data.files.icon,
readmeUrl: data.files.readme,
licenseUrl: data.files.license,
version: data.version,
verified: data.verified
}));
return extension;
} catch (e) {
return this.onDidFailRefresh(id, e);
}
}
/**
* Determines if the given extension should be refreshed.
* @param extension the extension to refresh.
*/
protected shouldRefresh(extension?: VSXExtension): boolean {
return extension === undefined || extension.plugin === undefined;
}
protected onDidFailRefresh(id: string, error: unknown): VSXExtension | undefined {
const cached = this.getExtension(id);
if (cached && cached.deployed) {
return cached;
}
console.error(`[${id}]: failed to refresh, reason:`, error);
return undefined;
}
/**
* Compare two extensions based on their display name, and publisher if applicable.
* @param a the first extension id for comparison.
* @param b the second extension id for comparison.
*/
protected compareExtensions(a: string, b: string): number {
const extensionA = this.getExtension(a);
const extensionB = this.getExtension(b);
if (!extensionA || !extensionB) {
return 0;
}
if (extensionA.displayName && extensionB.displayName) {
return extensionA.displayName.localeCompare(extensionB.displayName);
}
if (extensionA.publisher && extensionB.publisher) {
return extensionA.publisher.localeCompare(extensionB.publisher);
}
return 0;
}
}

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson 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 { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceContribution,
PreferenceProxy,
PreferenceSchema,
PreferenceService,
} from '@theia/core/lib/common/preferences';
import { nls } from '@theia/core';
export const VsxExtensionsPreferenceSchema: PreferenceSchema = {
properties: {
'extensions.onlyShowVerifiedExtensions': {
type: 'boolean',
default: false,
description: nls.localize('theia/vsx-registry/onlyShowVerifiedExtensionsDescription', 'This allows the {0} to only show verified extensions.', 'Open VSX Registry')
},
}
};
export interface VsxExtensionsConfiguration {
'extensions.onlyShowVerifiedExtensions': boolean;
}
export const VsxExtensionsPreferenceContribution = Symbol('VsxExtensionsPreferenceContribution');
export const VsxExtensionsPreferences = Symbol('VsxExtensionsPreferences');
export type VsxExtensionsPreferences = PreferenceProxy<VsxExtensionsConfiguration>;
export function createVsxExtensionsPreferences(preferences: PreferenceService, schema: PreferenceSchema = VsxExtensionsPreferenceSchema): VsxExtensionsPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindVsxExtensionsPreferences(bind: interfaces.Bind): void {
bind(VsxExtensionsPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(VsxExtensionsPreferenceContribution);
return createVsxExtensionsPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(VsxExtensionsPreferenceContribution).toConstantValue({ schema: VsxExtensionsPreferenceSchema });
bind(PreferenceContribution).toService(VsxExtensionsPreferenceContribution);
}

View File

@@ -0,0 +1,108 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { injectable, postConstruct, inject } from '@theia/core/shared/inversify';
import { ReactWidget, Message, codicon } from '@theia/core/lib/browser/widgets';
import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class VSXExtensionsSearchBar extends ReactWidget {
@inject(VSXExtensionsModel)
protected readonly extensionsModel: VSXExtensionsModel;
@inject(VSXExtensionsSearchModel)
protected readonly searchModel: VSXExtensionsSearchModel;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
protected input: HTMLInputElement | undefined;
protected onlyShowVerifiedExtensions: boolean | undefined;
@postConstruct()
protected init(): void {
this.onlyShowVerifiedExtensions = this.preferenceService.get('extensions.onlyShowVerifiedExtensions');
this.id = 'vsx-extensions-search-bar';
this.addClass('theia-vsx-extensions-search-bar');
this.searchModel.onDidChangeQuery((query: string) => this.updateSearchTerm(query));
this.preferenceService.onPreferenceChanged(change => {
if (change.preferenceName === 'extensions.onlyShowVerifiedExtensions') {
const newValue = this.preferenceService.get<boolean>('extensions.onlyShowVerifiedExtensions', false);
this.extensionsModel.setOnlyShowVerifiedExtensions(newValue);
this.onlyShowVerifiedExtensions = newValue;
this.update();
}
});
}
protected render(): React.ReactNode {
return <div className='vsx-search-container'>
<input type='text'
ref={input => this.input = input || undefined}
defaultValue={this.searchModel.query}
spellCheck={false}
className='theia-input'
placeholder={nls.localize('theia/vsx-registry/searchPlaceholder', 'Search Extensions in {0}', 'Open VSX Registry')}
onChange={this.updateQuery}>
</input>
{this.renderOptionContainer()}
</div>;
}
protected updateQuery = (e: React.ChangeEvent<HTMLInputElement>) => this.searchModel.query = e.target.value;
protected updateSearchTerm(term: string): void {
if (this.input) {
this.input.value = term;
}
}
protected renderOptionContainer(): React.ReactNode {
const showVerifiedExtensions = this.renderShowVerifiedExtensions();
return <div className='option-buttons'>{showVerifiedExtensions}</div>;
}
protected renderShowVerifiedExtensions(): React.ReactNode {
return <span
className={`${codicon('verified')} option action-label ${this.onlyShowVerifiedExtensions ? 'enabled' : ''}`}
title={nls.localize('theia/vsx-registry/onlyShowVerifiedExtensionsTitle', 'Only Show Verified Extensions')}
onClick={() => this.handleShowVerifiedExtensionsClick()}>
</span>;
}
protected handleShowVerifiedExtensionsClick(): void {
this.extensionsModel.setOnlyShowVerifiedExtensions(!this.onlyShowVerifiedExtensions);
this.update();
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.input) {
this.input.focus();
}
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.update();
}
}

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
export enum VSXSearchMode {
Initial,
None,
Search,
Installed,
Builtin,
Recommended,
}
export const BUILTIN_QUERY = '@builtin';
export const INSTALLED_QUERY = '@installed';
export const RECOMMENDED_QUERY = '@recommended';
@injectable()
export class VSXExtensionsSearchModel {
protected readonly onDidChangeQueryEmitter = new Emitter<string>();
readonly onDidChangeQuery = this.onDidChangeQueryEmitter.event;
protected readonly specialQueries = new Map<string, VSXSearchMode>([
[BUILTIN_QUERY, VSXSearchMode.Builtin],
[INSTALLED_QUERY, VSXSearchMode.Installed],
[RECOMMENDED_QUERY, VSXSearchMode.Recommended],
]);
protected _query = '';
set query(query: string) {
if (this._query === query) {
return;
}
this._query = query;
this.onDidChangeQueryEmitter.fire(this._query);
}
get query(): string {
return this._query;
}
getModeForQuery(): VSXSearchMode {
return this.query
? this.specialQueries.get(this.query) ?? VSXSearchMode.Search
: VSXSearchMode.None;
}
}

View File

@@ -0,0 +1,89 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree';
import { VSXExtensionsModel } from './vsx-extensions-model';
import debounce = require('@theia/core/shared/lodash.debounce');
import { PluginIdentifiers } from '@theia/plugin-ext';
@injectable()
export class VSXExtensionsSourceOptions {
static INSTALLED = 'installed';
static BUILT_IN = 'builtin';
static SEARCH_RESULT = 'searchResult';
static RECOMMENDED = 'recommended';
readonly id: string;
}
@injectable()
export class VSXExtensionsSource extends TreeSource {
@inject(VSXExtensionsSourceOptions)
protected readonly options: VSXExtensionsSourceOptions;
@inject(VSXExtensionsModel)
protected readonly model: VSXExtensionsModel;
@postConstruct()
protected init(): void {
this.fireDidChange();
this.toDispose.push(this.model.onDidChange(() => this.scheduleFireDidChange()));
}
protected scheduleFireDidChange = debounce(() => this.fireDidChange(), 100, { leading: false, trailing: true });
getModel(): VSXExtensionsModel {
return this.model;
}
*getElements(): IterableIterator<TreeElement> {
for (const id of this.doGetElements()) {
const extension = this.model.getExtension(id);
if (!extension) {
continue;
}
if (this.options.id === VSXExtensionsSourceOptions.RECOMMENDED) {
if (this.model.isInstalled(id)) {
continue;
}
}
if (this.options.id === VSXExtensionsSourceOptions.BUILT_IN) {
if (extension.builtin) {
yield extension;
}
} else if (!extension.builtin) {
yield extension;
}
}
}
protected doGetElements(): IterableIterator<string> {
if (this.options.id === VSXExtensionsSourceOptions.SEARCH_RESULT) {
return this.model.searchResult;
}
if (this.options.id === VSXExtensionsSourceOptions.RECOMMENDED) {
return this.model.recommended;
}
return this.mapInstalled();
}
protected *mapInstalled(): IterableIterator<string> {
for (const installed of this.model.installed) {
yield PluginIdentifiers.toUnversioned(installed as PluginIdentifiers.VersionedId);
};
}
}

View File

@@ -0,0 +1,193 @@
/********************************************************************************
* Copyright (C) 2020 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-only WITH Classpath-exception-2.0
********************************************************************************/
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ViewContainer, PanelLayout, ViewContainerPart, Message, codicon, Widget } from '@theia/core/lib/browser';
import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { VSXSearchMode } from './vsx-extensions-search-model';
import { generateExtensionWidgetId } from './vsx-extensions-widget';
import { VSXExtensionsSourceOptions } from './vsx-extensions-source';
import { VSXExtensionsCommands } from './vsx-extension-commands';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class VSXExtensionsViewContainer extends ViewContainer {
static ID = 'vsx-extensions-view-container';
static LABEL = nls.localizeByDefault('Extensions');
override disableDNDBetweenContainers = true;
@inject(VSXExtensionsSearchBar)
protected readonly searchBar: VSXExtensionsSearchBar;
@inject(VSXExtensionsModel)
protected readonly model: VSXExtensionsModel;
@postConstruct()
protected override init(): void {
super.init();
this.id = VSXExtensionsViewContainer.ID;
this.addClass('theia-vsx-extensions-view-container');
this.setTitleOptions({
label: VSXExtensionsViewContainer.LABEL,
iconClass: codicon('extensions'),
closeable: true
});
}
protected override onActivateRequest(msg: Message): void {
this.searchBar.activate();
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.updateMode();
this.toDisposeOnDetach.push(this.model.search.onDidChangeQuery(() => this.updateMode()));
}
protected override configureLayout(layout: PanelLayout): void {
layout.addWidget(this.searchBar);
super.configureLayout(layout);
}
protected currentMode: VSXSearchMode = VSXSearchMode.Initial;
protected readonly lastModeState = new Map<VSXSearchMode, ViewContainer.State>();
protected updateMode(): void {
const currentMode = this.model.search.getModeForQuery();
if (currentMode === this.currentMode) {
return;
}
if (this.currentMode !== VSXSearchMode.Initial) {
this.lastModeState.set(this.currentMode, super.doStoreState());
}
this.currentMode = currentMode;
const lastState = this.lastModeState.get(currentMode);
if (lastState) {
super.doRestoreState(lastState);
} else {
for (const part of this.getParts()) {
this.applyModeToPart(part);
}
}
const specialWidgets = this.getWidgetsForMode();
if (specialWidgets?.length) {
const widgetChecker = new Set(specialWidgets);
const relevantParts = this.getParts().filter(part => widgetChecker.has(part.wrapped.id));
relevantParts.forEach(part => {
part.collapsed = false;
part.show();
});
}
}
protected override registerPart(part: ViewContainerPart): void {
super.registerPart(part);
this.applyModeToPart(part);
}
protected applyModeToPart(part: ViewContainerPart): void {
if (this.shouldShowWidget(part)) {
part.show();
} else {
part.hide();
}
}
protected shouldShowWidget(part: ViewContainerPart): boolean {
const widgetsToShow = this.getWidgetsForMode();
if (widgetsToShow.length) {
return widgetsToShow.includes(part.wrapped.id);
}
return part.wrapped.id !== generateExtensionWidgetId(VSXExtensionsSourceOptions.SEARCH_RESULT);
}
protected getWidgetsForMode(): string[] {
switch (this.currentMode) {
case VSXSearchMode.Builtin:
return [generateExtensionWidgetId(VSXExtensionsSourceOptions.BUILT_IN)];
case VSXSearchMode.Installed:
return [generateExtensionWidgetId(VSXExtensionsSourceOptions.INSTALLED)];
case VSXSearchMode.Recommended:
return [generateExtensionWidgetId(VSXExtensionsSourceOptions.RECOMMENDED)];
case VSXSearchMode.Search:
return [generateExtensionWidgetId(VSXExtensionsSourceOptions.SEARCH_RESULT)];
default:
return [];
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected override doStoreState(): any {
const modes: VSXExtensionsViewContainer.State['modes'] = {};
for (const mode of this.lastModeState.keys()) {
modes[mode] = this.lastModeState.get(mode);
}
return {
query: this.model.search.query,
modes
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected override doRestoreState(state: any): void {
// eslint-disable-next-line guard-for-in
for (const key in state.modes) {
const mode = Number(key) as VSXSearchMode;
const modeState = state.modes[mode];
if (modeState) {
this.lastModeState.set(mode, modeState);
}
}
this.model.search.query = state.query;
}
protected override updateToolbarItems(allParts: ViewContainerPart[]): void {
super.updateToolbarItems(allParts);
this.toDisposeOnUpdateTitle.push(this.toolbarRegistry.registerItem({
id: VSXExtensionsCommands.INSTALL_FROM_VSIX.id,
command: VSXExtensionsCommands.INSTALL_FROM_VSIX.id,
text: VSXExtensionsCommands.INSTALL_FROM_VSIX.label,
group: 'other_1',
isVisible: (widget: Widget) => widget === this.getTabBarDelegate()
}));
this.toDisposeOnUpdateTitle.push(this.toolbarRegistry.registerItem({
id: VSXExtensionsCommands.CLEAR_ALL.id,
command: VSXExtensionsCommands.CLEAR_ALL.id,
text: VSXExtensionsCommands.CLEAR_ALL.label,
priority: 1,
onDidChange: this.model.onDidChange,
isVisible: (widget: Widget) => widget === this.getTabBarDelegate()
}));
}
protected override getToggleVisibilityGroupLabel(): string {
return nls.localizeByDefault('Views');
}
}
export namespace VSXExtensionsViewContainer {
export interface State {
query: string;
modes: {
[mode: number]: ViewContainer.State | undefined
}
}
}

View File

@@ -0,0 +1,165 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, interfaces, postConstruct, inject } from '@theia/core/shared/inversify';
import { Message, TreeModel, TreeNode } from '@theia/core/lib/browser';
import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree';
import { VSXExtensionsSource, VSXExtensionsSourceOptions } from './vsx-extensions-source';
import { nls } from '@theia/core/lib/common/nls';
import { BadgeWidget } from '@theia/core/lib/browser/view-container';
import { Emitter, Event } from '@theia/core/lib/common';
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
import * as React from '@theia/core/shared/react';
@injectable()
export class VSXExtensionsWidgetOptions extends VSXExtensionsSourceOptions {
title?: string;
}
export const generateExtensionWidgetId = (widgetId: string): string => VSXExtensionsWidget.ID + ':' + widgetId;
@injectable()
export class VSXExtensionsWidget extends SourceTreeWidget implements BadgeWidget {
static ID = 'vsx-extensions';
static createWidget(parent: interfaces.Container, options: VSXExtensionsWidgetOptions): VSXExtensionsWidget {
const child = SourceTreeWidget.createContainer(parent, {
virtualized: false,
scrollIfActive: true
});
child.bind(VSXExtensionsSourceOptions).toConstantValue(options);
child.bind(VSXExtensionsSource).toSelf();
child.unbind(SourceTreeWidget);
child.bind(VSXExtensionsWidgetOptions).toConstantValue(options);
child.bind(VSXExtensionsWidget).toSelf();
return child.get(VSXExtensionsWidget);
}
protected _badge?: number;
protected onDidChangeBadgeEmitter = new Emitter<void>();
protected _badgeTooltip?: string;
protected onDidChangeBadgeTooltipEmitter = new Emitter<void>();
@inject(VSXExtensionsWidgetOptions)
protected readonly options: VSXExtensionsWidgetOptions;
@inject(VSXExtensionsSource)
protected readonly extensionsSource: VSXExtensionsSource;
@postConstruct()
protected override init(): void {
super.init();
this.addClass('theia-vsx-extensions');
this.id = generateExtensionWidgetId(this.options.id);
this.toDispose.push(this.extensionsSource);
this.source = this.extensionsSource;
const title = this.options.title ?? this.computeTitle();
this.title.label = title;
this.title.caption = title;
this.toDispose.push(this.source.onDidChange(async () => {
this.badge = await this.resolveCount();
}));
}
get onDidChangeBadge(): Event<void> {
return this.onDidChangeBadgeEmitter.event;
}
get badge(): number | undefined {
return this._badge;
}
set badge(count: number | undefined) {
this._badge = count;
this.onDidChangeBadgeEmitter.fire();
}
get onDidChangeBadgeTooltip(): Event<void> {
return this.onDidChangeBadgeTooltipEmitter.event;
}
get badgeTooltip(): string | undefined {
return this._badgeTooltip;
}
set badgeTooltip(tooltip: string | undefined) {
this._badgeTooltip = tooltip;
this.onDidChangeBadgeTooltipEmitter.fire();
}
protected computeTitle(): string {
switch (this.options.id) {
case VSXExtensionsSourceOptions.INSTALLED:
return nls.localizeByDefault('Installed');
case VSXExtensionsSourceOptions.BUILT_IN:
return nls.localizeByDefault('Built-in');
case VSXExtensionsSourceOptions.RECOMMENDED:
return nls.localizeByDefault('Recommended');
case VSXExtensionsSourceOptions.SEARCH_RESULT:
return 'Open VSX Registry';
default:
return '';
}
}
protected async resolveCount(): Promise<number | undefined> {
if (this.options.id !== VSXExtensionsSourceOptions.SEARCH_RESULT) {
const elements = await this.source?.getElements() || [];
return [...elements].length;
}
return undefined;
}
protected override tapNode(node?: TreeNode): void {
super.tapNode(node);
this.model.openNode(node);
}
protected override handleDblClickEvent(): void {
// Don't open the editor view on a double click.
}
protected override renderTree(model: TreeModel): React.ReactNode {
if (this.options.id === VSXExtensionsSourceOptions.SEARCH_RESULT) {
const searchError = this.extensionsSource.getModel().searchError;
if (!!searchError) {
const message = nls.localize('theia/vsx-registry/errorFetching', 'Error fetching extensions.');
const configurationHint = nls.localize('theia/vsx-registry/errorFetchingConfigurationHint', 'This could be caused by network configuration issues.');
const hint = searchError.includes('ENOTFOUND') ? configurationHint : '';
return <AlertMessage
type='ERROR'
header={`${message} ${searchError} ${hint}`}
/>;
}
}
return super.renderTree(model);
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
if (this.options.id === VSXExtensionsSourceOptions.INSTALLED) {
// This is needed when an Extension was installed outside of the extension view.
// E.g. using explorer context menu.
this.doUpdateRows();
}
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { LanguageQuickPickItem, LanguageQuickPickService } from '@theia/core/lib/browser/i18n/language-quick-pick-service';
import { RequestContext, RequestService } from '@theia/core/shared/@theia/request';
import { inject, injectable } from '@theia/core/shared/inversify';
import { LanguageInfo } from '@theia/core/lib/common/i18n/localization';
import { PluginPackage, PluginServer } from '@theia/plugin-ext';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { VSXSearchEntry } from '@theia/ovsx-client';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { nls } from '@theia/core/lib/common/nls';
import { MessageService } from '@theia/core/lib/common/message-service';
@injectable()
export class VSXLanguageQuickPickService extends LanguageQuickPickService {
@inject(OVSXClientProvider)
protected readonly clientProvider: OVSXClientProvider;
@inject(RequestService)
protected readonly requestService: RequestService;
@inject(PluginServer)
protected readonly pluginServer: PluginServer;
@inject(MessageService)
protected readonly messageService: MessageService;
protected override async getAvailableLanguages(): Promise<LanguageQuickPickItem[]> {
const client = await this.clientProvider();
try {
const searchResult = await client.search({
category: 'Language Packs',
sortBy: 'downloadCount',
sortOrder: 'desc',
size: 20
});
const extensionLanguages = await Promise.all(
searchResult.extensions.map(async extension => ({
extension,
languages: await this.loadExtensionLanguages(extension)
}))
);
const languages = new Map<string, LanguageQuickPickItem>();
for (const extension of extensionLanguages) {
for (const localizationContribution of extension.languages) {
if (!languages.has(localizationContribution.languageId)) {
languages.set(localizationContribution.languageId, {
...this.createLanguageQuickPickItem(localizationContribution),
execute: async () => {
const progress = await this.messageService.showProgress({
text: nls.localizeByDefault('Installing {0} language support...',
localizationContribution.localizedLanguageName ?? localizationContribution.languageName ?? localizationContribution.languageId),
});
try {
const extensionUri = VSCodeExtensionUri.fromId(`${extension.extension.namespace}.${extension.extension.name}`).toString();
await this.pluginServer.install(extensionUri);
} finally {
progress.cancel();
}
}
});
}
}
}
return Array.from(languages.values());
} catch (error) {
console.error(error);
return [];
}
}
protected async loadExtensionLanguages(extension: VSXSearchEntry): Promise<LanguageInfo[]> {
// When searching for extensions on ovsx, we don't receive the `manifest` property.
// This property is only set when querying a specific extension.
// To improve performance, we assume that a manifest exists at `/package.json`.
const downloadUrl = extension.files.download;
const parentUrl = downloadUrl.substring(0, downloadUrl.lastIndexOf('/'));
const manifestUrl = parentUrl + '/package.json';
try {
const manifestRequest = await this.requestService.request({ url: manifestUrl });
const manifestContent = RequestContext.asJson<PluginPackage>(manifestRequest);
const localizations = manifestContent.contributes?.localizations ?? [];
return localizations.map(e => ({
languageId: e.languageId,
languageName: e.languageName,
localizedLanguageName: e.localizedLanguageName,
languagePack: true
}));
} catch {
// The `package.json` file might not actually exist, simply return an empty array
return [];
}
}
}

View File

@@ -0,0 +1,121 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager, WebSocketConnectionProvider,
WidgetStatusBarContribution,
noopWidgetStatusBarContribution
} from '@theia/core/lib/browser';
import { VSXExtensionsViewContainer } from './vsx-extensions-view-container';
import { VSXExtensionsContribution } from './vsx-extensions-contribution';
import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { VSXExtensionsWidget, VSXExtensionsWidgetOptions } from './vsx-extensions-widget';
import { VSXExtensionFactory, VSXExtension, VSXExtensionOptions } from './vsx-extension';
import { VSXExtensionEditor } from './vsx-extension-editor';
import { VSXExtensionEditorManager } from './vsx-extension-editor-manager';
import { VSXExtensionsSourceOptions } from './vsx-extensions-source';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { bindExtensionPreferences } from '../common/recommended-extensions-preference-contribution';
import { bindPreferenceProviderOverrides } from './recommended-extensions/preference-provider-overrides';
import { bindVsxExtensionsPreferences } from './vsx-extensions-preferences';
import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment';
import { LanguageQuickPickService } from '@theia/core/lib/browser/i18n/language-quick-pick-service';
import { VSXLanguageQuickPickService } from './vsx-language-quick-pick-service';
import { VsxExtensionArgumentProcessor } from './vsx-extension-argument-processor';
import { ArgumentProcessorContribution } from '@theia/plugin-ext/lib/main/browser/command-registry-main';
import { ExtensionSchemaContribution } from './recommended-extensions/recommended-extensions-json-schema';
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(VSXEnvironment)
.toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, VSX_ENVIRONMENT_PATH))
.inSingletonScope();
bind(VSXExtension).toSelf();
bind(VSXExtensionFactory).toFactory(ctx => (option: VSXExtensionOptions) => {
const child = ctx.container.createChild();
child.bind(VSXExtensionOptions).toConstantValue(option);
return child.get(VSXExtension);
});
bind(VSXExtensionsModel).toSelf().inSingletonScope();
bind(VSXExtensionEditor).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: VSXExtensionEditor.ID,
createWidget: async (options: VSXExtensionOptions) => {
const extension = await ctx.container.get(VSXExtensionsModel).resolve(options.id);
const child = ctx.container.createChild();
child.bind(VSXExtension).toConstantValue(extension);
return child.get(VSXExtensionEditor);
}
})).inSingletonScope();
bind(VSXExtensionEditorManager).toSelf().inSingletonScope();
bind(OpenHandler).toService(VSXExtensionEditorManager);
bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(VSXExtensionEditor));
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: VSXExtensionsWidget.ID,
createWidget: async (options: VSXExtensionsWidgetOptions) => VSXExtensionsWidget.createWidget(container, options)
})).inSingletonScope();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: VSXExtensionsViewContainer.ID,
createWidget: async () => {
const child = ctx.container.createChild();
child.bind(ViewContainerIdentifier).toConstantValue({
id: VSXExtensionsViewContainer.ID,
progressLocationId: 'extensions'
});
child.bind(VSXExtensionsViewContainer).toSelf();
child.bind(VSXExtensionsSearchBar).toSelf().inSingletonScope();
const viewContainer = child.get(VSXExtensionsViewContainer);
const widgetManager = child.get(WidgetManager);
for (const id of [
VSXExtensionsSourceOptions.SEARCH_RESULT,
VSXExtensionsSourceOptions.RECOMMENDED,
VSXExtensionsSourceOptions.INSTALLED,
VSXExtensionsSourceOptions.BUILT_IN,
]) {
const widget = await widgetManager.getOrCreateWidget(VSXExtensionsWidget.ID, { id });
viewContainer.addWidget(widget, {
initiallyCollapsed: id === VSXExtensionsSourceOptions.BUILT_IN
});
}
return viewContainer;
}
})).inSingletonScope();
bind(VSXExtensionsSearchModel).toSelf().inSingletonScope();
rebind(LanguageQuickPickService).to(VSXLanguageQuickPickService).inSingletonScope();
bindViewContribution(bind, VSXExtensionsContribution);
bind(FrontendApplicationContribution).toService(VSXExtensionsContribution);
bind(ColorContribution).toService(VSXExtensionsContribution);
bindExtensionPreferences(bind);
bindPreferenceProviderOverrides(bind, unbind);
bindVsxExtensionsPreferences(bind);
bind(VsxExtensionArgumentProcessor).toSelf().inSingletonScope();
bind(ArgumentProcessorContribution).toService(VsxExtensionArgumentProcessor);
bind(ExtensionSchemaContribution).toSelf().inSingletonScope();
bind(JsonSchemaContribution).toService(ExtensionSchemaContribution);
});

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson 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
// *****************************************************************************
export { OVSXClientProvider, OVSXUrlResolver } from './ovsx-client-provider';
export { VSXEnvironment } from './vsx-environment';
export { VSXExtensionUri } from './vsx-extension-uri';

View File

@@ -0,0 +1,35 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 { MaybePromise } from '@theia/core/lib/common';
import { RequestService } from '@theia/core/shared/@theia/request';
import type { interfaces } from '@theia/core/shared/inversify';
import { OVSXClient, OVSXHttpClient } from '@theia/ovsx-client';
import { VSXEnvironment } from './vsx-environment';
export const OVSXUrlResolver = Symbol('OVSXUrlResolver') as symbol & interfaces.Abstract<OVSXUrlResolver>;
export type OVSXUrlResolver = (value: string) => MaybePromise<string>;
export const OVSXClientProvider = Symbol('OVSXClientProvider') as symbol & interfaces.Abstract<OVSXClientProvider>;
export type OVSXClientProvider = () => MaybePromise<OVSXClient>;
/**
* @deprecated since 1.32.0
*/
export async function createOVSXClient(vsxEnvironment: VSXEnvironment, requestService: RequestService): Promise<OVSXClient> {
const apiUrl = await vsxEnvironment.getRegistryApiUri();
return new OVSXHttpClient(apiUrl, requestService);
}

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 { PreferenceSchema, PreferenceScope, nls, PreferenceContribution, PreferenceConfiguration, PreferenceService, createPreferenceProxy } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
export const extensionsSchemaID = 'vscode://schemas/extensions';
export interface RecommendedExtensions {
recommendations?: string[];
unwantedRecommendations?: string[];
}
export const recommendedExtensionsPreferencesSchema: PreferenceSchema = {
scope: PreferenceScope.Folder,
properties: {
extensions: {
$ref: extensionsSchemaID,
description: nls.localize('theia/vsx-registry/recommendedExtensions', 'A list of the names of extensions recommended for use in this workspace.'),
default: { recommendations: [] },
},
},
};
export const IGNORE_RECOMMENDATIONS_ID = 'extensions.ignoreRecommendations';
export const recommendedExtensionNotificationPreferencesSchema: PreferenceSchema = {
scope: PreferenceScope.Folder,
properties: {
[IGNORE_RECOMMENDATIONS_ID]: {
description: nls.localize('theia/vsx-registry/showRecommendedExtensions', 'Controls whether notifications are shown for extension recommendations.'),
default: false,
type: 'boolean'
}
}
};
export const ExtensionNotificationPreferences = Symbol('ExtensionNotificationPreferences');
export function bindExtensionPreferences(bind: interfaces.Bind): void {
bind(PreferenceContribution).toConstantValue({ schema: recommendedExtensionsPreferencesSchema });
bind(PreferenceConfiguration).toConstantValue({ name: 'extensions' });
bind(ExtensionNotificationPreferences).toDynamicValue(({ container }) => {
const preferenceService = container.get<PreferenceService>(PreferenceService);
return createPreferenceProxy(preferenceService, recommendedExtensionNotificationPreferencesSchema);
}).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: recommendedExtensionNotificationPreferencesSchema });
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson 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 type { OVSXRouterConfig } from '@theia/ovsx-client';
export const VSX_ENVIRONMENT_PATH = '/services/vsx-environment';
export const VSXEnvironment = Symbol('VSXEnvironment');
export interface VSXEnvironment {
getRateLimit(): Promise<number>;
getRegistryUri(): Promise<string>;
getRegistryApiUri(): Promise<string>;
getVscodeApiVersion(): Promise<string>;
getOvsxRouterConfig?(): Promise<OVSXRouterConfig | undefined>;
}

View File

@@ -0,0 +1,20 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { VSCodeExtensionUri as VSXExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
/** @deprecated since 1.25.0. Import `VSCodeExtensionUri from `plugin-ext-vscode` package instead. */
export { VSXExtensionUri };

View File

@@ -0,0 +1,85 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson 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 { ContainerModule } from '@theia/core/shared/inversify';
import { OVSXClientProvider, OVSXUrlResolver } from '../common';
import { RequestService } from '@theia/core/shared/@theia/request';
import {
ExtensionIdMatchesFilterFactory, OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory
} from '@theia/ovsx-client';
import { VSXEnvironment } from './vsx-environment';
import { RateLimiter } from 'limiter';
export default new ContainerModule(bind => {
bind(OVSXUrlResolver)
.toFunction((url: string) => url);
bind(OVSXClientProvider)
.toDynamicValue(ctx => {
const vsxEnvironment = ctx.container.get<VSXEnvironment>(VSXEnvironment);
const requestService = ctx.container.get<RequestService>(RequestService);
const urlResolver = ctx.container.get<OVSXUrlResolver>(OVSXUrlResolver);
const clientPromise = Promise
.all([
vsxEnvironment.getRegistryApiUri(),
vsxEnvironment.getOvsxRouterConfig?.(),
vsxEnvironment.getRateLimit()
])
.then<OVSXClient>(async ([apiUrl, ovsxRouterConfig, rateLimit]) => {
const rateLimiter = new RateLimiter({
interval: 'second',
tokensPerInterval: rateLimit
});
if (ovsxRouterConfig) {
const clientFactory = OVSXHttpClient.createClientFactory(requestService, rateLimiter);
return OVSXRouterClient.FromConfig(
ovsxRouterConfig,
async url => clientFactory(await urlResolver(url)),
[RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
);
}
return new OVSXHttpClient(
await urlResolver(apiUrl),
requestService,
rateLimiter
);
});
// reuse the promise for subsequent calls to this provider
return () => clientPromise;
})
.inSingletonScope();
bind(OVSXApiFilter)
.toDynamicValue(ctx => {
const vsxEnvironment = ctx.container.get<VSXEnvironment>(VSXEnvironment);
const apiFilter = new OVSXApiFilterImpl(undefined!, '-- temporary invalid version value --');
vsxEnvironment.getVscodeApiVersion()
.then(apiVersion => apiFilter.supportedApiVersion = apiVersion);
const clientProvider = ctx.container.get<OVSXClientProvider>(OVSXClientProvider);
Promise.resolve(clientProvider()).then(client => {
apiFilter.client = client;
});
return apiFilter;
})
.inSingletonScope();
bind(OVSXApiFilterProvider)
.toProvider(ctx => async () => {
const vsxEnvironment = ctx.container.get<VSXEnvironment>(VSXEnvironment);
const clientProvider = ctx.container.get<OVSXClientProvider>(OVSXClientProvider);
const client = await clientProvider();
const apiVersion = await vsxEnvironment.getVscodeApiVersion();
const apiFilter = new OVSXApiFilterImpl(client, apiVersion);
return apiFilter;
});
});

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2024 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext';
import { VsxCli } from './vsx-cli';
import { VSXExtensionUri } from '../common';
import * as fs from 'fs';
import { FileUri } from '@theia/core/lib/node';
import * as path from 'path';
@injectable()
export class VsxCliDeployerParticipant implements PluginDeployerParticipant {
@inject(VsxCli)
protected readonly vsxCli: VsxCli;
async onWillStart(context: PluginDeployerStartContext): Promise<void> {
const pluginUris = await Promise.all(this.vsxCli.pluginsToInstall.map(async id => {
try {
const resolvedPath = path.resolve(id);
const stat = await fs.promises.stat(resolvedPath);
if (stat.isFile()) {
return FileUri.create(resolvedPath).withScheme('local-file').toString();
}
} catch (e) {
// expected if file does not exist
}
return VSXExtensionUri.fromVersionedId(id).toString();
}));
context.userEntries.push(...pluginUris);
}
}

View File

@@ -0,0 +1,55 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson 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 { CliContribution } from '@theia/core/lib/node';
import { injectable } from '@theia/core/shared/inversify';
import { Argv } from '@theia/core/shared/yargs';
import { OVSX_RATE_LIMIT, OVSXRouterConfig } from '@theia/ovsx-client';
import * as fs from 'fs';
@injectable()
export class VsxCli implements CliContribution {
ovsxRouterConfig: OVSXRouterConfig | undefined;
ovsxRateLimit: number;
pluginsToInstall: string[] = [];
configure(conf: Argv<{}>): void {
conf.option('ovsx-router-config', { description: 'JSON configuration file for the OVSX router client', type: 'string' });
conf.option('ovsx-rate-limit', { description: 'Limits the number of requests to OVSX per second', type: 'number', default: OVSX_RATE_LIMIT });
conf.option('install-plugin', {
alias: 'install-extension',
nargs: 1,
desc: 'Installs or updates a plugin. Argument is a path to the *.vsix file or a plugin id of the form "publisher.name[@version]"'
});
}
async setArguments(args: Record<string, unknown>): Promise<void> {
const { 'ovsx-router-config': ovsxRouterConfig } = args;
if (typeof ovsxRouterConfig === 'string') {
this.ovsxRouterConfig = JSON.parse(await fs.promises.readFile(ovsxRouterConfig, 'utf8'));
}
let pluginsToInstall = args.installPlugin;
if (typeof pluginsToInstall === 'string') {
pluginsToInstall = [pluginsToInstall];
}
if (Array.isArray(pluginsToInstall)) {
this.pluginsToInstall = pluginsToInstall;
}
const ovsxRateLimit = args.ovsxRateLimit;
this.ovsxRateLimit = typeof ovsxRateLimit === 'number' ? ovsxRateLimit : OVSX_RATE_LIMIT;
}
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import URI from '@theia/core/lib/common/uri';
import { inject, injectable } from '@theia/core/shared/inversify';
import { OVSXRouterConfig } from '@theia/ovsx-client';
import { PluginVsCodeCliContribution } from '@theia/plugin-ext-vscode/lib/node/plugin-vscode-cli-contribution';
import { VSXEnvironment } from '../common/vsx-environment';
import { VsxCli } from './vsx-cli';
@injectable()
export class VSXEnvironmentImpl implements VSXEnvironment {
protected _registryUri = new URI(process.env['VSX_REGISTRY_URL']?.trim() || 'https://open-vsx.org');
@inject(PluginVsCodeCliContribution)
protected readonly pluginVscodeCli: PluginVsCodeCliContribution;
@inject(VsxCli)
protected vsxCli: VsxCli;
async getRateLimit(): Promise<number> {
return this.vsxCli.ovsxRateLimit;
}
async getRegistryUri(): Promise<string> {
return this._registryUri.toString(true);
}
async getRegistryApiUri(): Promise<string> {
return this._registryUri.resolve('api').toString(true);
}
async getVscodeApiVersion(): Promise<string> {
return this.pluginVscodeCli.vsCodeApiVersionPromise;
}
async getOvsxRouterConfig(): Promise<OVSXRouterConfig | undefined> {
return this.vsxCli.ovsxRouterConfig;
}
}

View File

@@ -0,0 +1,134 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as path from 'path';
import * as semver from 'semver';
import * as fs from '@theia/core/shared/fs-extra';
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { PluginDeployerHandler, PluginDeployerResolver, PluginDeployerResolverContext, PluginDeployOptions, PluginIdentifiers } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { FileUri } from '@theia/core/lib/node';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { OVSXClientProvider } from '../common/ovsx-client-provider';
import { OVSXApiFilterProvider, VSXExtensionRaw, VSXTargetPlatform } from '@theia/ovsx-client';
import { RequestService } from '@theia/core/shared/@theia/request';
import { PluginVSCodeEnvironment } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-environment';
import { PluginUninstallationManager } from '@theia/plugin-ext/lib/main/node/plugin-uninstallation-manager';
@injectable()
export class VSXExtensionResolver implements PluginDeployerResolver {
@inject(OVSXClientProvider) protected clientProvider: OVSXClientProvider;
@inject(PluginDeployerHandler) protected pluginDeployerHandler: PluginDeployerHandler;
@inject(RequestService) protected requestService: RequestService;
@inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment;
@inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager;
@inject(OVSXApiFilterProvider) protected vsxApiFilter: OVSXApiFilterProvider;
accept(pluginId: string): boolean {
return !!VSCodeExtensionUri.toId(new URI(pluginId));
}
static readonly TEMP_DIR_PREFIX = 'vscode-download';
static readonly TARGET_PLATFORM = `${process.platform}-${process.arch}` as VSXTargetPlatform;
async resolve(context: PluginDeployerResolverContext, options?: PluginDeployOptions): Promise<void> {
const id = VSCodeExtensionUri.toId(new URI(context.getOriginId()));
if (!id) {
return;
}
let extension: VSXExtensionRaw | undefined;
const filter = await this.vsxApiFilter();
const version = options?.version || id.version;
if (version) {
console.log(`[${id.id}]: trying to resolve version ${version}...`);
extension = await filter.findLatestCompatibleExtension({
extensionId: id.id,
extensionVersion: version,
includeAllVersions: true,
targetPlatform: VSXExtensionResolver.TARGET_PLATFORM
});
} else {
console.log(`[${id.id}]: trying to resolve latest version...`);
extension = await filter.findLatestCompatibleExtension({
extensionId: id.id,
includeAllVersions: true,
targetPlatform: VSXExtensionResolver.TARGET_PLATFORM
});
}
if (!extension) {
return;
}
if (extension.error) {
throw new Error(extension.error);
}
const resolvedId = id.id + '-' + extension.version;
const downloadUrl = extension.files.download;
console.log(`[${id.id}]: resolved to '${resolvedId}'`);
if (!options?.ignoreOtherVersions) {
const existingVersion = this.hasSameOrNewerVersion(id.id, extension);
if (existingVersion) {
console.log(`[${id.id}]: is already installed with the same or newer version '${existingVersion}'`);
return;
}
}
const downloadDir = await this.getTempDir();
await fs.ensureDir(downloadDir);
const downloadedExtensionPath = path.resolve(downloadDir, path.basename(downloadUrl));
console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`, 'to path', downloadDir);
if (!await this.download(downloadUrl, downloadedExtensionPath)) {
console.log(`[${resolvedId}]: not found`);
return;
}
console.log(`[${resolvedId}]: downloaded to ${downloadedExtensionPath}"`);
context.addPlugin(resolvedId, downloadedExtensionPath);
}
protected async getTempDir(): Promise<string> {
const tempDir = FileUri.fsPath(await this.environment.getTempDirUri(VSXExtensionResolver.TEMP_DIR_PREFIX));
if (!await fs.pathExists(tempDir)) {
await fs.mkdirs(tempDir);
}
return tempDir;
}
protected hasSameOrNewerVersion(id: string, extension: VSXExtensionRaw): string | undefined {
const existingPlugins = this.pluginDeployerHandler.getDeployedPluginsById(id)
.filter(plugin => !this.uninstallationManager.isUninstalled(PluginIdentifiers.componentsToVersionedId(plugin.metadata.model)));
const sufficientVersion = existingPlugins.find(existingPlugin => {
const existingVersion = semver.clean(existingPlugin.metadata.model.version);
const desiredVersion = semver.clean(extension.version);
if (desiredVersion && existingVersion && semver.gte(existingVersion, desiredVersion)) {
return existingVersion;
}
});
return sufficientVersion?.metadata.model.version;
}
protected async download(downloadUrl: string, downloadPath: string): Promise<boolean> {
if (await fs.pathExists(downloadPath)) { return true; }
const context = await this.requestService.request({ url: downloadUrl });
if (context.res.statusCode === 404) {
return false;
} else if (context.res.statusCode !== 200) {
throw new Error('Request returned status code: ' + context.res.statusCode);
} else {
await fs.writeFile(downloadPath, context.buffer);
return true;
}
}
}

View File

@@ -0,0 +1,40 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
import { CliContribution } from '@theia/core/lib/node';
import { ContainerModule } from '@theia/core/shared/inversify';
import { PluginDeployerParticipant, PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment';
import { VsxCli } from './vsx-cli';
import { VSXEnvironmentImpl } from './vsx-environment-impl';
import { VSXExtensionResolver } from './vsx-extension-resolver';
import { VsxCliDeployerParticipant } from './vsx-cli-deployer-participant';
import { bindExtensionPreferences } from '../common/recommended-extensions-preference-contribution';
export default new ContainerModule(bind => {
bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope();
bind(VsxCli).toSelf().inSingletonScope();
bind(CliContribution).toService(VsxCli);
bind(ConnectionHandler)
.toDynamicValue(ctx => new JsonRpcConnectionHandler(VSX_ENVIRONMENT_PATH, () => ctx.container.get(VSXEnvironment)))
.inSingletonScope();
bind(VSXExtensionResolver).toSelf().inSingletonScope();
bind(PluginDeployerResolver).toService(VSXExtensionResolver);
bind(VsxCliDeployerParticipant).toSelf().inSingletonScope();
bind(PluginDeployerParticipant).toService(VsxCliDeployerParticipant);
bindExtensionPreferences(bind);
});

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2024 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { RemoteCliContext, RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution';
import { inject, injectable } from '@theia/core/shared/inversify';
import { PluginDeployerHandler, PluginType } from '@theia/plugin-ext';
@injectable()
export class VsxRemoteCli implements RemoteCliContribution {
@inject(PluginDeployerHandler)
protected readonly pluginDeployerHandler: PluginDeployerHandler;
async enhanceArgs(context: RemoteCliContext): Promise<string[]> {
const deployedPlugins = await this.pluginDeployerHandler.getDeployedPlugins();
// Plugin IDs can be duplicated between frontend and backend plugins, so we create a set first
const installPluginArgs = Array.from(
new Set(
deployedPlugins
.filter(plugin => plugin.type === PluginType.User)
.map(p => `--install-plugin=${p.metadata.model.id}`)
)
);
return installPluginArgs;
}
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('vsx-registry package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,37 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../../dev-packages/ovsx-client"
},
{
"path": "../core"
},
{
"path": "../filesystem"
},
{
"path": "../navigator"
},
{
"path": "../plugin-ext"
},
{
"path": "../plugin-ext-vscode"
},
{
"path": "../preferences"
},
{
"path": "../workspace"
}
]
}