deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/vsx-registry/.eslintrc.js
Normal file
10
packages/vsx-registry/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
46
packages/vsx-registry/README.md
Normal file
46
packages/vsx-registry/README.md
Normal 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>
|
||||
65
packages/vsx-registry/package.json
Normal file
65
packages/vsx-registry/package.json
Normal 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"
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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() });
|
||||
}
|
||||
}
|
||||
BIN
packages/vsx-registry/src/browser/style/defaultIcon.png
Normal file
BIN
packages/vsx-registry/src/browser/style/defaultIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
4
packages/vsx-registry/src/browser/style/extensions.svg
Normal file
4
packages/vsx-registry/src/browser/style/extensions.svg
Normal 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 |
421
packages/vsx-registry/src/browser/style/index.css
Normal file
421
packages/vsx-registry/src/browser/style/index.css
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
76
packages/vsx-registry/src/browser/vsx-extension-commands.ts
Normal file
76
packages/vsx-registry/src/browser/vsx-extension-commands.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
96
packages/vsx-registry/src/browser/vsx-extension-editor.tsx
Normal file
96
packages/vsx-registry/src/browser/vsx-extension-editor.tsx
Normal 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}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
765
packages/vsx-registry/src/browser/vsx-extension.tsx
Normal file
765
packages/vsx-registry/src/browser/vsx-extension.tsx
Normal 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>
|
||||
<span className='version'>{VSXExtension.formatVersion(version)}
|
||||
</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));
|
||||
}
|
||||
};
|
||||
}
|
||||
398
packages/vsx-registry/src/browser/vsx-extensions-contribution.ts
Normal file
398
packages/vsx-registry/src/browser/vsx-extensions-contribution.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
532
packages/vsx-registry/src/browser/vsx-extensions-model.ts
Normal file
532
packages/vsx-registry/src/browser/vsx-extensions-model.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
108
packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx
Normal file
108
packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
89
packages/vsx-registry/src/browser/vsx-extensions-source.ts
Normal file
89
packages/vsx-registry/src/browser/vsx-extensions-source.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
165
packages/vsx-registry/src/browser/vsx-extensions-widget.tsx
Normal file
165
packages/vsx-registry/src/browser/vsx-extensions-widget.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
19
packages/vsx-registry/src/common/index.ts
Normal file
19
packages/vsx-registry/src/common/index.ts
Normal 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';
|
||||
35
packages/vsx-registry/src/common/ovsx-client-provider.ts
Normal file
35
packages/vsx-registry/src/common/ovsx-client-provider.ts
Normal 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);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
28
packages/vsx-registry/src/common/vsx-environment.ts
Normal file
28
packages/vsx-registry/src/common/vsx-environment.ts
Normal 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>;
|
||||
}
|
||||
20
packages/vsx-registry/src/common/vsx-extension-uri.ts
Normal file
20
packages/vsx-registry/src/common/vsx-extension-uri.ts
Normal 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 };
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
55
packages/vsx-registry/src/node/vsx-cli.ts
Normal file
55
packages/vsx-registry/src/node/vsx-cli.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
54
packages/vsx-registry/src/node/vsx-environment-impl.ts
Normal file
54
packages/vsx-registry/src/node/vsx-environment-impl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
134
packages/vsx-registry/src/node/vsx-extension-resolver.ts
Normal file
134
packages/vsx-registry/src/node/vsx-extension-resolver.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
39
packages/vsx-registry/src/node/vsx-remote-cli.ts
Normal file
39
packages/vsx-registry/src/node/vsx-remote-cli.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
packages/vsx-registry/src/package.spec.ts
Normal file
29
packages/vsx-registry/src/package.spec.ts
Normal 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);
|
||||
|
||||
});
|
||||
37
packages/vsx-registry/tsconfig.json
Normal file
37
packages/vsx-registry/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user