Add indentation and ancestor braces for nesting styles

Screenshots: https://imgur.com/a/Ulph3UF

Bug: b/40166888
Change-Id: Id0af2ae023643810c1a445375aeec7eb549df9e6
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5307622
Commit-Queue: Changhao Han <changhaohan@google.com>
Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org>
diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni
index 8f273b7..0b9a73e 100644
--- a/config/gni/devtools_grd_files.gni
+++ b/config/gni/devtools_grd_files.gni
@@ -1232,7 +1232,7 @@
   "front_end/panels/elements/nodeStackTraceWidget.css.js",
   "front_end/panels/elements/platformFontsWidget.css.js",
   "front_end/panels/elements/propertiesWidget.css.js",
-  "front_end/panels/elements/stylesSectionTree.css.js",
+  "front_end/panels/elements/stylePropertiesTreeOutline.css.js",
   "front_end/panels/elements/stylesSidebarPane.css.js",
   "front_end/panels/emulation/AdvancedApp.js",
   "front_end/panels/emulation/DeviceModeToolbar.js",
diff --git a/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js b/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js
index 4b9b305..3c7b444 100644
--- a/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js
+++ b/front_end/legacy_test_runner/elements_test_runner/ElementsTestRunner.js
@@ -510,7 +510,7 @@
 
   TestRunner.addResult(
       '[expanded] ' + ((section.propertiesTreeOutline.element.classList.contains('no-affect') ? '[no-affect] ' : '')));
-  const queries = section.titleElement.querySelectorAll('devtools-css-query');
+  const queries = section.element.querySelectorAll('devtools-css-query');
 
   for (const query of queries) {
     const queryElement = query.shadowRoot.querySelector('.query');
diff --git a/front_end/panels/elements/BUILD.gn b/front_end/panels/elements/BUILD.gn
index 88ef618..7529ceb 100644
--- a/front_end/panels/elements/BUILD.gn
+++ b/front_end/panels/elements/BUILD.gn
@@ -22,7 +22,7 @@
     "nodeStackTraceWidget.css",
     "platformFontsWidget.css",
     "propertiesWidget.css",
-    "stylesSectionTree.css",
+    "stylePropertiesTreeOutline.css",
     "stylesSidebarPane.css",
   ]
 }
diff --git a/front_end/panels/elements/StylePropertiesSection.test.ts b/front_end/panels/elements/StylePropertiesSection.test.ts
index a5954d8..ab0a1d9 100644
--- a/front_end/panels/elements/StylePropertiesSection.test.ts
+++ b/front_end/panels/elements/StylePropertiesSection.test.ts
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import * as Common from '../../core/common/common.js';
 import * as Platform from '../../core/platform/platform.js';
 import * as SDK from '../../core/sdk/sdk.js';
 import * as Protocol from '../../generated/protocol.js';
 import type * as TextUtils from '../../models/text_utils/text_utils.js';
-import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
+import {createTarget} from '../../testing/EnvironmentHelpers.js';
 import {describeWithMockConnection} from '../../testing/MockConnection.js';
 import * as Components from '../../ui/legacy/components/utils/utils.js';
 
@@ -158,8 +159,37 @@
     assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].url, '');
   });
 
+  it('properly renders ancestor rules', async () => {
+    Common.Settings.Settings.instance().moduleSetting('text-editor-indent').set('  ');
+    const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
+    Platform.assertNotNullOrUndefined(cssModel);
+    const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true});
+    const origin = Protocol.CSS.StyleSheetOrigin.Regular;
+    const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
+    const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6};
+    const matchedPayload: Protocol.CSS.RuleMatch[] = [{
+      rule: {
+        nestingSelectors: ['body', '& ul', 'div'],
+        ruleTypes: [
+          Protocol.CSS.CSSRuleType.StyleRule,
+          Protocol.CSS.CSSRuleType.StyleRule,
+          Protocol.CSS.CSSRuleType.StyleRule,
+        ],
+        selectorList: {selectors: [{text: 'div'}], text: 'div'},
+        origin,
+        style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: []},
+      },
+      matchingSelectors: [0],
+    }];
+    const matchedStyles = await setUpStyles(cssModel, origin, styleSheetId, {...range}, {matchedPayload});
+    const declaration = matchedStyles.nodeStyles()[0];
+    Platform.assertNotNullOrUndefined(declaration);
+    const section = new Elements.StylePropertiesSection.StylePropertiesSection(
+        stylesSidebarPane, matchedStyles, declaration, 0, null, null);
+    assert.strictEqual(section.element.textContent, 'div {  & ul {    body {      div {      }    }  }}');
+  });
+
   it('updates property rule property names', async () => {
-    stubNoopSettings();
     const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
     Platform.assertNotNullOrUndefined(cssModel);
     const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true});
