chrome-devtools-frontend 1.0.1563104 → 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.
package/AUTHORS CHANGED
@@ -36,6 +36,7 @@ Douglas Chiang <douglasdothnc@gmail.com>
36
36
  Dragonish <no.web.developer@outlook.com>
37
37
  Eden Wang <nedenwang@gmail.com>
38
38
  Edward Trist <edwardtrist@gmail.com>
39
+ Emir D <emrd434@gmail.com>
39
40
  Ergün Erdoğmuş <erdogmusergun@gmail.com>
40
41
  Eric Rannaud <eric.rannaud@gmail.com>
41
42
  Faisal Salman <fyzlman@gmail.com>
@@ -649,12 +649,13 @@ export class CSSMatchedStyles {
649
649
  // DOMInheritanceCascades.
650
650
  for (const [pseudoType, nodeCascade] of pseudoCascades.entries()) {
651
651
  pseudoInheritanceCascades.set(
652
- pseudoType, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties));
652
+ pseudoType, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties, this.#mainDOMCascade));
653
653
  }
654
654
 
655
655
  for (const [highlightName, nodeCascade] of customHighlightPseudoCascades.entries()) {
656
656
  customHighlightPseudoInheritanceCascades.set(
657
- highlightName, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties));
657
+ highlightName,
658
+ new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties, this.#mainDOMCascade));
658
659
  }
659
660
 
660
661
  return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
