Allow each theme to style all aspects of syntax highlighting

Max Brunsfeld and Keith Simmons created

Previously, some syntax highlights were controlled in editor.ts,
and shared across all themes.

Co-authored-by: Keith Simmons <keith@zed.dev>

Change summary

assets/themes/dark.json                          | 81 +++++++++++++++--
assets/themes/light.json                         | 81 +++++++++++++++--
crates/zed/src/languages/markdown/highlights.scm |  2 
styles/src/styleTree/editor.ts                   | 30 ++----
styles/src/themes/dark.ts                        | 12 +-
styles/src/themes/light.ts                       | 12 +-
styles/src/themes/theme.ts                       | 10 +
styles/src/utils/snakeCase.ts                    |  2 
8 files changed, 168 insertions(+), 62 deletions(-)

Detailed changes

assets/themes/dark.json 🔗

@@ -689,33 +689,88 @@
       }
     },
     "syntax": {
-      "keyword": "#4f8ff7",
-      "function": "#f9da82",
-      "string": "#f99d5f",
-      "type": "#3eeeda",
-      "number": "#aeef4b",
-      "comment": "#aaaaaa",
-      "property": "#4f8ff7",
-      "variant": "#53c1f5",
-      "constant": "#d5d5d5",
+      "primary": {
+        "color": "#d5d5d5",
+        "weight": "normal"
+      },
+      "comment": {
+        "color": "#aaaaaa",
+        "weight": "normal"
+      },
+      "punctuation": {
+        "color": "#c6c6c6",
+        "weight": "normal"
+      },
+      "constant": {
+        "color": "#d5d5d5",
+        "weight": "normal"
+      },
+      "keyword": {
+        "color": "#4f8ff7",
+        "weight": "normal"
+      },
+      "function": {
+        "color": "#f9da82",
+        "weight": "normal"
+      },
+      "type": {
+        "color": "#3eeeda",
+        "weight": "normal"
+      },
+      "variant": {
+        "color": "#53c1f5",
+        "weight": "normal"
+      },
+      "property": {
+        "color": "#4f8ff7",
+        "weight": "normal"
+      },
+      "enum": {
+        "color": "#ee670a",
+        "weight": "normal"
+      },
+      "operator": {
+        "color": "#ee670a",
+        "weight": "normal"
+      },
+      "string": {
+        "color": "#f99d5f",
+        "weight": "normal"
+      },
+      "number": {
+        "color": "#aeef4b",
+        "weight": "normal"
+      },
+      "boolean": {
+        "color": "#aeef4b",
+        "weight": "normal"
+      },
+      "predictive": {
+        "color": "#808080",
+        "weight": "normal"
+      },
       "title": {
         "color": "#de900c",
         "weight": "bold"
       },
-      "emphasis": "#4f8ff7",
-      "emphasis_strong": {
+      "emphasis": {
+        "color": "#4f8ff7",
+        "weight": "normal"
+      },
+      "emphasis.strong": {
         "color": "#4f8ff7",
         "weight": "bold"
       },
       "link_uri": {
         "color": "#79ba16",
+        "weight": "normal",
         "underline": true
       },
       "link_text": {
         "color": "#ee670a",
+        "weight": "normal",
         "italic": true
-      },
-      "list_marker": "#c6c6c6"
+      }
     }
   },
   "project_diagnostics": {

assets/themes/light.json 🔗

@@ -689,33 +689,88 @@
       }
     },
     "syntax": {
-      "keyword": "#1819a1",
-      "function": "#bb550e",
-      "string": "#eb2d2d",
-      "type": "#a8820e",
-      "number": "#484bed",
-      "comment": "#717171",
-      "property": "#106c4e",
-      "variant": "#97142a",
-      "constant": "#1c1c1c",
+      "primary": {
+        "color": "#1c1c1c",
+        "weight": "normal"
+      },
+      "comment": {
+        "color": "#717171",
+        "weight": "normal"
+      },
+      "punctuation": {
+        "color": "#555555",
+        "weight": "normal"
+      },
+      "constant": {
+        "color": "#1c1c1c",
+        "weight": "normal"
+      },
+      "keyword": {
+        "color": "#1819a1",
+        "weight": "normal"
+      },
+      "function": {
+        "color": "#bb550e",
+        "weight": "normal"
+      },
+      "type": {
+        "color": "#a8820e",
+        "weight": "normal"
+      },
+      "variant": {
+        "color": "#97142a",
+        "weight": "normal"
+      },
+      "property": {
+        "color": "#106c4e",
+        "weight": "normal"
+      },
+      "enum": {
+        "color": "#eb2d2d",
+        "weight": "normal"
+      },
+      "operator": {
+        "color": "#eb2d2d",
+        "weight": "normal"
+      },
+      "string": {
+        "color": "#eb2d2d",
+        "weight": "normal"
+      },
+      "number": {
+        "color": "#484bed",
+        "weight": "normal"
+      },
+      "boolean": {
+        "color": "#eb2d2d",
+        "weight": "normal"
+      },
+      "predictive": {
+        "color": "#808080",
+        "weight": "normal"
+      },
       "title": {
         "color": "#1096d3",
         "weight": "bold"
       },
-      "emphasis": "#484bed",
-      "emphasis_strong": {
+      "emphasis": {
+        "color": "#484bed",
+        "weight": "normal"
+      },
+      "emphasis.strong": {
         "color": "#484bed",
         "weight": "bold"
       },
       "link_uri": {
         "color": "#79ba16",
+        "weight": "normal",
         "underline": true
       },
       "link_text": {
         "color": "#eb2d2d",
+        "weight": "normal",
         "italic": true
-      },
-      "list_marker": "#555555"
+      }
     }
   },
   "project_diagnostics": {

styles/src/styleTree/editor.ts 🔗

@@ -37,8 +37,18 @@ export default function editor(theme: Theme) {
     };
   }
 
