@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.
Files changed (49) hide show
  1. package/lib/browser/ai-code-completion-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-code-completion-frontend-module.js +6 -4
  3. package/lib/browser/ai-code-completion-frontend-module.js.map +1 -1
  4. package/lib/browser/ai-code-completion-preference.d.ts +1 -0
  5. package/lib/browser/ai-code-completion-preference.d.ts.map +1 -1
  6. package/lib/browser/ai-code-completion-preference.js +10 -1
  7. package/lib/browser/ai-code-completion-preference.js.map +1 -1
  8. package/lib/browser/ai-code-frontend-application-contribution.d.ts +1 -0
  9. package/lib/browser/ai-code-frontend-application-contribution.d.ts.map +1 -1
  10. package/lib/browser/ai-code-frontend-application-contribution.js +18 -2
  11. package/lib/browser/ai-code-frontend-application-contribution.js.map +1 -1
  12. package/lib/browser/code-completion-agent.d.ts +0 -3
  13. package/lib/browser/code-completion-agent.d.ts.map +1 -1
  14. package/lib/browser/code-completion-agent.js +7 -45
  15. package/lib/browser/code-completion-agent.js.map +1 -1
  16. package/lib/browser/code-completion-cache.d.ts +40 -0
  17. package/lib/browser/code-completion-cache.d.ts.map +1 -0
  18. package/lib/browser/code-completion-cache.js +116 -0
  19. package/lib/browser/code-completion-cache.js.map +1 -0
  20. package/lib/browser/code-completion-prompt-template.d.ts.map +1 -1
  21. package/lib/browser/code-completion-prompt-template.js +41 -6
  22. package/lib/browser/code-completion-prompt-template.js.map +1 -1
  23. package/lib/browser/code-completion-variable-context.d.ts +11 -0
  24. package/lib/browser/code-completion-variable-context.d.ts.map +1 -0
  25. package/lib/browser/code-completion-variable-context.js +26 -0
  26. package/lib/browser/code-completion-variable-context.js.map +1 -0
  27. package/lib/browser/code-completion-variable-contribution.d.ts +16 -0
  28. package/lib/browser/code-completion-variable-contribution.d.ts.map +1 -0
  29. package/lib/browser/code-completion-variable-contribution.js +127 -0
  30. package/lib/browser/code-completion-variable-contribution.js.map +1 -0
  31. package/lib/browser/code-completion-variable-contribution.spec.d.ts +2 -0
  32. package/lib/browser/code-completion-variable-contribution.spec.d.ts.map +1 -0
  33. package/lib/browser/code-completion-variable-contribution.spec.js +143 -0
  34. package/lib/browser/code-completion-variable-contribution.spec.js.map +1 -0
  35. package/lib/browser/code-completion-variables.d.ts +6 -0
  36. package/lib/browser/code-completion-variables.d.ts.map +1 -0
  37. package/lib/browser/code-completion-variables.js +39 -0
  38. package/lib/browser/code-completion-variables.js.map +1 -0
  39. package/package.json +7 -7
  40. package/src/browser/ai-code-completion-frontend-module.ts +6 -4
  41. package/src/browser/ai-code-completion-preference.ts +10 -0
  42. package/src/browser/ai-code-frontend-application-contribution.ts +25 -3
  43. package/src/browser/code-completion-agent.ts +8 -57
  44. package/src/browser/code-completion-cache.ts +131 -0
  45. package/src/browser/code-completion-prompt-template.ts +41 -6
  46. package/src/browser/code-completion-variable-context.ts +30 -0
  47. package/src/browser/code-completion-variable-contribution.spec.ts +160 -0
  48. package/src/browser/code-completion-variable-contribution.ts +145 -0
  49. 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 maxContextLines = this.preferences.get<number>(PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES, -1);
66
-
67
- let prefixStartLine = 1;
68
- let suffixEndLine = model.getLineCount();
69
- // if maxContextLines is -1, use the full file as context without any line limit
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', { prefix, suffix, file, language })
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 {{file}}.
22
- The language of the file is {{language}}. Return your result as plain text without markdown formatting.
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
- {{prefix}}[[MARKER]]{{suffix}}
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
- {{ prefix }}[[MARKER]]{{ suffix }}
71
+ {{${PREFIX.id}}}[[MARKER]]{{${SUFFIX.id}}}
37
72
  \`\`\`
38
73
 
39
74
  ## Meta Data
40
- - File: {{file}}
41
- - Language: {{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
+ };