chrome-devtools-frontend 1.0.1562885 → 1.0.1563377

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.
@@ -1,11 +1,16 @@
1
1
  // Copyright 2025 The Chromium Authors
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
+ import '../../ui/components/tooltips/tooltips.js';
4
5
 
5
6
  import * as Host from '../../core/host/host.js';
6
7
  import * as i18n from '../../core/i18n/i18n.js';
8
+ import * as Root from '../../core/root/root.js';
9
+ import * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
10
+ import * as Buttons from '../../ui/components/buttons/buttons.js';
7
11
  import * as UI from '../../ui/legacy/legacy.js';
8
- import {html, nothing, render} from '../../ui/lit/lit.js';
12
+ import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
13
+ import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
9
14
 
10
15
  import styles from './aiCodeGenerationTeaser.css.js';
11
16
 
@@ -13,19 +18,55 @@ const UIStringsNotTranslate = {
13
18
  /**
14
19
  * @description Text for teaser to generate code.
15
20
  */
16
- ctrlItoGenerateCode: 'ctrl-i to generate code',
21
+ ctrlItoGenerateCode: 'Ctrl+I to generate code',
17
22
  /**
18
23
  * @description Text for teaser to generate code in Mac.
19
24
  */
20
- cmdItoGenerateCode: 'cmd-i to generate code',
25
+ cmdItoGenerateCode: 'Cmd+I to generate code',
21
26
  /**
22
- * Text for teaser when generating suggestion.
27
+ * @description Text for teaser when generating suggestion.
23
28
  */
24
29
  generating: 'Generating... (esc to cancel)',
25
30
  /**
26
- * Text for teaser for discoverability.
31
+ * @description Text for teaser for discoverability.
27
32
  */
28
33
  writeACommentToGenerateCode: 'Write a comment to generate code',
34
+ /**
35
+ * @description Text for teaser when suggestion has been generated.
36
+ */
37
+ tab: 'tab',
38
+ /**
39
+ * @description Text for teaser when suggestion has been generated.
40
+ */
41
+ toAccept: 'to accept',
42
+ /**
43
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Console panel.
44
+ */
45
+ tooltipDisclaimerTextForAiCodeGenerationInConsole:
46
+ 'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data may be seen by human reviewers to improve this feature.',
47
+ /**
48
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Console panel.
49
+ */
50
+ tooltipDisclaimerTextForAiCodeGenerationNoLoggingInConsole:
51
+ 'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
52
+ /**
53
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
54
+ */
55
+ tooltipDisclaimerTextForAiCodeGenerationInSources:
56
+ 'To generate code suggestions, the contents of the currently open file are shared with Google. This data may be seen by human reviewers to improve this feature.',
57
+ /**
58
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
59
+ */
60
+ tooltipDisclaimerTextForAiCodeGenerationNoLoggingInSources:
61
+ 'To generate code suggestions, the contents of the currently open file are shared with Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
62
+ /**
63
+ * @description Text for tooltip button which redirects to AI settings
64
+ */
65
+ manageInSettings: 'Manage in settings',
66
+ /**
67
+ * @description Title for disclaimer info button in the teaser to generate code.
68
+ */
69
+ learnMoreAboutHowYourDataIsBeingUsed: 'Learn more about how your data is being used',
29
70
  } as const;
30
71
 
31
72
  const lockedString = i18n.i18n.lockedString;
@@ -35,17 +76,96 @@ export enum AiCodeGenerationTeaserDisplayState {
35
76
  TRIGGER = 'trigger',
36
77
  DISCOVERY = 'discovery',
37
78
  LOADING = 'loading',
79
+ GENERATED = 'generated',
80
+ }
81
+
82
+ function getTooltipDisclaimerText(noLogging: boolean, panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor): string {
83
+ switch (panel) {
84
+ case AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE:
85
+ return noLogging ?
86
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeGenerationNoLoggingInConsole) :
87
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeGenerationInConsole);
88
+ case AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES:
89
+ return noLogging ?
90
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeGenerationNoLoggingInSources) :
91
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeGenerationInSources);
92
+ }
38
93
  }