diff --git a/front_end/panels/elements/StylePropertiesSection.ts b/front_end/panels/elements/StylePropertiesSection.ts
index 4498470..c5e691b 100644
--- a/front_end/panels/elements/StylePropertiesSection.ts
+++ b/front_end/panels/elements/StylePropertiesSection.ts
@@ -48,8 +48,8 @@
 import * as ElementsComponents from './components/components.js';
 import {linkifyDeferredNodeReference} from './DOMLinkifier.js';
 import {ElementsPanel} from './ElementsPanel.js';
+import stylePropertiesTreeOutlineStyles from './stylePropertiesTreeOutline.css.js';
 import {type Context, StylePropertyTreeElement} from './StylePropertyTreeElement.js';
-import stylesSectionTreeStyles from './stylesSectionTree.css.js';
 import {StylesSidebarPane} from './StylesSidebarPane.js';
 
 const UIStrings = {
@@ -127,7 +127,7 @@
   private forceShowAll: boolean;
   private readonly originalPropertiesCount: number;
   element: HTMLDivElement;
-  private readonly innerElement: HTMLElement;
+  readonly #styleRuleElement: HTMLElement;
   private readonly titleElement: HTMLElement;
   propertiesTreeOutline: UI.TreeOutline.TreeOutlineInShadow;
   private showAllButton: HTMLButtonElement;
@@ -143,7 +143,9 @@
   private hoverableSelectorsMode: boolean;
   private isHiddenInternal: boolean;
 
-  private ancestorRuleListElement: HTMLElement;
+  nestingLevel = 0;
+  #ancestorRuleListElement: HTMLElement;
+  #ancestorClosingBracesElement: HTMLElement;
 
   // Used to identify buttons that trigger a flexbox or grid editor.
   nextEditorTriggerButtonIdx = 1;
@@ -181,23 +183,34 @@
     UI.ARIAUtils.markAsListitem(this.element);
     this.element.addEventListener('keydown', this.onKeyDown.bind(this), false);
     parentPane.sectionByElement.set(this.element, this);
-    this.innerElement = this.element.createChild('div');
+    this.#styleRuleElement = this.element.createChild('div', 'style-rule');
 
-    this.titleElement = this.innerElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : ''));
+    this.#ancestorRuleListElement = document.createElement('div');
+    this.#ancestorRuleListElement.classList.add('ancestor-rule-list');
+    this.element.prepend(this.#ancestorRuleListElement);
+    this.#ancestorClosingBracesElement = document.createElement('div');
+    this.#ancestorClosingBracesElement.classList.add('ancestor-closing-braces');
+    this.element.append(this.#ancestorClosingBracesElement);
+    this.updateAncestorRuleList();
+
+    this.titleElement =
+        this.#styleRuleElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : ''));
 
     this.propertiesTreeOutline = new UI.TreeOutline.TreeOutlineInShadow();
     this.propertiesTreeOutline.setFocusable(false);
-    this.propertiesTreeOutline.registerCSSFiles([stylesSectionTreeStyles]);
+    this.propertiesTreeOutline.registerCSSFiles([stylePropertiesTreeOutlineStyles]);
     this.propertiesTreeOutline.element.classList.add('style-properties', 'matched-styles', 'monospace');
-    this.innerElement.appendChild(this.propertiesTreeOutline.element);
+    this.#styleRuleElement.appendChild(this.propertiesTreeOutline.element);
 
     this.showAllButton = UI.UIUtils.createTextButton('', this.showAllItems.bind(this), {
       className: 'styles-show-all',
       jslogContext: 'elements.show-all-style-properties',
     });
-    this.innerElement.appendChild(this.showAllButton);
+    this.#styleRuleElement.appendChild(this.showAllButton);
 
+    const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
     const selectorContainer = document.createElement('div');
+    selectorContainer.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel);
     selectorContainer.classList.add('selector-container');
     this.selectorElement = document.createElement('span');
     UI.ARIAUtils.setLabel(this.selectorElement, i18nString(UIStrings.cssSelector));
@@ -210,8 +223,9 @@
     const openBrace = selectorContainer.createChild('span', 'sidebar-pane-open-brace');
     openBrace.textContent = ' {';
 
-    const closeBrace = this.innerElement.createChild('div', 'sidebar-pane-closing-brace');
-    closeBrace.textContent = '}';
+    const closeBrace = this.#styleRuleElement.createChild('div', 'sidebar-pane-closing-brace');
+    closeBrace.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel);
+    closeBrace.createChild('span').textContent = '}';
 
     if (this.styleInternal.parentRule) {
       const newRuleButton = new UI.Toolbar.ToolbarButton(
@@ -220,14 +234,14 @@
       newRuleButton.element.tabIndex = -1;
       if (!this.newStyleRuleToolbar) {
         this.newStyleRuleToolbar =
-            new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar new-rule-toolbar', this.innerElement);
+            new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar new-rule-toolbar', this.element);
       }
       this.newStyleRuleToolbar.appendToolbarItem(newRuleButton);
       UI.ARIAUtils.markAsHidden(this.newStyleRuleToolbar.element);
     }
 
     if (Root.Runtime.experiments.isEnabled('font-editor') && this.editable) {
-      this.fontEditorToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar', this.innerElement);
+      this.fontEditorToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar', this.#styleRuleElement);
       this.fontEditorSectionManager = new FontEditorSectionManager(this.parentPane.swatchPopoverHelper(), this);
       this.fontEditorButton = new UI.Toolbar.ToolbarButton('Font Editor', 'custom-typography');
       this.fontEditorButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
@@ -274,9 +288,7 @@
       }
     }
 
