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,42 @@
<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 - API SAMPLES</h2>
<hr />
</div>
## Description
The `@theia/api-samples` extension contains programming examples on how to use internal APIs.
The purpose of the extension is to:
- provide developers with real-world coding examples using internal APIs, dependency injection, etc.
- provide easy-to-use and test examples for features when reviewing pull-requests.
The extension is for reference and test purposes only and is not published on `npm` (`private: true`).
### Sample mock OpenVSX server
These samples contain a mock implementation of an OpenVSX server. This is done
for testing purposes only. It is currently hosted at
`<backend-host>/mock-open-vsx/api/...`.
## Additional Information
- [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,69 @@
{
"private": true,
"name": "@theia/api-samples",
"version": "1.68.0",
"description": "Theia - Example code to demonstrate Theia API",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@theia/ai-chat": "1.68.0",
"@theia/ai-chat-ui": "1.68.0",
"@theia/ai-code-completion": "1.68.0",
"@theia/ai-core": "1.68.0",
"@theia/ai-mcp": "1.68.0",
"@theia/ai-mcp-server": "1.68.0",
"@theia/core": "1.68.0",
"@theia/file-search": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/output": "1.68.0",
"@theia/ovsx-client": "1.68.0",
"@theia/search-in-workspace": "1.68.0",
"@theia/test": "1.68.0",
"@theia/toolbar": "1.68.0",
"@theia/vsx-registry": "1.68.0",
"@theia/workspace": "1.68.0",
"zod": "^4.2.1"
},
"theiaExtensions": [
{
"frontend": "lib/browser/api-samples-frontend-module",
"backend": "lib/node/api-samples-backend-module"
},
{
"electronMain": "lib/electron-main/update/sample-updater-main-module",
"frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module"
},
{
"frontendOnly": "lib/browser-only/api-samples-frontend-only-module"
},
{
"frontendPreload": "lib/browser/api-samples-preload-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",
"files": [
"lib",
"src"
],
"scripts": {
"lint": "theiaext lint",
"build": "theiaext build",
"watch": "theiaext watch",
"clean": "theiaext clean"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
}
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource 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, interfaces } from '@theia/core/shared/inversify';
import { bindOPFSInitialization } from './filesystem/example-filesystem-initialization';
export default new ContainerModule((
bind: interfaces.Bind,
_unbind: interfaces.Unbind,
_isBound: interfaces.IsBound,
rebind: interfaces.Rebind,
) => {
bindOPFSInitialization(bind, rebind);
});

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource 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';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { EncodingService } from '@theia/core/lib/common/encoding-service';
import { OPFSInitialization, DefaultOPFSInitialization } from '@theia/filesystem/lib/browser-only/opfs-filesystem-initialization';
import { OPFSFileSystemProvider } from '@theia/filesystem/lib/browser-only/opfs-filesystem-provider';
@injectable()
export class ExampleOPFSInitialization extends DefaultOPFSInitialization {
@inject(EncodingService)
protected encodingService: EncodingService;
override getRootDirectory(): string {
return '/theia/';
}
override async initializeFS(provider: OPFSFileSystemProvider): Promise<void> {
// Check whether the directory exists (relative to the root directory)
if (await provider.exists(new URI('/workspace'))) {
await provider.readdir(new URI('/workspace'));
} else {
await provider.mkdir(new URI('/workspace'));
await provider.writeFile(new URI('/workspace/my-file.txt'), this.encodingService.encode('foo').buffer, { create: true, overwrite: false });
}
if (await provider.exists(new URI('/workspace2'))) {
await provider.readdir(new URI('/workspace2'));
} else {
await provider.mkdir(new URI('/workspace2'));
await provider.writeFile(new URI('/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer, { create: true, overwrite: false });
}
// You can also create an index of the files and directories in the file system
// await provider.clear();
// await provider.createIndex([
// [new URI('/workspace/my-file.txt'), this.encodingService.encode('bar').buffer],
// [new URI('/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer]
// ]);
}
}
export const bindOPFSInitialization = (bind: interfaces.Bind, rebind: interfaces.Rebind): void => {
bind(ExampleOPFSInitialization).toSelf();
rebind(OPFSInitialization).toService(ExampleOPFSInitialization);
};

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2025 Lonti.com Pty Ltd.
//
// 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 { CodeCompletionVariableContext } from '@theia/ai-code-completion/lib/browser/code-completion-variable-context';
import { AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIVariable } from '@theia/ai-core';
import { FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
import { MaybePromise } from '@theia/core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
const SAMPLE_VARIABLE: AIVariable = {
id: 'sampleCodeCompletionVariable',
name: 'sampleCodeCompletionVariable',
description: 'A sample variable for code completion.',
};
/**
* This variable is used to demonstrate how to create a custom variable for code completion.
* It is registered as a variable that can be resolved in the context of code completion.
*/
@injectable()
export class SampleCodeCompletionVariableContribution implements FrontendVariableContribution, AIVariableResolver {
registerVariables(service: FrontendVariableService): void {
service.registerResolver(SAMPLE_VARIABLE, this);
}
canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
return CodeCompletionVariableContext.is(context) && request.variable.id === SAMPLE_VARIABLE.id ? 1 : 0;
}
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
if (request.variable.id === SAMPLE_VARIABLE.id && CodeCompletionVariableContext.is(context) && context.model.uri.path.endsWith('.sample.js')) {
return Promise.resolve({
variable: SAMPLE_VARIABLE,
value: 'This is a special sample file, every line must end with a "// sample" comment.'
});
}
}
}
export const bindSampleCodeCompletionVariableContribution = (bind: interfaces.Bind) => {
bind(AIVariableContribution).to(SampleCodeCompletionVariableContribution).inSingletonScope();
};

View File

@@ -0,0 +1,78 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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, interfaces } from '@theia/core/shared/inversify';
import { bindDynamicLabelProvider } from './label/sample-dynamic-label-provider-command-contribution';
import { bindSampleFilteredCommandContribution } from './contribution-filter/sample-filtered-command-contribution';
import { bindSampleUnclosableView } from './view/sample-unclosable-view-contribution';
import { bindSampleOutputChannelWithSeverity } from './output/sample-output-channel-with-severity';
import { bindSampleMenu } from './menu/sample-menu-contribution';
import { bindSampleFileWatching } from './file-watching/sample-file-watching-contribution';
import { bindVSXCommand } from './vsx/sample-vsx-command-contribution';
import { bindSampleToolbarContribution } from './toolbar/sample-toolbar-contribution';
import '../../src/browser/style/branding.css';
import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monaco-editor-preference-extractor';
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { bindSampleAppInfo } from './vsx/sample-frontend-app-info';
import { bindTestSample } from './test/sample-test-contribution';
import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities';
import { bindChatNodeToolbarActionContribution } from './chat/chat-node-toolbar-action-contribution';
import { bindAskAndContinueChatAgentContribution } from './chat/ask-and-continue-chat-agent-contribution';
import { bindChangeSetChatAgentContribution } from './chat/change-set-chat-agent-contribution';
import { bindModeChatAgentContribution } from './chat/mode-chat-agent-contribution';
import { bindOriginalStateTestAgentContribution } from './chat/original-state-test-agent-contribution';
import { bindCustomResponseContentRendererContribution } from './chat/custom-response-content-agent-contribution';
import { bindSampleChatCommandContribution } from './chat/sample-chat-command-contribution';
import { bindSampleCodeCompletionVariableContribution } from './ai-code-completion/sample-code-completion-variable-contribution';
import { bindSamplePreferenceContribution } from './preferences/sample-preferences-contribution';
import { MCPFrontendContribution } from '@theia/ai-mcp-server/lib/browser/mcp-frontend-contribution';
import { SampleFrontendMCPContribution } from './mcp/sample-frontend-mcp-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ResolveMcpFrontendContribution } from './mcp/resolve-frontend-mcp-contribution';
export default new ContainerModule((
bind: interfaces.Bind,
unbind: interfaces.Unbind,
isBound: interfaces.IsBound,
rebind: interfaces.Rebind,
) => {
bindAskAndContinueChatAgentContribution(bind);
bindChangeSetChatAgentContribution(bind);
bindModeChatAgentContribution(bind);
bindOriginalStateTestAgentContribution(bind);
bindCustomResponseContentRendererContribution(bind);
bindChatNodeToolbarActionContribution(bind);
bindSampleChatCommandContribution(bind);
bindDynamicLabelProvider(bind);
bindSampleUnclosableView(bind);
bindSampleOutputChannelWithSeverity(bind);
bindSampleMenu(bind);
bindSampleFileWatching(bind);
bindVSXCommand(bind);
bindSampleFilteredCommandContribution(bind);
bindSampleToolbarContribution(bind, rebind);
bindMonacoPreferenceExtractor(bind);
bindSampleAppInfo(bind);
bindTestSample(bind);
bindSampleFileSystemCapabilitiesCommands(bind);
rebindOVSXClientFactory(rebind);
bindSampleCodeCompletionVariableContribution(bind);
bindSamplePreferenceContribution(bind);
bind(MCPFrontendContribution).to(SampleFrontendMCPContribution).inSingletonScope();
bind(ResolveMcpFrontendContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ResolveMcpFrontendContribution);
});

View File

@@ -0,0 +1,23 @@
// *****************************************************************************
// Copyright (C) 2025 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 { ContainerModule } from '@theia/core/shared/inversify';
import { TextReplacementContribution } from '@theia/core/lib/browser/preload/text-replacement-contribution';
import { TextSampleReplacementContribution } from './preload/text-replacement-sample';
export default new ContainerModule(bind => {
bind(TextReplacementContribution).to(TextSampleReplacementContribution).inSingletonScope();
});

View File

@@ -0,0 +1,182 @@
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
ChatModel,
MutableChatRequestModel,
lastProgressMessage,
QuestionResponseContentImpl,
unansweredQuestions,
ProgressChatResponseContentImpl
} from '@theia/ai-chat';
import { Agent, LanguageModelMessage, BasePromptFragment } from '@theia/ai-core';
import { injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
export function bindAskAndContinueChatAgentContribution(bind: interfaces.Bind): void {
bind(AskAndContinueChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(AskAndContinueChatAgent);
bind(ChatAgent).toService(AskAndContinueChatAgent);
}
const systemPrompt: BasePromptFragment = {
id: 'askAndContinue-system',
template: `
You are an agent demonstrating how to generate questions and continue the conversation based on the user's answers.
First answer the user's question or continue their story.
Then come up with an interesting question and 2-3 answers which will be presented to the user as multiple choice.
Use the following format exactly to define the questions and answers.
Especially add the <question> and </question> tags around the JSON.
<question>
{
"question": "YOUR QUESTION HERE",
"options": [
{
"text": "OPTION 1"
},
{
"text": "OPTION 2"
}
]
}
</question>
Examples:
<question>
{
"question": "What is the capital of France?",
"options": [
{
"text": "Paris"
},
{
"text": "Lyon"
}
]
}
</question>
<question>
{
"question": "What does the fox say?",
"options": [
{
"text": "Ring-ding-ding-ding-dingeringeding!"
},
{
"text": "Wa-pa-pa-pa-pa-pa-pow!"
}
]
}
</question>
The user will answer the question and you can continue the conversation.
Once they answered, the question will be replaced with a simple "Question/Answer" pair, for example
Question: What does the fox say?
Answer: Ring-ding-ding-ding-dingeringeding!
If the user did not answer the question, it will be marked with "No answer", for example
Question: What is the capital of France?
No answer
Do not generate such pairs yourself, instead treat them as a signal for a past question.
Do not ask further questions once the text contains 5 or more "Question/Answer" pairs.
`
};
/**
* This is a very simple example agent that asks questions and continues the conversation based on the user's answers.
*/
@injectable()
export class AskAndContinueChatAgent extends AbstractStreamParsingChatAgent {
id = 'AskAndContinueSample';
name = 'AskAndContinueSample';
override description = 'This chat will ask questions related to the input and continues after that.';
protected defaultLanguageModelPurpose = 'chat';
override languageModelRequirements = [
{
purpose: 'chat',
identifier: 'default/universal',
}
];
override prompts = [{ id: systemPrompt.id, defaultVariant: systemPrompt }];
protected override systemPromptId: string | undefined = systemPrompt.id;
@postConstruct()
addContentMatchers(): void {
this.contentMatchers.push({
start: /^<question>.*$/m,
end: /^<\/question>$/m,
contentFactory: (content: string, request: MutableChatRequestModel) => {
const question = content.replace(/^<question>\n|<\/question>$/g, '');
const parsedQuestion = JSON.parse(question);
return new QuestionResponseContentImpl(parsedQuestion.question, parsedQuestion.options, request, selectedOption => {
this.handleAnswer(selectedOption, request);
});
},
incompleteContentFactory: (content: string, request: MutableChatRequestModel) =>
// Display a progress indicator while the question is being parsed
new ProgressChatResponseContentImpl('Preparing question...')
});
}
protected override async onResponseComplete(request: MutableChatRequestModel): Promise<void> {
const unansweredQs = unansweredQuestions(request);
if (unansweredQs.length < 1) {
return super.onResponseComplete(request);
}
request.response.addProgressMessage({ content: 'Waiting for input...', show: 'whileIncomplete' });
request.response.waitForInput();
}
protected handleAnswer(selectedOption: { text: string; value?: string; }, request: MutableChatRequestModel): void {
const progressMessage = lastProgressMessage(request);
if (progressMessage) {
request.response.updateProgressMessage({ ...progressMessage, show: 'untilFirstContent', status: 'completed' });
}
request.response.stopWaitingForInput();
// We're reusing the original request here as a shortcut. In combination with the override of 'getMessages' we continue generating.
// In a real-world scenario, you would likely manually interact with an LLM here to generate and append the next response.
this.invoke(request);
}
/**
* As the question/answer are handled within the same response, we add an additional user message at the end to indicate to
* the LLM to continue generating.
*/
protected override async getMessages(model: ChatModel): Promise<LanguageModelMessage[]> {
const messages = await super.getMessages(model, true);
const requests = model.getRequests();
if (!requests[requests.length - 1].response.isComplete && requests[requests.length - 1].response.response?.content.length > 0) {
return [...messages,
{
type: 'text',
actor: 'user',
text: 'Continue generating based on the user\'s answer or finish the conversation if 5 or more questions were already answered.'
}];
}
return messages;
}
}

View File

@@ -0,0 +1,156 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription,
ChangeSetElement
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { URI } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
export function bindChangeSetChatAgentContribution(bind: interfaces.Bind): void {
bind(ChangeSetChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(ChangeSetChatAgent);
bind(ChatAgent).toService(ChangeSetChatAgent);
}
/**
* This is a test agent demonstrating how to create change sets in AI chats.
*/
@injectable()
export class ChangeSetChatAgent extends AbstractStreamParsingChatAgent {
readonly id = 'ChangeSetSample';
readonly name = 'ChangeSetSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'This chat will create and modify a change set.';
override languageModelRequirements: LanguageModelRequirement[] = [];
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(ChangeSetFileElementFactory)
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
override async invoke(request: MutableChatRequestModel): Promise<void> {
const roots = this.workspaceService.tryGetRoots();
if (roots.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'No workspace is open. For using this chat agent, please open a workspace with at least two files in the root.'
));
request.response.complete();
return;
}
const root = roots[0];
const files = root.children?.filter(child => child.isFile);
if (!files || files.length < 3) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'The workspace does not contain any files. For using this chat agent, please add at least two files in the root.'
));
request.response.complete();
return;
}
const fileToAdd = root.resource.resolve('hello/new-file.txt');
const fileToChange = files[Math.floor(Math.random() * files.length)];
const fileToDelete = files.filter(file => file.name !== fileToChange.name)[Math.floor(Math.random() * files.length)];
const changes: ChangeSetElement[] = [];
const chatSessionId = request.session.id;
const requestId = request.id;
changes.push(
this.fileChangeFactory({
uri: fileToAdd,
type: 'add',
state: 'pending',
targetState: 'Hello World!',
requestId,
chatSessionId
})
);
if (fileToChange && fileToChange.resource) {
changes.push(
this.fileChangeFactory({
uri: fileToChange.resource,
type: 'modify',
state: 'pending',
targetState: await this.computeTargetState(fileToChange.resource),
requestId,
chatSessionId
})
);
}
if (fileToDelete && fileToDelete.resource) {
changes.push(
this.fileChangeFactory({
uri: fileToDelete.resource,
type: 'delete',
state: 'pending',
requestId,
chatSessionId
})
);
}
request.session.changeSet.setTitle('My Test Change Set');
request.session.changeSet.setElements(...changes);
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'I have created a change set for you. You can now review and apply it.'
));
request.response.complete();
}
async computeTargetState(resource: URI): Promise<string> {
const content = await this.fileService.read(resource);
if (content.value.length < 20) {
return 'HelloWorldModify';
}
let readLocation = Math.random() * 0.1 * content.value.length;
let oldLocation = 0;
let output = '';
while (readLocation < content.value.length) {
output += content.value.substring(oldLocation, readLocation);
oldLocation = readLocation;
const type = Math.random();
if (type < 0.33) {
// insert
output += `this is an insert at ${readLocation}`;
} else {
// delete
oldLocation += 20;
}
readLocation += Math.random() * 0.1 * content.value.length;
}
return output;
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics 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 {
ChatNodeToolbarActionContribution
} from '@theia/ai-chat-ui/lib/browser/chat-node-toolbar-action-contribution';
import {
isResponseNode,
RequestNode,
ResponseNode
} from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import { interfaces } from '@theia/core/shared/inversify';
export function bindChatNodeToolbarActionContribution(bind: interfaces.Bind): void {
bind(ChatNodeToolbarActionContribution).toDynamicValue(context => ({
getToolbarActions: (args: RequestNode | ResponseNode) => {
if (isResponseNode(args)) {
return [{
commandId: 'sample-command',
icon: 'codicon codicon-feedback',
tooltip: 'API Samples: Example command'
}];
} else {
return [];
}
}
}));
}

View File

@@ -0,0 +1,281 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
ChatResponseContent,
MutableChatRequestModel,
SerializableChatResponseContentData,
} from '@theia/ai-chat';
import {
ChatContentDeserializerContribution,
ChatContentDeserializerRegistry
} from '@theia/ai-chat/lib/common/chat-content-deserializer';
import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer';
import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import { Agent } from '@theia/ai-core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { ReactNode } from '@theia/core/shared/react';
export function bindCustomResponseContentRendererContribution(bind: interfaces.Bind): void {
bind(CustomResponseContentRendererAgent).toSelf().inSingletonScope();
bind(Agent).toService(CustomResponseContentRendererAgent);
bind(ChatAgent).toService(CustomResponseContentRendererAgent);
bind(ChatContentDeserializerContribution).to(CustomContentDeserializerContribution).inSingletonScope();
bind(ChatResponsePartRenderer).to(CustomSerializableContentRenderer).inSingletonScope();
bind(ChatResponsePartRenderer).to(CustomNonSerializableContentRenderer).inSingletonScope();
}
// =============================================================================
// SERIALIZABLE CUSTOM CONTENT
// =============================================================================
/**
* Data interface for serializable custom content.
* This is shared between the implementation and the deserializer.
*/
export interface CustomSerializableContentData {
title: string;
items: string[];
timestamp: number;
}
/**
* Serializable custom content type.
* This can be persisted and restored from storage.
*/
export interface CustomSerializableContent extends ChatResponseContent {
kind: 'customSerializable';
title: string;
items: string[];
timestamp: number;
}
export class CustomSerializableContentImpl implements CustomSerializableContent {
readonly kind = 'customSerializable';
constructor(
public title: string,
public items: string[],
public timestamp: number
) { }
asString(): string {
return `${this.title}: ${this.items.join(', ')}`;
}
toSerializable(): SerializableChatResponseContentData<CustomSerializableContentData> {
return {
kind: 'customSerializable',
fallbackMessage: `Custom content: ${this.title} (${this.items.length} items)`,
data: {
title: this.title,
items: this.items,
timestamp: this.timestamp
}
};
}
}
/**
* Deserializer for custom serializable content.
*/
@injectable()
export class CustomContentDeserializerContribution implements ChatContentDeserializerContribution {
registerDeserializers(registry: ChatContentDeserializerRegistry): void {
registry.register({
kind: 'customSerializable',
deserialize: (data: CustomSerializableContentData) =>
new CustomSerializableContentImpl(
data.title,
data.items,
data.timestamp
)
});
}
}
/**
* Renderer for custom serializable content.
*/
@injectable()
export class CustomSerializableContentRenderer implements ChatResponsePartRenderer<CustomSerializableContent> {
canHandle(response: ChatResponseContent): number {
return response.kind === 'customSerializable' ? 10 : -1;
}
render(content: CustomSerializableContent, node: ResponseNode): ReactNode {
const date = new Date(content.timestamp).toLocaleString();
return React.createElement('div', {
className: 'theia-ChatResponseContent',
style: {
padding: '10px',
margin: '5px 0',
border: '2px solid var(--theia-editorWidget-border)',
borderRadius: '4px',
backgroundColor: 'var(--theia-editor-background)'
}
},
React.createElement('div', {
style: {
fontWeight: 'bold',
marginBottom: '8px',
color: 'var(--theia-descriptionForeground)'
}
}, `📦 ${content.title}`),
React.createElement('ul', {
style: {
margin: '0',
paddingLeft: '20px'
}
}, ...content.items.map((item, idx) =>
React.createElement('li', { key: idx }, item)
)),
React.createElement('div', {
style: {
marginTop: '8px',
fontSize: '0.85em',
color: 'var(--theia-descriptionForeground)',
fontStyle: 'italic'
}
}, `Created: ${date} • ✅ Serializable (will be persisted)`)
);
}
}
// =============================================================================
// NON-SERIALIZABLE CUSTOM CONTENT
// =============================================================================
/**
* Non-serializable custom content type.
*/
export interface CustomNonSerializableContent extends ChatResponseContent {
kind: 'customNonSerializable';
message: string;
onClick: () => void; // Functions cannot be serialized!
}
export class CustomNonSerializableContentImpl implements CustomNonSerializableContent {
readonly kind = 'customNonSerializable';
constructor(
public message: string,
public onClick: () => void
) { }
asString(): string {
return `Interactive: ${this.message}`;
}
}
/**
* Renderer for custom non-serializable content.
* This will only be used for active (non-restored) content.
*/
@injectable()
export class CustomNonSerializableContentRenderer implements ChatResponsePartRenderer<CustomNonSerializableContent> {
canHandle(response: ChatResponseContent): number {
return response.kind === 'customNonSerializable' ? 10 : -1;
}
render(content: CustomNonSerializableContent, node: ResponseNode): ReactNode {
return React.createElement('div', {
className: 'theia-ChatResponseContent',
style: {
padding: '10px',
margin: '5px 0',
border: '2px solid var(--theia-notificationsWarningIcon-foreground)',
borderRadius: '4px',
backgroundColor: 'var(--theia-editor-background)'
}
},
React.createElement('div', {
style: {
fontWeight: 'bold',
marginBottom: '8px',
color: 'var(--theia-descriptionForeground)'
}
}, '⚡ Interactive Content'),
React.createElement('div', {
style: { marginBottom: '10px' }
}, content.message),
React.createElement('button', {
className: 'theia-button',
onClick: content.onClick,
style: { marginRight: '8px' }
}, 'Click Me!'),
React.createElement('div', {
style: {
marginTop: '8px',
fontSize: '0.85em',
color: 'var(--theia-notificationsWarningIcon-foreground)',
fontStyle: 'italic'
}
}, '⚠️ No deserializer registered (will use fallback for serialization and deserialization)')
);
}
}
// =============================================================================
// DEMO AGENT
// =============================================================================
@injectable()
export class CustomResponseContentRendererAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
id = 'CustomContentSample';
name = this.id;
override description = 'Demonstrates custom serializable and non-serializable chat response content';
languageModelRequirements = [];
protected defaultLanguageModelPurpose = 'chat';
public override async invoke(request: MutableChatRequestModel): Promise<void> {
const response = request.response.response;
// Add serializable custom content
response.addContent(
new CustomSerializableContentImpl(
'Serializable Custom Content',
[
'This content has a custom data interface',
'It has a deserializer registered',
'It will be properly restored when loading a saved session',
'The renderer shows this custom UI with all data intact'
],
Date.now()
)
);
// Add interactive button as a demonstration of non-serializable content
let clickCount = 0;
response.addContent(
new CustomNonSerializableContentImpl(
'This is the LLM message received',
() => {
clickCount++;
alert(`Button clicked ${clickCount} time(s)`);
}
)
);
// Trigger completion immediately - no streaming needed
request.response.complete();
}
}

