chrome-devtools-frontend 1.0.1646714 → 1.0.1650035

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/extension-api/ExtensionAPI.d.ts +26 -0
  2. package/front_end/core/common/Debouncer.ts +9 -1
  3. package/front_end/core/sdk/CSSMatchedStyles.ts +3 -1
  4. package/front_end/core/sdk/CSSMetadata.ts +20 -1
  5. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +65 -0
  6. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +15 -0
  7. package/front_end/generated/Deprecation.ts +8 -0
  8. package/front_end/generated/InspectorBackendCommands.ts +3 -2
  9. package/front_end/generated/SupportedCSSProperties.js +533 -19
  10. package/front_end/generated/protocol.ts +26 -4
  11. package/front_end/models/ai_assistance/agents/AiAgent.ts +8 -1
  12. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +7 -0
  13. package/front_end/models/extensions/ExtensionAPI.ts +47 -21
  14. package/front_end/models/javascript_metadata/NativeFunctions.js +7 -3
  15. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +39 -1
  16. package/front_end/panels/ai_assistance/ai_assistance.ts +1 -0
  17. package/front_end/panels/ai_assistance/components/AIv2MarkdownRenderer.ts +228 -0
  18. package/front_end/panels/ai_assistance/components/ChatMessage.ts +39 -2
  19. package/front_end/panels/application/ApplicationPanelSidebar.ts +4 -0
  20. package/front_end/panels/application/CookieItemsView.ts +55 -6
  21. package/front_end/panels/application/DOMStorageItemsView.ts +43 -8
  22. package/front_end/panels/application/KeyValueStorageItemsView.ts +17 -9
  23. package/front_end/panels/console/ConsolePinPane.ts +1 -0
  24. package/front_end/panels/elements/ElementsTreeElement.ts +97 -57
  25. package/front_end/panels/elements/StylePropertyTreeElement.ts +73 -16
  26. package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +9 -1
  27. package/front_end/panels/elements/StylesSidebarPane.ts +22 -8
  28. package/front_end/panels/layer_viewer/PaintProfilerView.ts +309 -182
  29. package/front_end/panels/layer_viewer/paintProfiler.css +27 -23
  30. package/front_end/panels/layers/LayerPaintProfilerView.ts +86 -29
  31. package/front_end/panels/profiler/HeapSnapshotView.ts +6 -0
  32. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +210 -356
  33. package/front_end/panels/sources/watchExpressionsSidebarPane.css +7 -0
  34. package/front_end/panels/timeline/TimelinePaintProfilerView.ts +5 -5
  35. package/front_end/panels/timeline/components/NetworkTrackWidget.ts +122 -0
  36. package/front_end/panels/timeline/components/components.ts +2 -0
  37. package/front_end/panels/timeline/components/networkTrackWidget.css +31 -0
  38. package/front_end/panels/timeline/timeline.ts +2 -0
  39. package/front_end/third_party/chromium/README.chromium +1 -1
  40. package/front_end/ui/components/buttons/FloatingButton.ts +17 -2
  41. package/front_end/ui/components/buttons/floatingButton.css +2 -2
  42. package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +1 -1
  43. package/front_end/ui/legacy/TextPrompt.ts +24 -18
  44. package/front_end/ui/legacy/Treeoutline.ts +31 -4
  45. package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +50 -24
  46. package/front_end/ui/legacy/components/source_frame/SourceFrame.ts +10 -10
  47. package/front_end/ui/visual_logging/KnownContextValues.ts +5 -0
  48. package/package.json +1 -1
@@ -50,6 +50,14 @@ const UIStrings = {
50
50
  * @description Label for checkbox to show URL-decoded cookie values
51
51
  */
52
52
  showUrlDecoded: 'Show URL-decoded',
53
+ /**
54
+ * @description Text of a context menu item to start a chat with AI
55
+ */
56
+ startAChat: 'Start a chat',
57
+ /**
58
+ * @description Text of a context menu item to explain a web cookie with AI
59
+ */
60
+ explainCookie: 'Explain this cookie',
53
61
  /**
54
62
  * @description Text in Cookie Items View of the Application panel to indicate that no cookie has been selected for preview
55
63
  */
@@ -174,6 +182,10 @@ interface CookieItemsViewInput {
174
182
  onDeleteSelectedItems: () => void;
175
183
  onDeleteAllItems: () => void;
176
184
  onRefreshItems: () => void;
185
+ aiButtonIsEnabled: boolean;
186
+ onPopulateAiContextMenu: (cookie: SDK.Cookie.Cookie, contextMenu: UI.ContextMenu.ContextMenu) => void;
187
+ onAiButtonClick: (cookie: SDK.Cookie.Cookie, event: Event) => void;
188
+ aiButtonTitle?: string;
177
189
  selectedCookie: SDK.Cookie.Cookie|null;
178
190
  }
179
191
 
@@ -203,6 +215,10 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
203
215
  refreshCallback: input.onRefresh,
204
216
  selectedCallback: input.onSelect,
205
217
  deleteCallback: input.onDelete,
218
+ aiButtonIsEnabled: input.aiButtonIsEnabled,
219
+ onAiButtonClick: input.onAiButtonClick,
220
+ onPopulateAiContextMenu: input.onPopulateAiContextMenu,
221
+ aiButtonTitle: input.aiButtonTitle,
206
222
  editable: true,
207
223
  })}