@@ -959,17 +960,15 @@ class NodeCascade {
959
960
  #matchedStyles: CSSMatchedStyles;
960
961
  readonly styles: CSSStyleDeclaration[];
961
962
  readonly #isInherited: boolean;
962
- readonly #isHighlightPseudoCascade: boolean;
963
963
  readonly propertiesState = new Map<CSSProperty, PropertyState>();
964
964
  readonly activeProperties = new Map<string, CSSProperty>();
965
965
  readonly #node: DOMNode;
966
966
  constructor(
967
967
  matchedStyles: CSSMatchedStyles, styles: CSSStyleDeclaration[], node: DOMNode, isInherited: boolean,
968
- isHighlightPseudoCascade = false) {
968
+ readonly isHighlightPseudoCascade = false) {
969
969
  this.#matchedStyles = matchedStyles;
970
970
  this.styles = styles;
971
971
  this.#isInherited = isInherited;
972
- this.#isHighlightPseudoCascade = isHighlightPseudoCascade;
973
972
  this.#node = node;
974
973
  }
975
974
 
@@ -992,9 +991,16 @@ class NodeCascade {
992
991
  // Do not pick non-inherited properties from inherited styles.
993
992
  const metadata = cssMetadata();
994
993
 
995
- // All properties are inherited for highlight pseudos.
996
- if (this.#isInherited && !this.#isHighlightPseudoCascade && !metadata.isPropertyInherited(property.name)) {
997
- continue;
994
+ if (this.#isInherited) {
995
+ if (this.isHighlightPseudoCascade) {
996
+ // All properties are inherited for highlight pseudos, but custom
997
+ // variables do not come from the inherited pseudo elements.
998
+ if (property.name.startsWith('--')) {
999
+ continue;
1000
+ }
1001
+ } else if (!metadata.isPropertyInherited(property.name)) {
1002
+ continue;
1003
+ }
998
1004
  }
999
1005
 
1000
1006
  // When a property does not have a range in an otherwise ranged CSSStyleDeclaration,
@@ -1085,6 +1091,7 @@ class NodeCascade {
1085
1091
  function isRegular(declaration: CSSProperty|CSSRegisteredProperty): declaration is CSSProperty {
1086
1092
  return 'ownerStyle' in declaration;
1087
1093
  }
1094
+
1088
1095
  export class CSSValueSource {
1089
1096
  readonly declaration: CSSProperty|CSSRegisteredProperty;
1090
1097
  constructor(declaration: CSSProperty|CSSRegisteredProperty) {
@@ -1176,15 +1183,27 @@ class DOMInheritanceCascade {
1176
1183
  readonly #nodeCascades: NodeCascade[];
1177
1184
  #registeredProperties: CSSRegisteredProperty[];
1178
1185
  readonly #matchedStyles: CSSMatchedStyles;
1186
+ readonly #fallbackCascade: DOMInheritanceCascade|null = null;
1187
+ readonly #styles: CSSStyleDeclaration[] = [];
1179
1188
  constructor(
1180
- matchedStyles: CSSMatchedStyles, nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[]) {
1189
+ matchedStyles: CSSMatchedStyles, nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[],
1190
+ fallbackCascade: DOMInheritanceCascade|null = null) {
1181
1191
  this.#nodeCascades = nodeCascades;
1182
1192
  this.#matchedStyles = matchedStyles;
1183
1193
  this.#registeredProperties = registeredProperties;
1194
+ this.#fallbackCascade = fallbackCascade;
1184
1195
 
1185
1196
  for (const nodeCascade of nodeCascades) {
1186
1197
  for (const style of nodeCascade.styles) {
1187
1198
  this.#styleToNodeCascade.set(style, nodeCascade);
1199
+ this.#styles.push(style);
1200
+ }
1201
+ }
1202
+ if (fallbackCascade) {
1203
+ for (const [style, nodeCascade] of fallbackCascade.#styleToNodeCascade) {
1204
+ if (!this.#styles.includes(style)) {
1205
+ this.#styleToNodeCascade.set(style, nodeCascade);
1206
+ }
1188
1207
  }
1189
1208
  }
1190
1209
  }
@@ -1248,6 +1267,9 @@ class DOMInheritanceCascade {
1248
1267
  }
1249
1268
  }
1250
1269
  }
1270
+ if (this.#fallbackCascade && (!nodeCascade.isHighlightPseudoCascade || property.name.startsWith('--'))) {
1271
+ return this.#fallbackCascade.resolveProperty(property.name, property.ownerStyle);
1272
+ }
1251
1273
  return null;
1252
1274
  }
1253
1275
 
@@ -1538,7 +1560,7 @@ class DOMInheritanceCascade {
1538
1560
  }
1539
1561
 
1540
1562
  styles(): CSSStyleDeclaration[] {
1541
- return Array.from(this.#styleToNodeCascade.keys());
1563
+ return this.#styles;
1542
1564
  }
1543
1565
 
1544
1566
  propertyState(property: CSSProperty): PropertyState|null {
@@ -1610,6 +1632,20 @@ class DOMInheritanceCascade {
1610
1632
  rule.propertyName(),
1611
1633
  initialValue !== null ? {value: initialValue, declaration: new CSSValueSource(rule)} : null);
1612
1634
  }
1635
+ if (this.#fallbackCascade) {
1636
+ this.#fallbackCascade.ensureInitialized();
1637
+ for (const [cascade, available] of this.#fallbackCascade.#availableCSSVariables) {
1638
+ this.#availableCSSVariables.set(cascade, available);
1639
+ }
1640
+ for (const [cascade, computed] of this.#fallbackCascade.#computedCSSVariables) {
1641
+ this.#computedCSSVariables.set(cascade, computed);
1642
+ }
1643
+ for (const [key, value] of this.#fallbackCascade.#availableCSSVariables.get(
1644
+ this.#fallbackCascade.#nodeCascades[0]) ??
1645
+ []) {
1646
+ accumulatedCSSVariables.set(key, value);
1647
+ }
1648
+ }
1613
1649
  for (let i = this.#nodeCascades.length - 1; i >= 0; --i) {
1614
1650
  const nodeCascade = this.#nodeCascades[i];
1615
1651
  const variableNames = [];
@@ -2,15 +2,11 @@
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
4
 
5
- import * as Common from '../../core/common/common.js';
6
5
  import * as Host from '../../core/host/host.js';
7
6
  import * as Root from '../../core/root/root.js';
8
7
 
9
8
  import {debugLog} from './debug.js';
10
9
 
11
- export const DELAY_BEFORE_SHOWING_RESPONSE_MS = 500;
12
- export const AIDA_REQUEST_DEBOUNCE_TIMEOUT_MS = 200;
13
-
14
10
  /**
15
11
  * TODO(b/404796739): Remove these definitions of AgentOptions and RequestOptions and
16
12
  * use the existing ones which are used for AI assistance panel agents.
@@ -140,20 +136,9 @@ const console = {
140
136
 
141
137
  /**
142
138
  * The AiCodeCompletion class is responsible for fetching code completion suggestions
143
- * from the AIDA backend and displaying them in the text editor.
144
- *
145
- * 1. **Debouncing requests:** As the user types, we don't want to send a request
146
- * for every keystroke. Instead, we use debouncing to schedule a request
147
- * only after the user has paused typing for a short period
148
- * (AIDA_REQUEST_THROTTLER_TIMEOUT_MS). This prevents spamming the backend with
149
- * requests for intermediate typing states.
150
- *
151
- * 2. **Delaying suggestions:** When a suggestion is received from the AIDA
152
- * backend, we don't show it immediately. There is a minimum delay
153
- * (DELAY_BEFORE_SHOWING_RESPONSE_MS) from when the request was sent to when
154
- * the suggestion is displayed.
139
+ * from the AIDA backend.
155
140
  */
156
- export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
141
+ export class AiCodeCompletion {
157
142
  #stopSequences: string[];
158
143
  #renderingTimeout?: number;
159
144
  #aidaRequestCache?: CachedRequest;
@@ -166,7 +151,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
166
151
  readonly #serverSideLoggingEnabled: boolean;
167
152
 
168
153
  constructor(opts: AgentOptions, panel: ContextFlavor, callbacks?: Callbacks, stopSequences?: string[]) {
169
- super();
170
154
  this.#aidaClient = opts.aidaClient;
171
155
  this.#serverSideLoggingEnabled = opts.serverSideLoggingEnabled ?? false;
172
156
  this.#panel = panel;
@@ -174,14 +158,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
174
158
  this.#callbacks = callbacks;
175
159
  }
176
160
 
177
- #debouncedRequestAidaSuggestion = Common.Debouncer.debounce(
178
- (prefix: string, suffix: string, cursorPositionAtRequest: number,
179
- inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage) => {
180
- void this.#requestAidaSuggestion(
181
- this.#buildRequest(prefix, suffix, inferenceLanguage), cursorPositionAtRequest);
182
- },
183
- AIDA_REQUEST_DEBOUNCE_TIMEOUT_MS);
184
-
185
161
  #buildRequest(
186
162
  prefix: string, suffix: string,
187
163
  inferenceLanguage: Host.AidaClient.AidaInferenceLanguage = Host.AidaClient.AidaInferenceLanguage.JAVASCRIPT,
@@ -246,125 +222,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
246
222
  };
247
223
  }
248
224
 
249
- #pickSampleFromResponse(response: Host.AidaClient.CompletionResponse): Host.AidaClient.GenerationSample|null {
250
- if (!response.generatedSamples.length) {
251
- return null;
252
- }
253
-
254
- // `currentHint` is the portion of a standard autocomplete suggestion that the user has not yet typed.
255
- // For example, if the user types `document.queryS` and the autocomplete suggests `document.querySelector`,
256
- // the `currentHint` is `elector`.
257
- const currentHintInMenu = this.#callbacks?.getCompletionHint();
258
- // TODO(ergunsh): We should not do this check here. Instead, the AI code suggestions should be provided
259
- // as it is to the view plugin. The view plugin should choose which one to use based on the completion hint
260
- // and selected completion.
261
- if (!currentHintInMenu) {
262
- return response.generatedSamples[0];
263
- }
264
-
265
- // TODO(ergunsh): This does not handle looking for `selectedCompletion`. The `currentHint` is `null`
266
- // for the Sources panel case.
267
- // Even though there is no match, we still return the first suggestion which will be displayed
268
- // when the traditional autocomplete menu is closed.
269
- return response.generatedSamples.find(sample => sample.generationString.startsWith(currentHintInMenu)) ??
270
- response.generatedSamples[0];
271
- }
272
-
273
- async #generateSampleForRequest(request: Host.AidaClient.CompletionRequest, cursor: number): Promise<{
274
- suggestionText: string,
275
- fromCache: boolean,
276
- citations: Host.AidaClient.Citation[],
277
- rpcGlobalId?: Host.AidaClient.RpcGlobalId,
278
- sampleId?: number,
279
- }|null> {
280
- const {response, fromCache} = await this.#completeCodeCached(request);
281
- debugLog('At cursor position', cursor, {request, response, fromCache});
282
- if (!response) {
283
- return null;
284
- }
285
-
286
- const suggestionSample = this.#pickSampleFromResponse(response);
287
- if (!suggestionSample) {
288
- return null;
289
- }
290
-
291
- const shouldBlock =
292
- suggestionSample.attributionMetadata?.attributionAction === Host.AidaClient.RecitationAction.BLOCK;
293
- if (shouldBlock) {
294
- return null;
295
- }
296
-
297
- const isRepetitive = this.#checkIfSuggestionRepeatsExistingText(suggestionSample.generationString, request);
298
- if (isRepetitive) {
299
- return null;
300
- }
301
-
302
- const suggestionText = this.#trimSuggestionOverlap(suggestionSample.generationString, request);
303
- if (suggestionText.length === 0) {
304
- return null;
305
- }
306
-
307
- return {
308
- suggestionText,
309
- sampleId: suggestionSample.sampleId,
310
- fromCache,
311
- citations: suggestionSample.attributionMetadata?.citations ?? [],
312
- rpcGlobalId: response.metadata.rpcGlobalId,
313
- };
314
- }
315
-
316
- async #requestAidaSuggestion(request: Host.AidaClient.CompletionRequest, cursorPositionAtRequest: number):
317
- Promise<void> {
318
- const startTime = performance.now();
319
- this.dispatchEventToListeners(Events.REQUEST_TRIGGERED, {});
320
- // Registering AiCodeCompletionRequestTriggered metric even if the request is served from cache
321
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionRequestTriggered);
322
-
323
- try {
324
- const sampleResponse = await this.#generateSampleForRequest(request, cursorPositionAtRequest);
325
- if (!sampleResponse) {
326
- this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
327
- return;
328
- }
329
-
330
- const {
331
- suggestionText,
332
- sampleId,
333
- fromCache,
334
- citations,
335
- rpcGlobalId,
336
- } = sampleResponse;
337
- const remainingDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
338
- this.#renderingTimeout = window.setTimeout(() => {
339
- const currentCursorPosition = this.#callbacks?.getSelectionHead();
340
- if (currentCursorPosition !== cursorPositionAtRequest) {
341
- this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
342
- return;
343
- }
344
- this.#callbacks?.setAiAutoCompletion({
345
- text: suggestionText,
346
- from: cursorPositionAtRequest,
347
- rpcGlobalId,
348
- sampleId,
349
- startTime,
350
- onImpression: this.registerUserImpression.bind(this),
351
- clearCachedRequest: this.clearCachedRequest.bind(this),
352
- });
353
-
354
- if (fromCache) {
355
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionResponseServedFromCache);
356
- }
357
-
358
- debugLog('Suggestion dispatched to the editor', suggestionText, 'at cursor position', cursorPositionAtRequest);
359
- this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {citations});
360
- }, remainingDelay);
361
- } catch (e) {
362
- debugLog('Error while fetching code completion suggestions from AIDA', e);
363
- this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
364
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionError);
365
- }
366
- }
367
-
368
225
  get #userTier(): string|undefined {