View File

@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
export function bindModeChatAgentContribution(bind: interfaces.Bind): void {
bind(ModeChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(ModeChatAgent);
bind(ChatAgent).toService(ModeChatAgent);
}
/**
* This is a test agent demonstrating how to use chat modes.
* It responds differently based on the selected mode.
*/
@injectable()
export class ModeChatAgent extends AbstractStreamParsingChatAgent {
readonly id = 'ModeTestSample';
readonly name = 'ModeTestSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'A test agent that demonstrates different response modes (concise vs detailed).';
override languageModelRequirements: LanguageModelRequirement[] = [];
// Define the modes this agent supports
modes = [
{ id: 'concise', name: 'Concise' },
{ id: 'detailed', name: 'Detailed' }
];
override async invoke(request: MutableChatRequestModel): Promise<void> {
const modeId = request.request.modeId || 'concise';
const question = request.request.text;
let response: string;
if (modeId === 'concise') {
response = `**Concise Mode**: You asked: "${question}"\n\nThis is a brief response.`;
} else {
response = `**Detailed Mode**: You asked: "${question}"\n\n` +
'This is a more detailed response that provides additional context and explanation. ' +
'In detailed mode, the agent provides more comprehensive information, examples, and background. ' +
'This mode is useful when you need in-depth understanding of a topic.';
}
request.response.response.addContent(new MarkdownChatResponseContentImpl(response));
request.response.complete();
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,159 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// 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 {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { wait } from '@theia/core/lib/common/promise-util';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
export function bindOriginalStateTestAgentContribution(bind: interfaces.Bind): void {
bind(OriginalStateTestAgent).toSelf().inSingletonScope();
bind(Agent).toService(OriginalStateTestAgent);
bind(ChatAgent).toService(OriginalStateTestAgent);
}
/**
* This is a test agent demonstrating how to test originalState functionality in change sets.
* It creates change set elements with original content provided and tests sequential updates.
*/
@injectable()
export class OriginalStateTestAgent extends AbstractStreamParsingChatAgent {
readonly id = 'OriginalStateTestSample';
readonly name = 'OriginalStateTestSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'This chat will test originalState functionality with sequential changes.';
override languageModelRequirements: LanguageModelRequirement[] = [];
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(ChangeSetFileElementFactory)
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
override async invoke(request: MutableChatRequestModel): Promise<void> {
const roots = this.workspaceService.tryGetRoots();
if (roots.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'No workspace is open. For using this test agent, please open a workspace with at least one file.'
));
request.response.complete();
return;
}
const root = roots[0];
const files = root.children?.filter(child => child.isFile);
if (!files || files.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'The workspace does not contain any files. For using this test agent, please add at least one file in the root.'
));
request.response.complete();
return;
}
const chatSessionId = request.session.id;
const requestId = request.id;
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'Testing originalState functionality...\n\n' +
'Three sequential changes to an existing file with 1000ms delays between each.'
));
await wait(1000);
request.session.changeSet.setTitle('Original State Test Changes');
// Select an existing file for sequential modifications
const existingFile = files[Math.floor(Math.random() * files.length)];
const existingFileUri = existingFile.resource;
// Read the current content to use as originalState
const currentContent = await this.fileService.read(existingFileUri);
const originalState = currentContent.value.toString();
// First modification with originalState provided
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 1'));
const modifiedContent1 = await this.computeModifiedState(originalState, 1);
await this.fileService.write(existingFileUri, modifiedContent1);
const firstModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent1,
requestId,
chatSessionId
});
request.session.changeSet.addElements(firstModification);
await wait(1000);
// Second modification with originalState from previous change
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 2'));
const modifiedContent2 = await this.computeModifiedState(modifiedContent1, 2);
await this.fileService.write(existingFileUri, modifiedContent2);
const secondModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent2,
requestId,
chatSessionId
});
request.session.changeSet.addElements(secondModification);
await wait(1000);
// Third modification with originalState from previous change
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 3'));
const modifiedContent3 = await this.computeModifiedState(modifiedContent2, 3);
await this.fileService.write(existingFileUri, modifiedContent3);
const thirdModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent3,
requestId,
chatSessionId
});
request.session.changeSet.addElements(thirdModification);
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nTest completed!'));
request.response.complete();
}
async computeModifiedState(content: string, changeNumber: number): Promise<string> {
const changeComment = `// Modified by Original State Test Agent - Change ${changeNumber}\n`;
return changeComment + content + `\n// This line was added by change ${changeNumber} at ${new Date().toISOString()}`;
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,136 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource 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 { PromptService } from '@theia/ai-core/lib/common/prompt-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
export function bindSampleChatCommandContribution(bind: interfaces.Bind): void {
bind(SampleChatCommandContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SampleChatCommandContribution);
}
/**
* This contribution demonstrates how to register slash commands as prompt fragments for chat agents.
* Commands can use argument substitution ($ARGUMENTS, $1, $2, etc.) and be filtered by agent.
*
* The commands registered here will be available in the chat input autocomplete when typing '/'.
* For example, the '/explain' command is only available when using the 'Universal' agent.
*/
@injectable()
export class SampleChatCommandContribution implements FrontendApplicationContribution {
@inject(PromptService)
protected readonly promptService: PromptService;
onStart(): void {
this.registerCommands();
}
protected registerCommands(): void {
// Example 1: Simple command available for all agents
this.promptService.addBuiltInPromptFragment({
id: 'sample-hello',
template: 'Say hello to $ARGUMENTS in a friendly way.',
isCommand: true,
commandName: 'hello',
commandDescription: 'Say hello to someone',
commandArgumentHint: '<name>'
});
// Example 2: Command with $ARGUMENTS and specific agent
this.promptService.addBuiltInPromptFragment({
id: 'sample-explain',
template: `Provide a clear and detailed explanation of the following topic: $ARGUMENTS
Consider:
- Core concepts and definitions
- Practical examples
- Common use cases
- Best practices`,
isCommand: true,
commandName: 'explain',
commandDescription: 'Explain a concept in detail',
commandArgumentHint: '<topic>',
commandAgents: ['Universal']
});
// Example 3: Command with numbered arguments ($1, $2)
this.promptService.addBuiltInPromptFragment({
id: 'sample-compare',
template: `Compare and contrast the following two items:
Item 1: $1
Item 2: $2
Please analyze:
- Key similarities
- Important differences
- When to use each
- Specific advantages and disadvantages`,
isCommand: true,
commandName: 'compare',
commandDescription: 'Compare two concepts or items',
commandArgumentHint: '<item1> <item2>',
commandAgents: ['Universal']
});
// Example 4: Command combining $ARGUMENTS with variables
this.promptService.addBuiltInPromptFragment({
id: 'sample-analyze',
template: `Analyze the selected code with focus on: $ARGUMENTS
Selected code:
#selection
Consider the overall file context:
#file`,
isCommand: true,
commandName: 'analyze',
commandDescription: 'Analyze code with specific focus',
commandArgumentHint: '<focus-area>',
commandAgents: ['Universal']
});
// Example 5: Command with optional arguments (shown by [] in hint)
this.promptService.addBuiltInPromptFragment({
id: 'sample-summarize',
template: `Create a concise summary of the following content$1.
Content: $ARGUMENTS`,
isCommand: true,
commandName: 'summarize',
commandDescription: 'Summarize content',
commandArgumentHint: '<content> [style]'
});
// Example 6: Multi-agent command (available for multiple specific agents)
this.promptService.addBuiltInPromptFragment({
id: 'sample-debug',
template: `Help debug the following issue: $ARGUMENTS
Focus on:
- Identifying the root cause
- Providing specific solutions
- Suggesting preventive measures`,
isCommand: true,
commandName: 'debug',
commandDescription: 'Get help debugging an issue',
commandArgumentHint: '<problem-description>',
commandAgents: ['Universal', 'AskAndContinue']
});
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2021 STMicroelectronics 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 { Command, CommandContribution, CommandRegistry, ContributionFilterRegistry, FilterContribution, bindContribution } from '@theia/core/lib/common';
import { injectable, interfaces } from '@theia/core/shared/inversify';
export namespace SampleFilteredCommand {
const API_SAMPLES_CATEGORY = 'API Samples';
export const FILTERED: Command = {
id: 'example_command.filtered',
category: API_SAMPLES_CATEGORY,
label: 'This command should be filtered out'
};
export const FILTERED2: Command = {
id: 'example_command.filtered2',
category: API_SAMPLES_CATEGORY,
label: 'This command should be filtered out (2)'
};
}
/**
* This sample command is used to test the runtime filtering of already bound contributions.
*/
@injectable()
export class SampleFilteredCommandContribution implements CommandContribution {
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SampleFilteredCommand.FILTERED, { execute: () => { } });
}
}
@injectable()
export class SampleFilterAndCommandContribution implements FilterContribution, CommandContribution {
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SampleFilteredCommand.FILTERED2, { execute: () => { } });
}
registerContributionFilters(registry: ContributionFilterRegistry): void {
registry.addFilters([CommandContribution], [
// filter ourselves out
contrib => contrib.constructor !== this.constructor
]);
registry.addFilters('*', [
// filter a contribution based on its class type
contrib => !(contrib instanceof SampleFilteredCommandContribution)
]);
}
}
export function bindSampleFilteredCommandContribution(bind: interfaces.Bind): void {
bind(CommandContribution).to(SampleFilteredCommandContribution).inSingletonScope();
bind(SampleFilterAndCommandContribution).toSelf().inSingletonScope();
bindContribution(bind, SampleFilterAndCommandContribution, [CommandContribution, FilterContribution]);
}