208
224
  ></devtools-widget>
@@ -220,8 +236,8 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
220
236
  </devtools-split-view>
221
237
  </devtools-widget>
222
238
  `,
223
- // clang-format on
224
- target, {container: {attributes: {jslog: `${VisualLogging.pane('cookies-data')}`}}});
239
+ // clang-format on
240
+ target, {container: {attributes: {jslog: `${VisualLogging.pane('cookies-data')}`}}});
225
241
  };
226
242
 
227
243
  export class CookieItemsView extends UI.Widget.VBox {
@@ -261,7 +277,6 @@ export class CookieItemsView extends UI.Widget.VBox {
261
277
  this.cookieDomain = domain;
262
278
  this.refreshItems();
263
279
  this.model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, this.onCookieListUpdate, this);
264
- this.updateAiAssistanceContext(null);
265
280
  }
266
281
 
267
282
  override performUpdate(): void {
@@ -296,6 +311,12 @@ export class CookieItemsView extends UI.Widget.VBox {
296
311
  onDeleteAllItems: this.deleteAllItems.bind(this),
297
312
  onRefreshItems: this.refreshItems.bind(this),
298
313
  selectedCookie: this.selectedCookie,
314
+ aiButtonIsEnabled: this.isAiButtonEnabled(),
315
+ onPopulateAiContextMenu: this.#onPopulateAiContextMenu.bind(this),
316
+ onAiButtonClick: this.#onAiButtonClick.bind(this),
317
+ aiButtonTitle: this.isAiButtonEnabled() ?
318
+ UI.ActionRegistry.ActionRegistry.instance().getAction('ai-assistance.storage-floating-button').title() :
319
+ undefined,
299
320
  };
300
321
 
301
322
  this.view(input, output, this.contentElement);
@@ -304,7 +325,6 @@ export class CookieItemsView extends UI.Widget.VBox {
304
325
  override wasShown(): void {
305
326
  super.wasShown();
306
327
  this.refreshItems();
307
- this.updateAiAssistanceContext(this.selectedCookie);
308
328
  }
309
329
 
310
330
  private showPreview(cookie: SDK.Cookie.Cookie|null): void {
@@ -313,10 +333,9 @@ export class CookieItemsView extends UI.Widget.VBox {
313
333
  }
314
334
  this.selectedCookie = cookie;
315
335
  this.requestUpdate();
316
- this.updateAiAssistanceContext(cookie);
317
336
  }
318
337
 
319
- private updateAiAssistanceContext(cookie: SDK.Cookie.Cookie|null): void {
338
+ #updateAiAssistanceContext(cookie: SDK.Cookie.Cookie|null): void {
320
339
  if (cookie && cookie.httpOnly()) {
321
340
  UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, null);
322
341
  return;
@@ -342,6 +361,7 @@ export class CookieItemsView extends UI.Widget.VBox {
342
361
  }
343
362
  this.#toolbar.setCanDeleteSelected(Boolean(selectedCookie));
344
363
  this.showPreview(selectedCookie);
364
+ this.#updateAiAssistanceContext(selectedCookie);
345
365
  }
346
366
 
347
367
  private async saveCookie(newCookie: SDK.Cookie.Cookie, oldCookie: SDK.Cookie.Cookie|null): Promise<boolean> {
@@ -393,6 +413,7 @@ export class CookieItemsView extends UI.Widget.VBox {
393
413
  * This will only delete the currently visible cookies.
394
414
  */
395
415
  deleteAllItems(): void {
416
+ UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, null);
396
417
  this.showPreview(null);
397
418
  void this.model.deleteCookies(this.shownCookies);
398
419
  }
@@ -412,4 +433,32 @@ export class CookieItemsView extends UI.Widget.VBox {
412
433
  refreshItems(): void {
413
434
  void this.model.getCookiesForDomain(this.cookieDomain, true).then(this.updateWithCookies.bind(this));
414
435
  }
436
+
437
+ private isAiButtonEnabled(): boolean {
438
+ return UI.ActionRegistry.ActionRegistry.instance().hasAction('ai-assistance.storage-floating-button');
439
+ }
440
+
441
+ #onPopulateAiContextMenu(cookie: SDK.Cookie.Cookie, contextMenu: UI.ContextMenu.ContextMenu): void {
442
+ const openAiAssistanceId = 'ai-assistance.application-panel-context';
443
+ if (this.isAiButtonEnabled() && UI.ActionRegistry.ActionRegistry.instance().hasAction(openAiAssistanceId)) {
444
+ this.#updateAiAssistanceContext(cookie);
445
+ if (UI.Context.Context.instance().flavor(AiAssistanceModel.StorageItem.StorageItem)) {
446
+ const action = UI.ActionRegistry.ActionRegistry.instance().getAction(openAiAssistanceId);
447
+ const submenu = contextMenu.footerSection().appendSubMenuItem(action.title(), false, openAiAssistanceId);
448
+ submenu.defaultSection().appendAction(openAiAssistanceId, i18nString(UIStrings.startAChat));
449
+ submenu.defaultSection().appendItem(
450
+ i18nString(UIStrings.explainCookie), () => action.execute({prompt: 'What is the purpose of this cookie?'}),
451
+ {disabled: !action.enabled(), jslogContext: openAiAssistanceId + '.cookies'});
452
+ }
453
+ }
454
+ }
455
+
456
+ #onAiButtonClick(cookie: SDK.Cookie.Cookie, _event: Event): void {
457
+ this.#updateAiAssistanceContext(cookie);
458
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
459
+ const storageFloatingButtonId = 'ai-assistance.storage-floating-button';
460
+ if (actionRegistry.hasAction(storageFloatingButtonId)) {
461
+ void actionRegistry.getAction(storageFloatingButtonId).execute();
462
+ }
463
+ }
415
464
  }
@@ -55,6 +55,14 @@ const UIStrings = {
55
55
  * @description Text for announcing a DOM Storage key/value item has been deleted
56
56
  */
57
57
  domStorageItemDeleted: 'The storage item was deleted.',
58
+ /**
59
+ * @description Text of a context menu item to start a chat with AI
60
+ */
61
+ startAChat: 'Start a chat',
62
+ /**
63
+ * @description Text of a context menu item to explain a storage item of a storage bucket with AI
64
+ */
65
+ explainItem: 'Explain this item',
58
66
  } as const;
59
67
  const str_ = i18n.i18n.registerUIStrings('panels/application/DOMStorageItemsView.ts', UIStrings);
60
68
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -111,7 +119,6 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
111
119
  SDK.DOMStorageModel.DOMStorage.Events.DOM_STORAGE_ITEM_UPDATED, this.domStorageItemUpdated, this),
112
120
  ];
113
121
  this.refreshItems();
114
- this.selectedItemChanged(null);
115
122
  }
116
123
 
117
124
  private domStorageItemsCleared(): void {
@@ -174,13 +181,7 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
174
181
  this.showItems(filteredItems);
175
182
  }
176
183
 
177
- override deleteAllItems(): void {
178
- this.domStorage.clear();
179
- // explicitly clear the view because the event won't be fired when it has no items
180
- this.domStorageItemsCleared();
181
- }
182
-
183
- protected override selectedItemChanged(item: {key: string, value: string}|null): void {
184
+ #setAiStorageContext(item: {key: string, value: string}|null): void {
184
185
  const storageKey = this.domStorage.storageKey;
185
186
  if (!storageKey) {
186
187
  return;
@@ -205,10 +206,44 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
205
206
  UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, storageItem);
206
207
  }
207
208
 
209
+ override deleteAllItems(): void {
210
+ this.domStorage.clear();
211
+ // explicitly clear the view because the event won't be fired when it has no items
212
+ this.domStorageItemsCleared();
213
+ }
214
+
215
+ protected override selectedItemChanged(item: {key: string, value: string}|null): void {
216
+ this.#setAiStorageContext(item);
217
+ }
218
+
208
219
  protected override isAiButtonEnabled(): boolean {
209
220
  return UI.ActionRegistry.ActionRegistry.instance().hasAction('ai-assistance.storage-floating-button');
210
221
  }
211
222
 
223
+ protected override populateContextMenu(item: {key: string, value: string},
224
+ contextMenu: UI.ContextMenu.ContextMenu): void {
225
+ const openAiAssistanceId = 'ai-assistance.application-panel-context';
226
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
227
+ if (actionRegistry.hasAction(openAiAssistanceId)) {
228
+ this.#setAiStorageContext(item);
229
+ const action = actionRegistry.getAction(openAiAssistanceId);
230
+ const submenu = contextMenu.footerSection().appendSubMenuItem(action.title(), false, openAiAssistanceId);
231
+ submenu.defaultSection().appendAction(openAiAssistanceId, i18nString(UIStrings.startAChat));
232
+ submenu.defaultSection().appendItem(i18nString(UIStrings.explainItem),
233
+ () => action.execute({prompt: 'Explain this storage item.'}),
234
+ {disabled: !action.enabled(), jslogContext: openAiAssistanceId + '.storage'});
235
+ }
236
+ }
237
+
238
+ protected override onAiButtonClick(item: {key: string, value: string}, _event: Event): void {
239
+ this.#setAiStorageContext(item);
240
+ const aiFloatingActionId = 'ai-assistance.storage-floating-button';
241
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
242
+ if (actionRegistry.hasAction(aiFloatingActionId)) {
243
+ void actionRegistry.getAction(aiFloatingActionId).execute();
244
+ }
245
+ }
246
+
212
247
  protected removeItem(key: string): void {
213
248
  this.domStorage?.removeItem(key);
214
249
  }
@@ -92,6 +92,7 @@ export interface ViewInput {
92
92
  onDelete: (key: string) => void;
93
93
  onDeleteSelected: () => void;
94
94
  onDeleteAll: () => void;
95
+ onContextMenu?: (item: {key: string, value: string}, contextMenu: UI.ContextMenu.ContextMenu) => void;
95
96
  jslog?: string;
96
97
  classes?: string[];
97
98
  aiButtonTitle?: string;
@@ -175,6 +176,7 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
175
176
  @edit=${(e: CustomEvent<{columnId: string, valueBeforeEditing: string, newText: string}>) =>
176
177
  input.onEdit(item.key, item.value, e.detail.columnId, e.detail.valueBeforeEditing, e.detail.newText)}
177
178
  @delete=${() => input.onDelete(item.key)}
179
+ @contextmenu=${(e: CustomEvent<UI.ContextMenu.ContextMenu>) => input.onContextMenu?.(item, e.detail)}
178
180
  selected=${(input.selectedKey === item.key) || nothing}>
179
181
  <td>${input.showAiButton ? html`