369
226
  return Root.Runtime.hostConfig.devToolsAiCodeCompletion?.userTier;
370
227
  }
@@ -379,30 +236,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
379
236
  };
380
237
  }
381
238
 
382
- /**
383
- * Removes the end of a suggestion if it overlaps with the start of the suffix.
384
- */
385
- #trimSuggestionOverlap(generationString: string, request: Host.AidaClient.CompletionRequest): string {
386
- const suffix = request.suffix;
387
- if (!suffix) {
388
- return generationString;
389
- }
390
-
391
- // Iterate from the longest possible overlap down to the shortest
392
- for (let i = Math.min(generationString.length, suffix.length); i > 0; i--) {
393
- const overlapCandidate = suffix.substring(0, i);
394
- if (generationString.endsWith(overlapCandidate)) {
395
- return generationString.slice(0, -i);
396
- }
397
- }
398
- return generationString;
399
- }
400
-
401
- #checkIfSuggestionRepeatsExistingText(generationString: string, request: Host.AidaClient.CompletionRequest): boolean {
402
- const {prefix, suffix} = request;
403
- return Boolean(prefix.includes(generationString.trim()) || suffix?.includes(generationString.trim()));
404
- }
405
-
406
239
  #checkCachedRequestForResponse(request: Host.AidaClient.CompletionRequest): Host.AidaClient.CompletionResponse|null {