View File

@@ -0,0 +1,71 @@
/********************************************************************************
* 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 { CommandContribution, CommandRegistry } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
@injectable()
export class SampleFileSystemCapabilities implements CommandContribution {
@inject(RemoteFileSystemProvider)
protected readonly remoteFileSystemProvider: RemoteFileSystemProvider;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({
id: 'toggleFileSystemReadonly',
label: 'Toggle File System Readonly',
category: 'API Samples'
}, {
execute: () => {
const readonly = (this.remoteFileSystemProvider.capabilities & FileSystemProviderCapabilities.Readonly) !== 0;
if (readonly) {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities & ~FileSystemProviderCapabilities.Readonly);
} else {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities | FileSystemProviderCapabilities.Readonly);
}
}
});
commands.registerCommand({
id: 'addFileSystemReadonlyMessage',
label: 'Add File System ReadonlyMessage',
category: 'API Samples'
}, {
execute: () => {
const readonlyMessage = new MarkdownStringImpl(`Added new **Markdown** string '+${Date.now()}`);
this.remoteFileSystemProvider['setReadOnlyMessage'](readonlyMessage);
}
});
commands.registerCommand({
id: 'removeFileSystemReadonlyMessage',
label: 'Remove File System ReadonlyMessage',
category: 'API Samples'
}, {
execute: () => {
this.remoteFileSystemProvider['setReadOnlyMessage'](undefined);
}
});
}
}
export function bindSampleFileSystemCapabilitiesCommands(bind: interfaces.Bind): void {
bind(CommandContribution).to(SampleFileSystemCapabilities).inSingletonScope();
}

View File

@@ -0,0 +1,87 @@
// *****************************************************************************
// Copyright (C) 2020 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 { postConstruct, injectable, inject, interfaces, named } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution, LabelProvider,
} from '@theia/core/lib/browser';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { createPreferenceProxy, PreferenceService, PreferenceProxy, PreferenceContribution } from '@theia/core';
import { FileWatchingPreferencesSchema } from '../../common/preference-schema';
export function bindSampleFileWatching(bind: interfaces.Bind): void {
bind(FrontendApplicationContribution).to(SampleFileWatchingContribution).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: FileWatchingPreferencesSchema });
bind(FileWatchingPreferences).toDynamicValue(
ctx => createPreferenceProxy(ctx.container.get(PreferenceService), FileWatchingPreferencesSchema)
);
}
const FileWatchingPreferences = Symbol('FileWatchingPreferences');
type FileWatchingPreferences = PreferenceProxy<FileWatchingPreferencesSchema>;
interface FileWatchingPreferencesSchema {
'sample.file-watching.verbose': boolean
}
@injectable()
class SampleFileWatchingContribution implements FrontendApplicationContribution {
protected verbose: boolean;
@inject(FileService)
protected readonly fileService: FileService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileWatchingPreferences)
protected readonly fileWatchingPreferences: FileWatchingPreferences;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
@postConstruct()
protected init(): void {
this.verbose = this.fileWatchingPreferences['sample.file-watching.verbose'];
this.fileWatchingPreferences.onPreferenceChanged(e => {
if (e.preferenceName === 'sample.file-watching.verbose') {
this.verbose = this.fileWatchingPreferences['sample.file-watching.verbose'];
}
});
}
onStart(): void {
this.fileService.onDidFilesChange(event => {
// Only log if the verbose preference is set.
if (this.verbose) {
// Get the workspace roots for the current frontend:
const roots = this.workspaceService.tryGetRoots();
// Create some name to help find out which frontend logged the message:
const workspace = roots.length > 0
? roots.map(root => this.labelProvider.getLongName(root.resource)).join('+')
: '<no workspace>';
this.logger.info(`Sample File Watching: ${event.changes.length} file(s) changed! ${workspace}`);
}
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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, interfaces } from '@theia/core/shared/inversify';
import { Command, CommandContribution, CommandRegistry, CommandHandler } from '@theia/core';
import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
import { SampleDynamicLabelProviderContribution } from './sample-dynamic-label-provider-contribution';
export namespace ExampleLabelProviderCommands {
const API_SAMPLES_CATEGORY = 'API Samples';
export const TOGGLE_SAMPLE: Command = {
id: 'example_label_provider.toggle',
category: API_SAMPLES_CATEGORY,
label: 'Toggle Dynamically-Changing Labels'
};
}
@injectable()
export class SampleDynamicLabelProviderCommandContribution implements FrontendApplicationContribution, CommandContribution {
@inject(SampleDynamicLabelProviderContribution)
protected readonly labelProviderContribution: SampleDynamicLabelProviderContribution;
initialize(): void { }
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(ExampleLabelProviderCommands.TOGGLE_SAMPLE, new ExampleLabelProviderCommandHandler(this.labelProviderContribution));
}
}
export class ExampleLabelProviderCommandHandler implements CommandHandler {
constructor(private readonly labelProviderContribution: SampleDynamicLabelProviderContribution) {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute(...args: any[]): any {
this.labelProviderContribution.toggle();
}
}
export const bindDynamicLabelProvider = (bind: interfaces.Bind) => {
bind(SampleDynamicLabelProviderContribution).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(SampleDynamicLabelProviderContribution);
bind(CommandContribution).to(SampleDynamicLabelProviderCommandContribution).inSingletonScope();
};

View File

@@ -0,0 +1,91 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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 { DefaultUriLabelProviderContribution, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider';
import URI from '@theia/core/lib/common/uri';
import { Emitter, Event } from '@theia/core';
@injectable()
export class SampleDynamicLabelProviderContribution extends DefaultUriLabelProviderContribution {
protected isActive: boolean = false;
constructor() {
super();
const outer = this;
setInterval(() => {
if (this.isActive) {
outer.x++;
outer.fireLabelsDidChange();
}
}, 1000);
}
override canHandle(element: object): number {
if (this.isActive && element.toString().includes('test')) {
return 30;
}
return 0;
}
toggle(): void {
this.isActive = !this.isActive;
this.fireLabelsDidChange();
}
private fireLabelsDidChange(): void {
this.onDidChangeEmitter.fire({
affects: (element: URI) => element.toString().includes('test')
});
}
protected override getUri(element: URI): URI {
return new URI(element.toString());
}
override getIcon(element: URI): string {
const uri = this.getUri(element);
const icon = super.getFileIcon(uri);
if (!icon) {
return this.defaultFileIcon;
}
return icon;
}
protected override readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
private x: number = 0;
override getName(element: URI): string | undefined {
const uri = this.getUri(element);
if (this.isActive && uri.toString().includes('test')) {
return super.getName(uri) + '-' + this.x.toString(10);
} else {
return super.getName(uri);
}
}
override getLongName(element: URI): string | undefined {
const uri = this.getUri(element);
return super.getLongName(uri);
}
override get onDidChange(): Event<DidChangeLabelEvent> {
return this.onDidChangeEmitter.event;
}
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2025 Dirk Fauth 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 { MCPFrontendService, RemoteMCPServerDescription } from '@theia/ai-mcp';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, QuickInputService } from '@theia/core/lib/browser';
import { ILogger } from '@theia/core/lib/common/logger';
@injectable()
export class ResolveMcpFrontendContribution
implements FrontendApplicationContribution {
@inject(MCPFrontendService)
protected readonly mcpFrontendService: MCPFrontendService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
async onStart(): Promise<void> {
const githubServer: RemoteMCPServerDescription = {
name: 'github',
serverUrl: 'https://api.githubcopilot.com/mcp/',
resolve: async serverDescription => {
this.logger.debug('Resolving GitHub MCP server description');
// Prompt user for authentication token
const authToken = await this.quickInputService.input({
prompt: 'Enter authentication token for GitHubMCP server',
password: true,
value: 'serverAuthToken' in serverDescription ? serverDescription.serverAuthToken || '' : ''
});
if (authToken) {
// Return updated server description with new token
return {
...serverDescription,
serverAuthToken: authToken
} as RemoteMCPServerDescription;
}
// If no token provided, return original description
return serverDescription;
}
};
this.mcpFrontendService.addOrUpdateServer(githubServer);
}
}

View File

@@ -0,0 +1,218 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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, named } from '@theia/core/shared/inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { Tool, Resource, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
import { z } from 'zod';
import { MCPFrontendContribution, ToolProvider } from '@theia/ai-mcp-server/lib/browser/mcp-frontend-contribution';
import { ILogger } from '@theia/core/lib/common/logger';
/**
* Sample frontend MCP contribution that demonstrates accessing frontend-only services
*/
@injectable()
export class SampleFrontendMCPContribution implements MCPFrontendContribution {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
async getTools(): Promise<Tool[]> {
return [
{
name: 'sample-workspace-info',
description: 'Get information about the current workspace',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'sample-workspace-files',
description: 'List files in the workspace',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Optional pattern to filter files'
}
},
required: []
}
}
];
}
async getTool(name: string): Promise<ToolProvider | undefined> {
switch (name) {
case 'sample-workspace-info':
return {
handler: async args => {
try {
this.logger.debug('Getting workspace info with args:', args);
const roots = await this.workspaceService.roots;
return {
workspace: {
roots: roots.map(r => r.resource.toString()),
name: roots[0]?.name || 'Unknown'
}
};
} catch (error) {
this.logger.error('Error getting workspace info:', error);
throw error;
}
},
inputSchema: z.object({})
};
case 'sample-workspace-files':
return {
handler: async args => {
try {
this.logger.debug('Listing workspace files with args:', args);
const typedArgs = args as { pattern?: string };
// Here we could use the FileService to collect all file information from the workspace
// const roots = await this.workspaceService.roots;
// const files: string[] = [];
// for (const root of roots) {
// const rootUri = new URI(root.resource.toString());
// const stat = await this.fileService.resolve(rootUri);
// if (stat.children) {
// for (const child of stat.children) {
// files.push(child.resource.toString());
// }
// }
// }
// Return dummy content for demonstration purposes
const dummyFiles = [
'foo1.txt',
'foo2.txt',
'bar1.js',
'bar2.js',
'baz1.md',
'baz2.md',
'config.json',
'package.json',
'README.md'
];
return {
files: typedArgs.pattern ? dummyFiles.filter(f => f.includes(typedArgs.pattern!)) : dummyFiles
};
} catch (error) {
this.logger.error('Error listing workspace files:', error);
throw error;
}
},
inputSchema: z.object({
pattern: z.string().optional()
})
};
default:
return undefined;
}
}
async getResources(): Promise<Resource[]> {
return [
{
uri: 'sample-workspace://info',
name: 'Sample Workspace Information',
description: 'General information about the current workspace',
mimeType: 'application/json'
}
];
}
async readResource(uri: string): Promise<unknown> {
if (uri === 'sample-workspace://info') {
try {
const roots = await this.workspaceService.roots;
return {
workspace: {
roots: roots.map(r => ({
uri: r.resource.toString(),
name: r.name,
scheme: r.resource.scheme
})),
rootCount: roots.length
}
};
} catch (error) {
this.logger.error('Error reading workspace resource:', error);
throw error;
}
}
throw new Error(`Unknown resource: ${uri}`);
}
async getPrompts(): Promise<Prompt[]> {
return [
{
name: 'sample-workspace-context',
description: 'Generate context information about the workspace',
arguments: [
{
name: 'includeFiles',
description: 'Whether to include file listings',
required: false
}
]
}
];
}
async getPrompt(name: string, args: unknown): Promise<PromptMessage[]> {
if (name === 'sample-workspace-context') {
try {
const parsedArgs = args as { includeFiles?: boolean };
const roots = await this.workspaceService.roots;
let content = 'Current workspace information:\n\n';
content += `Number of workspace roots: ${roots.length}\n`;
for (const root of roots) {
content += `- Root: ${root.name} (${root.resource.toString()})\n`;
}
if (parsedArgs.includeFiles) {
content += '\nFile structure would be included here in a real implementation.';
}
return [
{
role: 'user',
content: {
type: 'text',
text: content
}
}
];
} catch (error) {
this.logger.error('Error generating workspace context prompt:', error);
throw error;
}
}
throw new Error(`Unknown prompt: ${name}`);
}
}

