diff --git a/doc/pages/faces.asciidoc b/doc/pages/faces.asciidoc index fc0f12cb..2801bf81 100644 --- a/doc/pages/faces.asciidoc +++ b/doc/pages/faces.asciidoc @@ -6,9 +6,9 @@ A 'face' refers how the specified text is displayed, it has a foreground color, a background color, and some attributes. The value of a face has the following format: --------------------------------- -fg_color[,bg_color][+attributes] --------------------------------- +--------------------------------------- +fg_color[,bg_color][+attributes][@base] +--------------------------------------- 'fg_color', 'bg_color':: a color whose value can be expressed in the following formats: @@ -49,6 +49,11 @@ fg_color[,bg_color][+attributes] final attributes, as final but only applies to face's attributes +'base':: + The base face on which this face applies, which can be any face name, + as long as a cycle is not introduced. A face can reference itself, in + which case it will apply on top of the parent scope version. + == Builtin faces The following default faces are used by color schemes to highlight certain diff --git a/src/face_registry.cc b/src/face_registry.cc index 5a5187b8..9fe318a2 100644 --- a/src/face_registry.cc +++ b/src/face_registry.cc @@ -7,43 +7,51 @@ namespace Kakoune { -static Face parse_face(StringView facedesc) +static FaceRegistry::FaceSpec parse_face(StringView facedesc) { - constexpr StringView invalid_face_error = "invalid face description, expected [,][+]"; + constexpr StringView invalid_face_error = "invalid face description, expected [,][+][@base] or just [base]"; + if (all_of(facedesc, [](char c){ return is_word(c); }) and not is_color_name(facedesc)) + return {Face{}, facedesc.str()}; + auto bg_it = find(facedesc, ','); auto attr_it = find(facedesc, '+'); + auto base_it = find(facedesc, '@'); if (bg_it != facedesc.end() and (attr_it < bg_it or (bg_it + 1) == facedesc.end())) throw runtime_error(invalid_face_error.str()); if (attr_it != facedesc.end() and (attr_it + 1) == facedesc.end()) throw runtime_error(invalid_face_error.str()); - Face res; - res.fg = attr_it != facedesc.begin() ? + + FaceRegistry::FaceSpec spec; + auto& face = spec.face; + face.fg = attr_it != facedesc.begin() ? str_to_color({facedesc.begin(), std::min(attr_it, bg_it)}) : Color::Default; if (bg_it != facedesc.end()) - res.bg = bg_it+1 != attr_it ? str_to_color({bg_it+1, attr_it}) : Color::Default; + face.bg = bg_it+1 != attr_it ? str_to_color({bg_it+1, attr_it}) : Color::Default; if (attr_it != facedesc.end()) { - for (++attr_it; attr_it != facedesc.end(); ++attr_it) + for (++attr_it; attr_it != base_it; ++attr_it) { switch (*attr_it) { - case 'u': res.attributes |= Attribute::Underline; break; - case 'r': res.attributes |= Attribute::Reverse; break; - case 'b': res.attributes |= Attribute::Bold; break; - case 'B': res.attributes |= Attribute::Blink; break; - case 'd': res.attributes |= Attribute::Dim; break; - case 'i': res.attributes |= Attribute::Italic; break; - case 'f': res.attributes |= Attribute::FinalFg; break; - case 'g': res.attributes |= Attribute::FinalBg; break; - case 'a': res.attributes |= Attribute::FinalAttr; break; - case 'F': res.attributes |= Attribute::Final; break; + case 'u': face.attributes |= Attribute::Underline; break; + case 'r': face.attributes |= Attribute::Reverse; break; + case 'b': face.attributes |= Attribute::Bold; break; + case 'B': face.attributes |= Attribute::Blink; break; + case 'd': face.attributes |= Attribute::Dim; break; + case 'i': face.attributes |= Attribute::Italic; break; + case 'f': face.attributes |= Attribute::FinalFg; break; + case 'g': face.attributes |= Attribute::FinalBg; break; + case 'a': face.attributes |= Attribute::FinalAttr; break; + case 'F': face.attributes |= Attribute::Final; break; default: throw runtime_error(format("no such face attribute: '{}'", StringView{*attr_it})); } } } - return res; + if (base_it != facedesc.end()) + spec.base = String{base_it+1, facedesc.end()}; + return spec; } String to_string(Attribute attributes) @@ -79,16 +87,33 @@ String to_string(Face face) Face FaceRegistry::operator[](StringView facedesc) const { - auto it = m_faces.find(facedesc); - if (it != m_faces.end()) + return resolve_spec(parse_face(facedesc)); +} + +Face FaceRegistry::resolve_spec(const FaceSpec& spec) const +{ + if (spec.base.empty()) + return spec.face; + + StringView base = spec.base; + Face face = spec.face; + for (auto* reg = this; reg != nullptr; reg = reg->m_parent.get()) { - if (it->value.alias.empty()) - return it->value.face; - return operator[](it->value.alias); + auto it = reg->m_faces.find(base); + if (it == reg->m_faces.end()) + continue; + + if (it->value.base.empty()) + return merge_faces(it->value.face, face); + if (it->value.base != it->key) + return merge_faces(reg->resolve_spec(it->value), face); + else + { + face = merge_faces(it->value.face, face); + base = it->value.base; + } } - if (m_parent) - return (*m_parent)[facedesc]; - return parse_face(facedesc); + return face; } void FaceRegistry::add_face(StringView name, StringView facedesc, bool override) @@ -97,33 +122,25 @@ void FaceRegistry::add_face(StringView name, StringView facedesc, bool override) throw runtime_error(format("face '{}' already defined", name)); if (name.empty() or is_color_name(name) or - std::any_of(name.begin(), name.end(), - [](char c){ return not is_word(c); })) + any_of(name, [](char c){ return not is_word(c); })) throw runtime_error(format("invalid face name: '{}'", name)); - if (name == facedesc) - throw runtime_error(format("cannot alias face '{}' to itself", name)); - - for (auto it = m_faces.find(facedesc); - it != m_faces.end() and not it->value.alias.empty(); - it = m_faces.find(it->value.alias)) + FaceSpec spec = parse_face(facedesc); + auto it = m_faces.find(spec.base); + if (spec.base == name and it != m_faces.end()) { - if (it->value.alias == name) - throw runtime_error("face cycle detected"); - } - - FaceOrAlias& face = m_faces[name]; - - for (auto* registry = this; registry != nullptr; registry = registry->m_parent.get()) - { - if (not registry->m_faces.contains(facedesc)) - continue; - face.alias = facedesc.str(); // This is referencing another face + it->value.face = merge_faces(it->value.face, spec.face); + it->value.base = spec.base; return; } - face.alias = ""; - face.face = parse_face(facedesc); + while (it != m_faces.end() and not it->value.base.empty()) + { + if (it->value.base == name) + throw runtime_error("face cycle detected"); + it = m_faces.find(it->value.base); + } + m_faces[name] = std::move(spec); } void FaceRegistry::remove_face(StringView name) diff --git a/src/face_registry.hh b/src/face_registry.hh index 1182a9d3..252256cf 100644 --- a/src/face_registry.hh +++ b/src/face_registry.hh @@ -20,12 +20,12 @@ public: void add_face(StringView name, StringView facedesc, bool override = false); void remove_face(StringView name); - struct FaceOrAlias + struct FaceSpec { Face face = {}; - String alias = {}; + String base = {}; }; - using FaceMap = HashMap; + using FaceMap = HashMap; auto flatten_faces() const { @@ -41,6 +41,8 @@ public: } private: + Face resolve_spec(const FaceSpec& spec) const; + friend class Scope; FaceRegistry(); diff --git a/src/window.cc b/src/window.cc index 1b145d66..f44ea92b 100644 --- a/src/window.cc +++ b/src/window.cc @@ -78,7 +78,7 @@ static uint32_t compute_faces_hash(const FaceRegistry& faces) { uint32_t hash = 0; for (auto&& face : faces.flatten_faces() | transform(&FaceRegistry::FaceMap::Item::value)) - hash = combine_hash(hash, face.alias.empty() ? hash_value(face.face) : hash_value(face.alias)); + hash = combine_hash(hash, face.base.empty() ? hash_value(face.face) : hash_value(face.base)); return hash; } diff --git a/test/highlight/face-override/cmd b/test/highlight/face-override/cmd new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/highlight/face-override/cmd @@ -0,0 +1 @@ + diff --git a/test/highlight/face-override/in b/test/highlight/face-override/in new file mode 100644 index 00000000..257cc564 --- /dev/null +++ b/test/highlight/face-override/in @@ -0,0 +1 @@ +foo diff --git a/test/highlight/face-override/rc b/test/highlight/face-override/rc new file mode 100644 index 00000000..b579d63e --- /dev/null +++ b/test/highlight/face-override/rc @@ -0,0 +1,3 @@ +face global Foo default,blue+i +face buffer Foo red,default+u@Foo +add-highlighter window/ regex foo 0:Foo diff --git a/test/highlight/face-override/ui-out b/test/highlight/face-override/ui-out new file mode 100644 index 00000000..8b40e862 --- /dev/null +++ b/test/highlight/face-override/ui-out @@ -0,0 +1,7 @@ +{ "jsonrpc": "2.0", "method": "set_ui_options", "params": [{}] } +{ "jsonrpc": "2.0", "method": "draw", "params": [[[{ "face": { "fg": "black", "bg": "white", "attributes": ["underline","italic"] }, "contents": "f" }, { "face": { "fg": "red", "bg": "blue", "attributes": ["underline","italic"] }, "contents": "oo" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "\u000a" }]], { "fg": "default", "bg": "default", "attributes": [] }, { "fg": "blue", "bg": "default", "attributes": [] }] } +{ "jsonrpc": "2.0", "method": "menu_hide", "params": [] } +{ "jsonrpc": "2.0", "method": "info_hide", "params": [] } +{ "jsonrpc": "2.0", "method": "draw_status", "params": [[], [{ "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "out 1:1 " }, { "face": { "fg": "black", "bg": "yellow", "attributes": [] }, "contents": "" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " " }, { "face": { "fg": "blue", "bg": "default", "attributes": [] }, "contents": "1 sel" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " - client0@[kak-tests]" }], { "fg": "cyan", "bg": "default", "attributes": [] }] } +{ "jsonrpc": "2.0", "method": "set_cursor", "params": ["buffer", { "line": 0, "column": 0 }] } +{ "jsonrpc": "2.0", "method": "refresh", "params": [true] }