180
182
  <span class="ai-button-container">
@@ -198,8 +200,8 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
198
200
  ${input.preview?.element}
199
201
  </devtools-widget>
200
202
  </devtools-split-view>`,
201
- // clang-format on
202
- target, {container: {attributes: {jslog: input.jslog}, classes: input.classes}});
203
+ // clang-format on
204
+ target, {container: {attributes: {jslog: input.jslog}, classes: input.classes}});
203
205
  };
204
206
  }
205
207
  super();
@@ -246,13 +248,13 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
246
248
  void this.#previewEntry(item);
247
249
  this.selectedItemChanged(item);
248
250
  },
249
- onAiButtonClick: (item: {key: string, value: string}, event: Event) => {
250
- event.stopPropagation();
251
- viewInput.onSelect(item);
252
- const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
253
- if (actionRegistry.hasAction(STORAGE_FLOATING_BUTTON_ACTION_ID)) {
254
- void actionRegistry.getAction(STORAGE_FLOATING_BUTTON_ACTION_ID).execute();
255
- }
251
+ onAiButtonClick: this.isAiButtonEnabled() ?
252
+ (item: {key: string, value: string}, event: Event) => {
253
+ this.onAiButtonClick(item, event);
254
+ } :
255
+ undefined,
256
+ onContextMenu: (item: {key: string, value: string}, contextMenu: UI.ContextMenu.ContextMenu) => {
257
+ this.populateContextMenu(item, contextMenu);
256
258
  },
257
259
 
258
260
  onSort: (ascending: boolean) => {
@@ -284,6 +286,12 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
284
286
  return false;
285
287
  }
286
288
 
289
+ protected populateContextMenu(_item: {key: string, value: string}, _contextMenu: UI.ContextMenu.ContextMenu): void {
290
+ }
291
+
292
+ protected onAiButtonClick(_item: {key: string, value: string}, _event: Event): void {
293
+ }
294
+
287
295
  protected get toolbar(): StorageItemsToolbar|undefined {
288
296
  return this.#toolbar;
289
297
  }
@@ -403,6 +403,7 @@ export class ConsolePinPresenter extends UI.Widget.Widget {
403
403
  CodeMirror.EditorView.domEventHandlers({
404
404
  blur: (_e, view) => this.#onBlur(view),
405
405
  paste: () => this.#onPaste(),
406
+ drop: event => event.preventDefault(),
406
407
  }),
407
408
  TextEditor.Config.baseConfiguration(doc),
408
409
  TextEditor.Config.closeBrackets.instance(),
@@ -738,65 +738,86 @@ function renderLinkifiedValue(value: string, node: SDK.DOMModel.DOMNode): Lit.Te
738
738
  });
739
739
  }
740
740
 
741
+ const relationPromisesCache = new WeakMap<SDK.DOMModel.DOMNode, Map<string, Promise<string|Lit.LitTemplate>>>();
742
+ const relatedElementsCache = new WeakMap<SDK.DOMModel.DOMNode, Map<string, SDK.DOMModel.DOMNode|null>>();
743
+
741
744
  function renderAttribute(
742
745
  attr: {name: string, value?: string}, updateRecord: Elements.ElementUpdateRecord.ElementUpdateRecord|null,
743
746
  isDiff: boolean, node: SDK.DOMModel.DOMNode): Lit.LitTemplate {
744
747
  const name = attr.name;
745
748
  const value = attr.value || '';
746
749
  const forceValue = isDiff;
750
+ const isRelation = name === 'popovertarget' || name === 'interesttarget' || name === 'commandfor';
747
751
  const hasText = (forceValue || value.length > 0);
748
- const jslog = VisualLogging.value(name === 'style' ? 'style-attribute' : 'attribute').track({
749
- change: true,
750
- dblclick: true,
751
- });
752
-
753
- const relationRef =
754
- (relation: Protocol.DOM.GetElementByRelationRequestRelation, tooltip: string): ReturnType<typeof ref> =>
755
- ref((el): void => {
756
- if (!el) {
757
- return;
758
- }
759
- void (async(): Promise<void> => {
760
- const relatedElementId = await node.domModel().getElementByRelation(node.id, relation);
761
- const relatedElement = node.domModel().nodeForId(relatedElementId);
762
- if (!relatedElement) {
763
- return;
764
- }
765
- const link = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(relatedElement, {
766
- preventKeyboardFocus: true,
767
- tooltip,
768
- textContent: el.textContent || undefined,
769
- isDynamicLink: true,
770
- });
771
- render(link, el as HTMLElement);
772
- })();
773
- });
752
+ const linkifyName = isRelation && value.length === 0;
753
+ const linkifyValue = isRelation && value.length > 0;
774
754
 
775
- let relationRefDirective: ReturnType<typeof relationRef> = ref(() => {});
776
- if (!value) {
755
+ let relation: Protocol.DOM.GetElementByRelationRequestRelation|undefined = undefined;
756
+ let tooltip = '';
757
+ if (isRelation) {
777
758
  if (name === 'popovertarget') {
778
- relationRefDirective = relationRef(
779
- Protocol.DOM.GetElementByRelationRequestRelation.PopoverTarget, i18nString(UIStrings.showPopoverTarget));
759
+ relation = Protocol.DOM.GetElementByRelationRequestRelation.PopoverTarget;
760
+ tooltip = i18nString(UIStrings.showPopoverTarget);
780
761
  } else if (name === 'interesttarget') {
781
- relationRefDirective = relationRef(
782
- Protocol.DOM.GetElementByRelationRequestRelation.InterestTarget, i18nString(UIStrings.showInterestTarget));
762
+ relation = Protocol.DOM.GetElementByRelationRequestRelation.InterestTarget;
763
+ tooltip = i18nString(UIStrings.showInterestTarget);
783
764
  } else if (name === 'commandfor') {
784
- relationRefDirective = relationRef(
785
- Protocol.DOM.GetElementByRelationRequestRelation.CommandFor, i18nString(UIStrings.showCommandForTarget));
786
- }
787
- }
765
+ relation = Protocol.DOM.GetElementByRelationRequestRelation.CommandFor;
766
+ tooltip = i18nString(UIStrings.showCommandForTarget);
767
+ }
768
+ }
769
+
770
+ let relationPromise: Promise<string|Lit.LitTemplate>|undefined = undefined;
771
+ if (isRelation && relation) {
772
+ let nodeCache = relationPromisesCache.get(node);
773
+ if (!nodeCache) {
774
+ nodeCache = new Map();
775
+ relationPromisesCache.set(node, nodeCache);
776
+ }
777
+ const cacheKey = `${relation}:${value}`;
778
+ relationPromise = nodeCache.get(cacheKey);
779
+ const relationType = relation;
780
+ if (!relationPromise) {
781
+ relationPromise = (async () => {
782
+ try {
783
+ const relatedElementId = await node.domModel().getElementByRelation(node.id, relationType);
784
+ const relatedElement = node.domModel().nodeForId(relatedElementId);
785
+
786
+ let elemCache = relatedElementsCache.get(node);
787
+ if (!elemCache) {
788
+ elemCache = new Map();
789
+ relatedElementsCache.set(node, elemCache);
790
+ }
791
+ elemCache.set(`${name}:${value}`, relatedElement || null);
788
792
 
789
- let valueRelationRefDirective: ReturnType<typeof relationRef> = ref(() => {});
790
- if (value) {
791
- if (name === 'popovertarget') {
792
- valueRelationRefDirective = relationRef(
793
- Protocol.DOM.GetElementByRelationRequestRelation.PopoverTarget, i18nString(UIStrings.showPopoverTarget));
794
- } else if (name === 'interesttarget') {
795
- valueRelationRefDirective = relationRef(
796
- Protocol.DOM.GetElementByRelationRequestRelation.InterestTarget, i18nString(UIStrings.showInterestTarget));
797
- } else if (name === 'commandfor') {
798
- valueRelationRefDirective = relationRef(
799
- Protocol.DOM.GetElementByRelationRequestRelation.CommandFor, i18nString(UIStrings.showCommandForTarget));
793
+ const isNameLinking = value.length === 0;
794
+ const fallback = isNameLinking ? name : value;
795
+
796
+ if (!relatedElement) {
797
+ return fallback;
798
+ }
799
+
800
+ const linkOptions: PanelsCommon.DOMLinkifier.Options = {
801
+ preventKeyboardFocus: true,
802
+ tooltip,
803
+ isDynamicLink: true,
804
+ };
805
+
806
+ if (isNameLinking) {
807
+ linkOptions.textContent = name;
808
+ } else {
809
+ const targetId = relatedElement.getAttribute('id');
810
+ if (targetId) {
811
+ linkOptions.textContent = targetId;
812
+ }
813
+ }
814
+
815
+ return PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(relatedElement, linkOptions);
816
+ } catch {
817
+ return value.length === 0 ? name : value;
818
+ }
819
+ })();
820
+ nodeCache.set(cacheKey, relationPromise);
800
821
  }
801
822
  }
802
823
 
@@ -815,23 +836,31 @@ function renderAttribute(
815
836
  valueType = ValueType.SRCSET;
816
837
  }
817
838
 
818
- const withEntitiesRef = valueType === ValueType.UNKNOWN ? ref(el => {
839
+ const withEntitiesRef = (valueType === ValueType.UNKNOWN && !isRelation) ? ref(el => {
819
840
  if (el) {
820
841
  setValueWithEntities(el, value);
821
842
  }
822
843
  }) :
823
- nothing;
844
+ nothing;
845
+
846
+ const jslog = VisualLogging.value(name === 'style' ? 'style-attribute' : 'attribute').track({
847
+ change: true,
848
+ dblclick: true,
849
+ });
824
850
 
825
- // clang-format off
826
851
  return html`<span class="webkit-html-attribute" jslog=${jslog}><span class="webkit-html-attribute-name"