View File

@@ -0,0 +1,335 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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 { ConfirmDialog, Dialog, QuickInputService } from '@theia/core/lib/browser';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { SelectComponent } from '@theia/core/lib/browser/widgets/select-component';
import {
Command, CommandContribution, CommandMenu, CommandRegistry, ContextExpressionMatcher, MAIN_MENU_BAR,
MenuContribution, MenuModelRegistry, MenuPath, MessageService
} from '@theia/core/lib/common';
import { ILogger } from '@theia/core/lib/common/logger';
import { inject, injectable, interfaces, named } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { ReactNode } from '@theia/core/shared/react';
const API_SAMPLES_CATEGORY = 'API Samples';
const SampleCommand: Command = {
id: 'sample-command',
label: 'Command',
category: API_SAMPLES_CATEGORY
};
const SampleCommand2: Command = {
id: 'sample-command2',
label: 'Command 2',
category: API_SAMPLES_CATEGORY
};
const SampleCommandConfirmDialog: Command = {
id: 'sample-command-confirm-dialog',
label: 'Confirm Dialog',
category: API_SAMPLES_CATEGORY
};
const SampleComplexCommandConfirmDialog: Command = {
id: 'sample-command-complex-confirm-dialog',
label: 'Complex Confirm Dialog',
category: API_SAMPLES_CATEGORY
};
const SampleCommandWithProgressMessage: Command = {
id: 'sample-command-with-progress',
label: 'Command With Progress Message',
category: API_SAMPLES_CATEGORY
};
const SampleCommandWithIndeterminateProgressMessage: Command = {
id: 'sample-command-with-indeterminate-progress',
label: 'Command With Indeterminate Progress Message',
category: API_SAMPLES_CATEGORY
};
const SampleQuickInputCommand: Command = {
id: 'sample-quick-input-command',
label: 'Test Positive Integer',
category: API_SAMPLES_CATEGORY
};
const SampleSelectDialog: Command = {
id: 'sample-command-select-dialog',
label: 'Select Component Dialog',
category: API_SAMPLES_CATEGORY
};
const SamplePersistentNotification: Command = {
id: 'sample-persistent-notification',
label: 'Persistent Notification (No Timeout)',
category: API_SAMPLES_CATEGORY
};
const SampleVanishingNotification: Command = {
id: 'sample-vanishing-notification',
label: 'Vanishing Notification (500ms Timeout)',
category: API_SAMPLES_CATEGORY
};
@injectable()
export class SampleCommandContribution implements CommandContribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'create-quick-pick-sample', label: 'Internal QuickPick', category: API_SAMPLES_CATEGORY }, {
execute: () => {
const pick = this.quickInputService.createQuickPick();
pick.items = [{ label: '1' }, { label: '2' }, { label: '3' }];
pick.onDidAccept(() => {
this.logger.debug(`accepted: ${pick.selectedItems[0]?.label}`);
pick.hide();
});
pick.show();
}
});
commands.registerCommand(SampleCommand, {
execute: () => {
alert('This is a sample command!');
}
});
commands.registerCommand(SampleCommand2, {
execute: () => {
alert('This is sample command2!');
}
});
commands.registerCommand(SampleCommandConfirmDialog, {
execute: async () => {
const choice = await new ConfirmDialog({
title: 'Sample Confirm Dialog',
msg: 'This is a sample with lots of text:' + Array(100)
.fill(undefined)
.map((element, index) => `\n\nExtra line #${index}`)
.join('')
}).open();
this.messageService.info(`Sample confirm dialog returned with: \`${JSON.stringify(choice)}\``);
}
});
commands.registerCommand(SampleComplexCommandConfirmDialog, {
execute: async () => {
const mainDiv = document.createElement('div');
for (const color of ['#FF00007F', '#00FF007F', '#0000FF7F']) {
const innerDiv = document.createElement('div');
innerDiv.textContent = 'This is a sample with lots of text:' + Array(50)
.fill(undefined)
.map((_, index) => `\n\nExtra line #${index}`)
.join('');
innerDiv.style.backgroundColor = color;
innerDiv.style.padding = '5px';
mainDiv.appendChild(innerDiv);
}
const choice = await new ConfirmDialog({
title: 'Sample Confirm Dialog',
msg: mainDiv
}).open();
this.messageService.info(`Sample confirm dialog returned with: \`${JSON.stringify(choice)}\``);
}
});
commands.registerCommand(SampleSelectDialog, {
execute: async () => {
await new class extends ReactDialog<boolean> {
constructor() {
super({ title: 'Sample Select Component Dialog' });
this.appendAcceptButton(Dialog.OK);
}
protected override render(): ReactNode {
return React.createElement(SelectComponent, {
options: Array.from(Array(10).keys()).map(i => ({ label: 'Option ' + ++i })),
defaultValue: 0
});
}
override get value(): boolean {
return true;
}
}().open();
}
});
commands.registerCommand(SampleQuickInputCommand, {
execute: async () => {
const result = await this.quickInputService.input({
placeHolder: 'Please provide a positive integer',
validateInput: async (input: string) => {
const numericValue = Number(input);
if (isNaN(numericValue)) {
return 'Invalid: NaN';
} else if (numericValue % 2 === 1) {
return 'Invalid: Odd Number';
} else if (numericValue < 0) {
return 'Invalid: Negative Number';
} else if (!Number.isInteger(numericValue)) {
return 'Invalid: Only Integers Allowed';
}
}
});
if (result) {
this.messageService.info(`Positive Integer: ${result}`);
}
}
});
commands.registerCommand(SampleCommandWithProgressMessage, {
execute: () => {
this.messageService
.showProgress({
text: 'Starting to report progress',
})
.then(progress => {
window.setTimeout(() => {
progress.report({
message: 'First step completed',
work: { done: 25, total: 100 }
});
}, 2000);
window.setTimeout(() => {
progress.report({
message: 'Next step completed',
work: { done: 60, total: 100 }
});
}, 4000);
window.setTimeout(() => {
progress.report({
message: 'Complete',
work: { done: 100, total: 100 }
});
}, 6000);
window.setTimeout(() => progress.cancel(), 7000);
});
}
});
commands.registerCommand(SampleCommandWithIndeterminateProgressMessage, {
execute: () => {
this.messageService
.showProgress({
text: 'Starting to report indeterminate progress',
})
.then(progress => {
window.setTimeout(() => {
progress.report({
message: 'First step completed',
});
}, 2000);
window.setTimeout(() => {
progress.report({
message: 'Next step completed',
});
}, 4000);
window.setTimeout(() => {
progress.report({
message: 'Complete',
});
}, 6000);
window.setTimeout(() => progress.cancel(), 7000);
});
}
});
commands.registerCommand(SamplePersistentNotification, {
execute: () => {
this.messageService.info(
'This notification will stay visible until you dismiss it manually.',
{ timeout: 0 }
);
}
});
commands.registerCommand(SampleVanishingNotification, {
execute: () => {
this.messageService.info(
'This notification will stay visible for 500ms.',
{ timeout: 500 }
);
}
});
}
}
@injectable()
export class SampleMenuContribution implements MenuContribution {
registerMenus(menus: MenuModelRegistry): void {
setTimeout(() => {
const subMenuPath = [...MAIN_MENU_BAR, 'sample-menu'];
menus.registerSubmenu(subMenuPath, 'Sample Menu', { sortString: '2' }); // that should put the menu right next to the File menu
menus.registerMenuAction(subMenuPath, {
commandId: SampleCommand.id,
order: '0'
});
menus.registerMenuAction(subMenuPath, {
commandId: SampleCommand2.id,
order: '2'
});
const subSubMenuPath = [...subMenuPath, 'sample-sub-menu'];
menus.registerSubmenu(subSubMenuPath, 'Sample sub menu', { sortString: '2' });
menus.registerMenuAction(subSubMenuPath, {
commandId: SampleCommand.id,
order: '1'
});
menus.registerMenuAction(subSubMenuPath, {
commandId: SampleCommand2.id,
order: '3'
});
const placeholder = new PlaceholderMenuNode([...subSubMenuPath, 'placeholder'].join('-'), 'Placeholder', '0');
menus.registerCommandMenu(subSubMenuPath, placeholder);
/**
* Register an action menu with an invalid command (un-registered and without a label) in order
* to determine that menus and the layout does not break on startup.
*/
menus.registerMenuAction(subMenuPath, { commandId: 'invalid-command' });
}, 10000);
}
}
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements CommandMenu {
constructor(readonly id: string, public readonly label: string, readonly order?: string, readonly icon?: string) { }
isEnabled(effectiveMenuPath: MenuPath, ...args: unknown[]): boolean {
return false;
}
isToggled(effectiveMenuPath: MenuPath): boolean {
return false;
}
run(effectiveMenuPath: MenuPath, ...args: unknown[]): Promise<void> {
throw new Error('Should never happen');
}
getAccelerator(context: HTMLElement | undefined): string[] {
return [];
}
get sortString(): string {
return this.order || this.label;
}
isVisible<T>(effectiveMenuPath: MenuPath, contextMatcher: ContextExpressionMatcher<T>, context: T | undefined, ...args: unknown[]): boolean {
return true;
}
}
export const bindSampleMenu = (bind: interfaces.Bind) => {
bind(CommandContribution).to(SampleCommandContribution).inSingletonScope();
bind(MenuContribution).to(SampleMenuContribution).inSingletonScope();
};

View File

