diff --git a/src/reason-textmate/ThemeScopes.re b/src/reason-textmate/ThemeScopes.re index 484f0a0f56..31df2b7a38 100644 --- a/src/reason-textmate/ThemeScopes.re +++ b/src/reason-textmate/ThemeScopes.re @@ -13,6 +13,8 @@ module Scope = { let ofString = s => String.split_on_char('.', s); + let toString = scope => String.concat(".", scope); + let rec matches = (selector: t, v: t) => { switch (selector, v) { | ([], _) => true @@ -44,6 +46,9 @@ module Scopes = { |> String.split_on_char(' ') |> List.map(v => Scope.ofString(String.trim(v))); + let toString = scopes => + String.concat("\n", scopes |> List.map(Scope.toString)); + let rec matches = (selector: t, v: t) => { switch (selector, v) { | ([], _) => true @@ -58,6 +63,22 @@ module Scopes = { }; }; +module ResolvedStyle = { + type t = { + foreground: string, + background: string, + bold: bool, + italic: bool, + }; + + let default = (~foreground, ~background, ()) => { + foreground, + background, + bold: false, + italic: false, + }; +}; + module TokenStyle = { [@deriving show({with_path: false})] type t = { @@ -74,6 +95,66 @@ module TokenStyle = { }; }; + let merge = (prev, style) => { + let foreground = + switch (prev.foreground, style.foreground) { + | (Some(v), _) => Some(v) + | (_, Some(v)) => Some(v) + | _ => None + }; + + let background = + switch (prev.background, style.background) { + | (Some(v), _) => Some(v) + | (_, Some(v)) => Some(v) + | _ => None + }; + + let bold = + switch (prev.bold, style.bold) { + | (Some(v), _) => Some(v) + | (_, Some(v)) => Some(v) + | _ => None + }; + + let italic = + switch (prev.italic, style.italic) { + | (Some(v), _) => Some(v) + | (_, Some(v)) => Some(v) + | _ => None + }; + + {background, foreground, bold, italic}; + }; + + let resolve = (~default: ResolvedStyle.t, style) => { + let foreground = + switch (style.foreground) { + | Some(v) => v + | None => default.foreground + }; + + let bold = + switch (style.bold) { + | Some(v) => v + | None => default.bold + }; + + let italic = + switch (style.italic) { + | Some(v) => v + | None => default.italic + }; + + let background = + switch (style.background) { + | Some(v) => v + | None => default.background + }; + + ResolvedStyle.{bold, italic, foreground, background}; + }; + let create = ( ~foreground: option(string)=None, @@ -96,22 +177,6 @@ module TokenStyle = { }; }; -module ResolvedStyle = { - type t = { - foreground: string, - background: string, - bold: bool, - italic: bool, - }; - - let default = (~foreground, ~background, ()) => { - foreground, - background, - bold: false, - italic: false, - }; -}; - module Selector = { type t = { scopes: Scopes.t, diff --git a/src/reason-textmate/TokenTheme.re b/src/reason-textmate/TokenTheme.re index 508aeaaba4..ad2d3aa638 100644 --- a/src/reason-textmate/TokenTheme.re +++ b/src/reason-textmate/TokenTheme.re @@ -47,7 +47,7 @@ let create = let trie = List.fold_left( - (prev, curr) => { + (prev: Trie.t(selectorWithParents), curr) => { open Selector; let {scopes, style} = curr; @@ -206,41 +206,106 @@ let show = (v: t) => { ); }; -let _applyStyle: (TokenStyle.t, TokenStyle.t) => TokenStyle.t = - (prev: TokenStyle.t, style: TokenStyle.t) => { - let foreground = - switch (prev.foreground, style.foreground) { - | (Some(v), _) => Some(v) - | (_, Some(v)) => Some(v) - | _ => None - }; - - let background = - switch (prev.background, style.background) { - | (Some(v), _) => Some(v) - | (_, Some(v)) => Some(v) - | _ => None - }; +let match = (theme: t, scopes: string) => { + let scopes = Scopes.ofString(scopes) |> List.rev; - let bold = - switch (prev.bold, style.bold) { - | (Some(v), _) => Some(v) - | (_, Some(v)) => Some(v) - | _ => None + let rec calculateStyle = (~parentScopes, ~acc: list(TokenStyle.t), scopes) => { + switch (scopes) { + | [] => acc + | [scope, ...nextScope] => + // Get the matching path from the Trie + let matchingPath = Trie.matches(theme.trie, scope); + + if (matchingPath == []) { + // No matches - let's try the next scope! + calculateStyle( + ~parentScopes=[scope, ...parentScopes], + ~acc, + nextScope, + ); + } else { + let maybeTokenStyle: option(TokenStyle.t) = + matchingPath + |> List.fold_left( + (maybePrev: option(TokenStyle.t), curr) => { + let (_name, selector: option(selectorWithParents)) = curr; + + switch (selector) { + // No selector at this node. This can happen when a node is on the + // path to a node with a style. Nothing to do here; continue on. + | None => maybePrev + // We have a selector at this node. Let's check it out. + | Some({style, parents}) => + let prevStyle = + maybePrev |> Option.value(~default=TokenStyle.default); + + // Get the list of matching parent selectors to apply + let parentsScopesToApply = + parents + |> List.filter(selector => + Selector.matches(selector, parentScopes) + ); + + switch (parentsScopesToApply, style) { + // Case 1: No parent selectors match AND there is no style. We should continue on. + | ([], None) => None + + // Case 2: No parent selectors match, but there is a style at the Node. We should apply the style. + | ([], Some(style)) => + Some(TokenStyle.merge(prevStyle, style)) + + // Case 3: We have parent selectors that match, and may or may not have a style at the node. + // Apply the parent styles, and the node style, if applicable. + | (_, maybeStyle) => + let newStyle = + maybeStyle + |> Option.map(TokenStyle.merge(prevStyle)) + |> Option.value(~default=TokenStyle.default); + + // Apply any parent selectors that match... + // we should be sorting this by score! + Some( + List.fold_left( + (prev, curr: Selector.t) => { + open Selector; + let {style, _} = curr; + // Reversing the order because the parent style + // should take precedence over previous style + TokenStyle.merge(style, prev); + }, + newStyle, + parentsScopesToApply, + ), + ); + }; + }; + }, + None, + ); + + let acc = + maybeTokenStyle + |> Option.map(tokenStyle => [tokenStyle, ...acc]) + |> Option.value(~default=acc); + + calculateStyle( + ~parentScopes=[scope, ...parentScopes], + ~acc, + nextScope, + ); }; + }; + }; - let italic = - switch (prev.italic, style.italic) { - | (Some(v), _) => Some(v) - | (_, Some(v)) => Some(v) - | _ => None - }; + let styles = calculateStyle(~parentScopes=[], ~acc=[], scopes); - {background, foreground, bold, italic}; - }; + let result: TokenStyle.t = + styles + |> List.fold_left( + (acc, style) => {TokenStyle.merge(acc, style)}, + TokenStyle.default, + ); -let match = (theme: t, scopes: string) => { - let scopes = Scopes.ofString(scopes) |> List.rev; let default = ResolvedStyle.default( ~foreground=theme.defaultForeground, @@ -248,108 +313,5 @@ let match = (theme: t, scopes: string) => { (), ); - let rec f = scopes => { - switch (scopes) { - | [] => default - | [scope, ...scopeParents] => - // Get the matching path from the Trie - let p = Trie.matches(theme.trie, scope); - - switch (p) { - // If there were no matches... try the next scope up. - | [] => f(scopeParents) - // Got matches - we'll apply them in sequence - | _ => - let result = - List.fold_left( - (prev: option(TokenStyle.t), curr) => { - let (_name, selector: option(selectorWithParents)) = curr; - - switch (selector) { - // No selector at this node. This can happen when a node is on the - // path to a node with a style. Nothing to do here; continue on. - | None => prev - // We have a selector at this node. Let's check it out. - | Some({style, parents}) => - let prevStyle = - switch (prev) { - | None => TokenStyle.default - | Some(v) => v - }; - - // Get the list of matching parent selectors to apply - let parentsScopesToApply = - parents - |> List.filter(selector => - Selector.matches(selector, scopeParents) - ); - - switch (parentsScopesToApply, style) { - // Case 1: No parent selectors match AND there is no style. We should continue on. - | ([], None) => None - // Case 2: No parent selectors match, but there is a style at the Node. We should apply the style. - | ([], Some(style)) => Some(_applyStyle(prevStyle, style)) - // Case 3: We have parent selectors that match, and may or may not have a style at the node. - // Apply the parent styles, and the node style, if applicable. - | (_, style) => - let newStyle = - switch (style) { - | Some(v) => _applyStyle(prevStyle, v) - | None => TokenStyle.default - }; - // Apply any parent selectors that match... - // we should be sorting this by score! - Some( - List.fold_left( - (prev, curr: Selector.t) => { - open Selector; - let {style, _} = curr; - // Reversing the order because the parent style - // should take precedence over previous style - _applyStyle(style, prev); - }, - newStyle, - parentsScopesToApply, - ), - ); - }; - }; - }, - None, - p, - ); - - switch (result) { - | None => f(scopeParents) - | Some(result) => - let foreground = - switch (result.foreground) { - | Some(v) => v - | None => default.foreground - }; - - let bold = - switch (result.bold) { - | Some(v) => v - | None => default.bold - }; - - let italic = - switch (result.italic) { - | Some(v) => v - | None => default.italic - }; - - let background = - switch (result.background) { - | Some(v) => v - | None => default.background - }; - - {background, foreground, bold, italic}; - }; - }; - }; - }; - f(scopes); + TokenStyle.resolve(~default, result); }; diff --git a/test/reason-textmate/TokenThemeTests.re b/test/reason-textmate/TokenThemeTests.re index db5a38520b..2e0ab8c6ae 100644 --- a/test/reason-textmate/TokenThemeTests.re +++ b/test/reason-textmate/TokenThemeTests.re @@ -70,7 +70,7 @@ describe("TokenTheme", ({describe, _}) => { let style: ResolvedStyle.t = TokenTheme.match( simpleTokenTheme, - "source.js constant.numeric.meta.js", + "constant.numeric.meta.js source.js", ); expect.string(style.foreground).toEqual("#990000"); @@ -82,7 +82,7 @@ describe("TokenTheme", ({describe, _}) => { let style: ResolvedStyle.t = TokenTheme.match( simpleTokenTheme, - "source.js constant.numeric.meta.js some-unmatched-style", + "some-unmatched-style constant.numeric.meta.js source.js", ); expect.string(style.foreground).toEqual("#990000"); @@ -92,20 +92,21 @@ describe("TokenTheme", ({describe, _}) => { }); test("background color should be picked up", ({expect, _}) => { let style: ResolvedStyle.t = - TokenTheme.match(simpleTokenTheme, "text.html.basic source.js"); + TokenTheme.match(simpleTokenTheme, "source.js text.html.basic"); expect.string(style.foreground).toEqual("navy"); expect.string(style.background).toEqual("cornflowerBlue"); expect.bool(style.bold).toBe(false); expect.bool(style.italic).toBe(false); }); + // Test case from https://macromates.com/manual/en/scope_selectors (13.2) test( "deeper rule should win (source.php string over text.html source.php)", ({expect, _}) => { let style: ResolvedStyle.t = TokenTheme.match( simpleTokenTheme, - "text.html.basic source.php.html string.quoted", + "string.quoted source.php.embedded.html text.html.basic", ); expect.string(style.foreground).toEqual("peachPuff"); @@ -115,7 +116,7 @@ describe("TokenTheme", ({describe, _}) => { }); test("parent rule (meta html) gets applied", ({expect, _}) => { let style: ResolvedStyle.t = - TokenTheme.match(simpleTokenTheme, "meta html"); + TokenTheme.match(simpleTokenTheme, "html meta"); expect.string(style.foreground).toEqual("smoke"); expect.string(style.background).toEqual("#000"); @@ -123,7 +124,7 @@ describe("TokenTheme", ({describe, _}) => { expect.bool(style.italic).toBe(false); let style: ResolvedStyle.t = - TokenTheme.match(simpleTokenTheme, "meta.source.js html"); + TokenTheme.match(simpleTokenTheme, "html meta.source.js"); expect.string(style.foreground).toEqual("smoke"); expect.string(style.background).toEqual("#000"); @@ -387,5 +388,49 @@ describe("TokenTheme", ({describe, _}) => { expect.bool(style.bold).toBe(false); expect.bool(style.italic).toBe(true); }); + test("dracula: source should not override all styles", ({expect, _}) => { + let json = + Yojson.Safe.from_string( + {| + [ + { + "scope": [ + "source" + ], + "settings": { + "foreground": "#FF0000" + } + }, + { + "name": "Built-in functions / properties", + "scope": [ + "support.function", + "support.type.property-name" + ], + "settings": { + "fontStyle": "regular", + "foreground": "#AAAAAA" + } + } + ] + |}, + ); + let theme = + TokenTheme.of_yojson( + ~defaultForeground="#fff", + ~defaultBackground="#000", + json, + ); + + // Just support.function.console.js should resolve to the support.function + let style: ResolvedStyle.t = + TokenTheme.match(theme, "support.function.console.js"); + expect.string(style.foreground).toEqual("#AAAAAA"); + + //...and introducing source.js should still be the same, since it is less specific + let style: ResolvedStyle.t = + TokenTheme.match(theme, "support.function.console.js source.js"); + expect.string(style.foreground).toEqual("#AAAAAA"); + }); }); });