Skip to content

Commit 2cf0305

Browse files
Samiya CaurDevtools-frontend LUCI CQ
authored andcommitted
Add AI code generation teaser to the provider
Pending: - Add logic for triggering and aborting AI code generation requests - Add logic to detect different types of comments Bug: 448063927 Change-Id: I1ce85081ed95c08b3c86616d99e2bb660d84f3c8 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7140739 Auto-Submit: Samiya Caur <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Samiya Caur <[email protected]>
1 parent 346408a commit 2cf0305

File tree

8 files changed

+385
-5
lines changed

8 files changed

+385
-5
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ grd_files_bundled_sources = [
401401
"front_end/js_app.html",
402402
"front_end/models/ai_assistance/ai_assistance.js",
403403
"front_end/models/ai_code_completion/ai_code_completion.js",
404+
"front_end/models/ai_code_generation/ai_code_generation.js",
404405
"front_end/models/autofill_manager/autofill_manager.js",
405406
"front_end/models/badges/badges.js",
406407
"front_end/models/bindings/bindings.js",
@@ -1063,6 +1064,8 @@ grd_files_unbundled_sources = [
10631064
"front_end/models/ai_assistance/performance/AIQueries.js",
10641065
"front_end/models/ai_code_completion/AiCodeCompletion.js",
10651066
"front_end/models/ai_code_completion/debug.js",
1067+
"front_end/models/ai_code_generation/AiCodeGeneration.js",
1068+
"front_end/models/ai_code_generation/debug.js",
10661069
"front_end/models/autofill_manager/AutofillManager.js",
10671070
"front_end/models/badges/AiExplorerBadge.js",
10681071
"front_end/models/badges/Badge.js",
@@ -2460,6 +2463,7 @@ grd_files_unbundled_sources = [
24602463
"front_end/ui/components/switch/switch.css.js",
24612464
"front_end/ui/components/text_editor/AiCodeCompletionProvider.js",
24622465
"front_end/ui/components/text_editor/AiCodeCompletionTeaserPlaceholder.js",
2466+
"front_end/ui/components/text_editor/AiCodeGenerationProvider.js",
24632467
"front_end/ui/components/text_editor/AutocompleteHistory.js",
24642468
"front_end/ui/components/text_editor/ExecutionPositionHighlighter.js",
24652469
"front_end/ui/components/text_editor/TextEditor.js",

front_end/models/ai_code_generation/AiCodeGeneration.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,16 @@ export class AiCodeGeneration {
150150

151151
return response;
152152
}
153+
154+
static isAiCodeGenerationEnabled(locale: string): boolean {
155+
if (!locale.startsWith('en-')) {
156+
return false;
157+
}
158+
const aidaAvailability = Root.Runtime.hostConfig.aidaAvailability;
159+
if (!aidaAvailability || aidaAvailability.blockedByGeo || aidaAvailability.blockedByAge ||
160+
aidaAvailability.blockedByEnterprisePolicy) {
161+
return false;
162+
}
163+
return Boolean(aidaAvailability.enabled && Root.Runtime.hostConfig.devToolsAiCodeGeneration?.enabled);
164+
}
153165
}

front_end/panels/sources/AiCodeCompletionPlugin.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ describeWithEnvironment('AiCodeCompletionPlugin', () => {
7575
});
7676
sinon.stub(TextEditor.AiCodeCompletionProvider.AiCodeCompletionProvider, 'createInstance')
7777
.returns(sinon.createStubInstance(TextEditor.AiCodeCompletionProvider.AiCodeCompletionProvider));
78-
sinon.stub(Host.AidaClient.AidaClient, 'checkAccessPreconditions')
79-
.resolves(Host.AidaClient.AidaAccessPreconditions.AVAILABLE);
78+
sinon.stub(Host.AidaClient.HostConfigTracker, 'instance').returns({
79+
addEventListener: () => {},
80+
removeEventListener: () => {},
81+
dispose: () => {},
82+
} as unknown as Host.AidaClient.HostConfigTracker);
8083
});
8184

