Change faces alias to be a base that can be modified

Using <fg>,<bg>+<attr>@<base> will apply the given fg color,
bg color and attributes on top of base dynamically. Simply giving
<base> is a shorthand for default,default@<base>.

Inspired by the discussion in #2862
This commit is contained in:
Maxime Coste 2019-04-23 16:44:30 +01:00
parent 17c5e7aa5f
commit 4e24ba86cc
8 changed files with 89 additions and 53 deletions

View File

@ -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 color, a background color, and some attributes. The value of a face has the
following format: following format:
-------------------------------- ---------------------------------------
fg_color[,bg_color][+attributes] fg_color[,bg_color][+attributes][@base]
-------------------------------- ---------------------------------------
'fg_color', 'bg_color':: 'fg_color', 'bg_color'::
a color whose value can be expressed in the following formats: 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 final attributes, as final but only applies to face's
attributes 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 == Builtin faces
The following default faces are used by color schemes to highlight certain The following default faces are used by color schemes to highlight certain

View File

@ -7,43 +7,51 @@
namespace Kakoune namespace Kakoune
{ {
static Face parse_face(StringView facedesc) static FaceRegistry::FaceSpec parse_face(StringView facedesc)
{ {
constexpr StringView invalid_face_error = "invalid face description, expected <fg>[,<bg>][+<attr>]"; constexpr StringView invalid_face_error = "invalid face description, expected <fg>[,<bg>][+<attr>][@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 bg_it = find(facedesc, ',');
auto attr_it = find(facedesc, '+'); auto attr_it = find(facedesc, '+');
auto base_it = find(facedesc, '@');
if (bg_it != facedesc.end() if (bg_it != facedesc.end()
and (attr_it < bg_it or (bg_it + 1) == facedesc.end())) and (attr_it < bg_it or (bg_it + 1) == facedesc.end()))
throw runtime_error(invalid_face_error.str()); throw runtime_error(invalid_face_error.str());
if (attr_it != facedesc.end() if (attr_it != facedesc.end()
and (attr_it + 1) == facedesc.end()) and (attr_it + 1) == facedesc.end())
throw runtime_error(invalid_face_error.str()); 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; str_to_color({facedesc.begin(), std::min(attr_it, bg_it)}) : Color::Default;
if (bg_it != facedesc.end()) 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()) 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) switch (*attr_it)
{ {
case 'u': res.attributes |= Attribute::Underline; break; case 'u': face.attributes |= Attribute::Underline; break;
case 'r': res.attributes |= Attribute::Reverse; break; case 'r': face.attributes |= Attribute::Reverse; break;
case 'b': res.attributes |= Attribute::Bold; break; case 'b': face.attributes |= Attribute::Bold; break;
case 'B': res.attributes |= Attribute::Blink; break; case 'B': face.attributes |= Attribute::Blink; break;
case 'd': res.attributes |= Attribute::Dim; break; case 'd': face.attributes |= Attribute::Dim; break;
case 'i': res.attributes |= Attribute::Italic; break; case 'i': face.attributes |= Attribute::Italic; break;
case 'f': res.attributes |= Attribute::FinalFg; break; case 'f': face.attributes |= Attribute::FinalFg; break;
case 'g': res.attributes |= Attribute::FinalBg; break; case 'g': face.attributes |= Attribute::FinalBg; break;
case 'a': res.attributes |= Attribute::FinalAttr; break; case 'a': face.attributes |= Attribute::FinalAttr; break;
case 'F': res.attributes |= Attribute::Final; break; case 'F': face.attributes |= Attribute::Final; break;
default: throw runtime_error(format("no such face attribute: '{}'", StringView{*attr_it})); 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) String to_string(Attribute attributes)
@ -79,16 +87,33 @@ String to_string(Face face)
Face FaceRegistry::operator[](StringView facedesc) const Face FaceRegistry::operator[](StringView facedesc) const
{ {
auto it = m_faces.find(facedesc); return resolve_spec(parse_face(facedesc));
if (it != m_faces.end())
{
if (it->value.alias.empty())
return it->value.face;
return operator[](it->value.alias);
} }
if (m_parent)
return (*m_parent)[facedesc]; Face FaceRegistry::resolve_spec(const FaceSpec& spec) const
return parse_face(facedesc); {
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())
{
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;
}
}
return face;
} }
void FaceRegistry::add_face(StringView name, StringView facedesc, bool override) 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)); throw runtime_error(format("face '{}' already defined", name));
if (name.empty() or is_color_name(name) or if (name.empty() or is_color_name(name) or
std::any_of(name.begin(), name.end(), any_of(name, [](char c){ return not is_word(c); }))
[](char c){ return not is_word(c); }))
throw runtime_error(format("invalid face name: '{}'", name)); throw runtime_error(format("invalid face name: '{}'", name));
if (name == facedesc) FaceSpec spec = parse_face(facedesc);
throw runtime_error(format("cannot alias face '{}' to itself", name)); auto it = m_faces.find(spec.base);
if (spec.base == name and it != m_faces.end())
for (auto it = m_faces.find(facedesc);
it != m_faces.end() and not it->value.alias.empty();
it = m_faces.find(it->value.alias))
{ {
if (it->value.alias == name) it->value.face = merge_faces(it->value.face, spec.face);
throw runtime_error("face cycle detected"); it->value.base = spec.base;
}
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
return; return;
} }
face.alias = ""; while (it != m_faces.end() and not it->value.base.empty())
face.face = parse_face(facedesc); {
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) void FaceRegistry::remove_face(StringView name)

View File

@ -20,12 +20,12 @@ public:
void add_face(StringView name, StringView facedesc, bool override = false); void add_face(StringView name, StringView facedesc, bool override = false);
void remove_face(StringView name); void remove_face(StringView name);
struct FaceOrAlias struct FaceSpec
{ {
Face face = {}; Face face = {};
String alias = {}; String base = {};
}; };
using FaceMap = HashMap<String, FaceOrAlias, MemoryDomain::Faces>; using FaceMap = HashMap<String, FaceSpec, MemoryDomain::Faces>;
auto flatten_faces() const auto flatten_faces() const
{ {
@ -41,6 +41,8 @@ public:
} }
private: private:
Face resolve_spec(const FaceSpec& spec) const;
friend class Scope; friend class Scope;
FaceRegistry(); FaceRegistry();

View File

@ -78,7 +78,7 @@ static uint32_t compute_faces_hash(const FaceRegistry& faces)
{ {
uint32_t hash = 0; uint32_t hash = 0;
for (auto&& face : faces.flatten_faces() | transform(&FaceRegistry::FaceMap::Item::value)) 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; return hash;
} }

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
foo

View File

@ -0,0 +1,3 @@
face global Foo default,blue+i
face buffer Foo red,default+u@Foo
add-highlighter window/ regex foo 0:Foo

View File

@ -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] }