@@ -0,0 +1,277 @@
/********************************************************************************
* 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
********************************************************************************/
/**
* The command contributed in this file allows us to generate a copy of the schema expected for editor preferences by Monaco,
* as well as an interface corresponding to those properties for use with our EditorPreferences PreferenceProxy.
* It examines the schemata registered with the Monaco `ConfigurationRegistry` and writes any configurations associated with the editor
* to a file in the `editor` package. It also generates an interface based on the types specified in the schema.
* The only manual work required during a Monaco uplift is to run the command and then update any fields of the interface where the
* schema type is `array` or `object`, since it is tricky to extract the type details for such fields automatically.
*/
import { ConfigurationScope, Extensions, IConfigurationRegistry } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationRegistry';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
import { CommandContribution, CommandRegistry, MaybeArray, MessageService, nls, PreferenceScope } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PreferenceValidationService } from '@theia/core/lib/browser';
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
import { JsonType } from '@theia/core/lib/common/json-schema';
import { editorOptionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { PreferenceDataProperty } from '@theia/core/lib/common/preferences/preference-schema';
function generateContent(properties: string, interfaceEntries: string[]): string {
return `/********************************************************************************
* 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
********************************************************************************/
import { isOSX, isWindows, nls } from '@theia/core';
import { PreferenceSchema } from '@theia/core/lib/browser';
/* eslint-disable @typescript-eslint/quotes,max-len,no-null/no-null */
/**
* Please do not modify this file by hand. It should be generated automatically
* during a Monaco uplift using the command registered by monaco-editor-preference-extractor.ts
* The only manual work required is fixing preferences with type 'array' or 'object'.
*/
export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] = ${properties};
export interface GeneratedEditorPreferences {
${interfaceEntries.join('\n ')}
}
`;
}
const dequoteMarker = '@#@';
// From src/vs/editor/common/config/editorOptions.ts
const DEFAULT_WINDOWS_FONT_FAMILY = "Consolas, \\'Courier New\\', monospace";
const DEFAULT_MAC_FONT_FAMILY = "Menlo, Monaco, \\'Courier New\\', monospace";
const DEFAULT_LINUX_FONT_FAMILY = "\\'Droid Sans Mono\\', \\'monospace\\', monospace";
const fontFamilyText = `${dequoteMarker}isOSX ? '${DEFAULT_MAC_FONT_FAMILY}' : isWindows ? '${DEFAULT_WINDOWS_FONT_FAMILY}' : '${DEFAULT_LINUX_FONT_FAMILY}'${dequoteMarker}`;
const fontSizeText = `${dequoteMarker}isOSX ? 12 : 14${dequoteMarker}`;
/**
* This class is intended for use when uplifting Monaco.
*/
@injectable()
export class MonacoEditorPreferenceSchemaExtractor implements CommandContribution {
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(FileService) protected readonly fileService: FileService;
@inject(PreferenceValidationService) protected readonly preferenceValidationService: PreferenceValidationService;
@inject(MonacoEditorProvider) protected readonly monacoEditorProvider: MonacoEditorProvider;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'check-for-unvalidated-editor-preferences', label: 'Check for unvalidated editor preferences in Monaco', category: 'API Samples' }, {
execute: () => {
const firstRootUri = this.workspaceService.tryGetRoots()[0]?.resource;
if (firstRootUri) {
const validatedEditorPreferences = new Set(editorOptionsRegistry.map(validator => validator.name));
const allEditorPreferenceKeys = Object.keys(this.monacoEditorProvider['createOptions'](
this.monacoEditorProvider['preferencePrefixes'], firstRootUri.toString(), 'typescript'
));
const unvalidatedKeys = allEditorPreferenceKeys.filter(key => !validatedEditorPreferences.has(key));
console.log('Unvalidated keys are:', unvalidatedKeys);
}
}
});
commands.registerCommand({ id: 'extract-editor-preference-schema', label: 'Extract editor preference schema from Monaco', category: 'API Samples' }, {
execute: async () => {
const roots = this.workspaceService.tryGetRoots();
if (roots.length !== 1 || !(roots[0].resource.path.toString() ?? '').includes('theia')) {
this.messageService.warn('This command should only be executed in the Theia workspace.');
}
const theiaRoot = roots[0];
const fileToWrite = theiaRoot.resource.resolve('packages/editor/src/common/editor-generated-preference-schema.ts');
const properties = {};
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().forEach(config => {
if (config.id === 'editor' && config.properties) {
Object.assign(properties, config.properties);
}
});
this.guaranteePlatformOptions(properties);
const interfaceEntries = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const [name, description] of Object.entries(properties) as Array<[string, any]>) {
const { scope, overridable } = this.getScope(description.scope);
description.scope = scope;
description.overridable = overridable;
delete description.defaultDefaultValue;
delete description.restricted;
if (name === 'editor.fontSize') {
description.default = fontSizeText;
} else if (name === 'editor.fontFamily') {
description.default = fontFamilyText;
}
interfaceEntries.push(`'${name}': ${this.formatSchemaForInterface(description)};`);
}
const stringified = JSON.stringify(properties, this.codeSnippetReplacer(), 4);
const propertyList = this.dequoteCodeSnippets(stringified);
const content = generateContent(propertyList, interfaceEntries);
await this.fileService.write(fileToWrite, content);
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected codeSnippetReplacer(): (key: string, value: any) => any {
// JSON.stringify doesn't give back the whole context when serializing so we use state...
let lastPreferenceName: string;
return (key, value) => {
if (key.startsWith('editor.') || key.startsWith('diffEditor.')) {
lastPreferenceName = key;
}
if ((key === 'description' || key === 'markdownDescription') && typeof value === 'string') {
if (value.length === 0) {
return value;
}
const defaultKey = nls.getDefaultKey(value);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
}
}
if ((key === 'enumDescriptions' || key === 'markdownEnumDescriptions') && Array.isArray(value)) {
return value.map((description, i) => {
if (description.length === 0) {
return description;
}
const defaultKey = nls.getDefaultKey(description);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${i}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
}
});
}
return value;
};
};
protected getScope(monacoScope: unknown): { scope: PreferenceScope, overridable: boolean } {
switch (monacoScope) {
case ConfigurationScope.MACHINE_OVERRIDABLE:
case ConfigurationScope.WINDOW:
case ConfigurationScope.RESOURCE:
return { scope: PreferenceScope.Folder, overridable: false };
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
return { scope: PreferenceScope.Folder, overridable: true };
case ConfigurationScope.APPLICATION:
case ConfigurationScope.MACHINE:
return { scope: PreferenceScope.User, overridable: false };
}
return { scope: PreferenceScope.Default, overridable: false };
}
protected formatSchemaForInterface(schema: PreferenceDataProperty): string {
const defaultValue = schema.default !== undefined ? schema.default : schema.default;
// There are a few preferences for which VSCode uses defaults that do not match the schema. We have to handle those manually.
if (defaultValue !== undefined && this.preferenceValidationService.validateBySchema('any-preference', defaultValue, schema) !== defaultValue) {
return 'HelpBadDefaultValue';
}
const jsonType = schema.const !== undefined ? schema.const : (schema.enum ?? schema.type);
if (jsonType === undefined) {
const subschemata = schema.anyOf ?? schema.oneOf;
if (subschemata) {
const permittedTypes = [].concat.apply(subschemata.map(subschema => this.formatSchemaForInterface(subschema).split(' | ')));
return Array.from(new Set(permittedTypes)).join(' | ');
}
}
return this.formatTypeForInterface(jsonType);
}
protected formatTypeForInterface(jsonType?: MaybeArray<JsonType | JSONValue> | undefined): string {
if (Array.isArray(jsonType)) {
return jsonType.map(subtype => this.formatTypeForInterface(subtype)).join(' | ');
}
switch (jsonType) {
case 'boolean':
case 'number':
case 'string':
case 'true':
case 'false':
return jsonType;
case true:
case false:
case null: // eslint-disable-line no-null/no-null
return `${jsonType}`;
case 'integer':
return 'number';
case 'array':
case 'object':
case undefined:
// These have to be fixed manually, so we output a type that will cause a TS error.
return 'Help';
}
// Most of the rest are string literals.
return `'${jsonType}'`;
}
protected dequoteCodeSnippets(stringification: string): string {
return stringification
.replace(new RegExp(`${dequoteMarker}"|"${dequoteMarker}|${dequoteMarker}\\\\`, 'g'), '')
.replace(new RegExp(`\\\\"${dequoteMarker}`, 'g'), '"')
.replace(/\\\\'/g, "\\'");
}
/**
* Ensures that options that are only relevant on certain platforms are caught.
* Check for use of `platform` in src/vs/editor/common/config/editorOptions.ts
*/
protected guaranteePlatformOptions(properties: object): void {
Object.assign(properties, {
'editor.find.globalFindClipboard': {
type: 'boolean',
default: false,
description: 'Controls whether the Find Widget should read or modify the shared find clipboard on macOS.',
included: `${dequoteMarker}isOSX${dequoteMarker}`,
},
'editor.selectionClipboard': {
type: 'boolean',
default: true,
description: 'Controls whether the Linux primary clipboard should be supported.',
included: `${dequoteMarker}!isOSX && !isWindows${dequoteMarker}`
}
});
}
}
// Utility to assist with Monaco uplifts to generate preference schema. Not for regular use in the application.
export function bindMonacoPreferenceExtractor(bind: interfaces.Bind): void {
// bind(MonacoEditorPreferenceSchemaExtractor).toSelf().inSingletonScope();
// bind(CommandContribution).toService(MonacoEditorPreferenceSchemaExtractor);
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2020 SAP SE or an SAP affiliate company 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 { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/browser/output-channel';
@injectable()
export class SampleOutputChannelWithSeverity
implements FrontendApplicationContribution {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
public onStart(): void {
const channel = this.outputChannelManager.getChannel('API Sample: my test channel');
channel.appendLine('hello info1'); // showed without color
channel.appendLine('hello info2', OutputChannelSeverity.Info);
channel.appendLine('hello error', OutputChannelSeverity.Error);
channel.appendLine('hello warning', OutputChannelSeverity.Warning);
channel.append('inlineInfo1 ');
channel.append('inlineWarning ', OutputChannelSeverity.Warning);
channel.append('inlineError ', OutputChannelSeverity.Error);
channel.append('inlineInfo2', OutputChannelSeverity.Info);
}
}
export const bindSampleOutputChannelWithSeverity = (bind: interfaces.Bind) => {
bind(FrontendApplicationContribution)
.to(SampleOutputChannelWithSeverity)
.inSingletonScope();
};

View File

@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 { CommandContribution, CommandRegistry, MessageService, QuickInputService } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { SampleBackendPreferencesService, sampleBackendPreferencesServicePath } from '../../common/preference-protocol';
import { ServiceConnectionProvider } from '@theia/core/lib/browser';
@injectable()
export class SamplePreferenceContribution implements CommandContribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(SampleBackendPreferencesService)
protected readonly preferencesService: SampleBackendPreferencesService;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'samplePreferences.get', label: 'Get Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Get Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Get Backend Preference',
prompt: 'Enter override identifier'
});
const value = await this.preferencesService.getPreference(key, override);
this.messageService.info(`The value is \n${JSON.stringify(value)}`);
}
}
}
);
commands.registerCommand({ id: 'samplePreferences.inspect', label: 'Inspect Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Inspect Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Inspect Backend Preference',
prompt: 'Enter override identifier'
});
const value = await this.preferencesService.inspectPreference(key, override);
this.messageService.info(`The value is \n${JSON.stringify(value)}`);
}
}
}
);
commands.registerCommand({ id: 'samplePreferences.set', label: 'Set Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter override identifier'
});
const valueString = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter JSON value'
});
if (valueString) {
await this.preferencesService.setPreference(key, override, JSON.parse(valueString));
}
}
}
}
);
}
}
export function bindSamplePreferenceContribution(bind: interfaces.Bind): void {
bind(CommandContribution).to(SamplePreferenceContribution).inSingletonScope();
bind(SampleBackendPreferencesService).toDynamicValue(ctx => ServiceConnectionProvider.createProxy(ctx.container, sampleBackendPreferencesServicePath)).inSingletonScope();
}

View File

@@ -0,0 +1,37 @@
// *****************************************************************************
// Copyright (C) 2025 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 { TextReplacementContribution } from '@theia/core/lib/browser/preload/text-replacement-contribution';
export class TextSampleReplacementContribution implements TextReplacementContribution {
getReplacement(locale: string): Record<string, string> {
switch (locale) {
case 'en': {
return {
'About': 'About Theia',
};
}
case 'de': {
return {
'About': 'Über Theia',
};
}
}
return {};
}
}

View File

@@ -0,0 +1,34 @@
/********************************************************************************
* Copyright (C) 2020 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
********************************************************************************/
.theia-icon {
background-image: url("../icons/theia.png");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
#theia-main-content-panel {
background-image: url("../icons/theia.png");
background-position: center center;
background-repeat: no-repeat;
background-size: 15%;
}
.unclosable-window-icon {
-webkit-mask: url("window-icon.svg");
mask: url("window-icon.svg");
}