827
- ${animateOn(Boolean(updateRecord?.isAttributeModified(name) && !hasText), DOM_UPDATE_ANIMATION_CLASS_NAME)} ${relationRefDirective}>${name}</span>${hasText ? html`=\u200B"<span class="webkit-html-attribute-value" ${animateOn(
828
- Boolean(updateRecord?.isAttributeModified(name) && hasText),
829
- DOM_UPDATE_ANIMATION_CLASS_NAME)} ${valueRelationRefDirective} ${withEntitiesRef}>
852
+ ${animateOn(Boolean(updateRecord?.isAttributeModified(name) && !hasText), DOM_UPDATE_ANIMATION_CLASS_NAME)}>${
853
+ linkifyName && relationPromise ? Lit.Directives.until(relationPromise, name) : name}</span>${
854
+ hasText ?
855
+ html`=\u200B"<span class="webkit-html-attribute-value" ${
856
+ animateOn(Boolean(updateRecord?.isAttributeModified(name) && hasText),
857
+ DOM_UPDATE_ANIMATION_CLASS_NAME)} ${withEntitiesRef}>
830
858
  ${valueType === ValueType.SRC ? renderLinkifiedValue(value, node) : nothing}
831
- ${valueType === ValueType.SRCSET ? renderLinkifiedSrcset(Common.Srcset.parseSrcset(value), node) : nothing}
859
+ ${
860
+ valueType === ValueType.SRCSET ? renderLinkifiedSrcset(Common.Srcset.parseSrcset(value), node) : nothing}
861
+ ${linkifyValue && relationPromise ? Lit.Directives.until(relationPromise, value) : nothing}
832
862
  </span>"` :