407
240
  if (!this.#aidaRequestCache || this.#aidaRequestCache.request.suffix !== request.suffix ||
408
241
  JSON.stringify(this.#aidaRequestCache.request.options) !== JSON.stringify(request.options)) {
@@ -476,12 +309,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
476
309
  this.#aidaRequestCache = undefined;
477
310
  }
478
311
 
479
- onTextChanged(
480
- prefix: string, suffix: string, cursorPositionAtRequest: number,
481
- inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage): void {
482
- this.#debouncedRequestAidaSuggestion(prefix, suffix, cursorPositionAtRequest, inferenceLanguage);
483
- }
484
-
485
312
  async completeCode(
486
313
  prefix: string, suffix: string, cursorPositionAtRequest: number,
487
314
  inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage,
@@ -525,18 +352,3 @@ export const enum ContextFlavor {
525
352
  CONSOLE = 'console', // generated code can contain console specific APIs like `$0`.
526
353
  SOURCES = 'sources',
527
354
  }
528
-
529
- export const enum Events {
530
- RESPONSE_RECEIVED = 'ResponseReceived',
531
- REQUEST_TRIGGERED = 'RequestTriggered',
532
- }
533
-
534
- export interface ResponseReceivedEvent {
535
- citations?: Host.AidaClient.Citation[];
536
- }
537
-
538
- export interface EventTypes {
539
- [Events.RESPONSE_RECEIVED]: ResponseReceivedEvent;
540
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
541
- [Events.REQUEST_TRIGGERED]: {};
542
- }
@@ -134,6 +134,7 @@ export interface ViewInput {
134
134
  onTakeScreenshot: () => void;
135
135
  onRemoveImageInput: () => void;
136
136
  onImageUpload: (ev: Event) => void;
137
+ onImagePaste: (event: ClipboardEvent) => void;
137
138
  }
138
139
 
139
140
  export type ViewOutput = undefined;
@@ -287,6 +288,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
287
288
  wrap="hard"
288
289
  maxlength="10000"
289
290
  @keydown=${input.onTextAreaKeyDown}
291
+ @paste=${input.onImagePaste}
290
292
  @input=${(event: KeyboardEvent) => {
291
293
  input.onTextInputChange((event.target as HTMLInputElement).value);
292
294
  }}
@@ -548,6 +550,25 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
548
550
  });
549
551
  }
550
552
 
553
+ #handleImagePaste = (event: ClipboardEvent): void => {
554
+ if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
555
+ return;
556
+ }
557
+
558
+ const files = event.clipboardData?.files;
559
+ if (!files || files.length === 0) {
560
+ return;
561
+ }
562
+
563
+ const imageFile = Array.from(files).find(file => file.type.startsWith('image/'));
564
+ if (!imageFile) {
565
+ return;
566
+ }
567
+
568
+ event.preventDefault();
569
+ void this.#handleLoadImage(imageFile);
570
+ };
571
+
551
572
  async #handleLoadImage(file: File): Promise<void> {