39
94
 
40
95
  export interface ViewInput {
41
96
  displayState: AiCodeGenerationTeaserDisplayState;
97
+ disclaimerTooltipId?: string;
98
+ noLogging: boolean;
99
+ onManageInSettingsTooltipClick: (event: Event) => void;
100
+ // TODO(b/472268298): Remove ContextFlavor explicitly and pass required values
101
+ panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
42
102
  }
43
103
 
44
- export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
104
+ export interface ViewOutput {
105
+ hideTooltip?: () => void;
106
+ setTimerText?: (text: string) => void;
107
+ }
108
+
109
+ export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
110
+
111
+ export const DEFAULT_VIEW: View = (input, output, target) => {
112
+ if (!input.panel) {
113
+ render(nothing, target);
114
+ return;
115
+ }
45
116
 
46
- export const DEFAULT_VIEW: View = (input, _output, target) => {
47
117
  let teaserLabel;
48
118
  switch (input.displayState) {
119
+ case AiCodeGenerationTeaserDisplayState.TRIGGER: {
120
+ if (!input.disclaimerTooltipId) {
121
+ render(nothing, target);
122
+ return;
123
+ }
124
+ const toGenerateCode = Host.Platform.isMac() ? lockedString(UIStringsNotTranslate.cmdItoGenerateCode) :
125
+ lockedString(UIStringsNotTranslate.ctrlItoGenerateCode);
126
+ const tooltipDisclaimerText = getTooltipDisclaimerText(input.noLogging, input.panel);
127
+ // TODO(b/472291834): Disclaimer icon should match the placeholder's color
128
+ // clang-format off
129
+ teaserLabel = html`<div class="ai-code-generation-teaser-trigger">
130
+ ${toGenerateCode}&nbsp;<devtools-button
131
+ .data=${{
132
+ title: lockedString(UIStringsNotTranslate.learnMoreAboutHowYourDataIsBeingUsed),
133
+ size: Buttons.Button.Size.MICRO,
134
+ iconName: 'info',
135
+ variant: Buttons.Button.Variant.ICON,
136
+ jslogContext: 'ai-code-generation-teaser.info-button',
137
+ } as Buttons.Button.ButtonData}
138
+ aria-details=${input.disclaimerTooltipId}
139
+ aria-describedby=${input.disclaimerTooltipId}
140
+ ></devtools-button>
141
+ <devtools-tooltip
142
+ id=${input.disclaimerTooltipId}
143
+ variant="rich"
144
+ jslogContext="ai-code-generation-disclaimer"
145
+ ${Directives.ref(el => {
146
+ if (el instanceof HTMLElement) {
147
+ output.hideTooltip = () => {
148
+ el.hidePopover();
149
+ };
150
+ }
151
+ })}>
152
+ <div class="disclaimer-tooltip-container"><div class="tooltip-text">
153
+ ${tooltipDisclaimerText}
154
+ </div>
155
+ <span
156
+ tabIndex="0"
157
+ class="link"
158
+ role="link"
159
+ jslog=${VisualLogging.link('open-ai-settings').track({
160
+ click: true,
161
+ })}
162
+ @click=${input.onManageInSettingsTooltipClick}
163
+ >${lockedString(UIStringsNotTranslate.manageInSettings)}</span></div></devtools-tooltip>
164
+ </div>`;
165
+ // clang-format on
166
+ break;
167
+ }
168
+
49
169
  case AiCodeGenerationTeaserDisplayState.DISCOVERY: {
50
170
  const newBadge = UI.UIUtils.maybeCreateNewBadge(PROMOTION_ID);
51
171
  teaserLabel = newBadge ?
@@ -55,14 +175,27 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
55
175
  }
56
176
 
57
177
  case AiCodeGenerationTeaserDisplayState.LOADING: {
58
- teaserLabel = html`${lockedString(UIStringsNotTranslate.generating)}`;
178
+ // clang-format off
179
+ teaserLabel = html`
180
+ <span class="ai-code-generation-spinner"></span>&nbsp;${lockedString(UIStringsNotTranslate.generating)}&nbsp;
181
+ <span class="ai-code-generation-timer" ${Directives.ref(el => {
182
+ if (el) {
183
+ output.setTimerText = (text: string) => {
184
+ el.textContent = text;
185
+ };
186
+ }
187
+ })}></span>`;
188
+ // clang-format on
59
189
  break;
60
190
  }
61
191
 
62
- case AiCodeGenerationTeaserDisplayState.TRIGGER: {
63
- const toGenerateCode = Host.Platform.isMac() ? lockedString(UIStringsNotTranslate.cmdItoGenerateCode) :
64
- lockedString(UIStringsNotTranslate.ctrlItoGenerateCode);
65
- teaserLabel = html`${toGenerateCode}`;
192
+ case AiCodeGenerationTeaserDisplayState.GENERATED: {
193
+ // clang-format off
194
+ teaserLabel = html`<div class="ai-code-generation-teaser-generated">
195
+ <span>${lockedString(UIStringsNotTranslate.tab)}</span>
196
+ &nbsp;${lockedString(UIStringsNotTranslate.toAccept)}
197
+ </div>`;
198
+ // clang-format on
66
199
  break;
67
200
  }
68
201
  }
@@ -73,7 +206,7 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
73
206
  <style>${styles}</style>
74
207
  <style>@scope to (devtools-widget > *) { ${UI.inspectorCommonStyles} }</style>
75
208
  <div class="ai-code-generation-teaser">
76
- &nbsp;${teaserLabel}
209
+ ${teaserLabel}
77
210
  </div>
78
211
  `, target
79
212
  );
@@ -83,23 +216,39 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
83
216
  // TODO(b/448063927): Add "Dont show again" for discovery teaser.
84
217
  export class AiCodeGenerationTeaser extends UI.Widget.Widget {
85
218
  readonly #view: View;
219
+ #viewOutput: ViewOutput = {};
86
220
 
87
221
  #displayState = AiCodeGenerationTeaserDisplayState.TRIGGER;
222
+ #disclaimerTooltipId?: string;
223
+ #noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
224
+ #panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
225
+ #timerIntervalId?: number;
226
+ #loadStartTime?: number;
88
227
 
89
228
  constructor(view?: View) {
90
229
  super();
91
230
  this.markAsExternallyManaged();
231
+ this.#noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
232
+ Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
92
233
  this.#view = view ?? DEFAULT_VIEW;
93
234
  this.requestUpdate();
94
235
  }
95
236
 
96
237
  override performUpdate(): void {
97
- const output = {};
98
238
  this.#view(
99
239
  {
100
240
  displayState: this.#displayState,
241
+ onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
242
+ disclaimerTooltipId: this.#disclaimerTooltipId,
243
+ noLogging: this.#noLogging,
244
+ panel: this.#panel,
101
245
  },
102
- output, this.contentElement);
246
+ this.#viewOutput, this.contentElement);
247
+ }
248
+
249
+ override willHide(): void {
250
+ super.willHide();
251
+ this.#stopLoadingAnimation();
103
252
  }
104
253
 
105
254
  get displayState(): AiCodeGenerationTeaserDisplayState {
@@ -112,5 +261,52 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
112
261
  }
113
262
  this.#displayState = displayState;
114
263
  this.requestUpdate();
264
+ if (this.#displayState === AiCodeGenerationTeaserDisplayState.LOADING) {
265
+ // wait update to complete so that setTimerText has been set properly
266
+ void this.updateComplete.then(() => {
267
+ void this.#startLoadingAnimation();
268
+ });
269
+ } else if (this.#loadStartTime) {
270
+ this.#stopLoadingAnimation();
271
+ }
272
+ }
273
+
274
+ #startLoadingAnimation(): void {
275
+ this.#stopLoadingAnimation();
276
+ this.#loadStartTime = performance.now();
277
+
278
+ this.#viewOutput.setTimerText?.('(0s)');
279
+
280
+ this.#timerIntervalId = window.setInterval(() => {
281
+ if (this.#loadStartTime) {
282
+ const elapsedSeconds = Math.floor((performance.now() - this.#loadStartTime) / 1000);
283
+ this.#viewOutput.setTimerText?.(`(${elapsedSeconds}s)`);
284
+ }
285
+ }, 1000);
286
+ }
287
+
288
+ #stopLoadingAnimation(): void {
289
+ if (this.#timerIntervalId) {
290
+ clearInterval(this.#timerIntervalId);
291
+ this.#timerIntervalId = undefined;
292
+ }
293
+ this.#loadStartTime = undefined;
294
+ }
295
+
296
+ set disclaimerTooltipId(disclaimerTooltipId: string) {
297
+ this.#disclaimerTooltipId = disclaimerTooltipId;
298
+ this.requestUpdate();
299
+ }
300
+
301
+ set panel(panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor) {
302
+ this.#panel = panel;
303
+ this.requestUpdate();
304
+ }
305
+
306
+ #onManageInSettingsTooltipClick(event: Event): void {
307
+ event.stopPropagation();
308
+ this.#viewOutput.hideTooltip?.();
309
+ void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
310
+ event.consume(true);
115
311
  }
116
312
  }
@@ -6,9 +6,73 @@
6
6
 
7
7
  @scope to (devtools-widget > *) {
8
8
  .ai-code-generation-teaser {
9
+ pointer-events: all;
10
+ font-style: italic;
11
+ padding-left: var(--sys-size-3);
12
+ line-height: var(--sys-size-7);
13
+
14
+ .ai-code-generation-teaser-trigger {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ }
18
+
19
+ .ai-code-generation-teaser-generated {
20
+ display: inline-flex;
21
+ gap: var(--sys-size-2);
22
+ color: var(--sys-color-primary);
23
+
24
+ span {
25
+ border: var(--sys-size-1) solid var(--sys-color-primary);
26
+ border-radius: var(--sys-shape-corner-extra-small);
27
+ padding: 0 var(--sys-size-3);
28
+ }
29
+ }
30
+
9
31
  .new-badge {
10
32
  font-style: normal;
11
33
  display: inline-block;
12
34
  }
35
+
36
+ devtools-tooltip:popover-open {
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+
41
+ .disclaimer-tooltip-container {
42
+ padding: var(--sys-size-4) 0;
43
+ max-width: var(--sys-size-30);
44
+ white-space: normal;
45
+
46
+ .tooltip-text {
47
+ color: var(--sys-color-on-surface-subtle);
48
+ padding: 0 var(--sys-size-5);
49
+ align-items: flex-start;
50
+ gap: 10px;
51
+ }
52
+
53
+ .link {
54
+ margin: var(--sys-size-5) var(--sys-size-8) 0 var(--sys-size-5);
55
+ display: inline-block;
56
+ }
57
+ }
58
+ }
59
+
60
+ .ai-code-generation-spinner::before {
61
+ content: "⠋";
62
+ animation: teaser-spinner-animation 1s linear infinite;
63
+ }
64
+ }
65
+
66
+ @keyframes teaser-spinner-animation {
67
+ 0% { content: "⠋"; }
68
+ 10% { content: "⠙"; }
69
+ 20% { content: "⠹"; }
70
+ 30% { content: "⠸"; }
71
+ 40% { content: "⠼"; }
72
+ 50% { content: "⠴"; }
73
+ 60% { content: "⠦"; }
74
+ 70% { content: "⠧"; }
75
+ 80% { content: "⠇"; }
76
+ 90% { content: "⠏"; }
13
77
  }
14
78
  }
@@ -649,7 +649,8 @@ export class ConsoleView extends UI.Widget.VBox implements
649
649
  this.aiCodeCompletionSummaryToolbar = new AiCodeCompletionSummaryToolbar({
650
650
  citationsTooltipId: CITATIONS_TOOLTIP_ID,
651
651
  disclaimerTooltipId: DISCLAIMER_TOOLTIP_ID,
652
- spinnerTooltipId: SPINNER_TOOLTIP_ID
652
+ spinnerTooltipId: SPINNER_TOOLTIP_ID,
653
+ panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE,
653
654
  });
654
655
  this.aiCodeCompletionSummaryToolbarContainer =
655
656
  this.element.createChild('div', 'ai-code-completion-summary-toolbar-container');
@@ -347,7 +347,8 @@ export class ObjectEventListenerBar extends UI.TreeOutline.TreeElement {
347
347
  }
348
348
 
349
349
  const subtitle = title.createChild('span', 'event-listener-tree-subtitle');
350
- const linkElement = linkifier.linkifyRawLocation(this.#eventListener.location(), this.#eventListener.sourceURL());
350
+ const linkElement = linkifier.linkifyRawLocation(
351
+ this.#eventListener.location(), this.#eventListener.sourceURL(), undefined, {tabStop: true});
351
352
  subtitle.appendChild(linkElement);
352
353
 
353
354
  this.listItemElement.addEventListener('contextmenu', event => {
@@ -102,6 +102,7 @@ export class AiCodeCompletionPlugin extends Plugin {
102
102
  this.#aiCodeCompletionDisclaimer = new PanelCommon.AiCodeCompletionDisclaimer();
103
103
  this.#aiCodeCompletionDisclaimer.disclaimerTooltipId = DISCLAIMER_TOOLTIP_ID;
104
104
  this.#aiCodeCompletionDisclaimer.spinnerTooltipId = SPINNER_TOOLTIP_ID;
105
+ this.#aiCodeCompletionDisclaimer.panel = AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES;
105
106
  this.#aiCodeCompletionDisclaimer.show(this.#aiCodeCompletionDisclaimerContainer, undefined, true);
106
107
  }
107
108
 
@@ -109,8 +110,11 @@ export class AiCodeCompletionPlugin extends Plugin {
109
110
  if (this.#aiCodeCompletionCitationsToolbar) {
110
111
  return;
111
112
  }
112
- this.#aiCodeCompletionCitationsToolbar =
113
- new PanelCommon.AiCodeCompletionSummaryToolbar({citationsTooltipId: CITATIONS_TOOLTIP_ID, hasTopBorder: true});
113
+ this.#aiCodeCompletionCitationsToolbar = new PanelCommon.AiCodeCompletionSummaryToolbar({
114
+ citationsTooltipId: CITATIONS_TOOLTIP_ID,
115
+ hasTopBorder: true,
116
+ panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES
117
+ });
114
118
  this.#aiCodeCompletionCitationsToolbar.show(this.#aiCodeCompletionCitationsToolbarContainer, undefined, true);
115
119
  }
116
120
 
@@ -1,7 +1,7 @@
1
1
  Name: Dependencies sourced from the upstream `chromium` repository
2
2
  URL: https://chromium.googlesource.com/chromium/src
3
3
  Version: N/A
4
- Revision: 142ca60f39457730eafcdd7058a4f812f9aa63dc
4
+ Revision: e3edb4435f06ac3de39688c44cc50378c47e35e8
5
5
  Update Mechanism: Manual (https://crbug.com/428069060)
6
6
  License: BSD-3-Clause
7
7
  License File: LICENSE
@@ -122,21 +122,25 @@ export class AiCodeCompletionProvider {
122
122
  this.#aiCodeCompletion?.clearCachedRequest();
123
123
  }
124
124
 
125
- // TODO(b/445394511): Update setup and cleanup method so that config callbacks are not
126
- // called twice.
127
125
  #setupAiCodeCompletion(): void {
128
126
  if (!this.#editor || !this.#aiCodeCompletionConfig) {
129
127
  return;
130
128
  }
131
- if (!this.#aiCodeCompletion) {
132
- this.#aiCodeCompletion = new AiCodeCompletion.AiCodeCompletion.AiCodeCompletion(
133
- {aidaClient: this.#aidaClient}, this.#aiCodeCompletionConfig.panel, undefined,
134
- this.#aiCodeCompletionConfig.completionContext.stopSequences);
129
+ if (this.#aiCodeCompletion) {
130
+ // early return as this means that code completion was previously setup
131
+ return;
135
132
  }
133
+ this.#aiCodeCompletion = new AiCodeCompletion.AiCodeCompletion.AiCodeCompletion(
134
+ {aidaClient: this.#aidaClient}, this.#aiCodeCompletionConfig.panel, undefined,
135
+ this.#aiCodeCompletionConfig.completionContext.stopSequences);
136
136
  this.#aiCodeCompletionConfig.onFeatureEnabled();
137
137
  }
138
138
 
139
139
  #cleanupAiCodeCompletion(): void {
140
+ if (!this.#aiCodeCompletion) {
141
+ // early return as this means there is no code completion to clean up
142
+ return;
143
+ }
140
144
  if (this.#suggestionRenderingTimeout) {
141
145
  clearTimeout(this.#suggestionRenderingTimeout);
142
146
  this.#suggestionRenderingTimeout = undefined;
@@ -208,6 +212,21 @@ export class AiCodeCompletionProvider {
208
212
  this.#editor?.editor.dispatch({effects: this.#teaserCompartment.reconfigure([])});
209
213
  }
210
214
 
215
+ /**
216
+ * This method is responsible for fetching code completion suggestions and
217
+ * displaying them in the text editor.
218
+ *
219
+ * 1. **Debouncing requests:** As the user types, we don't want to send a request
220
+ * for every keystroke. Instead, we use debouncing to schedule a request
221
+ * only after the user has paused typing for a short period
222
+ * (AIDA_REQUEST_THROTTLER_TIMEOUT_MS). This prevents spamming the backend with
223
+ * requests for intermediate typing states.
224
+ *
225
+ * 2. **Delaying suggestions:** When a suggestion is received from the AIDA
226
+ * backend, we don't show it immediately. There is a minimum delay
227
+ * (DELAY_BEFORE_SHOWING_RESPONSE_MS) from when the request was sent to when
228
+ * the suggestion is displayed.
229
+ */
211
230
  #triggerAiCodeCompletion(update: CodeMirror.ViewUpdate): void {
212
231
  if (!update.docChanged || !this.#editor || !this.#aiCodeCompletion) {
213
232
  return;
@@ -287,8 +306,7 @@ export class AiCodeCompletionProvider {
287
306
  citations,
288
307
  rpcGlobalId,
289
308
  } = sampleResponse;
290
- const remainingDelay = Math.max(
291
- AiCodeCompletion.AiCodeCompletion.DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
309
+ const remainingDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
292
310
  this.#suggestionRenderingTimeout = window.setTimeout(() => {
293
311
  const currentCursorPosition = this.#editor?.editor.state.selection.main.head;
294
312
  if (currentCursorPosition !== cursorPositionAtRequest) {
@@ -71,6 +71,10 @@ export class AiCodeCompletionTeaserPlaceholder extends CM.WidgetType {
71
71
  super.destroy(dom);
72
72
  this.teaser?.hideWidget();
73
73
  }
74
+
75
+ override eq(other: AiCodeCompletionTeaserPlaceholder): boolean {
76
+ return this.teaser === other.teaser;
77
+ }
74
78
  }
75
79
 
76
80
  export function aiCodeCompletionTeaserPlaceholder(teaser: UI.Widget.Widget): CM.Extension {
@@ -0,0 +1,77 @@
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 CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
6
+
7
+ const LINE_COMMENT_PATTERN = /^(?:\/\/|#)\s*/;
8
+ const BLOCK_COMMENT_START_PATTERN = /^\/\*+\s*/;
9
+ const BLOCK_COMMENT_END_PATTERN = /\s*\*+\/$/;
10
+ const BLOCK_COMMENT_LINE_PREFIX_PATTERN = /^\s*\*\s?/;
11
+
12
+ function findLastNonWhitespacePos(state: CodeMirror.EditorState, cursorPosition: number): number {
13
+ const line = state.doc.lineAt(cursorPosition);
14
+ const textBefore = line.text.substring(0, cursorPosition - line.from);
15
+ const effectiveEnd = line.from + textBefore.trimEnd().length;
16
+ return effectiveEnd;
17
+ }
18
+
19
+ function resolveCommentNode(state: CodeMirror.EditorState, cursorPosition: number): CodeMirror.SyntaxNode|undefined {
20
+ const tree = CodeMirror.syntaxTree(state);
21
+ const lookupPos = findLastNonWhitespacePos(state, cursorPosition);
22
+ // Find the innermost syntax node at the last non-whitespace character position.
23
+ // The bias of -1 makes it check the character to the left of the position.
24
+ const node = tree.resolveInner(lookupPos, -1);
25
+ const nodeType = node.type.name;
26
+ // Check if the node type is a comment AND the cursor is within the node's range.
27
+ if (nodeType.includes('Comment') && cursorPosition >= node.to) {
28
+ if (!nodeType.includes('BlockComment')) {
29
+ return node;
30
+ }
31
+ // An unclosed block comment can result in the parser inserting an error.
32
+ let hasInternalError = false;
33
+ tree.iterate({
34
+ from: node.from,
35
+ to: node.to,
36
+ enter: n => {
37
+ if (n.type.isError) {
38
+ hasInternalError = true;
39
+ return false;
40
+ }
41
+ return true;
42
+ },
43
+ });
44
+ return hasInternalError ? undefined : node;
45
+ }
46
+ return;
47
+ }
48
+
49
+ function extractBlockComment(rawText: string): string|undefined {
50
+ // Remove /* and */, whitespace, and common leading asterisks on new lines
51
+ let cleaned = rawText.replace(BLOCK_COMMENT_START_PATTERN, '').replace(BLOCK_COMMENT_END_PATTERN, '');
52
+ // Remove leading " * " from multi-line block comments
53
+ cleaned = cleaned.split('\n').map(line => line.replace(BLOCK_COMMENT_LINE_PREFIX_PATTERN, '')).join('\n').trim();
54
+ return cleaned;
55
+ }
56
+
57
+ function extractLineComment(rawText: string): string {
58
+ return rawText.replace(LINE_COMMENT_PATTERN, '').trim();
59
+ }
60
+
61
+ export class AiCodeGenerationParser {
62
+ static extractCommentText(state: CodeMirror.EditorState, cursorPosition: number): string|undefined {
63
+ const node = resolveCommentNode(state, cursorPosition);
64
+ if (!node) {
65
+ return;
66
+ }
67
+ const nodeType = node.type.name;
68
+ const rawText = state.doc.sliceString(node.from, node.to);
69
+ if (nodeType.includes('LineComment')) {
70
+ return extractLineComment(rawText);
71
+ }
72
+ if (nodeType.includes('BlockComment')) {
73
+ return extractBlockComment(rawText);
74
+ }
75
+ return rawText;
76
+ }
77
+ }