+  const syntax: any = {};
+  for (const syntaxKey in theme.syntax) {
+    const style = theme.syntax[syntaxKey];
+    syntax[syntaxKey] = {
+      color: style.color.value,
+      weight: style.weight.value,
+      underline: style.underline,
+      italic: style.italic,
+    };
+  }
+
   return {
-    // textColor: theme.syntax.primary.color,
     textColor: theme.syntax.primary.color.value,
     background: backgroundColor(theme, 500),
     activeLineBackground: theme.editor.line.active.value,
@@ -125,22 +135,6 @@ export default function editor(theme: Theme) {
     invalidHintDiagnostic: diagnostic(theme, "muted"),
     invalidInformationDiagnostic: diagnostic(theme, "muted"),
     invalidWarningDiagnostic: diagnostic(theme, "muted"),
-    syntax: {
-      keyword: theme.syntax.keyword.color.value,
-      function: theme.syntax.function.color.value,
-      string: theme.syntax.string.color.value,
-      type: theme.syntax.type.color.value,
-      number: theme.syntax.number.color.value,
-      comment: theme.syntax.comment.color.value,
-      property: theme.syntax.property.color.value,
-      variant: theme.syntax.variant.color.value,
-      constant: theme.syntax.constant.color.value,
-      title: { color: theme.syntax.title.color.value, weight: "bold" },
-      emphasis: theme.textColor.feature.value,
-      "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
-      link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
-      link_text: { color: theme.syntax.linkText.color.value, italic: true },
-      list_marker: theme.syntax.punctuation.color.value,
-    },
+    syntax,
   };
 }

styles/src/themes/dark.ts 🔗

@@ -202,22 +202,22 @@ const syntax: Syntax = {
     weight: fontWeights.bold,
   },
   emphasis: {
-    color: textColor.active,
+    color: textColor.feature,
     weight: fontWeights.normal,
   },
-  emphasisStrong: {
-    color: textColor.active,
+  "emphasis.strong": {
+    color: textColor.feature,
     weight: fontWeights.bold,
   },
-  linkUrl: {
+  linkUri: {
     color: colors.lime[500],
     weight: fontWeights.normal,
-    // TODO: add underline
+    underline: true,
   },
   linkText: {
     color: colors.orange[500],
     weight: fontWeights.normal,
-    // TODO: add italic
+    italic: true,
   },
 };
 

styles/src/themes/light.ts 🔗

@@ -200,22 +200,22 @@ const syntax: Syntax = {
     weight: fontWeights.bold,
   },
   emphasis: {
-    color: textColor.active,
+    color: textColor.feature,
     weight: fontWeights.normal,
   },
-  emphasisStrong: {
-    color: textColor.active,
+  "emphasis.strong": {
+    color: textColor.feature,
     weight: fontWeights.bold,
   },
-  linkUrl: {
+  linkUri: {
     color: colors.lime[500],
     weight: fontWeights.normal,
-    // TODO: add underline
+    underline: true
   },
   linkText: {
     color: colors.red[500],
     weight: fontWeights.normal,
-    // TODO: add italic
+    italic: true
   },
 };
 

styles/src/themes/theme.ts 🔗

@@ -3,7 +3,9 @@ import { withOpacity } from "../utils/color";
 
 export interface SyntaxHighlightStyle {
   color: ColorToken;
-  weight: FontWeightToken;
+  weight?: FontWeightToken;
+  underline?: boolean,
+  italic?: boolean,
 }
 
 export interface Player {
@@ -49,12 +51,12 @@ export interface Syntax {
   number: SyntaxHighlightStyle;
   boolean: SyntaxHighlightStyle;
   predictive: SyntaxHighlightStyle;
-  // TODO: Either move the following or rename
   title: SyntaxHighlightStyle;
   emphasis: SyntaxHighlightStyle;
-  emphasisStrong: SyntaxHighlightStyle;
-  linkUrl: SyntaxHighlightStyle;
+  linkUri: SyntaxHighlightStyle;
   linkText: SyntaxHighlightStyle;
+
+  [key: string]: SyntaxHighlightStyle;
 };
 
 export default interface Theme {

styles/src/utils/snakeCase.ts 🔗

@@ -17,7 +17,7 @@ type SnakeCased<Type> = {
 export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
   const snakeObject: any = {};
   for (const key in object) {
-    snakeObject[snakeCase(key)] = snakeCaseValue(object[key]);
+    snakeObject[snakeCase(key, { keepSpecialCharacters: true })] = snakeCaseValue(object[key]);
   }
   return snakeObject;
 }