833
- nothing}</span>`;
834
- // clang-format on
863
+ nothing}</span>`;
835
864
  }
836
865
 
837
866
  function renderTag(
@@ -2440,9 +2469,20 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2440
2469
  }
2441
2470
  }
2442
2471
 
2443
- const attributeValue = attributeName && attributeValueElement ?
2472
+ let attributeValue = attributeName && attributeValueElement ?
2444
2473
  this.nodeInternal.getAttribute(attributeName)?.replaceAll('"', '&quot;') :
2445
2474
  undefined;
2475
+
2476
+ const isRelation =
2477
+ attributeName === 'popovertarget' || attributeName === 'interesttarget' || attributeName === 'commandfor';
2478
+ if (isRelation && attributeName && attributeValueElement) {
2479
+ const rawValue = this.nodeInternal.getAttribute(attributeName) || '';
2480
+ const relatedElement = relatedElementsCache.get(this.nodeInternal)?.get(`${attributeName}:${rawValue}`);
2481
+ if (relatedElement) {
2482
+ attributeValue = relatedElement.getAttribute('id') || '';
2483
+ }
2484
+ }
2485
+
2446
2486
  if (attributeValue !== undefined) {
2447
2487
  attributeValueElement.setTextContentTruncatedIfNeeded(
2448
2488
  attributeValue, i18nString(UIStrings.valueIsTooLargeToEdit));
@@ -316,6 +316,21 @@ export class CSSWideKeywordRenderer extends rendererBase(SDK.CSSPropertyParserMa
316
316
  }
317
317
  }
