@theia/ai-code-completion 1.63.0-next.0 → 1.63.0-next.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-code-completion-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-code-completion-frontend-module.js +6 -4
- package/lib/browser/ai-code-completion-frontend-module.js.map +1 -1
- package/lib/browser/ai-code-completion-preference.d.ts +1 -0
- package/lib/browser/ai-code-completion-preference.d.ts.map +1 -1
- package/lib/browser/ai-code-completion-preference.js +10 -1
- package/lib/browser/ai-code-completion-preference.js.map +1 -1
- package/lib/browser/ai-code-frontend-application-contribution.d.ts +1 -0
- package/lib/browser/ai-code-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/ai-code-frontend-application-contribution.js +18 -2
- package/lib/browser/ai-code-frontend-application-contribution.js.map +1 -1
- package/lib/browser/code-completion-agent.d.ts +0 -3
- package/lib/browser/code-completion-agent.d.ts.map +1 -1
- package/lib/browser/code-completion-agent.js +7 -45
- package/lib/browser/code-completion-agent.js.map +1 -1
- package/lib/browser/code-completion-cache.d.ts +40 -0
- package/lib/browser/code-completion-cache.d.ts.map +1 -0
- package/lib/browser/code-completion-cache.js +116 -0
- package/lib/browser/code-completion-cache.js.map +1 -0
- package/lib/browser/code-completion-prompt-template.d.ts.map +1 -1
- package/lib/browser/code-completion-prompt-template.js +41 -6
- package/lib/browser/code-completion-prompt-template.js.map +1 -1
- package/lib/browser/code-completion-variable-context.d.ts +11 -0
- package/lib/browser/code-completion-variable-context.d.ts.map +1 -0
- package/lib/browser/code-completion-variable-context.js +26 -0
- package/lib/browser/code-completion-variable-context.js.map +1 -0
- package/lib/browser/code-completion-variable-contribution.d.ts +16 -0
- package/lib/browser/code-completion-variable-contribution.d.ts.map +1 -0
- package/lib/browser/code-completion-variable-contribution.js +127 -0
- package/lib/browser/code-completion-variable-contribution.js.map +1 -0
- package/lib/browser/code-completion-variable-contribution.spec.d.ts +2 -0
- package/lib/browser/code-completion-variable-contribution.spec.d.ts.map +1 -0
- package/lib/browser/code-completion-variable-contribution.spec.js +143 -0
- package/lib/browser/code-completion-variable-contribution.spec.js.map +1 -0
- package/lib/browser/code-completion-variables.d.ts +6 -0
- package/lib/browser/code-completion-variables.d.ts.map +1 -0
- package/lib/browser/code-completion-variables.js +39 -0
- package/lib/browser/code-completion-variables.js.map +1 -0
- package/package.json +7 -7
- package/src/browser/ai-code-completion-frontend-module.ts +6 -4
- package/src/browser/ai-code-completion-preference.ts +10 -0
- package/src/browser/ai-code-frontend-application-contribution.ts +25 -3
- package/src/browser/code-completion-agent.ts +8 -57
- package/src/browser/code-completion-cache.ts +131 -0
- package/src/browser/code-completion-prompt-template.ts +41 -6
- package/src/browser/code-completion-variable-context.ts +30 -0
- package/src/browser/code-completion-variable-contribution.spec.ts +160 -0
- package/src/browser/code-completion-variable-contribution.ts +145 -0
- package/src/browser/code-completion-variables.ts +41 -0
|
@@ -22,12 +22,11 @@ import {
|
|
|
22
22
|
UserRequest
|
|
23
23
|
} from '@theia/ai-core/lib/common';
|
|
24
24
|
import { generateUuid, ILogger, nls, ProgressService } from '@theia/core';
|
|
25
|
-
import { PreferenceService } from '@theia/core/lib/browser';
|
|
26
25
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
27
26
|
import * as monaco from '@theia/monaco-editor-core';
|
|
28
|
-
import { PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES } from './ai-code-completion-preference';
|
|
29
27
|
import { codeCompletionPrompts } from './code-completion-prompt-template';
|
|
30
28
|
import { CodeCompletionPostProcessor } from './code-completion-postprocessor';
|
|
29
|
+
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
|
31
30
|
|
|
32
31
|
export const CodeCompletionAgent = Symbol('CodeCompletionAgent');
|
|
33
32
|
export interface CodeCompletionAgent extends Agent {
|
|
@@ -62,56 +61,17 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
|
|
|
62
61
|
return undefined;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (maxContextLines === 0) {
|
|
72
|
-
// Only the cursor line
|
|
73
|
-
prefixStartLine = position.lineNumber;
|
|
74
|
-
suffixEndLine = position.lineNumber;
|
|
75
|
-
} else if (maxContextLines > 0) {
|
|
76
|
-
const linesBeforeCursor = position.lineNumber - 1;
|
|
77
|
-
const linesAfterCursor = model.getLineCount() - position.lineNumber;
|
|
78
|
-
|
|
79
|
-
// Allocate one more line to the prefix in case of an odd maxContextLines
|
|
80
|
-
const prefixLines = Math.min(
|
|
81
|
-
Math.ceil(maxContextLines / 2),
|
|
82
|
-
linesBeforeCursor
|
|
83
|
-
);
|
|
84
|
-
const suffixLines = Math.min(
|
|
85
|
-
Math.floor(maxContextLines / 2),
|
|
86
|
-
linesAfterCursor
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
prefixStartLine = Math.max(1, position.lineNumber - prefixLines);
|
|
90
|
-
suffixEndLine = Math.min(model.getLineCount(), position.lineNumber + suffixLines);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const prefix = model.getValueInRange({
|
|
94
|
-
startLineNumber: prefixStartLine,
|
|
95
|
-
startColumn: 1,
|
|
96
|
-
endLineNumber: position.lineNumber,
|
|
97
|
-
endColumn: position.column,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const suffix = model.getValueInRange({
|
|
101
|
-
startLineNumber: position.lineNumber,
|
|
102
|
-
startColumn: position.column,
|
|
103
|
-
endLineNumber: suffixEndLine,
|
|
104
|
-
endColumn: model.getLineMaxColumn(suffixEndLine),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const file = model.uri.toString(false);
|
|
108
|
-
const language = model.getLanguageId();
|
|
64
|
+
const variableContext: CodeCompletionVariableContext = {
|
|
65
|
+
model,
|
|
66
|
+
position,
|
|
67
|
+
context
|
|
68
|
+
};
|
|
109
69
|
|
|
110
70
|
if (token.isCancellationRequested) {
|
|
111
71
|
return undefined;
|
|
112
72
|
}
|
|
113
73
|
const prompt = await this.promptService
|
|
114
|
-
.getResolvedPromptFragment('code-completion-prompt',
|
|
74
|
+
.getResolvedPromptFragment('code-completion-prompt', undefined, variableContext)
|
|
115
75
|
.then(p => p?.text);
|
|
116
76
|
if (!prompt) {
|
|
117
77
|
this.logger.error('No prompt found for code-completion-agent');
|
|
@@ -174,9 +134,6 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
|
|
|
174
134
|
@inject(ProgressService)
|
|
175
135
|
protected progressService: ProgressService;
|
|
176
136
|
|
|
177
|
-
@inject(PreferenceService)
|
|
178
|
-
protected preferences: PreferenceService;
|
|
179
|
-
|
|
180
137
|
@inject(CodeCompletionPostProcessor)
|
|
181
138
|
protected postProcessor: CodeCompletionPostProcessor;
|
|
182
139
|
|
|
@@ -193,11 +150,5 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
|
|
|
193
150
|
];
|
|
194
151
|
readonly variables: string[] = [];
|
|
195
152
|
readonly functions: string[] = [];
|
|
196
|
-
readonly agentSpecificVariables: AgentSpecificVariables[] = [
|
|
197
|
-
{ name: 'file', usedInPrompt: true, description: 'The uri of the file being edited.' },
|
|
198
|
-
{ name: 'language', usedInPrompt: true, description: 'The languageId of the file being edited.' },
|
|
199
|
-
{ name: 'prefix', usedInPrompt: true, description: 'The code before the current position of the cursor.' },
|
|
200
|
-
{ name: 'suffix', usedInPrompt: true, description: 'The code after the current position of the cursor.' }
|
|
201
|
-
];
|
|
202
|
-
readonly tags?: string[] | undefined;
|
|
153
|
+
readonly agentSpecificVariables: AgentSpecificVariables[] = [];
|
|
203
154
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
19
|
+
|
|
20
|
+
@injectable()
|
|
21
|
+
export class CodeCompletionCache {
|
|
22
|
+
private cache: Map<string, CacheEntry>;
|
|
23
|
+
private maxSize = 100;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.cache = new Map<string, CacheEntry>();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a unique cache key based on input parameters
|
|
31
|
+
* @param filePath Path of the current file
|
|
32
|
+
* @param lineNumber Current line number
|
|
33
|
+
* @param lineText Context prefix for completion
|
|
34
|
+
* @returns Unique cache key
|
|
35
|
+
*/
|
|
36
|
+
generateKey(filePath: string, model: monaco.editor.ITextModel, position: monaco.Position): string {
|
|
37
|
+
const lineNumber = position.lineNumber;
|
|
38
|
+
const beforeCursorLineRange = new monaco.Range(
|
|
39
|
+
position.lineNumber, 1,
|
|
40
|
+
position.lineNumber, position.column
|
|
41
|
+
);
|
|
42
|
+
const prefix = model.getValueInRange(beforeCursorLineRange);
|
|
43
|
+
const afterCursorLineRange = new monaco.Range(
|
|
44
|
+
position.lineNumber, position.column,
|
|
45
|
+
position.lineNumber, model.getLineMaxColumn(position.lineNumber)
|
|
46
|
+
);
|
|
47
|
+
const suffix = model.getValueInRange(afterCursorLineRange);
|
|
48
|
+
const key = JSON.stringify({
|
|
49
|
+
filePath,
|
|
50
|
+
lineNumber,
|
|
51
|
+
prefix,
|
|
52
|
+
suffix
|
|
53
|
+
});
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a cached completion if available
|
|
59
|
+
* @param key Cache key
|
|
60
|
+
* @returns Cached completion or undefined
|
|
61
|
+
*/
|
|
62
|
+
get(key: string): monaco.languages.InlineCompletions | undefined {
|
|
63
|
+
const entry = this.cache.get(key);
|
|
64
|
+
if (entry) {
|
|
65
|
+
// Update the entry's last accessed time
|
|
66
|
+
entry.lastAccessed = Date.now();
|
|
67
|
+
return entry.value;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Store a completion in the cache
|
|
74
|
+
* @param key Cache key
|
|
75
|
+
* @param value Completion value to cache
|
|
76
|
+
*/
|
|
77
|
+
put(key: string, value: monaco.languages.InlineCompletions | undefined): void {
|
|
78
|
+
// If cache is full, remove the least recently used entry
|
|
79
|
+
if (this.cache.size >= this.maxSize) {
|
|
80
|
+
this.removeLeastRecentlyUsed();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.cache.set(key, {
|
|
84
|
+
value,
|
|
85
|
+
lastAccessed: Date.now()
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear the entire cache
|
|
91
|
+
*/
|
|
92
|
+
clear(): void {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove the least recently used entry from the cache
|
|
98
|
+
*/
|
|
99
|
+
private removeLeastRecentlyUsed(): void {
|
|
100
|
+
let oldestKey: string | undefined;
|
|
101
|
+
let oldestTime = Infinity;
|
|
102
|
+
|
|
103
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
104
|
+
if (entry.lastAccessed < oldestTime) {
|
|
105
|
+
oldestKey = key;
|
|
106
|
+
oldestTime = entry.lastAccessed;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (oldestKey) {
|
|
111
|
+
this.cache.delete(oldestKey);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set the maximum cache size
|
|
117
|
+
* @param size New maximum cache size
|
|
118
|
+
*/
|
|
119
|
+
setMaxSize(size: number): void {
|
|
120
|
+
this.maxSize = size;
|
|
121
|
+
// Trim cache if it exceeds new size
|
|
122
|
+
while (this.cache.size > this.maxSize) {
|
|
123
|
+
this.removeLeastRecentlyUsed();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface CacheEntry {
|
|
129
|
+
value: monaco.languages.InlineCompletions | undefined;
|
|
130
|
+
lastAccessed: number;
|
|
131
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// *****************************************************************************
|
|
11
11
|
|
|
12
12
|
import { PromptVariantSet } from '@theia/ai-core/lib/common';
|
|
13
|
+
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
|
13
14
|
|
|
14
15
|
export const codeCompletionPrompts: PromptVariantSet[] = [{
|
|
15
16
|
id: 'code-completion-prompt',
|
|
@@ -18,13 +19,47 @@ export const codeCompletionPrompts: PromptVariantSet[] = [{
|
|
|
18
19
|
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
|
19
20
|
Made improvements or adaptations to this prompt template? We’d love for you to share it with the community! Contribute back here:
|
|
20
21
|
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
|
21
|
-
You are a code completion agent. The current file you have to complete is named {{
|
|
22
|
-
The language of the file is {{
|
|
22
|
+
You are a code completion agent. The current file you have to complete is named {{${FILE.id}}}.
|
|
23
|
+
The language of the file is {{${LANGUAGE.id}}}. Return your result as plain text without markdown formatting.
|
|
23
24
|
Finish the following code snippet.
|
|
24
25
|
|
|
25
|
-
{{
|
|
26
|
+
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
|
26
27
|
|
|
27
28
|
Only return the exact replacement for [[MARKER]] to complete the snippet.`
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'code-completion-prompt-next',
|
|
32
|
+
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
|
33
|
+
Made improvements or adaptations to this prompt template? We'd love for you to share it with the community! Contribute back here:
|
|
34
|
+
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
|
35
|
+
# System Role
|
|
36
|
+
You are an expert AI code completion assistant focused on generating precise, contextually appropriate code snippets.
|
|
37
|
+
|
|
38
|
+
## Code Context
|
|
39
|
+
\`\`\`
|
|
40
|
+
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
## Metadata
|
|
44
|
+
- File: {{${FILE.id}}}
|
|
45
|
+
- Programming Language: {{${LANGUAGE.id}}}
|
|
46
|
+
- Project Context: {{prompt:project-info}}
|
|
47
|
+
|
|
48
|
+
# Completion Guidelines
|
|
49
|
+
1. Analyze the surrounding code context carefully.
|
|
50
|
+
2. Generate ONLY the code that should replace [[MARKER]].
|
|
51
|
+
3. Ensure the completion:
|
|
52
|
+
- Maintains the exact syntax of the surrounding code
|
|
53
|
+
- Follows best practices for the specific programming language
|
|
54
|
+
- Completes the code snippet logically and efficiently
|
|
55
|
+
4. Do NOT include any explanatory text, comments, or additional instructions.
|
|
56
|
+
5. Return ONLY the raw code replacement.
|
|
57
|
+
|
|
58
|
+
# Constraints
|
|
59
|
+
- Return strictly the code for [[MARKER]]
|
|
60
|
+
- Match indentation and style of surrounding code
|
|
61
|
+
- Prioritize readability and maintainability
|
|
62
|
+
- Consider language-specific idioms and patterns`
|
|
28
63
|
}],
|
|
29
64
|
defaultVariant: {
|
|
30
65
|
id: 'code-completion-prompt-default',
|
|
@@ -33,12 +68,12 @@ Made improvements or adaptations to this prompt template? We’d love for you to
|
|
|
33
68
|
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
|
|
34
69
|
## Code snippet
|
|
35
70
|
\`\`\`
|
|
36
|
-
{{
|
|
71
|
+
{{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
|
|
37
72
|
\`\`\`
|
|
38
73
|
|
|
39
74
|
## Meta Data
|
|
40
|
-
- File: {{
|
|
41
|
-
- Language: {{
|
|
75
|
+
- File: {{${FILE.id}}}
|
|
76
|
+
- Language: {{${LANGUAGE.id}}}
|
|
42
77
|
|
|
43
78
|
Replace [[MARKER]] with the exact code to complete the code snippet. Return only the replacement of [[MARKER]] as plain text.`,
|
|
44
79
|
},
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Lonti.com Pty Ltd.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { AIVariableContext } from '@theia/ai-core';
|
|
18
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
19
|
+
|
|
20
|
+
export interface CodeCompletionVariableContext {
|
|
21
|
+
model: monaco.editor.ITextModel,
|
|
22
|
+
position: monaco.Position,
|
|
23
|
+
context: monaco.languages.InlineCompletionContext
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export namespace CodeCompletionVariableContext {
|
|
27
|
+
export function is(context: AIVariableContext): context is CodeCompletionVariableContext {
|
|
28
|
+
return !!context && 'model' in context && 'position' in context && 'context' in context;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Lonti.com Pty Ltd.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
18
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { PreferenceService } from '@theia/core/lib/browser';
|
|
23
|
+
import { Container } from '@theia/core/shared/inversify';
|
|
24
|
+
import { editor, languages, Uri } from '@theia/monaco-editor-core/esm/vs/editor/editor.api';
|
|
25
|
+
import { expect } from 'chai';
|
|
26
|
+
import * as sinon from 'sinon';
|
|
27
|
+
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
|
28
|
+
import { CodeCompletionVariableContribution } from './code-completion-variable-contribution';
|
|
29
|
+
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
|
30
|
+
|
|
31
|
+
disableJSDOM();
|
|
32
|
+
|
|
33
|
+
describe('CodeCompletionVariableContribution', () => {
|
|
34
|
+
let contribution: CodeCompletionVariableContribution;
|
|
35
|
+
let model: editor.ITextModel;
|
|
36
|
+
|
|
37
|
+
before(() => {
|
|
38
|
+
disableJSDOM = enableJSDOM();
|
|
39
|
+
const container = new Container();
|
|
40
|
+
container.bind(PreferenceService).toConstantValue({
|
|
41
|
+
get: () => 1000,
|
|
42
|
+
});
|
|
43
|
+
container.bind(CodeCompletionVariableContribution).toSelf().inSingletonScope();
|
|
44
|
+
contribution = container.get(CodeCompletionVariableContribution);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
model = editor.createModel('//line 1\nconsole.\n//line 2', 'javascript', Uri.file('/home/user/workspace/test.js'));
|
|
49
|
+
sinon.stub(model, 'getLanguageId').returns('javascript');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
model.dispose();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
after(() => {
|
|
57
|
+
// Disable JSDOM after all tests
|
|
58
|
+
disableJSDOM();
|
|
59
|
+
model.dispose();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('canResolve', () => {
|
|
63
|
+
it('should be able to resolve the file from the CodeCompletionVariableContext', () => {
|
|
64
|
+
const context: CodeCompletionVariableContext = {
|
|
65
|
+
model,
|
|
66
|
+
position: model.getPositionAt(8),
|
|
67
|
+
context: {
|
|
68
|
+
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
|
69
|
+
selectedSuggestionInfo: undefined,
|
|
70
|
+
includeInlineEdits: false,
|
|
71
|
+
includeInlineCompletions: false
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
expect(contribution.canResolve({ variable: FILE }, context)).to.equal(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should not be able to resolve the file from unknown context', () => {
|
|
79
|
+
expect(contribution.canResolve({ variable: FILE }, {})).to.equal(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('resolve', () => {
|
|
84
|
+
it('should resolve the file variable', async () => {
|
|
85
|
+
const context: CodeCompletionVariableContext = {
|
|
86
|
+
model,
|
|
87
|
+
position: model.getPositionAt(17),
|
|
88
|
+
context: {
|
|
89
|
+
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
|
90
|
+
selectedSuggestionInfo: undefined,
|
|
91
|
+
includeInlineEdits: false,
|
|
92
|
+
includeInlineCompletions: false
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resolved = await contribution.resolve({ variable: FILE }, context);
|
|
97
|
+
expect(resolved).to.deep.equal({
|
|
98
|
+
variable: FILE,
|
|
99
|
+
value: 'file:///home/user/workspace/test.js'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should resolve the language variable', async () => {
|
|
104
|
+
const context: CodeCompletionVariableContext = {
|
|
105
|
+
model,
|
|
106
|
+
position: model.getPositionAt(17),
|
|
107
|
+
context: {
|
|
108
|
+
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
|
109
|
+
selectedSuggestionInfo: undefined,
|
|
110
|
+
includeInlineEdits: false,
|
|
111
|
+
includeInlineCompletions: false
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const resolved = await contribution.resolve({ variable: LANGUAGE }, context);
|
|
116
|
+
expect(resolved).to.deep.equal({
|
|
117
|
+
variable: LANGUAGE,
|
|
118
|
+
value: 'javascript'
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should resolve the prefix variable', async () => {
|
|
123
|
+
const context: CodeCompletionVariableContext = {
|
|
124
|
+
model,
|
|
125
|
+
position: model.getPositionAt(17),
|
|
126
|
+
context: {
|
|
127
|
+
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
|
128
|
+
selectedSuggestionInfo: undefined,
|
|
129
|
+
includeInlineEdits: false,
|
|
130
|
+
includeInlineCompletions: false
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const resolved = await contribution.resolve({ variable: PREFIX }, context);
|
|
135
|
+
expect(resolved).to.deep.equal({
|
|
136
|
+
variable: PREFIX,
|
|
137
|
+
value: '//line 1\nconsole.'
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should resolve the suffix variable', async () => {
|
|
142
|
+
const context: CodeCompletionVariableContext = {
|
|
143
|
+
model,
|
|
144
|
+
position: model.getPositionAt(17),
|
|
145
|
+
context: {
|
|
146
|
+
triggerKind: languages.InlineCompletionTriggerKind.Automatic,
|
|
147
|
+
selectedSuggestionInfo: undefined,
|
|
148
|
+
includeInlineEdits: false,
|
|
149
|
+
includeInlineCompletions: false
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const resolved = await contribution.resolve({ variable: SUFFIX }, context);
|
|
154
|
+
expect(resolved).to.deep.equal({
|
|
155
|
+
variable: SUFFIX,
|
|
156
|
+
value: '\n//line 2'
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Lonti.com Pty Ltd.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { AIVariableContext, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIVariable } from '@theia/ai-core';
|
|
18
|
+
import { FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
19
|
+
import { MaybePromise } from '@theia/core';
|
|
20
|
+
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
|
21
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
22
|
+
import { PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES } from './ai-code-completion-preference';
|
|
23
|
+
import { CodeCompletionVariableContext } from './code-completion-variable-context';
|
|
24
|
+
import { FILE, LANGUAGE, PREFIX, SUFFIX } from './code-completion-variables';
|
|
25
|
+
|
|
26
|
+
@injectable()
|
|
27
|
+
export class CodeCompletionVariableContribution implements FrontendVariableContribution, AIVariableResolver {
|
|
28
|
+
@inject(PreferenceService)
|
|
29
|
+
protected preferences: PreferenceService;
|
|
30
|
+
|
|
31
|
+
registerVariables(service: FrontendVariableService): void {
|
|
32
|
+
[
|
|
33
|
+
FILE,
|
|
34
|
+
PREFIX,
|
|
35
|
+
SUFFIX,
|
|
36
|
+
LANGUAGE
|
|
37
|
+
].forEach(variable => {
|
|
38
|
+
service.registerResolver(variable, this);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
canResolve(_request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
|
|
43
|
+
return CodeCompletionVariableContext.is(context) ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
|
47
|
+
if (!CodeCompletionVariableContext.is(context)) {
|
|
48
|
+
return Promise.resolve(undefined);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (request.variable.id) {
|
|
52
|
+
case FILE.id:
|
|
53
|
+
return this.resolveFile(context);
|
|
54
|
+
case LANGUAGE.id:
|
|
55
|
+
return this.resolveLanguage(context);
|
|
56
|
+
case PREFIX.id:
|
|
57
|
+
return this.resolvePrefix(context);
|
|
58
|
+
case SUFFIX.id:
|
|
59
|
+
return this.resolveSuffix(context);
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected resolvePrefix(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
|
66
|
+
const position = context.position;
|
|
67
|
+
const model = context.model;
|
|
68
|
+
const maxContextLines = this.preferences.get<number>(PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES, -1);
|
|
69
|
+
let prefixStartLine = 1;
|
|
70
|
+
|
|
71
|
+
if (maxContextLines === 0) {
|
|
72
|
+
// Only the cursor line
|
|
73
|
+
prefixStartLine = position.lineNumber;
|
|
74
|
+
} else if (maxContextLines > 0) {
|
|
75
|
+
const linesBeforeCursor = position.lineNumber - 1;
|
|
76
|
+
|
|
77
|
+
// Allocate one more line to the prefix in case of an odd maxContextLines
|
|
78
|
+
const prefixLines = Math.min(
|
|
79
|
+
Math.ceil(maxContextLines / 2),
|
|
80
|
+
linesBeforeCursor
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
prefixStartLine = Math.max(1, position.lineNumber - prefixLines);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const prefix = model.getValueInRange({
|
|
87
|
+
startLineNumber: prefixStartLine,
|
|
88
|
+
startColumn: 1,
|
|
89
|
+
endLineNumber: position.lineNumber,
|
|
90
|
+
endColumn: position.column,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
variable: PREFIX,
|
|
95
|
+
value: prefix
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected resolveSuffix(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
|
100
|
+
const position = context.position;
|
|
101
|
+
const model = context.model;
|
|
102
|
+
const maxContextLines = this.preferences.get<number>(PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES, -1);
|
|
103
|
+
let suffixEndLine = model.getLineCount();
|
|
104
|
+
|
|
105
|
+
if (maxContextLines === 0) {
|
|
106
|
+
suffixEndLine = position.lineNumber;
|
|
107
|
+
} else if (maxContextLines > 0) {
|
|
108
|
+
const linesAfterCursor = model.getLineCount() - position.lineNumber;
|
|
109
|
+
|
|
110
|
+
const suffixLines = Math.min(
|
|
111
|
+
Math.floor(maxContextLines / 2),
|
|
112
|
+
linesAfterCursor
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
suffixEndLine = Math.min(model.getLineCount(), position.lineNumber + suffixLines);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const suffix = model.getValueInRange({
|
|
119
|
+
startLineNumber: position.lineNumber,
|
|
120
|
+
startColumn: position.column,
|
|
121
|
+
endLineNumber: suffixEndLine,
|
|
122
|
+
endColumn: model.getLineMaxColumn(suffixEndLine),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
variable: SUFFIX,
|
|
127
|
+
value: suffix
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
protected resolveLanguage(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
|
132
|
+
return {
|
|
133
|
+
variable: LANGUAGE,
|
|
134
|
+
value: context.model.getLanguageId()
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected resolveFile(context: CodeCompletionVariableContext): ResolvedAIVariable | undefined {
|
|
139
|
+
return {
|
|
140
|
+
variable: FILE,
|
|
141
|
+
value: context.model.uri.toString(false)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Lonti.com Pty Ltd.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { AIVariable } from '@theia/ai-core/lib/common/variable-service';
|
|
18
|
+
|
|
19
|
+
export const FILE: AIVariable = {
|
|
20
|
+
id: 'codeCompletionFile',
|
|
21
|
+
name: 'codeCompletionFile',
|
|
22
|
+
description: 'The uri of the file being edited.',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PREFIX: AIVariable = {
|
|
26
|
+
id: 'codeCompletionPrefix',
|
|
27
|
+
name: 'codeCompletionPrefix',
|
|
28
|
+
description: 'The code before the current position of the cursor.',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SUFFIX: AIVariable = {
|
|
32
|
+
id: 'codeCompletionSuffix',
|
|
33
|
+
name: 'codeCompletionSuffix',
|
|
34
|
+
description: 'The code after the current position of the cursor.',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const LANGUAGE: AIVariable = {
|
|
38
|
+
id: 'codeCompletionLanguage',
|
|
39
|
+
name: 'codeCompletionLanguage',
|
|
40
|
+
description: 'The languageId of the file being edited.',
|
|
41
|
+
};
|