deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/ai-code-completion/.eslintrc.js
Normal file
10
packages/ai-code-completion/.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'
|
||||
}
|
||||
};
|
||||
32
packages/ai-code-completion/README.md
Normal file
32
packages/ai-code-completion/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
<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 - AI CODE COMPLETION EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/ai-code-completion` extension contributes Ai based code completion.
|
||||
The user can separately enable code completion items as well as inline code completion.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/ai-code-completion`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-code-completion.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
55
packages/ai-code-completion/package.json
Normal file
55
packages/ai-code-completion/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@theia/ai-code-completion",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - AI Core",
|
||||
"dependencies": {
|
||||
"@theia/ai-core": "1.68.0",
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/monaco-editor-core": "1.96.302",
|
||||
"@theia/output": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"main": "lib/common",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/ai-code-completion-frontend-module",
|
||||
"backend": "lib/node/ai-code-completion-backend-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": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// *****************************************************************************
|
||||
// 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 { Agent, AIVariableContribution } from '@theia/ai-core';
|
||||
import { FrontendApplicationContribution, KeybindingContribution, } from '@theia/core/lib/browser';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { AICodeCompletionPreferencesSchema } from '../common/ai-code-completion-preference';
|
||||
import { AIFrontendApplicationContribution } from './ai-code-frontend-application-contribution';
|
||||
import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider';
|
||||
import { CodeCompletionAgent, CodeCompletionAgentImpl } from './code-completion-agent';
|
||||
import { CodeCompletionPostProcessor, DefaultCodeCompletionPostProcessor } from './code-completion-postprocessor';
|
||||
import { CodeCompletionVariableContribution } from './code-completion-variable-contribution';
|
||||
import { PreferenceContribution } from '@theia/core';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(CodeCompletionAgentImpl).toSelf().inSingletonScope();
|
||||
bind(CodeCompletionAgent).toService(CodeCompletionAgentImpl);
|
||||
bind(Agent).toService(CodeCompletionAgentImpl);
|
||||
bind(AICodeInlineCompletionsProvider).toSelf().inSingletonScope();
|
||||
bind(AIFrontendApplicationContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).to(AIFrontendApplicationContribution);
|
||||
bind(KeybindingContribution).toService(AIFrontendApplicationContribution);
|
||||
bind(PreferenceContribution).toConstantValue({ schema: AICodeCompletionPreferencesSchema });
|
||||
bind(CodeCompletionPostProcessor).to(DefaultCodeCompletionPostProcessor).inSingletonScope();
|
||||
bind(AIVariableContribution).to(CodeCompletionVariableContribution).inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
// *****************************************************************************
|
||||
// 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 * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
import { AIActivationService } from '@theia/ai-core/lib/browser';
|
||||
import { Disposable, PreferenceService } from '@theia/core';
|
||||
import { FrontendApplicationContribution, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { InlineCompletionTriggerKind } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
|
||||
import {
|
||||
PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE,
|
||||
PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY,
|
||||
PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS,
|
||||
PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY
|
||||
} from '../common/ai-code-completion-preference';
|
||||
import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider';
|
||||
import { InlineCompletionDebouncer } from './code-completion-debouncer';
|
||||
import { CodeCompletionCache } from './code-completion-cache';
|
||||
|
||||
@injectable()
|
||||
export class AIFrontendApplicationContribution implements FrontendApplicationContribution, KeybindingContribution {
|
||||
@inject(AICodeInlineCompletionsProvider)
|
||||
private inlineCodeCompletionProvider: AICodeInlineCompletionsProvider;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(AIActivationService)
|
||||
protected readonly activationService: AIActivationService;
|
||||
|
||||
private completionCache = new CodeCompletionCache();
|
||||
private debouncer = new InlineCompletionDebouncer();
|
||||
private debounceDelay: number;
|
||||
|
||||
private toDispose = new Map<string, Disposable>();
|
||||
|
||||
onDidInitializeLayout(): void {
|
||||
this.preferenceService.ready.then(() => {
|
||||
this.handlePreferences();
|
||||
});
|
||||
}
|
||||
|
||||
protected handlePreferences(): void {
|
||||
const handler = () => this.handleInlineCompletions();
|
||||
|
||||
this.toDispose.set('inlineCompletions', handler());
|
||||
|
||||
this.debounceDelay = this.preferenceService.get<number>(PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY, 300);
|
||||
|
||||
const cacheCapacity = this.preferenceService.get<number>(PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY, 100);
|
||||
this.completionCache.setMaxSize(cacheCapacity);
|
||||
|
||||
this.preferenceService.onPreferenceChanged(event => {
|
||||
if (event.preferenceName === PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE
|
||||
|| event.preferenceName === PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS) {
|
||||
this.toDispose.get('inlineCompletions')?.dispose();
|
||||
this.toDispose.set('inlineCompletions', handler());
|
||||
}
|
||||
if (event.preferenceName === PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY) {
|
||||
this.debounceDelay = this.preferenceService.get<number>(PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY, 300);
|
||||
}
|
||||
if (event.preferenceName === PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY) {
|
||||
this.completionCache.setMaxSize(this.preferenceService.get<number>(PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY, 100));
|
||||
}
|
||||
});
|
||||
|
||||
this.activationService.onDidChangeActiveStatus(change => {
|
||||
this.toDispose.get('inlineCompletions')?.dispose();
|
||||
this.toDispose.set('inlineCompletions', handler());
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: 'editor.action.inlineSuggest.trigger',
|
||||
keybinding: 'Ctrl+Alt+Space',
|
||||
when: '!editorReadonly && editorTextFocus'
|
||||
});
|
||||
}
|
||||
|
||||
protected handleInlineCompletions(): Disposable {
|
||||
if (!this.activationService.isActive) {
|
||||
return Disposable.NULL;
|
||||
}
|
||||
const automatic = this.preferenceService.get<boolean>(PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE, true);
|
||||
const excludedExtensions = this.preferenceService.get<string[]>(PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS, []);
|
||||
|
||||
return monaco.languages.registerInlineCompletionsProvider(
|
||||
{ scheme: 'file' },
|
||||
{
|
||||
provideInlineCompletions: (model, position, context, token) => {
|
||||
if (!automatic && context.triggerKind === InlineCompletionTriggerKind.Automatic) {
|
||||
return { items: [] };
|
||||
}
|
||||
const fileName = model.uri.toString();
|
||||
if (excludedExtensions.some(ext => fileName.endsWith(ext))) {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
const completionHandler = async () => {
|
||||
try {
|
||||
const cacheKey = this.completionCache.generateKey(fileName, model, position);
|
||||
const cachedCompletion = this.completionCache.get(cacheKey);
|
||||
|
||||
if (cachedCompletion) {
|
||||
return cachedCompletion;
|
||||
}
|
||||
|
||||
const completion = await this.inlineCodeCompletionProvider.provideInlineCompletions(
|
||||
model,
|
||||
position,
|
||||
context,
|
||||
token
|
||||
);
|
||||
|
||||
if (completion && completion.items.length > 0) {
|
||||
this.completionCache.put(cacheKey, completion);
|
||||
}
|
||||
|
||||
return completion;
|
||||
} catch (error) {
|
||||
console.error('Error providing inline completions:', error);
|
||||
return { items: [] };
|
||||
}
|
||||
};
|
||||
|
||||
if (context.triggerKind === InlineCompletionTriggerKind.Automatic) {
|
||||
return this.debouncer.debounce(async () => completionHandler(), this.debounceDelay);
|
||||
} else if (context.triggerKind === InlineCompletionTriggerKind.Explicit) {
|
||||
return completionHandler();
|
||||
}
|
||||
},
|
||||
freeInlineCompletions: completions => {
|
||||
this.inlineCodeCompletionProvider.freeInlineCompletions(completions);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// 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 * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CodeCompletionAgent } from './code-completion-agent';
|
||||
import { AgentService } from '@theia/ai-core';
|
||||
|
||||
@injectable()
|
||||
export class AICodeInlineCompletionsProvider
|
||||
implements monaco.languages.InlineCompletionsProvider {
|
||||
@inject(CodeCompletionAgent)
|
||||
protected readonly agent: CodeCompletionAgent;
|
||||
@inject(AgentService)
|
||||
private readonly agentService: AgentService;
|
||||
|
||||
async provideInlineCompletions(
|
||||
model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
context: monaco.languages.InlineCompletionContext,
|
||||
token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.InlineCompletions | undefined> {
|
||||
if (!this.agentService.isEnabled(this.agent.id)) {
|
||||
return undefined;
|
||||
}
|
||||
return this.agent.provideInlineCompletions(
|
||||
model,
|
||||
position,
|
||||
context,
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
freeInlineCompletions(
|
||||
completions: monaco.languages.InlineCompletions<monaco.languages.InlineCompletion>
|
||||
): void {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
165
packages/ai-code-completion/src/browser/code-completion-agent.ts
Normal file
165
packages/ai-code-completion/src/browser/code-completion-agent.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
// *****************************************************************************
|
||||
// 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 { LanguageModelService } from '@theia/ai-core/lib/browser';
|
||||
import {
|
||||
Agent, AgentSpecificVariables, getTextOfResponse,
|
||||
LanguageModelRegistry, LanguageModelRequirement, PromptService,
|
||||
PromptVariantSet,
|
||||
UserRequest
|
||||
} from '@theia/ai-core/lib/common';
|
||||
import { generateUuid, ILogger, nls, ProgressService } from '@theia/core';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { codeCompletionPrompts } from './code-completion-prompt-template';
|
||||
import { CodeCompletionPostProcessor } from './code-completion-postprocessor';
|
||||
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
||||
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
||||
|
||||
export const CodeCompletionAgent = Symbol('CodeCompletionAgent');
|
||||
export interface CodeCompletionAgent extends Agent {
|
||||
provideInlineCompletions(model: monaco.editor.ITextModel, position: monaco.Position,
|
||||
context: monaco.languages.InlineCompletionContext, token: monaco.CancellationToken): Promise<monaco.languages.InlineCompletions | undefined>
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CodeCompletionAgentImpl implements CodeCompletionAgent {
|
||||
@inject(LanguageModelService)
|
||||
protected languageModelService: LanguageModelService;
|
||||
|
||||
async provideInlineCompletions(
|
||||
model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
context: monaco.languages.InlineCompletionContext,
|
||||
token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.InlineCompletions | undefined> {
|
||||
const progress = await this.progressService.showProgress(
|
||||
{ text: nls.localize('theia/ai/code-completion/progressText', 'Calculating AI code completion...'), options: { location: 'window' } }
|
||||
);
|
||||
try {
|
||||
const languageModel =
|
||||
await this.languageModelRegistry.selectLanguageModel({
|
||||
agent: this.id,
|
||||
...this.languageModelRequirements[0],
|
||||
});
|
||||
if (!languageModel) {
|
||||
this.logger.error(
|
||||
'No language model found for code-completion-agent'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const variableContext: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position,
|
||||
context
|
||||
};
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
const prompt = await this.promptService
|
||||
.getResolvedPromptFragment('code-completion-system', undefined, variableContext)
|
||||
.then(p => p?.text);
|
||||
if (!prompt) {
|
||||
this.logger.error('No prompt found for code-completion-agent');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const variantInfo = this.promptService.getPromptVariantInfo('code-completion-system');
|
||||
|
||||
// since we do not actually hold complete conversions, the request/response pair is considered a session
|
||||
const sessionId = generateUuid();
|
||||
const requestId = generateUuid();
|
||||
const request: UserRequest = {
|
||||
messages: [{ type: 'text', actor: 'user', text: prompt }],
|
||||
settings: {
|
||||
stream: false
|
||||
},
|
||||
agentId: this.id,
|
||||
sessionId,
|
||||
requestId,
|
||||
cancellationToken: token,
|
||||
promptVariantId: variantInfo?.variantId,
|
||||
isPromptVariantCustomized: variantInfo?.isCustomized
|
||||
};
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
const response = await this.languageModelService.sendRequest(languageModel, request);
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
const completionText = await getTextOfResponse(response);
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const postProcessedCompletionText = this.postProcessor.postProcess(completionText);
|
||||
|
||||
return {
|
||||
items: [{
|
||||
insertText: postProcessedCompletionText,
|
||||
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column)
|
||||
}],
|
||||
enableForwardStability: true
|
||||
};
|
||||
} catch (e) {
|
||||
if (!token.isCancellationRequested) {
|
||||
console.error(e.message, e);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
progress.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@inject(ILogger)
|
||||
@named('code-completion-agent')
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(LanguageModelRegistry)
|
||||
protected languageModelRegistry: LanguageModelRegistry;
|
||||
|
||||
@inject(PromptService)
|
||||
protected promptService: PromptService;
|
||||
|
||||
@inject(ProgressService)
|
||||
protected progressService: ProgressService;
|
||||
|
||||
@inject(CodeCompletionPostProcessor)
|
||||
protected postProcessor: CodeCompletionPostProcessor;
|
||||
|
||||
id = 'Code Completion';
|
||||
name = 'Code Completion';
|
||||
description =
|
||||
nls.localize('theia/ai/completion/agent/description', 'This agent provides inline code completion in the code editor in the Theia IDE.');
|
||||
prompts: PromptVariantSet[] = codeCompletionPrompts;
|
||||
languageModelRequirements: LanguageModelRequirement[] = [
|
||||
{
|
||||
purpose: 'code-completion',
|
||||
identifier: 'default/code-completion',
|
||||
},
|
||||
];
|
||||
readonly variables: string[] = [];
|
||||
readonly functions: string[] = [];
|
||||
readonly agentSpecificVariables: AgentSpecificVariables[] = [
|
||||
{ name: FILE.id, description: nls.localize('theia/ai/completion/agent/vars/file/description', 'The URI of the file being edited'), usedInPrompt: true },
|
||||
{ name: PREFIX.id, description: nls.localize('theia/ai/completion/agent/vars/prefix/description', 'The code before the current cursor position'), usedInPrompt: true },
|
||||
{ name: SUFFIX.id, description: nls.localize('theia/ai/completion/agent/vars/suffix/description', 'The code after the current cursor position'), usedInPrompt: true },
|
||||
{ name: LANGUAGE.id, description: nls.localize('theia/ai/completion/agent/vars/language/description', 'The languageId of the file being edited'), usedInPrompt: true }
|
||||
];
|
||||
}
|
||||
144
packages/ai-code-completion/src/browser/code-completion-cache.ts
Normal file
144
packages/ai-code-completion/src/browser/code-completion-cache.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// *****************************************************************************
|
||||
// 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 { injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
@injectable()
|
||||
export class CodeCompletionCache {
|
||||
private cache: Map<string, CacheEntry>;
|
||||
private maxSize = 100;
|
||||
|
||||
constructor() {
|
||||
this.cache = new Map<string, CacheEntry>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique cache key for code completion based on the file path, cursor position, and the hashed context (prefix and suffix).
|
||||
* The prefix and suffix are hashed to avoid storing large or sensitive content directly in the cache key.
|
||||
*
|
||||
* @param filePath Path of the current file
|
||||
* @param model Monaco text model of the file
|
||||
* @param position Current cursor position in the editor
|
||||
* @returns Unique cache key as a string
|
||||
*/
|
||||
generateKey(filePath: string, model: monaco.editor.ITextModel, position: monaco.Position): string {
|
||||
const lineNumber = position.lineNumber;
|
||||
const prefixRange = new monaco.Range(1, 1, position.lineNumber, position.column);
|
||||
const prefix = model.getValueInRange(prefixRange);
|
||||
const lastLine = model.getLineCount();
|
||||
const lastColumn = model.getLineMaxColumn(lastLine);
|
||||
const suffixRange = new monaco.Range(position.lineNumber, position.column, lastLine, lastColumn);
|
||||
const suffix = model.getValueInRange(suffixRange);
|
||||
const key = JSON.stringify({
|
||||
filePath,
|
||||
lineNumber,
|
||||
prefixHash: CodeCompletionCache.hashString(prefix),
|
||||
suffixHash: CodeCompletionCache.hashString(suffix)
|
||||
});
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a string using a simple hash algorithm (FNV-1a 32-bit).
|
||||
* This is not cryptographically secure but is sufficient for cache key uniqueness.
|
||||
* @param str The string to hash
|
||||
* @returns The hash as a hex string
|
||||
*/
|
||||
private static hashString(str: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash = (hash * 0x01000193) >>> 0;
|
||||
}
|
||||
return hash.toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached completion if available
|
||||
* @param key Cache key
|
||||
* @returns Cached completion or undefined
|
||||
*/
|
||||
get(key: string): monaco.languages.InlineCompletions | undefined {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry) {
|
||||
// Update the entry's last accessed time
|
||||
entry.lastAccessed = Date.now();
|
||||
return entry.value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a completion in the cache
|
||||
* @param key Cache key
|
||||
* @param value Completion value to cache
|
||||
*/
|
||||
put(key: string, value: monaco.languages.InlineCompletions | undefined): void {
|
||||
// If cache is full, remove the least recently used entry
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
this.removeLeastRecentlyUsed();
|
||||
}
|
||||
|
||||
this.cache.set(key, {
|
||||
value,
|
||||
lastAccessed: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the least recently used entry from the cache
|
||||
*/
|
||||
private removeLeastRecentlyUsed(): void {
|
||||
let oldestKey: string | undefined;
|
||||
let oldestTime = Infinity;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (entry.lastAccessed < oldestTime) {
|
||||
oldestKey = key;
|
||||
oldestTime = entry.lastAccessed;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestKey) {
|
||||
this.cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum cache size
|
||||
* @param size New maximum cache size
|
||||
*/
|
||||
setMaxSize(size: number): void {
|
||||
this.maxSize = size;
|
||||
// Trim cache if it exceeds new size
|
||||
while (this.cache.size > this.maxSize) {
|
||||
this.removeLeastRecentlyUsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
value: monaco.languages.InlineCompletions | undefined;
|
||||
lastAccessed: number;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
export class InlineCompletionDebouncer {
|
||||
|
||||
private timeoutId?: number;
|
||||
|
||||
debounce<T>(callback: () => Promise<T>, debounceDelay: number): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
callback()
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
this.timeoutId = undefined;
|
||||
}, debounceDelay);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// *****************************************************************************
|
||||
// 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
let disableJSDOM = enableJSDOM();
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { DefaultCodeCompletionPostProcessor } from './code-completion-postprocessor';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
describe('CodeCompletionAgentImpl', () => {
|
||||
let codeCompletionProcessor: DefaultCodeCompletionPostProcessor;
|
||||
before(() => {
|
||||
disableJSDOM = enableJSDOM();
|
||||
codeCompletionProcessor = new DefaultCodeCompletionPostProcessor();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Disable JSDOM after all tests
|
||||
disableJSDOM();
|
||||
});
|
||||
|
||||
describe('stripBackticks', () => {
|
||||
|
||||
it('should remove surrounding backticks and language (TypeScript)', () => {
|
||||
const input = '```TypeScript\nconsole.log(\"Hello, World!\");```';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('console.log("Hello, World!");');
|
||||
});
|
||||
|
||||
it('should remove surrounding backticks and language (md)', () => {
|
||||
const input = '```md\nconsole.log(\"Hello, World!\");```';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('console.log("Hello, World!");');
|
||||
});
|
||||
|
||||
it('should remove all text after second occurrence of backticks', () => {
|
||||
const input = '```js\nlet x = 10;\n```\nTrailing text should be removed';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('let x = 10;');
|
||||
});
|
||||
|
||||
it('should return the text unchanged if no surrounding backticks', () => {
|
||||
const input = 'console.log(\"Hello, World!\");';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('console.log("Hello, World!");');
|
||||
});
|
||||
|
||||
it('should remove surrounding backticks without language', () => {
|
||||
const input = '```\nconsole.log(\"Hello, World!\");```';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('console.log("Hello, World!");');
|
||||
});
|
||||
|
||||
it('should handle text starting with backticks but no second delimiter', () => {
|
||||
const input = '```python\nprint(\"Hello, World!\")';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('print("Hello, World!")');
|
||||
});
|
||||
|
||||
it('should handle multiple internal backticks correctly', () => {
|
||||
const input = '```\nFoo```Bar```FooBar```';
|
||||
const output = codeCompletionProcessor.stripBackticks(input);
|
||||
expect(output).to.equal('Foo```Bar```FooBar');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// *****************************************************************************
|
||||
// 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 { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { PreferenceService } from '@theia/core/lib/common';
|
||||
import { PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS } from '../common/ai-code-completion-preference';
|
||||
|
||||
export interface CodeCompletionPostProcessor {
|
||||
postProcess(text: string): string;
|
||||
}
|
||||
export const CodeCompletionPostProcessor = Symbol('CodeCompletionPostProcessor');
|
||||
|
||||
@injectable()
|
||||
export class DefaultCodeCompletionPostProcessor {
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
public postProcess(text: string): string {
|
||||
if (this.preferenceService.get<boolean>(PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS, true)) {
|
||||
return this.stripBackticks(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public stripBackticks(text: string): string {
|
||||
if (text.startsWith('```')) {
|
||||
// Remove the first backticks and any language identifier
|
||||
const startRemoved = text.slice(3).replace(/^\w*\n/, '');
|
||||
const lastBacktickIndex = startRemoved.lastIndexOf('```');
|
||||
return lastBacktickIndex !== -1 ? startRemoved.slice(0, lastBacktickIndex).trim() : startRemoved.trim();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable @typescript-eslint/tslint/config */
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH and others.
|
||||
//
|
||||
// This file is licensed under the MIT License.
|
||||
// See LICENSE-MIT.txt in the project root for license information.
|
||||
// https://opensource.org/license/mit.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
// *****************************************************************************
|
||||
|
||||
import { PromptVariantSet } from '@theia/ai-core/lib/common';
|
||||
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
||||
|
||||
export const codeCompletionPrompts: PromptVariantSet[] = [{
|
||||
id: 'code-completion-system',
|
||||
variants: [{
|
||||
id: 'code-completion-system-previous',
|
||||
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
||||
Made improvements or adaptations to this prompt template? We’d love for you to share it with the community! Contribute back here:
|
||||
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
||||
You are a code completion agent. The current file you have to complete is named {{${FILE.id}}}.
|
||||
The language of the file is {{${LANGUAGE.id}}}. Return your result as plain text without markdown formatting.
|
||||
Finish the following code snippet.
|
||||
|
||||
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
||||
|
||||
Only return the exact replacement for [[MARKER]] to complete the snippet.`
|
||||
},
|
||||
{
|
||||
id: 'code-completion-system-next',
|
||||
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
||||
Made improvements or adaptations to this prompt template? We'd love for you to share it with the community! Contribute back here:
|
||||
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
||||
# System Role
|
||||
You are an expert AI code completion assistant focused on generating precise, contextually appropriate code snippets.
|
||||
|
||||
## Code Context
|
||||
\`\`\`
|
||||
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
||||
\`\`\`
|
||||
|
||||
## Metadata
|
||||
- File: {{${FILE.id}}}
|
||||
- Programming Language: {{${LANGUAGE.id}}}
|
||||
- Project Context: {{prompt:project-info}}
|
||||
|
||||
# Completion Guidelines
|
||||
1. Analyze the surrounding code context carefully.
|
||||
2. Generate ONLY the code that should replace [[MARKER]].
|
||||
3. Ensure the completion:
|
||||
- Maintains the exact syntax of the surrounding code
|
||||
- Follows best practices for the specific programming language
|
||||
- Completes the code snippet logically and efficiently
|
||||
4. Do NOT include any explanatory text, comments, or additional instructions.
|
||||
5. Return ONLY the raw code replacement.
|
||||
|
||||
# Constraints
|
||||
- Return strictly the code for [[MARKER]]
|
||||
- Match indentation and style of surrounding code
|
||||
- Prioritize readability and maintainability
|
||||
- Consider language-specific idioms and patterns`
|
||||
}],
|
||||
defaultVariant: {
|
||||
id: 'code-completion-system-default',
|
||||
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
||||
Made improvements or adaptations to this prompt template? We’d love for you to share it with the community! Contribute back here:
|
||||
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
||||
## Code snippet
|
||||
\`\`\`
|
||||
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
||||
\`\`\`
|
||||
|
||||
## Meta Data
|
||||
- File: {{${FILE.id}}}
|
||||
- Language: {{${LANGUAGE.id}}}
|
||||
|
||||
Replace [[MARKER]] with the exact code to complete the code snippet. Return only the replacement of [[MARKER]] as plain text.`,
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
// *****************************************************************************
|
||||
// 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 { AIVariableContext } from '@theia/ai-core';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
export interface CodeCompletionVariableContext {
|
||||
model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
context: monaco.languages.InlineCompletionContext
|
||||
}
|
||||
|
||||
export namespace CodeCompletionVariableContext {
|
||||
export function is(context: AIVariableContext): context is CodeCompletionVariableContext {
|
||||
return !!context && 'model' in context && 'position' in context && 'context' in context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// *****************************************************************************
|
||||
// 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 { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
let disableJSDOM = enableJSDOM();
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { PreferenceService } from '@theia/core/lib/common';
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { editor, languages, Uri } from '@theia/monaco-editor-core/esm/vs/editor/editor.api';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
||||
import { CodeCompletionVariableContribution } from './code-completion-variable-contribution';
|
||||
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
describe('CodeCompletionVariableContribution', () => {
|
||||
let contribution: CodeCompletionVariableContribution;
|
||||
let model: editor.ITextModel;
|
||||
|
||||
before(() => {
|
||||
disableJSDOM = enableJSDOM();
|
||||
const container = new Container();
|
||||
container.bind(PreferenceService).toConstantValue({
|
||||
get: () => 1000,
|
||||
});
|
||||
container.bind(CodeCompletionVariableContribution).toSelf().inSingletonScope();
|
||||
contribution = container.get(CodeCompletionVariableContribution);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
model = editor.createModel('//line 1\nconsole.\n//line 2', 'javascript', Uri.file('/home/user/workspace/test.js'));
|
||||
sinon.stub(model, 'getLanguageId').returns('javascript');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// Disable JSDOM after all tests
|
||||
disableJSDOM();
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
describe('canResolve', () => {
|
||||
it('should be able to resolve the file from the CodeCompletionVariableContext', () => {
|
||||
const context: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position: model.getPositionAt(8),
|
||||
context: {
|
||||
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
||||
selectedSuggestionInfo: undefined,
|
||||
includeInlineEdits: false,
|
||||
includeInlineCompletions: false
|
||||
}
|
||||
};
|
||||
|
||||
expect(contribution.canResolve({ variable: FILE }, context)).to.equal(1);
|
||||
});
|
||||
|
||||
it('should not be able to resolve the file from unknown context', () => {
|
||||
expect(contribution.canResolve({ variable: FILE }, {})).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolve', () => {
|
||||
it('should resolve the file variable', async () => {
|
||||
const context: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position: model.getPositionAt(17),
|
||||
context: {
|
||||
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
||||
selectedSuggestionInfo: undefined,
|
||||
includeInlineEdits: false,
|
||||
includeInlineCompletions: false
|
||||
}
|
||||
};
|
||||
|
||||
const resolved = await contribution.resolve({ variable: FILE }, context);
|
||||
expect(resolved).to.deep.equal({
|
||||
variable: FILE,
|
||||
value: 'file:///home/user/workspace/test.js'
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve the language variable', async () => {
|
||||
const context: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position: model.getPositionAt(17),
|
||||
context: {
|
||||
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
||||
selectedSuggestionInfo: undefined,
|
||||
includeInlineEdits: false,
|
||||
includeInlineCompletions: false
|
||||
}
|
||||
};
|
||||
|
||||
const resolved = await contribution.resolve({ variable: LANGUAGE }, context);
|
||||
expect(resolved).to.deep.equal({
|
||||
variable: LANGUAGE,
|
||||
value: 'javascript'
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve the prefix variable', async () => {
|
||||
const context: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position: model.getPositionAt(17),
|
||||
context: {
|
||||
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
||||
selectedSuggestionInfo: undefined,
|
||||
includeInlineEdits: false,
|
||||
includeInlineCompletions: false
|
||||
}
|
||||
};
|
||||
|
||||
const resolved = await contribution.resolve({ variable: PREFIX }, context);
|
||||
expect(resolved).to.deep.equal({
|
||||
variable: PREFIX,
|
||||
value: '//line 1\nconsole.'
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve the suffix variable', async () => {
|
||||
const context: CodeCompletionVariableContext = {
|
||||
model,
|
||||
position: model.getPositionAt(17),
|
||||
context: {
|
||||
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
||||
selectedSuggestionInfo: undefined,
|
||||
includeInlineEdits: false,
|
||||
includeInlineCompletions: false
|
||||
}
|
||||
};
|
||||
|
||||
const resolved = await contribution.resolve({ variable: SUFFIX }, context);
|
||||
expect(resolved).to.deep.equal({
|
||||
variable: SUFFIX,
|
||||
value: '\n//line 2'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
// *****************************************************************************
|
||||
// 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 { AIVariableContext, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIVariable } from '@theia/ai-core';
|
||||
import { FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
||||
import { MaybePromise, PreferenceService } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES } from '../common/ai-code-completion-preference';
|
||||
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
||||
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
||||
|
||||
@injectable()
|
||||
export class CodeCompletionVariableContribution implements FrontendVariableContribution, AIVariableResolver {
|
||||
@inject(PreferenceService)
|
||||
protected preferences: PreferenceService;
|
||||
|
||||
registerVariables(service: FrontendVariableService): void {
|
||||
[
|
||||
FILE,
|
||||
PREFIX,
|
||||
SUFFIX,
|
||||
LANGUAGE
|
||||
].forEach(variable => {
|
||||
service.registerResolver(variable, this);
|
||||
});
|
||||
}
|
||||
|
||||
canResolve(_request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
|
||||
return CodeCompletionVariableContext.is(context) ? 1 : 0;
|
||||
}
|
||||
|
||||
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
||||
if (!CodeCompletionVariableContext.is(context)) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
switch (request.variable.id) {
|
||||
case FILE.id:
|
||||
return this.resolveFile(context);
|
||||
case LANGUAGE.id:
|
||||
return this.resolveLanguage(context);
|
||||
case PREFIX.id:
|
||||
return this.resolvePrefix(context);
|
||||
case SUFFIX.id:
|
||||
return this.resolveSuffix(context);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected resolvePrefix(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
||||
const position = context.position;
|
||||
const model = context.model;
|
||||
const maxContextLines = this.preferences.get<number>(PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES, -1);
|
||||
let prefixStartLine = 1;
|
||||
|
||||
if (maxContextLines === 0) {
|
||||
// Only the cursor line
|
||||
prefixStartLine = position.lineNumber;
|
||||
} else if (maxContextLines > 0) {
|
||||
const linesBeforeCursor = position.lineNumber - 1;
|
||||
|
||||
// Allocate one more line to the prefix in case of an odd maxContextLines
|
||||
const prefixLines = Math.min(
|
||||
Math.ceil(maxContextLines / 2),
|
||||
linesBeforeCursor
|
||||
);
|
||||
|
||||
prefixStartLine = Math.max(1, position.lineNumber - prefixLines);
|
||||
}
|
||||
|
||||
const prefix = model.getValueInRange({
|
||||
startLineNumber: prefixStartLine,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
|
||||
return {
|
||||
variable: PREFIX,
|
||||
value: prefix
|
||||
};
|
||||
}
|
||||
|
||||
protected resolveSuffix(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
||||
const position = context.position;
|
||||
const model = context.model;
|
||||
const maxContextLines = this.preferences.get<number>(PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES, -1);
|
||||
let suffixEndLine = model.getLineCount();
|
||||
|
||||
if (maxContextLines === 0) {
|
||||
suffixEndLine = position.lineNumber;
|
||||
} else if (maxContextLines > 0) {
|
||||
const linesAfterCursor = model.getLineCount() - position.lineNumber;
|
||||
|
||||
const suffixLines = Math.min(
|
||||
Math.floor(maxContextLines / 2),
|
||||
linesAfterCursor
|
||||
);
|
||||
|
||||
suffixEndLine = Math.min(model.getLineCount(), position.lineNumber + suffixLines);
|
||||
}
|
||||
|
||||
const suffix = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endLineNumber: suffixEndLine,
|
||||
endColumn: model.getLineMaxColumn(suffixEndLine),
|
||||
});
|
||||
|
||||
return {
|
||||
variable: SUFFIX,
|
||||
value: suffix
|
||||
};
|
||||
}
|
||||
|
||||
protected resolveLanguage(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
||||
return {
|
||||
variable: LANGUAGE,
|
||||
value: context.model.getLanguageId()
|
||||
};
|
||||
}
|
||||
|
||||
protected resolveFile(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
||||
return {
|
||||
variable: FILE,
|
||||
value: context.model.uri.toString(false)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// 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 { AIVariable } from '@theia/ai-core/lib/common/variable-service';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export const FILE: AIVariable = {
|
||||
id: 'codeCompletionFile',
|
||||
name: 'codeCompletionFile',
|
||||
description: nls.localize('theia/ai/completion/fileVariable/description', 'The URI of the file being edited. Only available in code completion context.'),
|
||||
};
|
||||
|
||||
export const PREFIX: AIVariable = {
|
||||
id: 'codeCompletionPrefix',
|
||||
name: 'codeCompletionPrefix',
|
||||
description: nls.localize('theia/ai/completion/prefixVariable/description', 'The code before the current cursor position. Only available in code completion context.'),
|
||||
};
|
||||
|
||||
export const SUFFIX: AIVariable = {
|
||||
id: 'codeCompletionSuffix',
|
||||
name: 'codeCompletionSuffix',
|
||||
description: nls.localize('theia/ai/completion/suffixVariable/description', 'The code after the current cursor position. Only available in code completion context.'),
|
||||
};
|
||||
|
||||
export const LANGUAGE: AIVariable = {
|
||||
id: 'codeCompletionLanguage',
|
||||
name: 'codeCompletionLanguage',
|
||||
description: nls.localize('theia/ai/completion/languageVariable/description', 'The languageId of the file being edited. Only available in code completion context.'),
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
// *****************************************************************************
|
||||
// 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 { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/common/ai-core-preferences';
|
||||
import { nls, PreferenceSchema } from '@theia/core';
|
||||
|
||||
export const PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE = 'ai-features.codeCompletion.automaticCodeCompletion';
|
||||
export const PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY = 'ai-features.codeCompletion.debounceDelay';
|
||||
export const PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS = 'ai-features.codeCompletion.excludedFileExtensions';
|
||||
export const PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES = 'ai-features.codeCompletion.maxContextLines';
|
||||
export const PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS = 'ai-features.codeCompletion.stripBackticks';
|
||||
export const PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY = 'ai-features.codeCompletion.cacheCapacity';
|
||||
|
||||
export const AICodeCompletionPreferencesSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
[PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE]: {
|
||||
title: AI_CORE_PREFERENCES_TITLE,
|
||||
type: 'boolean',
|
||||
description: nls.localize('theia/ai/completion/automaticEnable/description',
|
||||
'Automatically trigger AI completions inline within any (Monaco) editor while editing.\
|
||||
\n\
|
||||
Alternatively, you can manually trigger the code via the command "Trigger Inline Suggestion" or the default shortcut "Ctrl+Alt+Space".'),
|
||||
default: false
|
||||
},
|
||||
[PREF_AI_INLINE_COMPLETION_DEBOUNCE_DELAY]: {
|
||||
title: nls.localize('theia/ai/completion/debounceDelay/title', 'Debounce Delay'),
|
||||
type: 'number',
|
||||
description: nls.localize('theia/ai/completion/debounceDelay/description',
|
||||
'Controls the delay in milliseconds before triggering AI completions after changes have been detected in the editor.\
|
||||
Requires `Automatic Code Completion` to be enabled. Enter 0 to disable the debounce delay.'),
|
||||
default: 300
|
||||
},
|
||||
[PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS]: {
|
||||
title: nls.localize('theia/ai/completion/excludedFileExts/title', 'Excluded File Extensions'),
|
||||
type: 'array',
|
||||
description: nls.localize('theia/ai/completion/excludedFileExts/description', 'Specify file extensions (e.g., .md, .txt) where AI completions should be disabled.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
},
|
||||
[PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES]: {
|
||||
title: nls.localize('theia/ai/completion/maxContextLines/title', 'Maximum Context Lines'),
|
||||
type: 'number',
|
||||
description: nls.localize('theia/ai/completion/maxContextLines/description',
|
||||
'The maximum number of lines used as context, distributed among the lines before and after the cursor position (prefix and suffix).\
|
||||
Set this to -1 to use the full file as context without any line limit and 0 to only use the current line.'),
|
||||
default: -1,
|
||||
minimum: -1
|
||||
},
|
||||
[PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS]: {
|
||||
title: nls.localize('theia/ai/completion/stripBackticks/title', 'Strip Backticks from Inline Completions'),
|
||||
type: 'boolean',
|
||||
description: nls.localize('theia/ai/completion/stripBackticks/description',
|
||||
'Remove surrounding backticks from the code returned by some LLMs. If a backtick is detected, all content after the closing\
|
||||
backtick is stripped as well. This setting helps ensure plain code is returned when language models use markdown-like formatting.'),
|
||||
default: true
|
||||
},
|
||||
[PREF_AI_INLINE_COMPLETION_CACHE_CAPACITY]: {
|
||||
title: nls.localize('theia/ai/completion/cacheCapacity/title', 'Code Completion Cache Capacity'),
|
||||
type: 'number',
|
||||
description: nls.localize('theia/ai/completion/cacheCapacity/description',
|
||||
'Maximum number of code completions to store in the cache. A higher number can improve performance but will consume more memory.\
|
||||
Minimum value is 10, recommended range is between 50-200.'),
|
||||
default: 100,
|
||||
minimum: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// *****************************************************************************
|
||||
// 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 { PreferenceContribution } from '@theia/core';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { AICodeCompletionPreferencesSchema } from '../common/ai-code-completion-preference';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: AICodeCompletionPreferencesSchema });
|
||||
});
|
||||
28
packages/ai-code-completion/src/package.spec.ts
Normal file
28
packages/ai-code-completion/src/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('ai-code-completion package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
});
|
||||
28
packages/ai-code-completion/tsconfig.json
Normal file
28
packages/ai-code-completion/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ai-core"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../output"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user