View File

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

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics 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 { TestContribution, TestItem, TestRunProfileKind, TestService } from '@theia/test/lib/browser/test-service';
import { CommandContribution, CommandRegistry, Path, URI } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { inject, injectable, interfaces, named, postConstruct } from '@theia/core/shared/inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { FileStatWithMetadata } from '@theia/filesystem/lib/common/files';
import { TestControllerImpl, TestItemImpl, TestRunImpl } from './test-controller';
function stringifyTransformer(key: string, value: any): any {
if (value instanceof URI) {
return value.toString();
}
if (value instanceof TestItemImpl) {
return {
id: value.id,
label: value.label,
range: value.range,
sortKey: value.sortKey,
tags: value.tags,
uri: value.uri,
busy: value.busy,
canResolveChildren: value.canResolveChildren,
children: value.tests,
description: value.description,
error: value.error
};
}
return value;
}
@injectable()
export class SampleTestContribution implements TestContribution, CommandContribution {
@inject(WorkspaceService)
private workspaceService: WorkspaceService;
@inject(FileSearchService)
private searchService: FileSearchService;
@inject(FileService)
private fileService: FileService;
@inject(ILogger) @named('api-samples')
private logger: ILogger;
private testController = new TestControllerImpl('SampleTestController', 'Sample Test Controller');
private usedUris = new Set<string>();
private nextTestId = 0;
private nextRunId = 0;
@postConstruct()
protected init(): void {
this.testController.onItemsChanged(e => {
this.logger.debug(JSON.stringify(e, stringifyTransformer, 4));
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'testController.addSomeTests', label: 'Add Some Tests', category: 'API Samples' }, {
execute: async (...args: any): Promise<any> => {
const root = (await this.workspaceService.roots)[0];
const files = (await this.searchService.find('.json', {
rootUris: [root.resource.toString()],
limit: 1000
})).filter(uri => !this.usedUris.has(uri));
for (let i = 0; i < Math.min(10, files.length); i++) {
const fileUri = new URI(files[i]);
const relativePath = root.resource.path.relative(fileUri.path);
let collection = this.testController.items;
let dirUri = root.resource;
relativePath?.toString().split(Path.separator).forEach(name => {
dirUri = dirUri.withPath(dirUri.path.join(name));
let item = collection.get(name);
if (!item) {
item = new TestItemImpl(dirUri, name);
item.label = name;
collection.add(item);
}
collection = item._children;
});
const meta: FileStatWithMetadata = await this.fileService.resolve(fileUri, { resolveMetadata: true });
const testItem = new TestItemImpl(fileUri, `test-id-${this.nextTestId}`);
testItem.label = `Test number ${this.nextTestId++}`;
testItem.range = {
start: { line: 0, character: 0 },
end: { line: 0, character: Math.min(10, meta.size) }
};
collection.add(testItem);
}
}
});
commands.registerCommand({ id: 'testController.dumpController', label: 'Dump Controller Contents', category: 'API Samples' }, {
execute: (...args: any): any => {
this.logger.debug(JSON.stringify(this.testController, stringifyTransformer, 4));
}
});
}
registerTestControllers(service: TestService): void {
this.testController.addProfile({
kind: TestRunProfileKind.Run,
label: 'Sample run profile #1',
isDefault: false,
canConfigure: true,
tag: '',
run: (name: string, included: readonly TestItem[], excluded: readonly TestItem[]) => {
this.testController.addRun(new TestRunImpl(this.testController, `sample-run-id-${this.nextRunId}`, `sample-profile-1-${this.nextRunId++}`));
},
configure: (): void => {
this.logger.debug('configuring the sample profile 1');
}
});
this.testController.addProfile({
kind: TestRunProfileKind.Run,
label: 'Sample run profile #2',
isDefault: false,
canConfigure: true,
tag: '',
run: (name: string, included: readonly TestItem[], excluded: readonly TestItem[]) => {
this.testController.addRun(new TestRunImpl(this.testController, `sample-run-id-${this.nextRunId}`, `sample-profile-2-${this.nextRunId++}`));
},
configure: (): void => {
this.logger.debug('configuring the sample profile 2');
}
});
service.registerTestController(this.testController);
}
}
export function bindTestSample(bind: interfaces.Bind): void {
bind(SampleTestContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(SampleTestContribution);
bind(TestContribution).toService(SampleTestContribution);
};

View File

@@ -0,0 +1,387 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics 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 { CancellationToken, Emitter, Event, URI } from '@theia/core';
import { Range, Location, CancellationTokenSource } from '@theia/core/shared/vscode-languageserver-protocol';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { SimpleObservableCollection, TreeCollection, observableProperty } from '@theia/test/lib/common/collections';
import {
TestController, TestExecutionState, TestFailure, TestItem,
TestOutputItem, TestRun, TestRunProfile, TestState, TestStateChangedEvent
} from '@theia/test/lib/browser/test-service';
import { AccumulatingTreeDeltaEmitter, CollectionDelta, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta';
import { timeout } from '@theia/core/lib/common/promise-util';
export class TestItemCollection extends TreeCollection<string, TestItemImpl, TestItemImpl | TestControllerImpl> {
override add(item: TestItemImpl): TestItemImpl | undefined {
item.realParent = this.owner;
return super.add(item);
}
}
export class TestItemImpl implements TestItem {
constructor(readonly uri: URI, readonly id: string) {
this._children = new TestItemCollection(this, (v: TestItemImpl) => v.path, (v: TestItemImpl) => v.deltaBuilder);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected notifyPropertyChange(property: keyof TestItemImpl, value: any): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
if (this.path) {
this.deltaBuilder?.reportChanged(this.path, val);
}
}
_deltaBuilder: TreeDeltaBuilder<string, TestItemImpl> | undefined;
get deltaBuilder(): TreeDeltaBuilder<string, TestItemImpl> | undefined {
if (this._deltaBuilder) {
return this._deltaBuilder;
} else if (this.realParent) {
this._deltaBuilder = this.realParent.deltaBuilder;
return this._deltaBuilder;
} else {
return undefined;
}
}
_path: string[] | undefined;
get path(): string[] {
if (this._path) {
return this._path;
} else if (this.realParent instanceof TestItemImpl) {
this._path = [...this.realParent.path, this.id];
return this._path;
} else {
return [this.id];
}
};
private _parent?: TestItemImpl | TestControllerImpl;
get realParent(): TestItemImpl | TestControllerImpl | undefined {
return this._parent;
}
set realParent(v: TestItemImpl | TestControllerImpl | undefined) {
this.iterate(item => {
item._path = undefined;
return true;
});
this._parent = v;
}
get parent(): TestItem | undefined {
const realParent = this.realParent;
if (realParent instanceof TestItemImpl) {
return realParent;
}
return undefined;
}
get controller(): TestControllerImpl | undefined {
if (this.realParent instanceof TestItemImpl) {
return this.realParent.controller;
}
return this.realParent;
}
protected iterate(toDo: (v: TestItemImpl) => boolean): boolean {
if (toDo(this)) {
for (let i = 0; i < this._children.values.length; i++) {
if (!this._children.values[i].iterate(toDo)) {
return false;
}
}
return true;
} else {
return false;
}
}
@observableProperty('notifyPropertyChange')
label: string = '';
@observableProperty('notifyPropertyChange')
range?: Range;
@observableProperty('notifyPropertyChange')
sortKey?: string | undefined;
@observableProperty('notifyPropertyChange')
tags: string[] = [];
@observableProperty('notifyPropertyChange')
busy: boolean = false;
@observableProperty('notifyPropertyChange')
canResolveChildren: boolean = false;
@observableProperty('notifyPropertyChange')
description?: string | undefined;
@observableProperty('notifyPropertyChange')
error?: string | MarkdownString | undefined;
_children: TestItemCollection;
get tests(): readonly TestItemImpl[] {
return this._children.values;
}
resolveChildren(): void {
// do nothing
}
}
export class TestRunImpl implements TestRun {
private testStates: Map<TestItem, TestState> = new Map();
private outputIndices: Map<TestItem, number[]> = new Map();
private outputs: TestOutputItem[] = [];
private onDidChangePropertyEmitter = new Emitter<{ name?: string; isRunning?: boolean; }>();
onDidChangeProperty: Event<{ name?: string; isRunning?: boolean; }> = this.onDidChangePropertyEmitter.event;
private cts: CancellationTokenSource;
constructor(readonly controller: TestControllerImpl, readonly id: string, name: string) {
this.name = name;
this.isRunning = false;
this.start();
}
private start(): void {
this.cts = new CancellationTokenSource();
Promise.allSettled(this.collectTestsForRun().map(item => this.simulateTestRun(item, this.cts.token))).then(() => this.ended());
}
collectTestsForRun(): TestItemImpl[] {
const result: TestItemImpl[] = [];
this.collectTests(this.controller.tests, result);
return result;
}
collectTests(tests: readonly TestItemImpl[], result: TestItemImpl[]): void {
tests.forEach(test => this.collectTest(test, result));
}
collectTest(test: TestItemImpl, result: TestItemImpl[]): void {
if (test.tests.length > 0) {
this.collectTests(test.tests, result);
} else if (Math.random() < 0.8) {
result.push(test);
}
}
simulateTestRun(item: TestItemImpl, token: CancellationToken): Promise<void> {
let outputCounter = 0;
let messageCounter = 0;
return timeout(Math.random() * 3000, token)
.then(() => this.setTestState(item, { state: TestExecutionState.Queued }))
.then(() => timeout(Math.random() * 3000, token))
.then(() => this.setTestState(item, { state: TestExecutionState.Running }))
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
}).then(() => {
const random = Math.random();
if (random > 0.9) {
this.setTestState(item, { state: TestExecutionState.Skipped });
} else if (random > 0.8) {
const failure: TestFailure = {
state: TestExecutionState.Errored,
messages: [
{
message: {
value: `**Error** from Test ${item.label} nr ${messageCounter++}`
},
location: {
uri: item.uri.toString(),
range: item.range!
},
}
],
duration: 33
};
this.setTestState(item, failure);
} else if (random > 0.7) {
const failure: TestFailure = {
state: TestExecutionState.Failed,
messages: [
{
message: {
value: `**Failure** from Test ${item.label} nr ${messageCounter++}`
},
location: {
uri: item.uri.toString(),
range: item.range!
},
}
],
duration: 33
};
this.setTestState(item, failure);
} else {
this.setTestState(item, { state: TestExecutionState.Passed });
}
});
}
@observableProperty('notifyPropertyChange')
isRunning: boolean;
@observableProperty('notifyPropertyChange')
name: string;
protected notifyPropertyChange(property: 'name' | 'isRunning', value: unknown): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
this.onDidChangePropertyEmitter.fire(val);
}
cancel(): void {
this.cts.cancel();
}
getTestState(item: TestItem): TestState | undefined {
return this.testStates.get(item);
}
private onDidChangeTestStateEmitter: Emitter<TestStateChangedEvent[]> = new Emitter();
onDidChangeTestState: Event<TestStateChangedEvent[]> = this.onDidChangeTestStateEmitter.event;
getOutput(item?: TestItem | undefined): readonly TestOutputItem[] {
if (!item) {
return this.outputs;
} else {
const indices = this.outputIndices.get(item);
if (!indices) {
return [];
} else {
return indices.map(index => this.outputs[index]);
}
}
}
private onDidChangeTestOutputEmitter: Emitter<[TestItem | undefined, TestOutputItem][]> = new Emitter();
onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]> = this.onDidChangeTestOutputEmitter.event;
setTestState<T extends TestState>(test: TestItemImpl, newState: TestState): void {
const oldState = this.testStates.get(test);
this.testStates.set(test, newState);
this.onDidChangeTestStateEmitter.fire([{
oldState: oldState, newState: newState, test: test
}]);
}
appendOutput(text: string, location?: Location, item?: TestItem): void {
const output = {
output: text,
location: location
};
this.outputs.push(output);
if (item) {
let indices = this.outputIndices.get(item);
if (!indices) {
indices = [];
this.outputIndices.set(item, indices);
}
indices.push(this.outputs.length - 1);
}
this.onDidChangeTestOutputEmitter.fire([[item, output]]);
}
get items(): readonly TestItem[] {
return [...this.testStates.keys()];
}
ended(): void {
const stateEvents: TestStateChangedEvent[] = [];
this.testStates.forEach((state, item) => {
if (state.state <= TestExecutionState.Running) {
stateEvents.push({
oldState: state,
newState: undefined,
test: item
});
this.testStates.delete(item);
}
});
if (stateEvents.length > 0) {
this.onDidChangeTestStateEmitter.fire(stateEvents);
}
this.isRunning = false;
}
}
export class TestControllerImpl implements TestController {
private _profiles = new SimpleObservableCollection<TestRunProfile>();
private _runs = new SimpleObservableCollection<TestRun>();
readonly deltaBuilder = new AccumulatingTreeDeltaEmitter<string, TestItemImpl>(300);
items = new TestItemCollection(this, item => item.path, () => this.deltaBuilder);
constructor(readonly id: string, readonly label: string) {
}
refreshTests(token: CancellationToken): Promise<void> {
// not implemented
return Promise.resolve();
}
get testRunProfiles(): readonly TestRunProfile[] {
return this._profiles.values;
}
addProfile(profile: TestRunProfile): void {
this._profiles.add(profile);
}
onProfilesChanged: Event<CollectionDelta<TestRunProfile, TestRunProfile>> = this._profiles.onChanged;
get testRuns(): readonly TestRun[] {
return this._runs.values;
}
addRun(run: TestRun): void {
this._runs.add(run);
}
onRunsChanged: Event<CollectionDelta<TestRun, TestRun>> = this._runs.onChanged;
get tests(): readonly TestItemImpl[] {
return this.items.values;
}
onItemsChanged: Event<TreeDelta<string, TestItemImpl>[]> = this.deltaBuilder.onDidFlush;
resolveChildren(item: TestItem): void {
// nothing to do
}
clearRuns(): void {
this._runs.clear();
}
}

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics 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 { TestItemImpl } from './test-controller';
import { URI } from '@theia/core';
import { DeltaKind, TreeDeltaBuilderImpl } from '@theia/test/lib/common/tree-delta';
const expect = chai.expect;
describe('TestItem tests', () => {
it('should notify property changes', () => {
const deltaBuilder = new TreeDeltaBuilderImpl<string, TestItemImpl>();
const item = new TestItemImpl(new URI('https://foo/bar'), 'b');
item._deltaBuilder = deltaBuilder;
item._path = ['a', 'b'];
item.label = 'theLabel';
const range = { start: { line: 17, character: 5 }, end: { line: 17, character: 37 } };
item.range = range;
expect(deltaBuilder.currentDelta).deep.equal([{
path: ['a', 'b'],
type: DeltaKind.CHANGED,
value: {
label: 'theLabel',
range: range
},
}]);
});
});

View File

@@ -0,0 +1,46 @@
/********************************************************************************
* 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
********************************************************************************/
#theia-sample-toolbar-contribution {
position: relative;
}
#theia-sample-toolbar-contribution .icon-wrapper {
cursor: pointer;
margin-left: 0;
}
#theia-sample-toolbar-contribution:focus,
#theia-sample-toolbar-contribution .icon-wrapper:focus,
#theia-sample-toolbar-contribution .codicon-search:focus {
outline: none;
}
#theia-sample-toolbar-contribution
.icon-wrapper.action-label.item.enabled:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
#theia-sample-toolbar-contribution #easy-search-item-icon.codicon-search {
position: relative;
}
#theia-sample-toolbar-contribution .icon-wrapper .codicon-triangle-down {
position: absolute;
font-size: 10px;
bottom: -7px;
right: -2px;
}

View File

@@ -0,0 +1,151 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { CommandContribution, CommandRegistry, CommandService, MenuContribution, MenuModelRegistry } from '@theia/core';
import { LabelProvider, quickCommand, QuickInputService, QuickPickItem } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { quickFileOpen } from '@theia/file-search/lib/browser/quick-file-open';
import { SearchInWorkspaceCommands } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { AbstractToolbarContribution } from '@theia/toolbar/lib/browser/abstract-toolbar-contribution';
import { ToolbarMenus, ReactInteraction } from '@theia/toolbar/lib/browser/toolbar-constants';
import { ToolbarContribution } from '@theia/toolbar/lib/browser/toolbar-interfaces';
import { ToolbarDefaultsFactory } from '@theia/toolbar/lib/browser/toolbar-defaults';
import { SampleToolbarDefaultsOverride } from './sample-toolbar-defaults-override';
import '../../../src/browser/toolbar/sample-toolbar-contribution.css';
export const bindSampleToolbarContribution = (bind: interfaces.Bind, rebind: interfaces.Rebind) => {
bind(SampleToolbarContribution).toSelf().inSingletonScope();
bind(ToolbarContribution).to(SampleToolbarContribution);
bind(CommandContribution).to(SampleToolbarContribution);
bind(MenuContribution).to(SampleToolbarContribution);
bind(SearchInWorkspaceQuickInputService).toSelf().inSingletonScope();
rebind(ToolbarDefaultsFactory).toConstantValue(SampleToolbarDefaultsOverride);
};
export const FIND_IN_WORKSPACE_ROOT = {
id: 'easy.search.find.in.workspace.root',
category: 'API Samples',
label: 'Search Workspace Root for Text',
};
@injectable()
export class SearchInWorkspaceQuickInputService {
@inject(QuickInputService) protected readonly quickInputService: QuickInputService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(CommandService) protected readonly commandService: CommandService;
protected quickPickItems: QuickPickItem[] = [];
open(): void {
this.quickPickItems = this.createWorkspaceList();
this.quickInputService.showQuickPick(this.quickPickItems, {
placeholder: 'Workspace root to search',
});
}
protected createWorkspaceList(): QuickPickItem[] {
const roots = this.workspaceService.tryGetRoots();
return roots.map(root => {
const uri = root.resource;
return {
label: this.labelProvider.getName(uri),
execute: (): Promise<void> => this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [uri]),
};
});
}
}
@injectable()
export class SampleToolbarContribution extends AbstractToolbarContribution
implements CommandContribution,
MenuContribution {
@inject(SearchInWorkspaceQuickInputService) protected readonly searchPickService: SearchInWorkspaceQuickInputService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
static ID = 'theia-sample-toolbar-contribution';
id = SampleToolbarContribution.ID;
protected handleOnClick = (e: ReactInteraction<HTMLSpanElement>): void => this.doHandleOnClick(e);
protected doHandleOnClick(e: ReactInteraction<HTMLSpanElement>): void {
e.stopPropagation();
const toolbar = document.querySelector<HTMLDivElement>('#main-toolbar');
if (toolbar) {
const { bottom } = toolbar.getBoundingClientRect();
const { left } = e.currentTarget.getBoundingClientRect();
this.contextMenuRenderer.render({
includeAnchorArg: false,
menuPath: ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU,
anchor: { x: left, y: bottom },
context: e.currentTarget
});
}
}
render(): React.ReactNode {
return (
<div
role='button'
tabIndex={0}
className='icon-wrapper action-label item enabled codicon codicon-search'
id='easy-search-item-icon'
onClick={this.handleOnClick}
title='API Samples: Search for files, text, commands, and more...'
>
<div className='codicon codicon-triangle-down' />
</div>);
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(FIND_IN_WORKSPACE_ROOT, {
execute: async () => {
const wsRoots = await this.workspaceService.roots;
if (!wsRoots.length) {
await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id);
} else if (wsRoots.length === 1) {
const { resource } = wsRoots[0];
await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [resource]);
} else {
this.searchPickService.open();
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: quickCommand.id,
label: 'Find a Command',
order: 'a',
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: quickFileOpen.id,
order: 'b',
label: 'Search for a file'
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id,
label: 'Search Entire Workspace for Text',
order: 'c',
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: FIND_IN_WORKSPACE_ROOT.id,
order: 'd',
});
}
}