-    this.ancestorRuleListElement = this.titleElement.createChild('div', 'ancestor-rule-list');
     this.selectorRefElement = this.titleElement.createChild('div', 'styles-section-subtitle');
-    this.updateQueryList();
     this.updateRuleOrigin();
     this.titleElement.appendChild(selectorContainer);
 
@@ -735,7 +747,7 @@
       this.styleInternal.rebase(edit);
     }
 
-    this.updateQueryList();
+    this.updateAncestorRuleList();
     this.updateRuleOrigin();
   }
 
@@ -745,6 +757,8 @@
     let scopeIndex = 0;
     let supportsIndex = 0;
     let nestingIndex = 0;
+    this.nestingLevel = 0;
+    const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
     for (const ruleType of rule.ruleTypes) {
       let ancestorRuleElement;
       switch (ruleType) {
@@ -764,7 +778,26 @@
           ancestorRuleElement = this.createNestingElement(rule.nestingSelectors?.[nestingIndex++]);
           break;
       }
-      ancestorRuleElement && this.ancestorRuleListElement.prepend(ancestorRuleElement);
+      if (ancestorRuleElement) {
+        this.#ancestorRuleListElement.prepend(ancestorRuleElement);
+        const closingBrace = document.createElement('div');
+        closingBrace.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel);
+        closingBrace.style.paddingLeft = `${this.nestingLevel}ch`;
+        closingBrace.append('}');
+        this.#ancestorClosingBracesElement.prepend(closingBrace);
+        this.nestingLevel++;
+      }
+    }
+
+    let curNestingLevel = 0;
+    for (const element of this.#ancestorRuleListElement.children) {
+      const indentElement = document.createElement('span');
+      indentElement.classList.add('styles-clipboard-only');
+      indentElement.setAttribute('slot', 'indent');
+      indentElement.textContent = indent.repeat(curNestingLevel);
+      element.prepend(indentElement);
+      (element as HTMLElement).style.paddingLeft = `${curNestingLevel}ch`;
+      curNestingLevel++;
     }
   }
 