552
573
  const showLoadingTimeout = setTimeout(() => {
553
574
  this.#imageInput = {isLoading: true};
@@ -631,6 +652,7 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
631
652
  textAreaRef: this.#textAreaRef,
632
653
  onContextClick: this.onContextClick,
633
654
  onInspectElementClick: this.onInspectElementClick,
655
+ onImagePaste: this.#handleImagePaste,
634
656
  onNewConversation: this.onNewConversation,
635
657
  onTextInputChange: () => {
636
658
  this.requestUpdate();
@@ -8,6 +8,7 @@ import '../../ui/components/tooltips/tooltips.js';
8
8
  import * as Host from '../../core/host/host.js';
9
9
  import * as i18n from '../../core/i18n/i18n.js';
10
10
  import * as Root from '../../core/root/root.js';
11
+ import * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
11
12
  import * as UI from '../../ui/legacy/legacy.js';
12
13
  import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
13
14
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
@@ -26,13 +27,23 @@ const UIStringsNotTranslate = {
26
27
  /**
27
28
  * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
28
29
  */
29
- tooltipDisclaimerTextForAiCodeCompletion:
30
+ tooltipDisclaimerTextForAiCodeCompletionInConsole:
30
31
  '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.',
31
32
  /**
32
33
  * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
33
34
  */
34
- tooltipDisclaimerTextForAiCodeCompletionNoLogging:
35
- '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.',
35
+ tooltipDisclaimerTextForAiCodeCompletionNoLoggingInConsole:
36
+ '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.',
37
+ /**
38
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
39
+ */
40
+ tooltipDisclaimerTextForAiCodeCompletionInSources:
41
+ '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.',
42
+ /**
43
+ * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
44
+ */
45
+ tooltipDisclaimerTextForAiCodeCompletionNoLoggingInSources:
46
+ '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.',
36
47
  /**
37
48
  * Text for tooltip shown on hovering over spinner.
38
49
  */
@@ -49,12 +60,27 @@ const UIStringsNotTranslate = {
49
60
 
50
61
  const lockedString = i18n.i18n.lockedString;
51
62
 
63
+ function getTooltipDisclaimerText(noLogging: boolean, panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor): string {
64
+ switch (panel) {
65
+ case AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE:
66
+ return noLogging ?
67
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLoggingInConsole) :
68
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionInConsole);
69
+ case AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES:
70
+ return noLogging ?
71
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLoggingInSources) :
72
+ lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionInSources);
73
+ }
74
+ }
75
+
52
76
  export interface ViewInput {
53
77
  disclaimerTooltipId?: string;
54
78
  spinnerTooltipId?: string;
55
79
  noLogging: boolean;
56
80
  aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
57
81
  onManageInSettingsTooltipClick: () => void;
82
+ // TODO(b/472268298): Remove ContextFlavor explicitly and pass required values
83
+ panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
58
84
  }
59
85
 
60
86
  export interface ViewOutput {
@@ -67,10 +93,11 @@ export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) =
67
93
  export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
68
94
  (input, output, target) => {
69
95
  if (input.aidaAvailability !== Host.AidaClient.AidaAccessPreconditions.AVAILABLE || !input.disclaimerTooltipId ||
70
- !input.spinnerTooltipId) {
96
+ !input.spinnerTooltipId || !input.panel) {
71
97
  render(nothing, target);
72
98
  return;
73
99
  }
100
+ const tooltipDisclaimerText = getTooltipDisclaimerText(input.noLogging, input.panel);
74
101
  // clang-format off
75
102
  render(
76
103
  html`
@@ -118,7 +145,7 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
118
145
  }
119
146
  })}>
120
147
  <div class="disclaimer-tooltip-container"><div class="tooltip-text">
121
- ${input.noLogging ? lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLogging) : lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletion)}
148
+ ${tooltipDisclaimerText}
122
149
  </div>
123
150
  <span
124
151
  tabIndex="0"
@@ -146,6 +173,7 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
146
173
  #loading = false;
147
174
  #loadingStartTime = 0;
148
175
  #spinnerLoadingTimeout: number|undefined;
176
+ #panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
149
177
 
150
178
  #aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
151
179
  #boundOnAidaAvailabilityChange: () => Promise<void>;
@@ -196,6 +224,11 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
196
224
  }
197
225
  }
198
226
 
227
+ set panel(panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor) {
228
+ this.#panel = panel;
229
+ this.requestUpdate();
230
+ }
231
+
199
232
  async #onAidaAvailabilityChange(): Promise<void> {
200
233
  const currentAidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
201
234
  if (currentAidaAvailability !== this.#aidaAvailability) {
@@ -217,6 +250,7 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
217
250
  noLogging: this.#noLogging,
218
251
  aidaAvailability: this.#aidaAvailability,
219
252
  onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
253
+ panel: this.#panel,
220
254
  },
221
255
  this.#viewOutput, this.contentElement);
222
256
  }
@@ -7,6 +7,7 @@ import '../../ui/components/tooltips/tooltips.js';
7
7
 
8
8
  import * as Host from '../../core/host/host.js';
9
9
  import * as i18n from '../../core/i18n/i18n.js';
10
+ import type * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
10
11
  import * as UI from '../../ui/legacy/legacy.js';
11
12
  import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
12
13
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
@@ -32,6 +33,7 @@ export interface AiCodeCompletionSummaryToolbarProps {
32
33
  disclaimerTooltipId?: string;
33
34
  spinnerTooltipId?: string;
34
35
  hasTopBorder?: boolean;
36
+ panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
35
37
  }
36
38
 
37
39
  export interface ViewInput {
@@ -42,6 +44,7 @@ export interface ViewInput {
42
44
  loading: boolean;
43
45
  hasTopBorder: boolean;
44
46
  aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
47
+ panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
45
48
  }
46
49
 
47
50
  export type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
@@ -65,6 +68,7 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, _output, target) => {
65
68
  disclaimerTooltipId: input.disclaimerTooltipId,
66
69
  spinnerTooltipId: input.spinnerTooltipId,
67
70
  loading: input.loading,
71
+ panel: input.panel,
68
72
  })} class="disclaimer-widget"></devtools-widget>` : nothing;
69
73
 
70
74
  const recitationNotice = input.citations && input.citations.size > 0 ?
@@ -110,6 +114,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
110
114
  #citations = new Set<string>();
111
115
  #loading = false;
112
116
  #hasTopBorder = false;
117
+ #panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
113
118
 
114
119
  #aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
115
120
  #boundOnAidaAvailabilityChange: () => Promise<void>;
@@ -120,6 +125,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
120
125
  this.#spinnerTooltipId = props.spinnerTooltipId;
121
126
  this.#citationsTooltipId = props.citationsTooltipId;
122
127
  this.#hasTopBorder = props.hasTopBorder ?? false;
128
+ this.#panel = props.panel;
123
129
  this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
124
130
  this.#view = view ?? DEFAULT_SUMMARY_TOOLBAR_VIEW;
125
131
  this.requestUpdate();
@@ -158,6 +164,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
158
164
  loading: this.#loading,
159
165
  hasTopBorder: this.#hasTopBorder,
160
166
  aidaAvailability: this.#aidaAvailability,
167
+ panel: this.#panel,
161
168
  },
162
169
  undefined, this.contentElement);
163
170
  }