318
318
 
319
+ function handleVarDefinitionActivate(variable: string|SDK.CSSMatchedStyles.CSSValueSource,
320
+ stylesContainer: StylesContainer): void {
321
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.CustomPropertyLinkClicked);
322
+ Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.VAR_LINK);
323
+
324
+ if (typeof variable === 'string') {
325
+ stylesContainer.jumpToProperty(variable) ||
326
+ stylesContainer.jumpToProperty('initial-value', variable, REGISTERED_PROPERTY_SECTION_NAME);
327
+ } else if (variable.declaration instanceof SDK.CSSProperty.CSSProperty) {
328
+ stylesContainer.revealProperty(variable.declaration);
329
+ } else if (variable.declaration instanceof SDK.CSSMatchedStyles.CSSRegisteredProperty) {
330
+ stylesContainer.jumpToProperty('initial-value', variable.name, REGISTERED_PROPERTY_SECTION_NAME);
331
+ }
332
+ }
333
+
319
334
  // clang-format off
320
335
  export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.VariableMatch) {
321
336
  // clang-format on
@@ -344,7 +359,8 @@ export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
344
359
  const {declaration, value: variableValue} = match.resolveVariable() ?? {};
345
360
  const fromFallback = variableValue === undefined;
346
361
  const computedValue = variableValue ?? match.fallbackValue();
347
- const onLinkActivate = (name: string): void => this.#handleVarDefinitionActivate(declaration ?? name);
362
+ const onLinkActivate = (name: string): void =>
363
+ handleVarDefinitionActivate(declaration ?? name, this.#stylesContainer);
348
364
  const varSwatch = document.createElement('span');
349
365
 
350
366
  const substitution = context.tracing?.substitution({match, context});
@@ -416,19 +432,59 @@ export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
416
432
 
417
433
  return [colorSwatch, varSwatch];
418
434
  }
435
+ }
419
436
 