@@ -872,7 +905,7 @@
       return;
     }
     const nestingElement = document.createElement('div');
-    nestingElement.textContent = nestingSelector;
+    nestingElement.textContent = `${nestingSelector} {`;
     return nestingElement;
   }
 
@@ -900,14 +933,16 @@
       }
     });
 
-    this.ancestorRuleListElement.prepend(containerElement);
+    this.#ancestorRuleListElement.prepend(containerElement);
   }
 
-  private updateQueryList(): void {
-    this.ancestorRuleListElement.removeChildren();
+  private updateAncestorRuleList(): void {
+    this.#ancestorRuleListElement.removeChildren();
+    this.#ancestorClosingBracesElement.removeChildren();
     if (this.styleInternal.parentRule && this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule) {
       this.createAncestorRules(this.styleInternal.parentRule);
     }
+    this.#styleRuleElement.style.paddingLeft = `${this.nestingLevel}ch`;
   }
 
   isPropertyInherited(propertyName: string): boolean {
diff --git a/front_end/panels/elements/StylePropertyTreeElement.ts b/front_end/panels/elements/StylePropertyTreeElement.ts
index 1dbd04a..0277a18 100644
--- a/front_end/panels/elements/StylePropertyTreeElement.ts
+++ b/front_end/panels/elements/StylePropertyTreeElement.ts
@@ -1242,7 +1242,7 @@
     const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
     UI.UIUtils.createTextChild(
         this.listItemElement.createChild('span', 'styles-clipboard-only'),
-        indent + (this.property.disabled ? '/* ' : ''));
+        indent.repeat(this.section().nestingLevel + 1) + (this.property.disabled ? '/* ' : ''));
     if (this.nameElement) {
       this.listItemElement.appendChild(this.nameElement);
     }
diff --git a/front_end/panels/elements/components/CSSQuery.test.ts b/front_end/panels/elements/components/CSSQuery.test.ts
index 64b2929..1d71937 100644
--- a/front_end/panels/elements/components/CSSQuery.test.ts
+++ b/front_end/panels/elements/components/CSSQuery.test.ts
@@ -27,7 +27,7 @@
 
     assert.strictEqual(
         queryElement.innerText,
-        '@container (min-width: 10px)',
+        '@container (min-width: 10px) {',
         'text content of query element should match query text',
     );
   });
@@ -55,7 +55,7 @@
 
     assert.strictEqual(
         queryElement.innerText,
-        '@container container-query-1 (max-width: 10px)',
+        '@container container-query-1 (max-width: 10px) {',
         'text content of query element should match query text',
     );
 