@@ -24,13 +24,21 @@ const UIStringsNotTranslate = {
24
24
  */
25
25
  cmdItoGenerateCode: 'Cmd+I to generate code',
26
26
  /**
27
- * Text for teaser when generating suggestion.
27
+ * @description Text for teaser when generating suggestion.
28
28
  */
29
29
  generating: 'Generating... (esc to cancel)',
30
30
  /**
31
- * Text for teaser for discoverability.
31
+ * @description Text for teaser for discoverability.
32
32
  */
33
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',
34
42
  /**
35
43
  * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Console panel.
36
44
  */
@@ -68,6 +76,7 @@ export enum AiCodeGenerationTeaserDisplayState {
68
76
  TRIGGER = 'trigger',
69
77
  DISCOVERY = 'discovery',
70
78
  LOADING = 'loading',
79
+ GENERATED = 'generated',
71
80
  }
72
81
 
73
82
  function getTooltipDisclaimerText(noLogging: boolean, panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor): string {
@@ -94,6 +103,7 @@ export interface ViewInput {
94
103
 
95
104
  export interface ViewOutput {
96
105
  hideTooltip?: () => void;
106
+ setTimerText?: (text: string) => void;
97
107
  }
98
108
 
99
109
  export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
@@ -165,7 +175,27 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
165
175
  }
166
176
 
167
177
  case AiCodeGenerationTeaserDisplayState.LOADING: {
168
- 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
189
+ break;
190
+ }
191
+
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
169
199
  break;
170
200
  }
171
201
  }
@@ -192,6 +222,8 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
192
222
  #disclaimerTooltipId?: string;
193
223
  #noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
194
224
  #panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
225
+ #timerIntervalId?: number;
226
+ #loadStartTime?: number;
195
227
 
196
228
  constructor(view?: View) {
197
229
  super();
@@ -214,6 +246,11 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
214
246
  this.#viewOutput, this.contentElement);
215
247
  }
216
248
 
249
+ override willHide(): void {
250
+ super.willHide();
251
+ this.#stopLoadingAnimation();
252
+ }
253
+
217
254
  get displayState(): AiCodeGenerationTeaserDisplayState {
218
255
  return this.#displayState;
219
256
  }
@@ -224,6 +261,36 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
224
261
  }
225
262
  this.#displayState = displayState;
226
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;
227
294
  }
228
295
 
229
296
  set disclaimerTooltipId(disclaimerTooltipId: string) {
@@ -16,6 +16,18 @@
16
16
  align-items: center;
17
17
  }
18
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
+
19
31
  .new-badge {
20
32
  font-style: normal;
21
33
  display: inline-block;
@@ -44,5 +56,23 @@
44
56
  }
45
57
  }
46
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: "⠏"; }
47
77
  }
48
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');
@@ -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
 
@@ -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) {
@@ -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
+ }
@@ -13,6 +13,7 @@ import * as UI from '../../../ui/legacy/legacy.js';
13
13
  import * as VisualLogging from '../../visual_logging/visual_logging.js';
14
14
 
15
15
  import {AiCodeCompletionTeaserPlaceholder} from './AiCodeCompletionTeaserPlaceholder.js';