8285
afterEach(() => {

front_end/ui/components/text_editor/AiCodeCompletionTeaserPlaceholder.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// found in the LICENSE file.
44
/* eslint-disable @devtools/no-imperative-dom-api */
55

6-
import type * as PanelCommon from '../../../panels/common/common.js';
76
import * as CM from '../../../third_party/codemirror.next/codemirror.next.js';
7+
import type * as UI from '../../../ui/legacy/legacy.js';
88

99
export function flattenRect(rect: DOMRect, left: boolean): {
1010
left: number,
@@ -16,8 +16,17 @@ export function flattenRect(rect: DOMRect, left: boolean): {
1616
return {left: x, right: x, top: rect.top, bottom: rect.bottom};
1717
}
1818

19+
// TODO(b/462393094): Rename this to be a generic accessible placeholder
20+
/**
21+
* A CodeMirror WidgetType that displays a UI.Widget.Widget as a placeholder.
22+
*
23+
* This custom placeholder implementation is used in place of the default
24+
* CodeMirror placeholder to provide better accessibility. Specifically,
25+
* it ensures that screen readers can properly announce the content within
26+
* the encapsulated widget.
27+
*/
1928
export class AiCodeCompletionTeaserPlaceholder extends CM.WidgetType {
20-
constructor(readonly teaser: PanelCommon.AiCodeCompletionTeaser) {
29+
constructor(readonly teaser: UI.Widget.Widget) {
2130
super();
2231
}
2332

@@ -64,7 +73,7 @@ export class AiCodeCompletionTeaserPlaceholder extends CM.WidgetType {
6473
}
6574
}
6675

67-
export function aiCodeCompletionTeaserPlaceholder(teaser: PanelCommon.AiCodeCompletionTeaser): CM.Extension {
76+
export function aiCodeCompletionTeaserPlaceholder(teaser: UI.Widget.Widget): CM.Extension {
6877
const plugin = CM.ViewPlugin.fromClass(class {
6978
placeholder: CM.DecorationSet;
7079

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Common from '../../../core/common/common.js';
6+
import * as Host from '../../../core/host/host.js';
7+
import * as PanelCommon from '../../../panels/common/common.js';
8+
import {renderElementIntoDOM} from '../../../testing/DOMHelpers.js';
9+
import {describeWithEnvironment, updateHostConfig} from '../../../testing/EnvironmentHelpers.js';
10+
import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
11+
12+
import {AiCodeGenerationProvider, TextEditor} from './text_editor.js';
13+
14+
function createEditorWithProvider(doc: string):
15+
{editor: TextEditor.TextEditor, provider: AiCodeGenerationProvider.AiCodeGenerationProvider} {
16+
const provider = AiCodeGenerationProvider.AiCodeGenerationProvider.createInstance();
17+
const editor = new TextEditor.TextEditor(
18+
CodeMirror.EditorState.create({
19+
doc,
20+
extensions: [
21+
provider.extension(),
22+
],
23+
}),
24+
);
25+
renderElementIntoDOM(editor);
26+
provider.editorInitialized(editor);
27+
return {editor, provider};
28+
}
29+
30+
describeWithEnvironment('AiCodeGenerationProvider', () => {
31+
let clock: sinon.SinonFakeTimers;
32+
33+
beforeEach(() => {
34+
clock = sinon.useFakeTimers();
35+
updateHostConfig({
36+
devToolsAiCodeGeneration: {
37+
enabled: true,
38+
},
39+
aidaAvailability: {
40+
enabled: true,
41+
blockedByAge: false,
42+
blockedByGeo: false,
43+
}
44+
});
45+
sinon.stub(Host.AidaClient.AidaClient, 'checkAccessPreconditions')
46+
.resolves(Host.AidaClient.AidaAccessPreconditions.AVAILABLE);
47+
sinon.stub(Host.AidaClient.HostConfigTracker, 'instance').returns({
48+
addEventListener: () => {},
49+
removeEventListener: () => {},
50+
dispose: () => {},
51+
} as unknown as Host.AidaClient.HostConfigTracker);
52+
Common.Settings.Settings.instance().settingForTest('ai-code-completion-enabled').set(true);
53+
});
54+
55+
afterEach(() => {
56+
clock.restore();
57+
});
58+
59+
it('does not create a provider when the feature is disabled', () => {
60+
updateHostConfig({
61+
devToolsAiCodeGeneration: {
62+
enabled: false,
63+
},
64+
});
65+
assert.throws(() => createEditorWithProvider(''), 'AI code generation feature is not enabled.');
66+
});
67+
68+
describe('Teaser decoration', () => {
69+
it('shows teaser when cursor is at the end of a comment line', async () => {
70+
const {editor, provider} = createEditorWithProvider('// Hello');
71+
editor.dispatch({selection: {anchor: 8}});
72+
await clock.tickAsync(0);
73+
assert.isNotNull(editor.editor.dom.querySelector('.cm-placeholder'));
74+
provider.dispose();
75+
});
76+
77+
it('hides teaser when cursor is not at the end of the line', async () => {
78+
const {editor, provider} = createEditorWithProvider('// Hello');
79+
editor.dispatch({selection: {anchor: 5}});
80+
await clock.tickAsync(0);
81+
assert.isNull(editor.editor.dom.querySelector('.cm-placeholder'));
82+
provider.dispose();
83+
});
84+
85+
it('hides teaser when the line is not a comment', async () => {
86+
const {editor, provider} = createEditorWithProvider('console');
87+
editor.dispatch({selection: {anchor: 7}});
88+
await clock.tickAsync(0);
89+
assert.isNull(editor.editor.dom.querySelector('.cm-placeholder'));
90+
provider.dispose();
91+
});
92+
93+
it('hides teaser when mode is DISMISSED', async () => {
94+
const {editor, provider} = createEditorWithProvider('// Hello');
95+
editor.dispatch({selection: {anchor: 8}});
96+
await clock.tickAsync(0);
97+
assert.isNotNull(editor.editor.dom.querySelector('.cm-placeholder'));
98+
99+
editor.dispatch({
100+
effects: AiCodeGenerationProvider.setAiCodeGenerationTeaserMode.of(
101+
AiCodeGenerationProvider.AiCodeGenerationTeaserMode.DISMISSED)
102+
});
103+
await clock.tickAsync(0);
104+
assert.isNull(editor.editor.dom.querySelector('.cm-placeholder'));
105+
provider.dispose();
106+
});
107+
108+
it('shows teaser again after a document change', async () => {
109+
const {editor, provider} = createEditorWithProvider('// Hello');
110+
editor.dispatch({selection: {anchor: 8}});
111+
await clock.tickAsync(0);
112+
assert.isNotNull(editor.editor.dom.querySelector('.cm-placeholder'));
113+
114+
editor.dispatch({
115+
effects: AiCodeGenerationProvider.setAiCodeGenerationTeaserMode.of(
116+
AiCodeGenerationProvider.AiCodeGenerationTeaserMode.DISMISSED)
117+
});
118+
await clock.tickAsync(0);
119+
assert.isNull(editor.editor.dom.querySelector('.cm-placeholder'));
120+
121+
editor.dispatch({changes: {from: 8, insert: 'W'}, selection: {anchor: 9}});
122+
await clock.tickAsync(0);
123+
assert.isNotNull(editor.editor.dom.querySelector('.cm-placeholder'));
124+
provider.dispose();
125+
});
126+
});
127+
128+
describe('Editor keymap', () => {
129+
it('dismisses teaser on Escape when loading', async () => {
130+
const {editor, provider} = createEditorWithProvider('// Hello');
131+
sinon.stub(PanelCommon.AiCodeGenerationTeaser.prototype, 'loading').value(true);
132+
sinon.stub(PanelCommon.AiCodeGenerationTeaser.prototype, 'isShowing').returns(true);
133+
editor.dispatch({selection: {anchor: 8}});
134+
await clock.tickAsync(0);
135+
136+
const dispatchSpy = sinon.spy(editor, 'dispatch');
137+
editor.editor.contentDOM.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'}));
138+
139+
sinon.assert.calledOnce(dispatchSpy);
140+
sinon.assert.calledWith(dispatchSpy, {
141+
effects: AiCodeGenerationProvider.setAiCodeGenerationTeaserMode.of(
142+
AiCodeGenerationProvider.AiCodeGenerationTeaserMode.DISMISSED)
143+
});
144+
provider.dispose();
145+
});
146+
147+
it('triggers loading state on Ctrl+I', async () => {
148+
const {editor, provider} = createEditorWithProvider('// Hello');
149+
const generationTeaser = sinon.spy(PanelCommon.AiCodeGenerationTeaser.prototype, 'loading', ['set']);
150+
sinon.stub(PanelCommon.AiCodeGenerationTeaser.prototype, 'isShowing').returns(true);
151+
editor.dispatch({selection: {anchor: 8}});
152+
await clock.tickAsync(0);
153+
154+
const event = new KeyboardEvent('keydown', {
155+
key: 'i',
156+
ctrlKey: Host.Platform.isMac() ? false : true,
157+
metaKey: Host.Platform.isMac() ? true : false,
158+
});
159+
editor.editor.contentDOM.dispatchEvent(event);
160+
161+
sinon.assert.calledOnce(generationTeaser.set);
162+
sinon.assert.calledWith(generationTeaser.set, true);
163+
provider.dispose();
164+
});
165+
});
166+
});

0 commit comments

Comments
 (0)