Skip to content

Commit d9a6525

Browse files
authored
Allow filerting backticks in AI code completion (#14777)
* Allow filerting backticks in AI code completion fixed #14461 Signed-off-by: Jonas Helming <jhelming@eclipsesource.com>
1 parent d59071a commit d9a6525

File tree

5 files changed

+166
-2
lines changed

5 files changed

+166
-2
lines changed

packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { FrontendApplicationContribution, KeybindingContribution, PreferenceCont
2222
import { Agent } from '@theia/ai-core';
2323
import { AICodeCompletionPreferencesSchema } from './ai-code-completion-preference';
2424
import { AICodeInlineCompletionsProvider } from './ai-code-inline-completion-provider';
25+
import { CodeCompletionPostProcessor, DefaultCodeCompletionPostProcessor } from './code-completion-postprocessor';
2526

2627
export default new ContainerModule(bind => {
2728
bind(ILogger).toDynamicValue(ctx => {
@@ -36,4 +37,5 @@ export default new ContainerModule(bind => {
3637
bind(FrontendApplicationContribution).to(AIFrontendApplicationContribution);
3738
bind(KeybindingContribution).toService(AIFrontendApplicationContribution);
3839
bind(PreferenceContribution).toConstantValue({ schema: AICodeCompletionPreferencesSchema });
40+
bind(CodeCompletionPostProcessor).to(DefaultCodeCompletionPostProcessor).inSingletonScope();
3941
});

packages/ai-code-completion/src/browser/ai-code-completion-preference.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-pr
2020
export const PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE = 'ai-features.codeCompletion.automaticCodeCompletion';
2121
export const PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS = 'ai-features.codeCompletion.excludedFileExtensions';
2222
export const PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES = 'ai-features.codeCompletion.maxContextLines';
23+
export const PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS = 'ai-features.codeCompletion.stripBackticks';
2324

2425
export const AICodeCompletionPreferencesSchema: PreferenceSchema = {
2526
type: 'object',
@@ -48,6 +49,13 @@ export const AICodeCompletionPreferencesSchema: PreferenceSchema = {
4849
Set this to -1 to use the full file as context without any line limit and 0 to only use the current line.',
4950
default: -1,
5051
minimum: -1
52+
},
53+
[PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS]: {
54+
title: 'Strip Backticks from Inline Completions',
55+
type: 'boolean',
56+
description: 'Remove surrounding backticks from the code returned by some LLMs. If a backtick is detected, all content after the closing\
57+
backtick is stripped as well. This setting helps ensure plain code is returned when language models use markdown-like formatting.',
58+
default: true
5159
}
5260
}
5361
};

packages/ai-code-completion/src/browser/code-completion-agent.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { inject, injectable, named } from '@theia/core/shared/inversify';
2323
import * as monaco from '@theia/monaco-editor-core';
2424
import { PREF_AI_INLINE_COMPLETION_MAX_CONTEXT_LINES } from './ai-code-completion-preference';
2525
import { PreferenceService } from '@theia/core/lib/browser';
26+
import { CodeCompletionPostProcessor } from './code-completion-postprocessor';
2627

2728
export const CodeCompletionAgent = Symbol('CodeCompletionAgent');
2829
export interface CodeCompletionAgent extends Agent {
@@ -142,8 +143,10 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
142143
response: completionText,
143144
});
144145

146+
const postProcessedCompletionText = this.postProcessor.postProcess(completionText);
147+
145148
return {
146-
items: [{ insertText: completionText }],
149+
items: [{ insertText: postProcessedCompletionText }],
147150
enableForwardStability: true,
148151
};
149152
} catch (e) {
@@ -175,6 +178,9 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent {
175178
@inject(PreferenceService)
176179
protected preferences: PreferenceService;
177180

181+
@inject(CodeCompletionPostProcessor)
182+
protected postProcessor: CodeCompletionPostProcessor;
183+
178184
id = 'Code Completion';
179185
name = 'Code Completion';
180186
description =
@@ -190,7 +196,23 @@ Finish the following code snippet.
190196
191197
{{prefix}}[[MARKER]]{{suffix}}
192198
193-
Only return the exact replacement for [[MARKER]] to complete the snippet.`,
199+
Only return the exact replacement for [[MARKER]] to complete the snippet.`
200+
},
201+
{
202+
id: 'code-completion-prompt-next',
203+
variantOf: 'code-completion-prompt',
204+
template: `{{!-- Made improvements or adaptations to this prompt template? We’d love for you to share it with the community! Contribute back here:
205+
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
206+
## Code snippet
207+
\`\`\`
208+
{{ prefix }}[[MARKER]]{{ suffix }}
209+
\`\`\`
210+
211+
## Meta Data
212+
- File: {{file}}
213+
- Language: {{language}}
214+
215+
Replace [[MARKER]] with the exact code to complete the code snippet. Return only the replacement of [[MAKRER]] as plain text.`,
194216
},
195217
];
196218
languageModelRequirements: LanguageModelRequirement[] = [
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18+
let disableJSDOM = enableJSDOM();
19+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
20+
FrontendApplicationConfigProvider.set({});
21+
22+
import { expect } from 'chai';
23+
import { DefaultCodeCompletionPostProcessor } from './code-completion-postprocessor';
24+
25+
disableJSDOM();
26+
27+
describe('CodeCompletionAgentImpl', () => {
28+
let codeCompletionProcessor: DefaultCodeCompletionPostProcessor;
29+
before(() => {
30+
disableJSDOM = enableJSDOM();
31+
codeCompletionProcessor = new DefaultCodeCompletionPostProcessor();
32+
});
33+
34+
after(() => {
35+
// Disable JSDOM after all tests
36+
disableJSDOM();
37+
});
38+
39+
describe('stripBackticks', () => {
40+
41+
it('should remove surrounding backticks and language (TypeScript)', () => {
42+
const input = '```TypeScript\nconsole.log(\"Hello, World!\");```';
43+
const output = codeCompletionProcessor.stripBackticks(input);
44+
expect(output).to.equal('console.log("Hello, World!");');
45+
});
46+
47+
it('should remove surrounding backticks and language (md)', () => {
48+
const input = '```md\nconsole.log(\"Hello, World!\");```';
49+
const output = codeCompletionProcessor.stripBackticks(input);
50+
expect(output).to.equal('console.log("Hello, World!");');
51+
});
52+
53+
it('should remove all text after second occurrence of backticks', () => {
54+
const input = '```js\nlet x = 10;\n```\nTrailing text should be removed';
55+
const output = codeCompletionProcessor.stripBackticks(input);
56+
expect(output).to.equal('let x = 10;');
57+
});
58+
59+
it('should return the text unchanged if no surrounding backticks', () => {
60+
const input = 'console.log(\"Hello, World!\");';
61+
const output = codeCompletionProcessor.stripBackticks(input);
62+
expect(output).to.equal('console.log("Hello, World!");');
63+
});
64+
65+
it('should remove surrounding backticks without language', () => {
66+
const input = '```\nconsole.log(\"Hello, World!\");```';
67+
const output = codeCompletionProcessor.stripBackticks(input);
68+
expect(output).to.equal('console.log("Hello, World!");');
69+
});
70+
71+
it('should handle text starting with backticks but no second delimiter', () => {
72+
const input = '```python\nprint(\"Hello, World!\")';
73+
const output = codeCompletionProcessor.stripBackticks(input);
74+
expect(output).to.equal('print("Hello, World!")');
75+
});
76+
77+
it('should handle multiple internal backticks correctly', () => {
78+
const input = '```\nFoo```Bar```FooBar```';
79+
const output = codeCompletionProcessor.stripBackticks(input);
80+
expect(output).to.equal('Foo```Bar```FooBar');
81+
});
82+
83+
});
84+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 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 { inject, injectable } from '@theia/core/shared/inversify';
18+
import { PreferenceService } from '@theia/core/lib/browser';
19+
import { PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS } from './ai-code-completion-preference';
20+
21+
export interface CodeCompletionPostProcessor {
22+
postProcess(text: string): string;
23+
}
24+
export const CodeCompletionPostProcessor = Symbol('CodeCompletionPostProcessor');
25+
26+
@injectable()
27+
export class DefaultCodeCompletionPostProcessor {
28+
29+
@inject(PreferenceService)
30+
protected readonly preferenceService: PreferenceService;
31+
32+
public postProcess(text: string): string {
33+
if (this.preferenceService.get<boolean>(PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS, true)) {
34+
return this.stripBackticks(text);
35+
}
36+
return text;
37+
}
38+
39+
public stripBackticks(text: string): string {
40+
if (text.startsWith('```')) {
41+
// Remove the first backticks and any language identifier
42+
const startRemoved = text.slice(3).replace(/^\w*\n/, '');
43+
const lastBacktickIndex = startRemoved.lastIndexOf('```');
44+
return lastBacktickIndex !== -1 ? startRemoved.slice(0, lastBacktickIndex).trim() : startRemoved.trim();
45+
}
46+
return text;
47+
}
48+
}

0 commit comments

Comments
 (0)