16
+ import {AiCodeGenerationParser} from './AiCodeGenerationParser.js';
16
17
  import {
17
18
  acceptAiAutoCompleteSuggestion,
18
19
  aiAutoCompleteSuggestion,
@@ -79,11 +80,11 @@ export class AiCodeGenerationProvider {
79
80
  extension(): CodeMirror.Extension[] {
80
81
  return [
81
82
  CodeMirror.EditorView.updateListener.of(update => this.#activateTeaser(update)),
82
- CodeMirror.EditorView.updateListener.of(update => this.#abortGenerationDuringUpdate(update)),
83
+ CodeMirror.EditorView.updateListener.of(update => this.#abortOrDismissGenerationDuringUpdate(update)),
83
84
  aiAutoCompleteSuggestion,
84
85
  aiAutoCompleteSuggestionState,
85
86
  aiCodeGenerationTeaserModeState,
86
- this.#generationTeaserCompartment.of([]),
87
+ CodeMirror.Prec.highest(this.#generationTeaserCompartment.of([])),
87
88
  CodeMirror.Prec.highest(CodeMirror.keymap.of(this.#editorKeymap())),
88
89
  ];
89
90
  }
@@ -142,9 +143,7 @@ export class AiCodeGenerationProvider {
142
143
  return false;
143
144
  }
144
145
  if (hasActiveAiSuggestion(this.#editor.state)) {
145
- this.#editor.dispatch({
146
- effects: setAiAutoCompleteSuggestion.of(null),
147
- });
146
+ this.#dismissTeaserAndSuggestion();
148
147
  return true;
149
148
  }
150
149
  const generationTeaserIsLoading = this.#generationTeaser.displayState ===
@@ -152,7 +151,7 @@ export class AiCodeGenerationProvider {
152
151
  if (this.#generationTeaser.isShowing() && generationTeaserIsLoading) {
153
152
  this.#controller.abort();
154
153
  this.#controller = new AbortController();
155
- this.#dismissTeaser();
154
+ this.#dismissTeaserAndSuggestion();
156
155
  return true;
157
156
  }
158
157
  return false;
@@ -194,9 +193,14 @@ export class AiCodeGenerationProvider {
194
193
  ];
195
194
  }
196
195
 
197
- #dismissTeaser(): void {
196
+ #dismissTeaserAndSuggestion(): void {
198
197
  this.#generationTeaser.displayState = PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.TRIGGER;
199
- this.#editor?.dispatch({effects: setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.DISMISSED)});
198
+ this.#editor?.dispatch({
199
+ effects: [
200
+ setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.DISMISSED),
201
+ setAiAutoCompleteSuggestion.of(null),
202
+ ]
203
+ });
200
204
  }
201
205
 
202
206
  #activateTeaser(update: CodeMirror.ViewUpdate): void {
@@ -211,26 +215,35 @@ export class AiCodeGenerationProvider {
211
215
  }
212
216
 
213
217
  /**
214
- * Monitors editor changes to cancel an ongoing AI generation.
215
- * We abort the request and dismiss the teaser if the user modifies the
216
- * document or moves their cursor/selection. These actions indicate the user
217
- * is no longer focused on the current generation point or has manually
218
- * resumed editing, making the pending suggestion irrelevant.
218
+ * Monitors editor changes to cancel an ongoing AI generation or dismiss one
219
+ * if it already exists.
220
+ * We abort the request (or dismiss suggestion) and dismiss the teaser if the
221
+ * user modifies the document or moves their cursor/selection. These actions
222
+ * indicate the user is no longer focused on the current generation point or
223
+ * has manually resumed editing, making the suggestion irrelevant.
219
224
  */
220
- #abortGenerationDuringUpdate(update: CodeMirror.ViewUpdate): void {
225
+ #abortOrDismissGenerationDuringUpdate(update: CodeMirror.ViewUpdate): void {
221
226
  if (!update.docChanged && update.state.selection.main.head === update.startState.selection.main.head) {
222
227
  return;
223
228
  }
224
229
  const currentTeaserMode = update.state.field(aiCodeGenerationTeaserModeState);
225
- const generationTeaserIsLoading = this.#generationTeaser.displayState ===
226
- PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING;
227
- // Generation should be in progress
228
- if (currentTeaserMode === AiCodeGenerationTeaserMode.DISMISSED || !generationTeaserIsLoading) {
230
+ if (currentTeaserMode === AiCodeGenerationTeaserMode.DISMISSED) {
231
+ return;
232
+ }
233
+ if (this.#generationTeaser.displayState ===
234
+ PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING) {
235
+ this.#controller.abort();
236
+ this.#controller = new AbortController();
237
+ this.#dismissTeaserAndSuggestion();
238
+ return;
239
+ }
240
+ if (this.#generationTeaser.displayState ===
241
+ PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED) {
242
+ update.view.dispatch({effects: setAiAutoCompleteSuggestion.of(null)});
243
+ this.#generationTeaser.displayState =
244
+ PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.TRIGGER;
229
245
  return;
230
246
  }
231
- this.#controller.abort();
232
- this.#controller = new AbortController();
233
- this.#dismissTeaser();
234
247
  }
235
248
 
236
249
  async #triggerAiCodeGeneration(options?: {signal?: AbortSignal}): Promise<void> {
@@ -240,9 +253,8 @@ export class AiCodeGenerationProvider {
240
253
 
241
254
  this.#generationTeaser.displayState = PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING;
242
255
  const cursor = this.#editor.state.selection.main.head;
243
- // TODO(b/445899453): Detect all types of comments
244
- const query = this.#editor.state.doc.lineAt(cursor).text;
245
- if (query.trim().length === 0) {
256
+ const query = AiCodeGenerationParser.extractCommentText(this.#editor.state, cursor);
257
+ if (!query || query.trim().length === 0) {
246
258
  return;
247
259
  }
248
260
 
@@ -256,7 +268,7 @@ export class AiCodeGenerationProvider {
256
268
  this.#aiCodeGenerationConfig?.generationContext.inferenceLanguage, options);
257
269
 
258
270
  if (this.#generationTeaser) {
259
- this.#dismissTeaser();
271
+ this.#dismissTeaserAndSuggestion();
260
272
  }
261
273
 
262
274
  if (!generationResponse || generationResponse.samples.length === 0) {
@@ -275,19 +287,25 @@ export class AiCodeGenerationProvider {
275
287
  const suggestionText = matchArray ? matchArray[1].trim() : topSample.generationString;
276
288
 
277
289
  this.#editor.dispatch({
278
- effects: setAiAutoCompleteSuggestion.of({
279
- text: '\n' + suggestionText,
280
- from: cursor,
281
- rpcGlobalId: generationResponse.metadata.rpcGlobalId,
282
- sampleId: topSample.sampleId,
283
- startTime,
284
- onImpression: this.#aiCodeGeneration?.registerUserImpression.bind(this.#aiCodeGeneration),
285
- })
290
+ effects: [
291
+ setAiAutoCompleteSuggestion.of({
292
+ text: '\n' + suggestionText,
293
+ from: cursor,
294
+ rpcGlobalId: generationResponse.metadata.rpcGlobalId,
295
+ sampleId: topSample.sampleId,
296
+ startTime,
297
+ onImpression: this.#aiCodeGeneration?.registerUserImpression.bind(this.#aiCodeGeneration),
298
+ }),
299
+ setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.ACTIVE)
300
+ ]
286
301
  });
302
+ this.#generationTeaser.displayState =
303
+ PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED;
287
304
 
288
305
  AiCodeGeneration.debugLog('Suggestion dispatched to the editor', suggestionText);
289
306
  const citations = topSample.attributionMetadata?.citations ?? [];
290
307
  this.#aiCodeGenerationConfig?.onResponseReceived(citations);
308
+ return;
291
309
  } catch (e) {
292
310
  AiCodeGeneration.debugLog('Error while fetching code generation suggestions from AIDA', e);
293
311
  this.#aiCodeGenerationConfig?.onResponseReceived([]);
@@ -295,7 +313,7 @@ export class AiCodeGenerationProvider {
295
313
  }
296
314
 
297
315
  if (this.#generationTeaser) {
298
- this.#dismissTeaser();
316
+ this.#dismissTeaserAndSuggestion();
299
317
  }
300
318
  }
301
319
  }
@@ -327,8 +345,7 @@ function aiCodeGenerationTeaserExtension(teaser: PanelCommon.AiCodeGenerationTea
327
345
  const line = this.#view.state.doc.lineAt(cursorPosition);
328
346
 
329
347
  const isEmptyLine = line.length === 0;
330
- // TODO(b/445899453): Detect all types of comments
331
- const isComment = line.text.startsWith('//');
348
+ const isComment = Boolean(AiCodeGenerationParser.extractCommentText(this.#view.state, cursorPosition));
332
349
  const isCursorAtEndOfLine = cursorPosition >= line.to;
333
350
 
334
351
  if ((isEmptyLine) || (isComment && isCursorAtEndOfLine)) {
@@ -341,9 +358,10 @@ function aiCodeGenerationTeaserExtension(teaser: PanelCommon.AiCodeGenerationTea
341
358
  }
342
359
 
343
360
  #updateTeaserState(state: CodeMirror.EditorState): void {
344
- // Only handle non loading states, as updates during generation are handled by
345
- // #abortGenerationDuringUpdate in AiCodeGenerationProvider
346
- if (teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING) {
361
+ // Only handle non loading and non generated states, as updates during and after generation are handled by
362
+ // #abortOrDismissGenerationDuringUpdate in AiCodeGenerationProvider
363
+ if (teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING ||
364
+ teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED) {
347
365
  return;
348
366
  }
349
367
  const cursorPosition = state.selection.main.head;
@@ -4,6 +4,7 @@
4
4
 
5
5
  export * as AiCodeCompletionProvider from './AiCodeCompletionProvider.js';
6
6
  export * as AiCodeCompletionTeaserPlaceholder from './AiCodeCompletionTeaserPlaceholder.js';
7
+ export * as AiCodeGenerationParser from './AiCodeGenerationParser.js';
7
8
  export * as AiCodeGenerationProvider from './AiCodeGenerationProvider.js';
8
9
  export * as AutocompleteHistory from './AutocompleteHistory.js';
9
10
  export * as Config from './config.js';
package/package.json CHANGED
@@ -105,5 +105,5 @@
105
105
  "flat-cache": "6.1.12"
106
106
  }
107
107
  },
108
- "version": "1.0.1563104"
108
+ "version": "1.0.1563377"
109
109
  }