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

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

View File

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

View File

@@ -0,0 +1,110 @@
<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 - VARIABLE-RESOLVER EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/variable-resolved` extension provides variable substitution mechanism inside of strings using `${variableName}` syntax.
### Variable Contribution Point
Extension provides a hook that allows any extensions to contribute its own variables.
Here's the example of contributing two variables:
- `${file}` - returns the name of the file opened in the current editor
- `${lineNumber}` - returns the current line number in the current file
```typescript
@injectable()
export class EditorVariableContribution implements VariableContribution {
constructor(
@inject(EditorManager) protected readonly editorManager: EditorManager
) { }
registerVariables(variables: VariableRegistry): void {
variables.registerVariable({
name: 'file',
description: 'The name of the file opened in the current editor',
resolve: () => {
const currentEditor = this.getCurrentEditor();
if (currentEditor) {
return currentEditor.uri.displayName;
}
return undefined;
}
});
variables.registerVariable({
name: 'lineNumber',
description: 'The current line number in the current file',
resolve: () => {
const currentEditor = this.getCurrentEditor();
if (currentEditor) {
return `${currentEditor.cursor.line + 1}`;
}
return undefined;
}
});
}
protected getCurrentEditor(): TextEditor | undefined {
const currentEditor = this.editorManager.currentEditor;
if (currentEditor) {
return currentEditor.editor;
}
return undefined;
}
}
```
Note that a Variable is resolved to `MaybePromise<string | undefined>` which means that it can be resolved synchronously or within a Promise.
### Using the Variable Resolver Service
There's the example of how one can use Variable Resolver Service in its own plugin:
```typescript
@injectable()
export class MyService {
constructor(
@inject(VariableResolverService) protected readonly variableResolver: VariableResolverService
) { }
async resolve(): Promise<void> {
const text = 'cursor is in file ${file} on line ${lineNumber}';
const resolved = await this.variableResolver.resolve(text);
console.log(resolved);
}
}
```
If `package.json` file is currently opened and cursor is on line 5 then the following output will be logged to the console:
```sh
cursor is in file package.json on line 5
```
## Additional Information
- [API documentation for `@theia/variable-resolver`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_variable-resolver.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,54 @@
{
"name": "@theia/variable-resolver",
"version": "1.68.0",
"description": "Theia - Variable Resolver Extension",
"dependencies": {
"@theia/core": "1.68.0",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/variable-resolver-frontend-module"
}
],
"keywords": [
"theia-extension"
],
"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",
"contributors": [
{
"name": "Artem Zatsarynnyi",
"email": "azatsary@redhat.com"
}
],
"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"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,151 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, optional } from '@theia/core/shared/inversify';
import { VariableContribution, VariableRegistry } from './variable';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { CommandService } from '@theia/core/lib/common/command';
import { OS } from '@theia/core/lib/common/os';
import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
import { ResourceContextKey } from '@theia/core/lib/browser/resource-context-key';
import { VariableInput } from './variable-input';
import { QuickInputService, QuickPickValue } from '@theia/core/lib/browser';
import { MaybeArray, RecursivePartial } from '@theia/core/lib/common/types';
import { cancelled } from '@theia/core/lib/common/cancellation';
import URI from '@theia/core/lib/common/uri';
@injectable()
export class CommonVariableContribution implements VariableContribution {
@inject(EnvVariablesServer)
protected readonly env: EnvVariablesServer;
@inject(CommandService)
protected readonly commands: CommandService;
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
@inject(ResourceContextKey)
protected readonly resourceContextKey: ResourceContextKey;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(ApplicationServer)
protected readonly appServer: ApplicationServer;
async registerVariables(variables: VariableRegistry): Promise<void> {
const execPath = await this.env.getExecPath();
variables.registerVariable({
name: 'execPath',
resolve: () => execPath
});
variables.registerVariable({
name: 'pathSeparator',
resolve: () => OS.backend.isWindows ? '\\' : '/'
});
variables.registerVariable({
name: 'env',
resolve: async (_, envVariableName) => {
const envVariable = envVariableName && await this.env.getValue(envVariableName);
const envValue = envVariable && envVariable.value;
return envValue || '';
}
});
variables.registerVariable({
name: 'config',
resolve: (resourceUri = new URI(this.resourceContextKey.get()), preferenceName) => {
if (!preferenceName) {
return undefined;
}
return this.preferences.get(preferenceName, undefined, resourceUri && resourceUri.toString());
}
});
variables.registerVariable({
name: 'command',
resolve: async (contextUri, commandId, configurationSection, commandIdVariables, configuration) => {
if (commandId) {
if (commandIdVariables?.[commandId]) {
commandId = commandIdVariables[commandId];
}
const result = await this.commands.executeCommand(commandId, configuration);
// eslint-disable-next-line no-null/no-null
if (result === null) {
throw cancelled();
}
return result;
}
}
});
variables.registerVariable({
name: 'input',
resolve: async (resourceUri = new URI(this.resourceContextKey.get()), variable, section) => {
if (!variable || !section) {
return undefined;
}
const configuration = this.preferences.get<RecursivePartial<{ inputs: MaybeArray<VariableInput> }>>(section, undefined, resourceUri && resourceUri.toString());
const inputs = !!configuration && 'inputs' in configuration ? configuration.inputs : undefined;
const input = Array.isArray(inputs) && inputs.find(item => !!item && item.id === variable);
if (!input) {
return undefined;
}
if (input.type === 'promptString') {
if (typeof input.description !== 'string') {
return undefined;
}
return this.quickInputService?.input({
prompt: input.description,
value: input.default
});
}
if (input.type === 'pickString') {
if (typeof input.description !== 'string' || !Array.isArray(input.options)) {
return undefined;
}
const elements: Array<QuickPickValue<string>> = [];
for (const option of input.options) {
if (typeof option !== 'string') {
return undefined;
}
if (option === input.default) {
elements.unshift({
description: 'Default',
label: option,
value: option
});
} else {
elements.push({
label: option,
value: option
});
}
}
const selectedPick = await this.quickInputService?.showQuickPick(elements, { placeholder: input.description });
return selectedPick?.value;
}
if (input.type === 'command') {
if (typeof input.command !== 'string') {
return undefined;
}
return this.commands.executeCommand(input.command, input.args);
}
return undefined;
}
});
}
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './variable';
export * from './variable-quick-open-service';
export * from './variable-resolver-service';

View File

@@ -0,0 +1,131 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
* copied from
* https://github.com/microsoft/vscode/blob/0a34756cae4fc67739e60c708b04637089f8bb0d/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts#L23
*/
const idDescription = "The input's id is used to associate an input with a variable of the form ${input:id}.";
const typeDescription = 'The type of user input prompt to use.';
const descriptionDescription = 'The description is shown when the user is prompted for input.';
const defaultDescription = 'The default value for the input.';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
export const inputsSchema: IJSONSchema = {
definitions: {
inputs: {
type: 'array',
description: 'User inputs. Used for defining user input prompts, such as free string input or a choice from several options.',
items: {
oneOf: [
{
type: 'object',
required: ['id', 'type', 'description'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['promptString'],
enumDescriptions: [
"The 'promptString' type opens an input box to ask the user for input."
]
},
description: {
type: 'string',
description: descriptionDescription
},
default: {
type: 'string',
description: defaultDescription
},
}
},
{
type: 'object',
required: ['id', 'type', 'description', 'options'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['pickString'],
enumDescriptions: [
"The 'pickString' type shows a selection list.",
]
},
description: {
type: 'string',
description: descriptionDescription
},
default: {
type: 'string',
description: defaultDescription
},
options: {
type: 'array',
description: 'An array of strings that defines the options for a quick pick.',
items: {
type: 'string'
}
}
}
},
{
type: 'object',
required: ['id', 'type', 'command'],
additionalProperties: false,
properties: {
id: {
type: 'string',
description: idDescription
},
type: {
type: 'string',
description: typeDescription,
enum: ['command'],
enumDescriptions: [
"The 'command' type executes a command.",
]
},
command: {
type: 'string',
description: 'The command to execute for this input variable.'
},
args: {
type: 'object',
description: 'Optional arguments passed to the command.'
}
}
}
]
}
}
}
};

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
* copied from
* https://github.com/microsoft/vscode/blob/0a34756cae4fc67739e60c708b04637089f8bb0d/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts#L41-L63
*/
export interface VariablePromptStringInput {
id: string;
type: 'promptString';
description: string;
default?: string;
}
export interface VariablePickStringInput {
id: string;
type: 'pickString';
description: string;
options: string[];
default?: string;
}
export interface VariableCommandInput {
id: string;
type: 'command';
command: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any;
}
export type VariableInput = VariablePromptStringInput | VariablePickStringInput | VariableCommandInput;

View File

@@ -0,0 +1,62 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, optional } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { VariableRegistry, Variable } from './variable';
import { VariableResolverService } from './variable-resolver-service';
import { QuickPickItem, QuickInputService } from '@theia/core/lib/browser';
@injectable()
export class VariableQuickOpenService {
protected items: Array<QuickPickItem>;
@inject(MessageService)
protected readonly messages: MessageService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(VariableResolverService)
protected readonly variableResolver: VariableResolverService;
constructor(
@inject(VariableRegistry) protected readonly variableRegistry: VariableRegistry
) { }
open(): void {
this.items = this.variableRegistry.getVariables().map(v => ({
label: '${' + v.name + '}',
detail: v.description,
execute: () => {
setTimeout(() => this.showValue(v));
}
}));
this.quickInputService?.showQuickPick(this.items, { placeholder: 'Registered variables' });
}
protected async showValue(variable: Variable): Promise<void> {
const argument = await this.quickInputService?.input({
placeHolder: 'Type a variable argument'
});
const value = await this.variableResolver.resolve('${' + variable.name + ':' + argument + '}');
if (typeof value === 'string') {
this.messages.info(value);
}
}
}

View File

@@ -0,0 +1,98 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import * as chai from 'chai';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { ILogger, bindContributionProvider } from '@theia/core/lib/common';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { VariableContribution, VariableRegistry } from './variable';
import { VariableQuickOpenService } from './variable-quick-open-service';
import { VariableResolverFrontendContribution } from './variable-resolver-frontend-contribution';
disableJSDOM();
const expect = chai.expect;
before(() => {
chai.config.showDiff = true;
chai.config.includeStack = true;
});
describe('variable-resolver-frontend-contribution', () => {
let testContainer: Container;
let variableRegistry: VariableRegistry;
before(() => {
disableJSDOM = enableJSDOM();
testContainer = new Container();
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
bindContributionProvider(bind, VariableContribution);
bind(VariableContribution).toConstantValue(new TestVariableContribution());
bind(ILogger).to(MockLogger);
bind(VariableRegistry).toSelf().inSingletonScope();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
bind(VariableQuickOpenService).toConstantValue({} as any); // mock VariableQuickOpenService
bind(VariableResolverFrontendContribution).toSelf();
});
testContainer.load(module);
});
after(() => {
disableJSDOM();
});
beforeEach(() => {
variableRegistry = testContainer.get<VariableRegistry>(VariableRegistry);
const variableRegistrar = testContainer.get(VariableResolverFrontendContribution);
variableRegistrar.onStart();
});
it('should register all variables from the contribution points', () => {
const variables = variableRegistry.getVariables();
expect(variables.length).to.be.equal(2);
expect(variables[0].name).to.be.equal('file');
expect(variables[1].name).to.be.equal('lineNumber');
});
});
export class TestVariableContribution implements VariableContribution {
registerVariables(variables: VariableRegistry): void {
variables.registerVariable({
name: 'file',
description: 'Resolves to file name opened in the current editor',
resolve: () => Promise.resolve('package.json')
});
variables.registerVariable({
name: 'lineNumber',
description: 'Resolves to current line number',
resolve: () => Promise.resolve('5')
});
}
}

View File

@@ -0,0 +1,50 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { Command, CommandContribution, CommandRegistry, ContributionProvider } from '@theia/core/lib/common';
import { VariableContribution, VariableRegistry } from './variable';
import { VariableQuickOpenService } from './variable-quick-open-service';
export const LIST_VARIABLES: Command = Command.toLocalizedCommand({
id: 'variable.list',
label: 'Variable: List All'
}, 'theia/variableResolver/listAllVariables');
@injectable()
export class VariableResolverFrontendContribution implements FrontendApplicationContribution, CommandContribution {
constructor(
@inject(ContributionProvider) @named(VariableContribution)
protected readonly contributionProvider: ContributionProvider<VariableContribution>,
@inject(VariableRegistry) protected readonly variableRegistry: VariableRegistry,
@inject(VariableQuickOpenService) protected readonly variableQuickOpenService: VariableQuickOpenService
) { }
onStart(): void {
this.contributionProvider.getContributions().forEach(contrib =>
contrib.registerVariables(this.variableRegistry)
);
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(LIST_VARIABLES, {
isEnabled: () => true,
execute: () => this.variableQuickOpenService.open()
});
}
}

View File

@@ -0,0 +1,40 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { bindContributionProvider, CommandContribution } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { VariableRegistry, VariableContribution } from './variable';
import { VariableQuickOpenService } from './variable-quick-open-service';
import { VariableResolverFrontendContribution } from './variable-resolver-frontend-contribution';
import { VariableResolverService } from './variable-resolver-service';
import { CommonVariableContribution } from './common-variable-contribution';
export default new ContainerModule(bind => {
bind(VariableRegistry).toSelf().inSingletonScope();
bind(VariableResolverService).toSelf().inSingletonScope();
bindContributionProvider(bind, VariableContribution);
bind(VariableResolverFrontendContribution).toSelf().inSingletonScope();
for (const identifier of [FrontendApplicationContribution, CommandContribution]) {
bind(identifier).toService(VariableResolverFrontendContribution);
}
bind(VariableQuickOpenService).toSelf().inSingletonScope();
bind(CommonVariableContribution).toSelf().inSingletonScope();
bind(VariableContribution).toService(CommonVariableContribution);
});

View File

@@ -0,0 +1,83 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as chai from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { cancelled } from '@theia/core/lib/common';
import { VariableRegistry } from './variable';
import { VariableResolverService } from './variable-resolver-service';
const expect = chai.expect;
before(() => {
chai.config.showDiff = true;
chai.config.includeStack = true;
});
describe('variable-resolver-service', () => {
let testContainer: Container;
let variableRegistry: VariableRegistry;
let variableResolverService: VariableResolverService;
beforeEach(() => {
testContainer = new Container();
testContainer.bind(VariableRegistry).toSelf().inSingletonScope();
testContainer.bind(VariableResolverService).toSelf().inSingletonScope();
variableRegistry = testContainer.get(VariableRegistry);
variableRegistry.registerVariable({
name: 'file',
description: 'current file',
resolve: () => Promise.resolve('package.json')
});
variableRegistry.registerVariable({
name: 'lineNumber',
description: 'current line number',
resolve: () => Promise.resolve('6')
});
variableResolverService = testContainer.get(VariableResolverService);
});
it('should resolve known variables in a text', async () => {
const resolved = await variableResolverService.resolve('file: ${file}; line: ${lineNumber}');
expect(resolved).is.equal('file: package.json; line: 6');
});
it('should resolve known variables in a string array', async () => {
const resolved = await variableResolverService.resolveArray(['file: ${file}', 'line: ${lineNumber}']);
expect(resolved!.length).to.be.equal(2);
expect(resolved).to.contain('file: package.json');
expect(resolved).to.contain('line: 6');
});
it('should skip unknown variables', async () => {
const resolved = await variableResolverService.resolve('workspace: ${workspaceRoot}; file: ${file}; line: ${lineNumber}');
expect(resolved).is.equal('workspace: ${workspaceRoot}; file: package.json; line: 6');
});
it('should return undefined when a variable throws with `cancelled()` while resolving', async () => {
variableRegistry.registerVariable({
name: 'command',
resolve: (contextUri, commandId) => {
if (commandId === 'testCommand') {
throw cancelled();
}
}
});
const resolved = await variableResolverService.resolve('workspace: ${command:testCommand}; file: ${file}; line: ${lineNumber}');
expect(resolved).equal(undefined);
});
});

View File

@@ -0,0 +1,185 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { injectable, inject } from '@theia/core/shared/inversify';
import { VariableRegistry } from './variable';
import URI from '@theia/core/lib/common/uri';
import { CommandIdVariables } from '../common/variable-types';
import { isCancelled } from '@theia/core';
export interface VariableResolveOptions {
context?: URI;
/**
* Used for resolving inputs, see https://code.visualstudio.com/docs/editor/variables-reference#_input-variables
*/
configurationSection?: string;
commandIdVariables?: CommandIdVariables;
configuration?: unknown;
}
/**
* The variable resolver service should be used to resolve variables in strings.
*/
@injectable()
export class VariableResolverService {
protected static VAR_REGEXP = /\$\{(.*?)\}/g;
@inject(VariableRegistry) protected readonly variableRegistry: VariableRegistry;
/**
* Resolve the variables in the given string array.
* @param value The array of data to resolve variables in.
* @param options Options of the variable resolution.
* @returns Promise to array with variables resolved. Never rejects.
*
* @deprecated since 1.28.0 use {@link resolve} instead.
*/
resolveArray(value: string[], options: VariableResolveOptions = {}): Promise<string[] | undefined> {
return this.resolve(value, options);
}
/**
* Resolve the variables for all strings found in the object and nested objects.
* @param value Data to resolve variables in.
* @param options Options of the variable resolution
* @returns Promise to object with variables resolved. Returns `undefined` if a variable resolution was cancelled.
*/
async resolve<T>(value: T, options: VariableResolveOptions = {}): Promise<T | undefined> {
const context = new VariableResolverService.Context(this.variableRegistry, options);
try {
return await this.doResolve(value, context);
} catch (error) {
if (isCancelled(error)) {
return undefined;
}
throw error;
}
}
protected async doResolve(value: any, context: VariableResolverService.Context): Promise<any> {
// eslint-disable-next-line no-null/no-null
if (value === undefined || value === null) {
return value;
}
if (typeof value === 'string') {
return this.doResolveString(value, context);
}
if (Array.isArray(value)) {
return this.doResolveArray(value, context);
}
if (typeof value === 'object') {
return this.doResolveObject(value, context);
}
return value;
}
protected async doResolveObject(obj: object, context: VariableResolverService.Context): Promise<object> {
const result: {
[prop: string]: Object | undefined
} = {};
for (const name of Object.keys(obj)) {
const value = (obj as any)[name];
const resolved = await this.doResolve(value, context);
result[name] = resolved;
}
return result;
}
protected async doResolveArray(values: Array<Object | undefined>, context: VariableResolverService.Context): Promise<Array<Object | undefined>> {
const result: (Object | undefined)[] = [];
for (const value of values) {
const resolved = await this.doResolve(value, context);
result.push(resolved);
}
return result;
}
protected async doResolveString(value: string, context: VariableResolverService.Context): Promise<string> {
await this.resolveVariables(value, context);
return value.replace(VariableResolverService.VAR_REGEXP, (match: string, varName: string) => {
const varValue = context.get(varName);
return varValue !== undefined ? varValue : match;
});
}
protected async resolveVariables(value: string, context: VariableResolverService.Context): Promise<void> {
const variableRegExp = new RegExp(VariableResolverService.VAR_REGEXP);
let match;
// eslint-disable-next-line no-null/no-null
while ((match = variableRegExp.exec(value)) !== null) {
const variableName = match[1];
await context.resolve(variableName);
}
}
}
export namespace VariableResolverService {
export class Context {
protected readonly resolved = new Map<string, string | undefined>();
constructor(
protected readonly variableRegistry: VariableRegistry,
protected readonly options: VariableResolveOptions
) { }
get(name: string): string | undefined {
return this.resolved.get(name);
}
async resolve(name: string): Promise<void> {
if (this.resolved.has(name)) {
return;
}
try {
let variableName = name;
let argument: string | undefined;
const parts = name.split(':', 2);
if (parts.length > 1) {
variableName = parts[0];
argument = parts[1];
}
const variable = this.variableRegistry.getVariable(variableName);
const resolved = await variable?.resolve(
this.options.context,
argument,
this.options.configurationSection,
this.options.commandIdVariables,
this.options.configuration
);
if (
typeof resolved === 'bigint' ||
typeof resolved === 'boolean' ||
typeof resolved === 'number' ||
typeof resolved === 'string'
) {
this.resolved.set(name, `${resolved}`);
} else {
this.resolved.set(name, undefined);
}
} catch (e) {
if (isCancelled(e)) {
throw e;
}
this.resolved.set(name, undefined);
console.error(`Failed to resolve '${name}' variable:`, e);
}
}
}
}

View File

@@ -0,0 +1,106 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as chai from 'chai';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { ILogger, Disposable } from '@theia/core/lib/common';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { Variable, VariableRegistry } from './variable';
const expect = chai.expect;
let variableRegistry: VariableRegistry;
before(() => {
chai.config.showDiff = true;
chai.config.includeStack = true;
});
describe('variable api', () => {
let testContainer: Container;
before(() => {
testContainer = new Container();
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ILogger).to(MockLogger);
bind(VariableRegistry).toSelf();
});
testContainer.load(module);
});
beforeEach(() => {
variableRegistry = testContainer.get<VariableRegistry>(VariableRegistry);
});
it('should register and return variable', () => {
registerTestVariable();
const variable = variableRegistry.getVariable(TEST_VARIABLE.name);
expect(variable).is.not.undefined;
if (variable) {
expect(variable.name).is.equal(TEST_VARIABLE.name);
}
});
it('should not register a variable for already existed name', () => {
const variables: Variable[] = [
{
name: 'workspaceRoot',
description: 'workspace root URI',
resolve: () => Promise.resolve('')
},
{
name: 'workspaceRoot',
description: 'workspace root URI 2',
resolve: () => Promise.resolve('')
}
];
variables.forEach(v => variableRegistry.registerVariable(v));
const registeredVariables = variableRegistry.getVariables();
expect(registeredVariables.length).to.be.equal(1);
expect(registeredVariables[0].name).to.be.equal('workspaceRoot');
expect(registeredVariables[0].description).to.be.equal('workspace root URI');
});
it('should dispose variable', () => {
const disposable = registerTestVariable();
disposable.dispose();
const variable = variableRegistry.getVariable(TEST_VARIABLE.name);
expect(variable).is.undefined;
});
it('should unregister variables on dispose', () => {
registerTestVariable();
let variables = variableRegistry.getVariables();
expect(variables.length).to.be.equal(1);
variableRegistry.dispose();
variables = variableRegistry.getVariables();
expect(variables.length).to.be.equal(0);
});
});
const TEST_VARIABLE: Variable = {
name: 'workspaceRoot',
resolve: () => Promise.resolve('')
};
function registerTestVariable(): Disposable {
return variableRegistry.registerVariable(TEST_VARIABLE);
}

View File

@@ -0,0 +1,111 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Disposable, DisposableCollection, MaybePromise } from '@theia/core';
import URI from '@theia/core/lib/common/uri';
import { CommandIdVariables } from '../common/variable-types';
/**
* Variable can be used inside of strings using ${variableName} syntax.
*/
export interface Variable {
/**
* A unique name of this variable.
*/
readonly name: string;
/**
* A human-readable description of this variable.
*/
readonly description?: string;
/**
* Resolve to a string value of this variable or
* `undefined` if variable cannot be resolved.
* Never reject.
*/
resolve(
context?: URI,
argument?: string,
configurationSection?: string,
commandIdVariables?: CommandIdVariables,
configuration?: unknown
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): MaybePromise<any>;
}
export const VariableContribution = Symbol('VariableContribution');
/**
* The variable contribution should be implemented to register custom variables.
*/
export interface VariableContribution {
registerVariables(variables: VariableRegistry): void;
}
/**
* The variable registry manages variables.
*/
@injectable()
export class VariableRegistry implements Disposable {
protected readonly variables: Map<string, Variable> = new Map();
protected readonly toDispose = new DisposableCollection();
dispose(): void {
this.toDispose.dispose();
}
/**
* Register the given variable.
* Do nothing if a variable is already registered for the given variable name.
*/
registerVariable(variable: Variable): Disposable {
if (this.variables.has(variable.name)) {
console.warn(`A variables with name ${variable.name} is already registered.`);
return Disposable.NULL;
}
this.variables.set(variable.name, variable);
const disposable = {
dispose: () => this.variables.delete(variable.name)
};
this.toDispose.push(disposable);
return disposable;
}
/**
* Return all registered variables.
*/
getVariables(): Variable[] {
return [...this.variables.values()];
}
/**
* Get a variable for the given name or `undefined` if none.
*/
getVariable(name: string): Variable | undefined {
return this.variables.get(name);
}
/**
* Register an array of variables.
* Do nothing if a variable is already registered for the given variable name.
*/
registerVariables(variables: Variable[]): Disposable[] {
return variables.map(v => this.registerVariable(v));
}
}

View File

@@ -0,0 +1,23 @@
// *****************************************************************************
// Copyright (C) 2022 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
// *****************************************************************************
/**
* Holds variable-names to command id mappings (e.g. Provided by specific plugins / extensions)
* see "variables": https://code.visualstudio.com/api/references/contribution-points#contributes.debuggers
*/
export interface CommandIdVariables {
[id: string]: string
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
}
]
}