diff --git a/front_end/panels/elements/components/CSSQuery.ts b/front_end/panels/elements/components/CSSQuery.ts
index 656d245..e0b5560 100644
--- a/front_end/panels/elements/components/CSSQuery.ts
+++ b/front_end/panels/elements/components/CSSQuery.ts
@@ -55,7 +55,7 @@
 
     render(html`
       <div class=${queryClasses}>
-        ${this.#queryPrefix ? html`<span>${this.#queryPrefix + ' '}</span>` : LitHtml.nothing}${this.#queryName ? html`<span>${this.#queryName + ' '}</span>` : LitHtml.nothing}${queryText}
+        <slot name="indent"></slot>${this.#queryPrefix ? html`<span>${this.#queryPrefix + ' '}</span>` : LitHtml.nothing}${this.#queryName ? html`<span>${this.#queryName + ' '}</span>` : LitHtml.nothing}${queryText} {
       </div>
     `, this.#shadow, {
       host: this,
diff --git a/front_end/panels/elements/stylesSectionTree.css b/front_end/panels/elements/stylePropertiesTreeOutline.css
similarity index 97%
rename from front_end/panels/elements/stylesSectionTree.css
rename to front_end/panels/elements/stylePropertiesTreeOutline.css
index f8ec990..4927d0c 100644
--- a/front_end/panels/elements/stylesSectionTree.css
+++ b/front_end/panels/elements/stylePropertiesTreeOutline.css
@@ -114,8 +114,8 @@
   height: 13px;
 }
 
-.tree-outline li.editing .enabled-button {
-  display: none !important; /* stylelint-disable-line declaration-no-important */
+.tree-outline li.child-editing .enabled-button {
+  display: none;
 }
 
 .overloaded:not(.has-ignorable-error, .invalid-property-value),
diff --git a/front_end/panels/elements/stylesSidebarPane.css b/front_end/panels/elements/stylesSidebarPane.css
index 837641f..64ec36e 100644
--- a/front_end/panels/elements/stylesSidebarPane.css
+++ b/front_end/panels/elements/stylesSidebarPane.css
@@ -11,10 +11,7 @@
   border-bottom: 1px solid var(--sys-color-divider);
   position: relative;
   overflow: hidden;
-
-  > div {
-    padding: 2px 2px 4px 4px;
-  }
+  padding: 2px 2px 4px 4px;
 
   &:last-child {
     border-bottom: none;
@@ -54,8 +51,14 @@
   }
 
   .selector,
-  .try-rule-selector-element {
-    color: var(--sys-color-state-disabled);
+  .try-rule-selector-element,
+  .ancestor-rule-list,
+  .ancestor-closing-braces {
+    color: var(--sys-color-token-meta);
+  }
+
+  .ancestor-rule-list devtools-css-query {
+    display: block;
   }
 
   .simple-selector.selector-matches,
@@ -96,10 +99,6 @@
   background-clip: padding;
   word-wrap: break-word;
   white-space: normal;
-
-  .ancestor-rule-list {
-    color: var(--sys-color-state-disabled);
-  }
 }
 
 .styles-section-subtitle {
@@ -155,6 +154,15 @@
   cursor: text;
 }
 
+/* TODO(changhaohan): restructure this in relation to stylePropertiesTreeOutline.css. */
+.styles-clipboard-only {
+  display: inline-block;
+  width: 0;
+  opacity: 0%;
+  pointer-events: none;
+  white-space: pre;
+}
+
 .styles-sidebar-pane-toolbar-container {
   flex-shrink: 0;
   overflow: hidden;
diff --git a/front_end/testing/EnvironmentHelpers.ts b/front_end/testing/EnvironmentHelpers.ts
index 2f22d61..fda536d 100644
--- a/front_end/testing/EnvironmentHelpers.ts
+++ b/front_end/testing/EnvironmentHelpers.ts
@@ -273,6 +273,9 @@
     createSettingValue(
         Common.Settings.SettingCategory.PERFORMANCE, 'flamechart-mouse-wheel-action', false,
         Common.Settings.SettingType.ENUM),
+    createSettingValue(
+        Common.Settings.SettingCategory.ELEMENTS, 'show-css-property-documentation-on-hover', false,
+        Common.Settings.SettingType.BOOLEAN),
   ];
 
   Common.Settings.registerSettingsForTest(settings, reset);
diff --git a/test/e2e/elements/style-pane-properties_test.ts b/test/e2e/elements/style-pane-properties_test.ts
index 9297b9a..1fdad78 100644
--- a/test/e2e/elements/style-pane-properties_test.ts
+++ b/test/e2e/elements/style-pane-properties_test.ts
@@ -389,7 +389,7 @@
     const rule1PropertiesSection = await getStyleRule(RULE1_SELECTOR);
     const supportsQuery = await waitFor('.query.editable', rule1PropertiesSection);
     const supportsQueryText = await supportsQuery.evaluate(node => (node as HTMLElement).innerText as string);
-    assert.deepEqual(supportsQueryText, '@supports (width: 10px)', 'incorrectly displayed @supports rule');
+    assert.deepEqual(supportsQueryText, '@supports (width: 10px) {', 'incorrectly displayed @supports rule');
   });
 
   it('can display @layer separators', async () => {