View File

@@ -0,0 +1,59 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { DeflatedToolbarTree, ToolbarAlignment } from '@theia/toolbar/lib/browser/toolbar-interfaces';
export const SampleToolbarDefaultsOverride: () => DeflatedToolbarTree = () => ({
items: {
[ToolbarAlignment.LEFT]: [
[
{
id: 'textEditor.commands.go.back',
command: 'textEditor.commands.go.back',
icon: 'codicon codicon-arrow-left',
},
{
id: 'textEditor.commands.go.forward',
command: 'textEditor.commands.go.forward',
icon: 'codicon codicon-arrow-right',
},
],
[
{
id: 'workbench.action.splitEditorRight',
command: 'workbench.action.splitEditor',
icon: 'codicon codicon-split-horizontal',
},
],
],
[ToolbarAlignment.CENTER]: [[
{
id: 'theia-sample-toolbar-contribution',
group: 'contributed'
}
]],
[ToolbarAlignment.RIGHT]: [
[
{
id: 'workbench.action.showCommands',
command: 'workbench.action.showCommands',
icon: 'codicon codicon-terminal',
tooltip: 'Command Palette',
},
]
]
},
});

View File

@@ -0,0 +1,120 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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, interfaces } from '@theia/core/shared/inversify';
import { AbstractViewContribution, bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { Command, CommandRegistry, MessageService } from '@theia/core/lib/common';
import { ApplicationShell, codicon, DockLayout, ShellLayoutTransformer, Widget, WidgetFactory } from '@theia/core/lib/browser';
import { SampleViewUnclosableView } from './sample-unclosable-view';
export const SampleToolBarCommand: Command = {
id: 'sample.toggle.toolbarCommand',
iconClass: codicon('add'),
category: 'API Samples'
};
@injectable()
export class SampleUnclosableViewContribution extends AbstractViewContribution<SampleViewUnclosableView> implements TabBarToolbarContribution, ShellLayoutTransformer {
static readonly SAMPLE_UNCLOSABLE_VIEW_TOGGLE_COMMAND_ID = 'sampleUnclosableView:toggle';
protected toolbarItemState = false;
@inject(MessageService) protected readonly messageService: MessageService;
constructor() {
super({
widgetId: SampleViewUnclosableView.ID,
widgetName: 'Sample Unclosable View',
toggleCommandId: SampleUnclosableViewContribution.SAMPLE_UNCLOSABLE_VIEW_TOGGLE_COMMAND_ID,
defaultWidgetOptions: {
area: 'main'
}
});
}
override registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(SampleToolBarCommand, {
execute: () => {
this.toolbarItemState = !this.toolbarItemState;
this.messageService.info(`Sample Toolbar Command is toggled = ${this.toolbarItemState}`);
},
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
isToggled: () => this.toolbarItemState
});
}
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
toolbarRegistry.registerItem({
id: SampleToolBarCommand.id,
command: SampleToolBarCommand.id,
tooltip: 'API Samples: Click to Toggle Toolbar Item',
priority: 0
});
}
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), cb: (sampleView: SampleViewUnclosableView) => T): T | false {
if (widget instanceof SampleViewUnclosableView && widget.id === SampleViewUnclosableView.ID) {
return cb(widget);
}
return false;
}
// Makes sure the 'Sample Unclosable View' view is never restored after app restarts.
transformLayoutOnRestore(layoutData: ApplicationShell.LayoutData): void {
this.pruneConfig(layoutData.mainPanel?.main);
}
protected pruneConfig(area: DockLayout.AreaConfig | null | undefined): void {
if (area?.type === 'tab-area') {
this.pruneTabConfig(area);
} else if (area?.type === 'split-area') {
this.pruneSplitConfig(area);
}
}
protected pruneTabConfig(area: DockLayout.AreaConfig): void {
if (area.type === 'tab-area') {
const newwidgets = area.widgets.filter(widget => {
if (widget.id.startsWith(SampleViewUnclosableView.ID)) {
return false;
}
return true;
});
area.widgets = newwidgets;
}
}
protected pruneSplitConfig(area: DockLayout.AreaConfig): void {
if (area.type === 'split-area') {
area.children.forEach(c => this.pruneConfig(c));
}
}
}
export const bindSampleUnclosableView = (bind: interfaces.Bind) => {
bindViewContribution(bind, SampleUnclosableViewContribution);
bind(TabBarToolbarContribution).to(SampleUnclosableViewContribution).inSingletonScope();
bind(SampleViewUnclosableView).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: SampleViewUnclosableView.ID,
createWidget: () => ctx.container.get<SampleViewUnclosableView>(SampleViewUnclosableView)
}));
bind(ShellLayoutTransformer).toService(SampleUnclosableViewContribution);
};

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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 { ReactWidget } from '@theia/core/lib/browser';
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
/**
* This sample view is used to demo the behavior of "Widget.title.closable".
*/
@injectable()
export class SampleViewUnclosableView extends ReactWidget {
static readonly ID = 'sampleUnclosableView';
@postConstruct()
init(): void {
this.id = SampleViewUnclosableView.ID;
this.title.caption = 'Sample Unclosable View';
this.title.label = 'Sample Unclosable View';
this.title.iconClass = 'unclosable-window-icon';
this.title.closable = false;
this.update();
}
protected render(): React.ReactNode {
return (
<div>
Closable
<input type="checkbox" defaultChecked={this.title.closable} onChange={e => this.title.closable = e.target.checked} />
</div>
);
}
}

View File

@@ -0,0 +1,31 @@
// *****************************************************************************
// 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 { Endpoint } from '@theia/core/lib/browser';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { SampleAppInfo } from '../../common/vsx/sample-app-info';
@injectable()
export class SampleFrontendAppInfo implements SampleAppInfo {
async getSelfOrigin(): Promise<string> {
return new Endpoint().origin;
}
}
export function bindSampleAppInfo(bind: interfaces.Bind): void {
bind(SampleAppInfo).to(SampleFrontendAppInfo).inSingletonScope();
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2020 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, interfaces } from '@theia/core/shared/inversify';
import { VSXEnvironment } from '@theia/vsx-registry/lib/common/vsx-environment';
import { Command, CommandContribution, CommandRegistry, MessageService } from '@theia/core/lib/common';
@injectable()
export class VSXCommandContribution implements CommandContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(VSXEnvironment)
protected readonly environment: VSXEnvironment;
protected readonly command: Command = {
id: 'vsx.echo-api-version',
label: 'Show VS Code API Version',
category: 'API Samples'
};
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(this.command, {
execute: async () => {
const version = await this.environment.getVscodeApiVersion();
this.messageService.info(`Supported VS Code API Version: ${version}`);
}
});
}
}
export const bindVSXCommand = (bind: interfaces.Bind) => {
bind(CommandContribution).to(VSXCommandContribution).inSingletonScope();
};

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 { PreferenceInspection } from '@theia/core';
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
export const sampleBackendPreferencesServicePath = '/services/sampleBackendPreferences';
export const SampleBackendPreferencesService = Symbol('SampleBackendPreferencesService');
export interface SampleBackendPreferencesService {
getPreference(key: string, overrideIdentifier?: string): Promise<JSONValue | undefined>;
inspectPreference(key: string, overrideIdentifier?: string): Promise<PreferenceInspection | undefined>;
setPreference(key: string, overrideIdentifier: string | undefined, value: JSONValue): Promise<void>
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 } from '@theia/core';
export const FileWatchingPreferencesSchema: PreferenceSchema = {
properties: {
'sample.file-watching.verbose': {
type: 'boolean',
default: false,
description: 'Enable verbose file watching logs.'
}
}
};

View File

@@ -0,0 +1,37 @@
// *****************************************************************************
// 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 { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
export enum UpdateStatus {
InProgress = 'in-progress',
Available = 'available',
NotAvailable = 'not-available'
}
export const SampleUpdaterPath = '/services/sample-updater';
export const SampleUpdater = Symbol('SampleUpdater');
export interface SampleUpdater extends RpcServer<SampleUpdaterClient> {
checkForUpdates(): Promise<{ status: UpdateStatus }>;
onRestartToUpdateRequested(): void;
disconnectClient(client: SampleUpdaterClient): void;
setUpdateAvailable(available: boolean): Promise<void>; // Mock
}
export const SampleUpdaterClient = Symbol('SampleUpdaterClient');
export interface SampleUpdaterClient {
notifyReadyToInstall(): void;
}

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// 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';
export const SampleAppInfo = Symbol('SampleAppInfo') as symbol & interfaces.Abstract<SampleAppInfo>;
export interface SampleAppInfo {
getSelfOrigin(): Promise<string>;
}

View File

@@ -0,0 +1,30 @@
// *****************************************************************************
// 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 { OVSXUrlResolver } from '@theia/vsx-registry/lib/common';
import { SampleAppInfo } from './sample-app-info';
export function rebindOVSXClientFactory(rebind: interfaces.Rebind): void {
// rebind the OVSX client factory so that we can replace patterns like "${self}" in the configs:
rebind(OVSXUrlResolver)
.toDynamicValue(ctx => {
const appInfo = ctx.container.get<SampleAppInfo>(SampleAppInfo);
const selfOrigin = appInfo.getSelfOrigin();
return async (url: string) => url.replace('${self}', await selfOrigin);
})
.inSingletonScope();
}

View File

@@ -0,0 +1,176 @@
// *****************************************************************************
// 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { CommonMenus } from '@theia/core/lib/browser';
import {
Emitter,
Command,
MenuPath,
MessageService,
MenuModelRegistry,
MenuContribution,
CommandRegistry,
CommandContribution
} from '@theia/core/lib/common';
import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { SampleUpdater, UpdateStatus, SampleUpdaterClient } from '../../common/updater/sample-updater';
export namespace SampleUpdaterCommands {
const category = 'API Samples';
export const CHECK_FOR_UPDATES: Command = {
id: 'electron-sample:check-for-updates',
label: 'Check for Updates...',
category
};
export const RESTART_TO_UPDATE: Command = {
id: 'electron-sample:restart-to-update',
label: 'Restart to Update',
category
};
// Mock
export const MOCK_UPDATE_AVAILABLE: Command = {
id: 'electron-sample:mock-update-available',
label: 'Mock Update - Available',
category
};
export const MOCK_UPDATE_NOT_AVAILABLE: Command = {
id: 'electron-sample:mock-update-not-available',
label: 'Mock Update - Not Available',
category
};
}
export namespace SampleUpdaterMenu {
export const MENU_PATH: MenuPath = [...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_submenu_update'];
}
@injectable()
export class SampleUpdaterClientImpl implements SampleUpdaterClient {
protected readonly onReadyToInstallEmitter = new Emitter<void>();
readonly onReadyToInstall = this.onReadyToInstallEmitter.event;
notifyReadyToInstall(): void {
this.onReadyToInstallEmitter.fire();
}
}
// Dynamic menus aren't yet supported by electron: https://github.com/eclipse-theia/theia/issues/446
@injectable()
export class ElectronMenuUpdater {
@inject(ElectronMainMenuFactory)
protected readonly factory: ElectronMainMenuFactory;
public update(): void {
this.setMenu();
}
private setMenu(): void {
window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar());
}
}
@injectable()
export class SampleUpdaterFrontendContribution implements CommandContribution, MenuContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(ElectronMenuUpdater)
protected readonly menuUpdater: ElectronMenuUpdater;
@inject(SampleUpdater)
protected readonly updater: SampleUpdater;
@inject(SampleUpdaterClientImpl)
protected readonly updaterClient: SampleUpdaterClientImpl;
protected readyToUpdate = false;
@postConstruct()
protected init(): void {
this.updaterClient.onReadyToInstall(async () => {
this.readyToUpdate = true;
this.menuUpdater.update();
this.handleUpdatesAvailable();
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SampleUpdaterCommands.CHECK_FOR_UPDATES, {
execute: async () => {
const { status } = await this.updater.checkForUpdates();
switch (status) {
case UpdateStatus.Available: {
this.handleUpdatesAvailable();
break;
}
case UpdateStatus.NotAvailable: {
const { applicationName } = FrontendApplicationConfigProvider.get();
this.messageService.info(`[Sample Updater - Not Available]: You're all good. You've got the latest version of ${applicationName}.`, { timeout: 3000 });
break;
}
case UpdateStatus.InProgress: {
this.messageService.warn('[Sample Updater - Downloading]: Work in progress...', { timeout: 3000 });
break;
}
default: throw new Error(`Unexpected status: ${status}`);
}
},
isEnabled: () => !this.readyToUpdate,
isVisible: () => !this.readyToUpdate
});
registry.registerCommand(SampleUpdaterCommands.RESTART_TO_UPDATE, {
execute: () => this.updater.onRestartToUpdateRequested(),
isEnabled: () => this.readyToUpdate,
isVisible: () => this.readyToUpdate
});
registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_AVAILABLE, {
execute: () => this.updater.setUpdateAvailable(true)
});
registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_NOT_AVAILABLE, {
execute: () => this.updater.setUpdateAvailable(false)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, {
commandId: SampleUpdaterCommands.CHECK_FOR_UPDATES.id
});
registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, {
commandId: SampleUpdaterCommands.RESTART_TO_UPDATE.id
});
}
protected async handleUpdatesAvailable(): Promise<void> {
const answer = await this.messageService.info('[Sample Updater - Available]: Found updates, do you want update now?', 'No', 'Yes');
if (answer === 'Yes') {
this.updater.onRestartToUpdateRequested();
}
}
}

View File

@@ -0,0 +1,34 @@
// *****************************************************************************
// 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 { ContainerModule } from '@theia/core/shared/inversify';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution';
export default new ContainerModule(bind => {
bind(ElectronMenuUpdater).toSelf().inSingletonScope();
bind(SampleUpdaterClientImpl).toSelf().inSingletonScope();
bind(SampleUpdaterClient).toService(SampleUpdaterClientImpl);
bind(SampleUpdater).toDynamicValue(context => {
const client = context.container.get(SampleUpdaterClientImpl);
return ElectronIpcConnectionProvider.createProxy(context.container, SampleUpdaterPath, client);
}).inSingletonScope();
bind(SampleUpdaterFrontendContribution).toSelf().inSingletonScope();
bind(MenuContribution).toService(SampleUpdaterFrontendContribution);
bind(CommandContribution).toService(SampleUpdaterFrontendContribution);
});