420
- #handleVarDefinitionActivate(variable: string|SDK.CSSMatchedStyles.CSSValueSource): void {
421
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.CustomPropertyLinkClicked);
422
- Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.VAR_LINK);
437
+ // clang-format off
438
+ export class VariableNameRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.VariableNameMatch) {
439
+ // clang-format on
440
+ readonly #stylesContainer: StylesContainer;
441
+ readonly #treeElement: StylePropertyTreeElement|null;
442
+ readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles;
443
+ constructor(stylesContainer: StylesContainer, treeElement: StylePropertyTreeElement|null,
444
+ matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles) {
445
+ super();
446
+ this.#treeElement = treeElement;
447
+ this.#stylesContainer = stylesContainer;
448
+ this.#matchedStyles = matchedStyles;
449
+ }
423
450
 
424
- if (typeof variable === 'string') {
425
- this.#stylesContainer.jumpToProperty(variable) ||
426
- this.#stylesContainer.jumpToProperty('initial-value', variable, REGISTERED_PROPERTY_SECTION_NAME);
427
- } else if (variable.declaration instanceof SDK.CSSProperty.CSSProperty) {
428
- this.#stylesContainer.revealProperty(variable.declaration);
429
- } else if (variable.declaration instanceof SDK.CSSMatchedStyles.CSSRegisteredProperty) {
430
- this.#stylesContainer.jumpToProperty('initial-value', variable.name, REGISTERED_PROPERTY_SECTION_NAME);
451
+ override render(match: SDK.CSSPropertyParserMatchers.VariableNameMatch, context: RenderingContext): Node[] {
452
+ if (this.#treeElement?.property.ownerStyle.parentRule instanceof SDK.CSSRule.CSSFunctionRule) {
453
+ return Renderer.render(ASTUtils.children(match.node), context).nodes;
431
454
  }
455
+
456
+ const {declaration, value: variableValue} = match.resolveVariable() ?? {};
457
+ const isDefined = variableValue !== undefined;
458
+ const onLinkActivate = (name: string): void =>
459
+ handleVarDefinitionActivate(declaration ?? name, this.#stylesContainer);
460
+ const varSwatch = document.createElement('span');
461
+
462
+ const tooltipContents =
463
+ this.#stylesContainer.getVariablePopoverContents(this.#matchedStyles, match.text, variableValue ?? null);
464
+ const tooltipId = this.#treeElement?.getTooltipId('custom-property-var');
465
+ const tooltip = tooltipId ? {tooltipId} : undefined;
466
+ // clang-format off
467
+ render(html`
468
+ <devtools-link-swatch class=css-var-link .data=${{
469
+ tooltip,
470
+ text: match.text,
471
+ isDefined,
472
+ onLinkActivate,
473
+ }}>
474
+ </devtools-link-swatch>
475
+ ${tooltipId ? html`
476
+ <devtools-tooltip
477
+ id=${tooltipId}
478
+ variant=rich
479
+ jslogContext=elements.css-var
480
+ >
481
+ ${tooltipContents}
482
+ </devtools-tooltip>
483
+ ` : ''}
484
+ `, varSwatch);
485
+ // clang-format on
486
+
487
+ return [varSwatch];
432
488
  }
433
489
  }
434
490
 
@@ -2007,6 +2063,7 @@ export function getPropertyRenderers(
2007
2063
  Array<MatchRenderer<SDK.CSSPropertyParser.Match>> {
2008
2064
  return [
2009
2065
  new VariableRenderer(stylesContainer, treeElement, matchedStyles, computedStyles, computedStyleExtraFields),
2066
+ new VariableNameRenderer(stylesContainer, treeElement, matchedStyles),
2010
2067
  new ColorRenderer(stylesContainer, treeElement),
2011
2068
  new ColorMixRenderer(stylesContainer, matchedStyles, computedStyles, computedStyleExtraFields, treeElement),
2012
2069
  new ContrastColorRenderer(stylesContainer, treeElement),
@@ -2026,10 +2083,10 @@ export function getPropertyRenderers(
2026
2083
  new EnvFunctionRenderer(treeElement, matchedStyles, computedStyles, computedStyleExtraFields),
2027
2084
  new PositionTryRenderer(matchedStyles),
2028
2085
  new LengthRenderer(stylesContainer, propertyName, treeElement),
2029
- new MathFunctionRenderer(
2030
- stylesContainer, matchedStyles, computedStyles, computedStyleExtraFields, propertyName, treeElement),
2031
- new CustomFunctionRenderer(
2032
- stylesContainer, matchedStyles, computedStyles, computedStyleExtraFields, propertyName, treeElement),
2086
+ new MathFunctionRenderer(stylesContainer, matchedStyles, computedStyles, computedStyleExtraFields, propertyName,
2087
+ treeElement),
2088
+ new CustomFunctionRenderer(stylesContainer, matchedStyles, computedStyles, computedStyleExtraFields, propertyName,
2089
+ treeElement),
2033
2090
  new AutoBaseRenderer(computedStyles, computedStyleExtraFields),
2034
2091
  new BinOpRenderer(),
2035
2092
  new RelativeColorChannelRenderer(treeElement),
@@ -2082,7 +2139,7 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
2082
2139
  this.#parentSection = section;
2083
2140
  this.isShorthand = isShorthand;
2084
2141
  this.newProperty = newProperty;
2085
- this.#lazyRender = stylesContainer.shouldRenderLazily();
2142
+ this.#lazyRender = !this.newProperty && stylesContainer.shouldRenderLazily();
2086
2143
  if (this.newProperty) {
2087
2144
  this.listItemElement.textContent = '';
2088
2145
  }
@@ -137,7 +137,15 @@ export class StylesAiCodeCompletionProvider {
137
137
  }
138
138
  currentPropertyString = currentPropertyString + text;
139
139
  prefix = prefix + text;
140
- const suffix = content.substring(propertyEndOffset);
140
+ let suffix = content.substring(propertyEndOffset);
141
+
142
+ const maxLength = TextEditor.AiCodeCompletionProvider.MAX_PREFIX_SUFFIX_LENGTH;
143
+ if (prefix.length > maxLength) {
144
+ prefix = prefix.substring(prefix.length - maxLength);
145
+ }
146
+ if (suffix.length > maxLength) {
147
+ suffix = suffix.substring(0, maxLength);
148
+ }
141
149
 
142
150
  const startTime = performance.now();
143
151
  // TODO(b/476098133): Consider adjusting cursor position