deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
examples/api-samples.disabled/.eslintrc.js
Normal file
10
examples/api-samples.disabled/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
42
examples/api-samples.disabled/README.md
Normal file
42
examples/api-samples.disabled/README.md
Normal 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>
|
||||
69
examples/api-samples.disabled/package.json
Normal file
69
examples/api-samples.disabled/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
BIN
examples/api-samples.disabled/src/browser/icons/theia.png
Normal file
BIN
examples/api-samples.disabled/src/browser/icons/theia.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
34
examples/api-samples.disabled/src/browser/style/branding.css
Normal file
34
examples/api-samples.disabled/src/browser/style/branding.css
Normal 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");
|
||||
}
|
||||
@@ -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 |
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
]
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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.'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
58
examples/api-samples.disabled/tsconfig.json
Normal file
58
examples/api-samples.disabled/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user