View File

@@ -0,0 +1,91 @@
// *****************************************************************************
// 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 { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
import { SampleUpdater, SampleUpdaterClient, UpdateStatus } from '../../common/updater/sample-updater';
@injectable()
export class SampleUpdaterImpl implements SampleUpdater, ElectronMainApplicationContribution {
protected clients: Array<SampleUpdaterClient> = [];
protected inProgressTimer: NodeJS.Timeout | undefined;
protected available = false;
async checkForUpdates(): Promise<{ status: UpdateStatus }> {
if (this.inProgressTimer) {
return { status: UpdateStatus.InProgress };
}
return { status: this.available ? UpdateStatus.Available : UpdateStatus.NotAvailable };
}
onRestartToUpdateRequested(): void {
console.info("[api-samples] 'Update to Restart' was requested by the frontend.");
// Here comes your install and restart implementation. For example: `autoUpdater.quitAndInstall();`
}
async setUpdateAvailable(available: boolean): Promise<void> {
if (this.inProgressTimer) {
clearTimeout(this.inProgressTimer);
}
if (!available) {
this.inProgressTimer = undefined;
this.available = false;
} else {
this.inProgressTimer = setTimeout(() => {
this.inProgressTimer = undefined;
this.available = true;
for (const client of this.clients) {
client.notifyReadyToInstall();
}
}, 5000);
}
}
onStart(application: ElectronMainApplication): void {
// Called when the contribution is starting. You can use both async and sync code from here.
}
onStop(application: ElectronMainApplication): void {
// Invoked when the contribution is stopping. You can clean up things here. You are not allowed call async code from here.
}
setClient(client: SampleUpdaterClient | undefined): void {
if (client) {
this.clients.push(client);
console.info('[api-samples] Registered a new sample updater client.');
} else {
console.warn("[api-samples] Couldn't register undefined client.");
}
}
disconnectClient(client: SampleUpdaterClient): void {
const index = this.clients.indexOf(client);
if (index !== -1) {
this.clients.splice(index, 1);
console.info('[api-samples] Disposed a sample updater client.');
} else {
console.warn("[api-samples] Couldn't dispose client; it was not registered.");
}
}
dispose(): void {
console.info('[api-samples] >>> Disposing sample updater service...');
this.clients.forEach(this.disconnectClient.bind(this));
console.info('[api-samples] >>> Disposed sample updater service.');
}
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// 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 { ContainerModule } from '@theia/core/shared/inversify';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-main/messaging/electron-connection-handler';
import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterImpl } from './sample-updater-impl';
export default new ContainerModule(bind => {
bind(SampleUpdaterImpl).toSelf().inSingletonScope();
bind(SampleUpdater).toService(SampleUpdaterImpl);
bind(ElectronMainApplicationContribution).toService(SampleUpdater);
bind(ElectronConnectionHandler).toDynamicValue(context =>
new RpcConnectionHandler<SampleUpdaterClient>(SampleUpdaterPath, client => {
const server = context.container.get<SampleUpdater>(SampleUpdater);
server.setClient(client);
client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
})
).inSingletonScope();
});

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// 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 { ContainerModule } from '@theia/core/shared/inversify';
import { BackendApplicationContribution, BackendApplicationServer } from '@theia/core/lib/node';
import { SampleBackendApplicationServer } from './sample-backend-application-server';
import { SampleMockOpenVsxServer } from './sample-mock-open-vsx-server';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
import { SampleBackendAppInfo } from './sample-backend-app-info';
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { ConnectionHandler, PreferenceContribution, RpcConnectionHandler } from '@theia/core';
import { FileWatchingPreferencesSchema } from '../common/preference-schema';
import { MCPBackendContribution } from '@theia/ai-mcp-server/lib/node/mcp-theia-server';
import { MCPTestContribution } from './sample-mcp-test-contribution';
import { SampleBackendPreferencesService, sampleBackendPreferencesServicePath } from '../common/preference-protocol';
import { SampleBackendPreferencesBackendServiceImpl } from './sample-backend-preferences-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SampleBackendPreferencesBackendServiceImpl).toSelf().inSingletonScope();
bind(MCPBackendContribution).to(MCPTestContribution).inSingletonScope();
bind(SampleBackendPreferencesService).toService(SampleBackendPreferencesBackendServiceImpl);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(sampleBackendPreferencesServicePath, () => ctx.container.get(SampleBackendPreferencesService))
).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: FileWatchingPreferencesSchema });
rebindOVSXClientFactory(rebind);
bind(SampleBackendAppInfo).toSelf().inSingletonScope();
bind(SampleAppInfo).toService(SampleBackendAppInfo);
bind(BackendApplicationContribution).toService(SampleBackendAppInfo);
// bind a mock/sample OpenVSX registry:
bind(BackendApplicationContribution).to(SampleMockOpenVsxServer).inSingletonScope();
if (process.env.SAMPLE_BACKEND_APPLICATION_SERVER) {
bind(BackendApplicationServer).to(SampleBackendApplicationServer).inSingletonScope();
}
});

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// 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 { environment } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BackendApplicationCliContribution, BackendApplicationContribution } from '@theia/core/lib/node';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as net from 'net';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
@injectable()
export class SampleBackendAppInfo implements SampleAppInfo, BackendApplicationContribution {
protected addressDeferred = new Deferred<net.AddressInfo>();
@inject(BackendApplicationCliContribution)
protected backendCli: BackendApplicationCliContribution;
onStart(server: net.Server): void {
const address = server.address();
// eslint-disable-next-line no-null/no-null
if (typeof address === 'object' && address !== null) {
this.addressDeferred.resolve(address);
} else {
this.addressDeferred.resolve({
address: '127.0.0.1',
port: 3000,
family: '4'
});
}
}
async getSelfOrigin(): Promise<string> {
const { ssl } = this.backendCli;
const protocol = ssl ? 'https' : 'http';
const { address, port } = await this.addressDeferred.promise;
const hostname = environment.electron.is() ? 'localhost' : address;
return `${protocol}://${hostname}:${port}`;
}
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// 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 { injectable } from '@theia/core/shared/inversify';
import { BackendApplicationServer } from '@theia/core/lib/node';
import express = require('@theia/core/shared/express');
@injectable()
export class SampleBackendApplicationServer implements BackendApplicationServer {
configure(app: express.Application): void {
app.get('*', (req, res) => {
res.status(200).send('SampleBackendApplicationServer OK');
});
}
}

View File

@@ -0,0 +1,56 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 { JSONValue } from '@theia/core/shared/@lumino/coreutils';
import { SampleBackendPreferencesService } from '../common/preference-protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { PreferenceInspection, PreferenceLanguageOverrideService, PreferenceScope, PreferenceService } from '@theia/core';
@injectable()
export class SampleBackendPreferencesBackendServiceImpl implements SampleBackendPreferencesService {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(PreferenceLanguageOverrideService)
protected readonly languageOverrideService: PreferenceLanguageOverrideService;
async getPreference(key: string, overrideIdentifier: string): Promise<JSONValue | undefined> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
return this.preferenceService.get(preferenceName);
}
async inspectPreference(key: string, overrideIdentifier?: string): Promise<PreferenceInspection | undefined> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
return this.preferenceService.inspect(preferenceName);
}
async setPreference(key: string, overrideIdentifier: string | undefined, value: JSONValue): Promise<void> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
this.preferenceService.set(preferenceName, value, PreferenceScope.User);
}
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { ILogger } from '@theia/core/lib/common/logger';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { MCPBackendContribution } from '@theia/ai-mcp-server/lib/node/mcp-theia-server';
import { z } from 'zod';
@injectable()
export class MCPTestContribution implements MCPBackendContribution {
@inject(ILogger)
protected readonly logger: ILogger;
async configure(server: McpServer): Promise<void> {
this.logger.info('MCPTestContribution.configure() called - MCP system is working!');
server.registerTool('test-tool', {
description: 'Theia MCP server test-tool',
inputSchema: z.object({})
}, async () => {
this.logger.info('test-tool called');
return {
content: [{
type: 'text',
text: 'Test tool executed successfully!'
}]
};
});
this.logger.info('MCPTestContribution: test-tool registered successfully');
}
}

View File

@@ -0,0 +1,189 @@
// *****************************************************************************
// 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 { BackendApplicationContribution } from '@theia/core/lib/node';
import * as express from '@theia/core/shared/express';
import * as fs from 'fs';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { OVSXMockClient, VSXExtensionRaw } from '@theia/ovsx-client';
import * as path from 'path';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
import * as http from 'http';
import * as https from 'https';
import { Deferred } from '@theia/core/lib/common/promise-util';
type VersionedId = `${string}.${string}@${string}`;
/**
* This class implements a very crude OpenVSX mock server for testing.
*
* See {@link configure}'s implementation for supported REST APIs.
*/
@injectable()
export class SampleMockOpenVsxServer implements BackendApplicationContribution {
@inject(SampleAppInfo)
protected appInfo: SampleAppInfo;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
protected mockClient: OVSXMockClient;
protected staticFileHandlers: Map<string, express.RequestHandler<{
namespace: string;
name: string;
version: string;
}, express.Response>>;
private readyDeferred = new Deferred<void>();
private ready = this.readyDeferred.promise;
get mockServerPath(): string {
return '/mock-open-vsx';
}
get pluginsDbPath(): string {
return '../../sample-plugins';
}
async onStart?(server: http.Server | https.Server): Promise<void> {
const selfOrigin = await this.appInfo.getSelfOrigin();
const baseUrl = `${selfOrigin}${this.mockServerPath}`;
const pluginsDb = await this.findMockPlugins(this.pluginsDbPath, baseUrl);
this.staticFileHandlers = new Map(Array.from(pluginsDb.entries(), ([key, value]) => [key, express.static(value.path)]));
this.mockClient = new OVSXMockClient(Array.from(pluginsDb.values(), value => value.data));
this.readyDeferred.resolve();
}
async configure(app: express.Application): Promise<void> {
app.use(
this.mockServerPath + '/api',
express.Router()
.get('/v2/-/query', async (req, res) => {
await this.ready;
res.json(await this.mockClient.query(this.sanitizeQuery(req.query)));
})
.get('/-/search', async (req, res) => {
await this.ready;
res.json(await this.mockClient.search(this.sanitizeQuery(req.query)));
})
.get('/:namespace', async (req, res) => {
await this.ready;
const extensions = this.mockClient.extensions
.filter(ext => req.params.namespace === ext.namespace)
.map(ext => `${ext.namespaceUrl}/${ext.name}`);
if (extensions.length === 0) {
res.status(404).json({ error: `Namespace not found: ${req.params.namespace}` });
} else {
res.json({
name: req.params.namespace,
extensions
});
}
})
.get('/:namespace/:name', async (req, res) => {
await this.ready;
res.json(this.mockClient.extensions.find(ext => req.params.namespace === ext.namespace && req.params.name === ext.name));
})
.get('/:namespace/:name/reviews', async (req, res) => {
res.json([]);
})
// implicitly GET/HEAD because of the express.static handlers
.use('/:namespace/:name/:version/file', async (req, res, next) => {
await this.ready;
const versionedId = this.getVersionedId(req.params.namespace, req.params.name, req.params.version);
const staticFileHandler = this.staticFileHandlers.get(versionedId);
if (!staticFileHandler) {
return next();
}
staticFileHandler(req, res, next);
})
);
}
protected getVersionedId(namespace: string, name: string, version: string): VersionedId {
return `${namespace}.${name}@${version}`;
}
protected sanitizeQuery(query?: Record<string, unknown>): Record<string, string> {
return typeof query === 'object'
? Object.fromEntries(Object.entries(query).filter(([key, value]) => typeof value === 'string') as [string, string][])
: {};
}
/**
* This method expects the following folder hierarchy: `pluginsDbPath/namespace/pluginName/pluginFiles...`
* @param pluginsDbPath where to look for plugins on the disk.
* @param baseUrl used when generating the URLs for {@link VSXExtensionRaw} properties.
*/
protected async findMockPlugins(pluginsDbPath: string, baseUrl: string): Promise<Map<VersionedId, { path: string, data: VSXExtensionRaw }>> {
const url = new OVSXMockClient.UrlBuilder(baseUrl);
const result = new Map<VersionedId, { path: string, data: VSXExtensionRaw }>();
if (!await this.isDirectory(pluginsDbPath)) {
this.logger.error(`ERROR: ${pluginsDbPath} is not a directory!`);
return result;
}
const namespaces = await fs.promises.readdir(pluginsDbPath);
await Promise.all(namespaces.map(async namespace => {
const namespacePath = path.join(pluginsDbPath, namespace);
if (!await this.isDirectory(namespacePath)) {
return;
}
const names = await fs.promises.readdir(namespacePath);
await Promise.all(names.map(async pluginName => {
const pluginPath = path.join(namespacePath, pluginName);
if (!await this.isDirectory(pluginPath)) {
return;
}
const packageJsonPath = path.join(pluginPath, 'package.json');
const { name, version } = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
const versionedId = this.getVersionedId(namespace, name, version);
result.set(versionedId, {
path: pluginPath,
data: {
allVersions: {},
downloadCount: 0,
files: {
// the default generated name from vsce is NAME-VERSION.vsix
download: url.extensionFileUrl(namespace, name, version, `/${name}-${version}.vsix`),
icon: url.extensionFileUrl(namespace, name, version, '/icon128.png'),
readme: url.extensionFileUrl(namespace, name, version, '/README.md')
},
name,
namespace,
namespaceAccess: 'public',
namespaceUrl: url.namespaceUrl(namespace),
publishedBy: {
loginName: 'mock-open-vsx'
},
reviewCount: 0,
reviewsUrl: url.extensionReviewsUrl(namespace, name),
timestamp: new Date().toISOString(),
version,
namespaceDisplayName: name,
preRelease: false
}
});
}));
}));
return result;
}
protected async isDirectory(fsPath: string): Promise<boolean> {
return (await fs.promises.stat(fsPath)).isDirectory();
}
}

View File

@@ -0,0 +1,58 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../../dev-packages/ovsx-client"
},
{
"path": "../../packages/ai-chat"
},
{
"path": "../../packages/ai-chat-ui"
},
{
"path": "../../packages/ai-code-completion"
},
{
"path": "../../packages/ai-core"
},
{
"path": "../../packages/core"
},
{
"path": "../../packages/file-search"
},
{
"path": "../../packages/filesystem"
},
{
"path": "../../packages/monaco"
},
{
"path": "../../packages/output"
},
{
"path": "../../packages/search-in-workspace"
},
{
"path": "../../packages/test"
},
{
"path": "../../packages/toolbar"
},
{
"path": "../../packages/vsx-registry"
},
{
"path": "../../packages/workspace"
}
]
}