/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file csh.cpp The definition of classes for Color Syntax Highlighting.
 * @ingroup libcgencommon_files */
/** @defgroup hintpopup_callbacks Callback functions for hint popup listbox symbols */

#define _USE_MATH_DEFINES
#include <math.h>
#include <map>
#include "utf8utils.h" //for UTF8len
#include "csh.h"
#include "cgen_color.h"
#include "stringparse.h" //for extracting csh out of strings
#include "cgen_attribute.h"  //for CaseInsensitive compares
#include "style.h"  //for Design::Reset() to obtain forbidden style names
#include "chartbase.h"
#include "cgencommon.h"
#include "parser_tools.h"

using namespace std;

bool ColorSyntaxAppearance::operator==(const struct ColorSyntaxAppearance &p) const
{
    if ((effects & mask) != (p.effects & p.mask)) return false;
    if (mask != p.mask) return false;
    return r==p.r && g==p.g && b==p.b;
}

/** Creates a description of this color appearance.
 * Result is "<r> <g> <b> <bold> <italics> <underline>" */
std::string ColorSyntaxAppearance::Print() const
{
    std::string tmp(std::to_string(r));
    tmp.push_back(' ');
    tmp.append(std::to_string(g));
    tmp.push_back(' ');
    tmp.append(std::to_string(b));
    tmp.append(effects & COLOR_FLAG_BOLD ? " 1" : " 0");
    tmp.append(effects & COLOR_FLAG_ITALICS ? " 1" : " 0");
    tmp.append(effects & COLOR_FLAG_UNDERLINE ? " 1" : " 0");
    return tmp;
}


/**Checks if 'e' overlaps an entry already in the list */
bool CshListType::CheckIfOverlap(const CshPos &e) const
{
    for (const auto &p :*this)
        if (e.Overlaps(p))
            return true;
    return false;
}


/** Checks if all elements are either COLOR_ERROR or COLOR_MAX
*  Used in debug mode only.*/
bool CshErrorList::CheckIfErrorOrMaxColorsOnly() const
{
    for (auto &e : error_ranges)
        if (e.color!=COLOR_ERROR && e.color!=COLOR_MAX)
            return false;
    return true;
}


/** Helper to add
 * - checks if we have already added the exact same error or not
 * - ensures that error_ranges is sorted
 * - ensures no overlap between its entries
 * - ensures that an error inserted later does not overwrite an error inserted earlier.
 * If we have a long error to add, which covers many small errors, we break it into
 * smaller pieces to fill the gaps between the smaller ones.
 * Returns true if we have have created a new text entry. */
bool CshErrorList::Add(CshPos pos, std::string &&t)
{
    size_t i = std::ranges::upper_bound(error_ranges, pos.first_pos, {}, &CshPos::end_pos) - error_ranges.begin(); 
    //pos.first_pos < i.end_pos, so either they overlap or pos is completely after 'i'.
    //It is guaranteed that any element before 'i' is completely before 'pos'
    if (i == error_ranges.size() || pos.end_pos <= error_ranges[i].first_pos) 
        error_ranges.emplace(error_ranges.begin()+i, pos, COLOR_ERROR, std::move(t));
    else { // 'pos' overlaps with 'i' (and maybe also with later elements)
        do {
            const CshPos after = pos.Substract(error_ranges[i], false);
            if (pos.first_pos < error_ranges[i].first_pos)
                error_ranges.emplace(error_ranges.begin()+i, pos.Substract(error_ranges[i]), COLOR_ERROR, t);
            pos = after;
            i++;
        } while (!pos.empty() && i < error_ranges.size());
        if (!pos.empty())
            error_ranges.emplace(error_ranges.begin()+i, pos, COLOR_ERROR, std::move(t));
    }
    _ASSERT(CheckOrderedAndNonOverlapping(error_ranges));
    return true;
}



ColorSyntaxAppearance MscCshAppearanceList[CSH_SCHEME_MAX][COLOR_MAX];
void MscInitializeCshAppearanceList(void)
{
    //shorter alias to make things easier to see below
    ColorSyntaxAppearance (&l)[CSH_SCHEME_MAX][COLOR_MAX] = MscCshAppearanceList;

    //case should be taken. Any value that equals to COLOR_NORMAL may not get a CSH entry
    //(to optimize away CSH entries of no effect).
    //However, some of the CSH entries are not only applied to plain text, but also
    //on top of other CSH entries. E.g., text escapes on top of text, and often comments on
    //top of text, perhaps also error.
    //Some others are needed for smart indent (LABEL_TEXT, COLON)
    //For these we widen the mask with COLOR_FLAG_DIFFERENT, so they become different from
    //COLOR_NORMAL

    //Set the mask to default: we set all parameters
    for (unsigned scheme=0; scheme<CSH_SCHEME_MAX; scheme++) {
        for (unsigned i = 0; i<COLOR_MAX; i++) {
            l[scheme][i].mask = COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS | COLOR_FLAG_UNDERLINE | COLOR_FLAG_COLOR;
            l[scheme][i].effects = 0;
        }
        l[scheme][COLOR_LABEL_ESCAPE].mask |= COLOR_FLAG_DIFFERENT_DRAW;
        l[scheme][COLOR_COMMENT].mask |= COLOR_FLAG_DIFFERENT_DRAW;
        l[scheme][COLOR_LABEL_TEXT].mask |= COLOR_FLAG_DIFFERENT_NO_DRAW;
        l[scheme][COLOR_COLON].mask |= COLOR_FLAG_DIFFERENT_NO_DRAW;
    }


    //CSH_SCHEME ==0 is the Minimal one
    l[0][COLOR_KEYWORD].           SetColor(  0,  0,  0); l[0][COLOR_KEYWORD].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_KEYWORD_PARTIAL].   SetColor(  0,  0,  0); l[0][COLOR_KEYWORD_PARTIAL].effects |= 0;
    l[0][COLOR_KEYWORD_MSCGEN].    SetColor( 50, 50, 50); l[0][COLOR_KEYWORD_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[0][COLOR_ATTRNAME].          SetColor(  0,  0,  0); l[0][COLOR_ATTRNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_ATTRNAME_PARTIAL].  SetColor(  0,  0,  0); l[0][COLOR_ATTRNAME_PARTIAL].effects |= 0;
    l[0][COLOR_ATTRNAME_MSCGEN].   SetColor( 50, 50, 50); l[0][COLOR_ATTRNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[0][COLOR_OPTIONNAME].        SetColor(  0,  0,  0); l[0][COLOR_OPTIONNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_OPTIONNAME_PARTIAL].SetColor(  0,  0,  0); l[0][COLOR_OPTIONNAME_PARTIAL].effects |= 0;
    l[0][COLOR_OPTIONNAME_MSCGEN]. SetColor( 50, 50, 50); l[0][COLOR_OPTIONNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[0][COLOR_EQUAL].             SetColor(  0,  0,  0); l[0][COLOR_EQUAL].effects |= 0;
    l[0][COLOR_SEMICOLON].         SetColor(  0,  0,  0); l[0][COLOR_SEMICOLON].effects |= 0;
    l[0][COLOR_COLON].             SetColor(  0,  0,  0); l[0][COLOR_COLON].effects |= 0;
    l[0][COLOR_BRACE].             SetColor(  0,  0,  0); l[0][COLOR_BRACE].effects |= 0;
    l[0][COLOR_BRACKET].           SetColor(  0,  0,  0); l[0][COLOR_BRACKET].effects |= 0;
    l[0][COLOR_PARENTHESIS].       SetColor(  0,  0,  0); l[0][COLOR_BRACKET].effects |= 0;
    l[0][COLOR_SYMBOL].            SetColor( 20, 20,  0); l[0][COLOR_SYMBOL].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_SYMBOL_MSCGEN].     SetColor( 80, 80,  0); l[0][COLOR_SYMBOL_MSCGEN].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_DESIGNNAME].        SetColor( 50,  0,  0); l[0][COLOR_DESIGNNAME].effects |= 0;
    l[0][COLOR_STYLENAME].         SetColor( 50,  0,  0); l[0][COLOR_STYLENAME].effects |= 0;
    l[0][COLOR_COLORNAME].         SetColor( 50,  0,  0); l[0][COLOR_COLORNAME].effects |= 0;
    l[0][COLOR_ENTITYNAME].        SetColor(  0, 50,  0); l[0][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_ENTITYNAME_PARTIAL].SetColor( 25, 50, 25); l[0][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_ENTITYNAME_FIRST].  SetColor(  0,  0,  0); l[0][COLOR_ENTITYNAME_FIRST].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_MARKERNAME].        SetColor(  0,  0, 50); l[0][COLOR_MARKERNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_MARKERNAME_PARTIAL].SetColor( 25, 25, 50); l[0][COLOR_MARKERNAME_PARTIAL].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_PROCNAME].          SetColor(  0,  0, 50); l[0][COLOR_PROCNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_INCLUDEFILE].       SetColor(  0,  0, 50); l[0][COLOR_INCLUDEFILE].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_PARAMNAME].         SetColor(  0,  0, 50); l[0][COLOR_PARAMNAME].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_ATTRVALUE].         SetColor(  0,  0,  0); l[0][COLOR_ATTRVALUE].effects |= 0;
    l[0][COLOR_ATTRVALUE_EMPH].    SetColor(  0,  0,  0); l[0][COLOR_ATTRVALUE_EMPH].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_COLORDEF].          SetColor(  0,  0,  0); l[0][COLOR_COLORDEF].effects |= 0;
    l[0][COLOR_LABEL_TEXT].        SetColor(  0,  0,  0); l[0][COLOR_LABEL_TEXT].effects |= 0;
    l[0][COLOR_LABEL_ESCAPE].      SetColor(  0,  0,  0); l[0][COLOR_LABEL_ESCAPE].effects |= COLOR_FLAG_BOLD;
    l[0][COLOR_COMMENT].           SetColor(100,100,100); l[0][COLOR_COMMENT].effects |= COLOR_FLAG_ITALICS;
    //For errors we keep bold, italics and color settings, just underline
    l[0][COLOR_ERROR].mask = COLOR_FLAG_UNDERLINE;        l[0][COLOR_ERROR].effects = COLOR_FLAG_UNDERLINE;
    l[0][COLOR_NO_ERROR].mask = COLOR_FLAG_UNDERLINE;     l[0][COLOR_NO_ERROR].effects = 0;

    //CSH_SCHEME ==1 is the Standard one
    l[1][COLOR_KEYWORD].           SetColor(128,128,  0); l[1][COLOR_KEYWORD].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_KEYWORD_PARTIAL].   SetColor(128,128,  0); l[1][COLOR_KEYWORD_PARTIAL].effects |= 0;
    l[1][COLOR_KEYWORD_MSCGEN].    SetColor(178,178,  0); l[1][COLOR_KEYWORD_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[1][COLOR_ATTRNAME].          SetColor(128,128,  0); l[1][COLOR_ATTRNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_ATTRNAME_PARTIAL].  SetColor(128,128,  0); l[1][COLOR_ATTRNAME_PARTIAL].effects |= 0;
    l[1][COLOR_ATTRNAME_MSCGEN].   SetColor(178,178,  0); l[1][COLOR_ATTRNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[1][COLOR_OPTIONNAME].        SetColor(128,128,  0); l[1][COLOR_OPTIONNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_OPTIONNAME_PARTIAL].SetColor(128,128,  0); l[1][COLOR_OPTIONNAME_PARTIAL].effects |= 0;
    l[1][COLOR_OPTIONNAME_MSCGEN]. SetColor(178,178,  0); l[1][COLOR_OPTIONNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[1][COLOR_EQUAL].             SetColor(  0,  0,  0); l[1][COLOR_EQUAL].effects |= 0;
    l[1][COLOR_SEMICOLON].         SetColor(  0,  0,  0); l[1][COLOR_SEMICOLON].effects |= 0;
    l[1][COLOR_COLON].             SetColor(  0,  0,  0); l[1][COLOR_COLON].effects |= 0;
    l[1][COLOR_BRACE].             SetColor(  0,  0,  0); l[1][COLOR_BRACE].effects |= 0;
    l[1][COLOR_BRACKET].           SetColor(  0,  0,  0); l[1][COLOR_BRACKET].effects |= 0;
    l[1][COLOR_PARENTHESIS].       SetColor(  0,  0,  0); l[1][COLOR_BRACKET].effects |= 0;
    l[1][COLOR_SYMBOL].            SetColor(255,  0,  0); l[1][COLOR_SYMBOL].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_SYMBOL_MSCGEN].     SetColor(255, 50, 50); l[1][COLOR_SYMBOL_MSCGEN].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_DESIGNNAME].        SetColor(  0,  0,  0); l[1][COLOR_DESIGNNAME].effects |= 0;
    l[1][COLOR_STYLENAME].         SetColor(  0,  0,100); l[1][COLOR_STYLENAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_COLORNAME].         SetColor(  0,  0,  0); l[1][COLOR_COLORNAME].effects |= 0;
    l[1][COLOR_ENTITYNAME].        SetColor(200,  0,  0); l[1][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_ENTITYNAME_PARTIAL].SetColor(200, 50, 50); l[1][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_ENTITYNAME_FIRST].  SetColor(200,  0,  0); l[1][COLOR_ENTITYNAME_FIRST].effects |= COLOR_FLAG_BOLD|COLOR_FLAG_UNDERLINE;
    l[1][COLOR_MARKERNAME].        SetColor(  0,200,  0); l[1][COLOR_MARKERNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_MARKERNAME_PARTIAL].SetColor( 50,200, 50); l[1][COLOR_MARKERNAME_PARTIAL].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_PROCNAME].          SetColor(  0,200,  0); l[1][COLOR_PROCNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_INCLUDEFILE].       SetColor(  0,200,  0); l[1][COLOR_INCLUDEFILE].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_PARAMNAME].         SetColor(  0,  0,200); l[1][COLOR_PARAMNAME].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_ATTRVALUE].         SetColor(  0,  0,200); l[1][COLOR_ATTRVALUE].effects |= 0;
    l[1][COLOR_ATTRVALUE_EMPH].    SetColor(  0,  0,200); l[1][COLOR_ATTRVALUE_EMPH].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_COLORDEF].          SetColor(  0,  0,200); l[1][COLOR_COLORDEF].effects |= 0;
    l[1][COLOR_LABEL_TEXT].        SetColor(  0,  0,  0); l[1][COLOR_LABEL_TEXT].effects |= 0;
    l[1][COLOR_LABEL_ESCAPE].      SetColor(  0,150,  0); l[1][COLOR_LABEL_ESCAPE].effects |= COLOR_FLAG_BOLD;
    l[1][COLOR_COMMENT].           SetColor(100,100,100); l[1][COLOR_COMMENT].effects |= COLOR_FLAG_ITALICS;
    //For errors we keep bold, italics and color settings, just underline
    l[1][COLOR_ERROR].mask = COLOR_FLAG_UNDERLINE;        l[1][COLOR_ERROR].effects = COLOR_FLAG_UNDERLINE;
    l[1][COLOR_NO_ERROR].mask = COLOR_FLAG_UNDERLINE;     l[1][COLOR_NO_ERROR].effects = 0;

    //CSH_SCHEME ==2 is the Colorful one
    l[2][COLOR_KEYWORD].           SetColor(128,128,  0); l[2][COLOR_KEYWORD].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_KEYWORD_PARTIAL].   SetColor(128,128,  0); l[2][COLOR_KEYWORD_PARTIAL].effects |= 0;
    l[2][COLOR_KEYWORD_MSCGEN].    SetColor(178,178,  0); l[2][COLOR_KEYWORD_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[2][COLOR_ATTRNAME].          SetColor(128,128,  0); l[2][COLOR_ATTRNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_ATTRNAME_PARTIAL].  SetColor(128,128,  0); l[2][COLOR_ATTRNAME_PARTIAL].effects |= 0;
    l[2][COLOR_ATTRNAME_MSCGEN].   SetColor(178,178,  0); l[2][COLOR_ATTRNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[2][COLOR_OPTIONNAME].        SetColor(128,128,  0); l[2][COLOR_OPTIONNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_OPTIONNAME_PARTIAL].SetColor(128,128,  0); l[2][COLOR_OPTIONNAME_PARTIAL].effects |= 0;
    l[2][COLOR_OPTIONNAME_MSCGEN]. SetColor(178,178,  0); l[2][COLOR_OPTIONNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[2][COLOR_EQUAL].             SetColor(  0,  0,  0); l[2][COLOR_EQUAL].effects |= 0;
    l[2][COLOR_SEMICOLON].         SetColor(  0,  0,  0); l[2][COLOR_SEMICOLON].effects |= 0;
    l[2][COLOR_COLON].             SetColor(  0,  0,  0); l[2][COLOR_COLON].effects |= 0;
    l[2][COLOR_BRACE].             SetColor(  0,  0,  0); l[2][COLOR_BRACE].effects |= 0;
    l[2][COLOR_BRACKET].           SetColor(  0,  0,  0); l[2][COLOR_BRACKET].effects |= 0;
    l[2][COLOR_PARENTHESIS].       SetColor(  0,  0,  0); l[2][COLOR_BRACKET].effects |= 0;
    l[2][COLOR_SYMBOL].            SetColor(  0,128,128); l[2][COLOR_SYMBOL].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_SYMBOL_MSCGEN].     SetColor(  0,178,178); l[2][COLOR_SYMBOL_MSCGEN].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_DESIGNNAME].        SetColor(128,  0,  0); l[2][COLOR_DESIGNNAME].effects |= 0;
    l[2][COLOR_STYLENAME].         SetColor(  0,  0,128); l[2][COLOR_STYLENAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_COLORNAME].         SetColor(128,  0,  0); l[2][COLOR_COLORNAME].effects |= 0;
    l[2][COLOR_ENTITYNAME].        SetColor(200,  0,  0); l[2][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_ENTITYNAME_PARTIAL].SetColor(200, 50, 50); l[2][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_ENTITYNAME_FIRST].  SetColor(200,  0,  0); l[2][COLOR_ENTITYNAME_FIRST].effects |= COLOR_FLAG_BOLD|COLOR_FLAG_UNDERLINE;
    l[2][COLOR_MARKERNAME].        SetColor(  0,255,  0); l[2][COLOR_MARKERNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_MARKERNAME_PARTIAL].SetColor( 50,255, 50); l[2][COLOR_MARKERNAME_PARTIAL].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_PROCNAME].          SetColor(  0,255,  0); l[2][COLOR_PROCNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_INCLUDEFILE].       SetColor(  0,255,  0); l[2][COLOR_INCLUDEFILE].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_PARAMNAME].         SetColor(  0,  0,255); l[2][COLOR_PARAMNAME].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_ATTRVALUE].         SetColor(  0,  0,255); l[2][COLOR_ATTRVALUE].effects |= 0;
    l[2][COLOR_ATTRVALUE_EMPH].    SetColor(  0,  0,255); l[2][COLOR_ATTRVALUE_EMPH].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_COLORDEF].          SetColor(  0,  0,255); l[2][COLOR_COLORDEF].effects |= 0;
    l[2][COLOR_LABEL_TEXT].        SetColor(  0,200,  0); l[2][COLOR_LABEL_TEXT].effects |= 0;
    l[2][COLOR_LABEL_ESCAPE].      SetColor(255,  0,  0); l[2][COLOR_LABEL_ESCAPE].effects |= COLOR_FLAG_BOLD;
    l[2][COLOR_COMMENT].           SetColor(100,100,100); l[2][COLOR_COMMENT].effects |= COLOR_FLAG_ITALICS;
    //For errors we keep bold, italics and color settings, just underline
    l[2][COLOR_ERROR].mask = COLOR_FLAG_UNDERLINE;        l[2][COLOR_ERROR].effects = COLOR_FLAG_UNDERLINE;
    l[2][COLOR_NO_ERROR].mask = COLOR_FLAG_UNDERLINE;     l[2][COLOR_NO_ERROR].effects = 0;

    //CSH_SCHEME ==3 is the Error oriented one
    l[3][COLOR_KEYWORD].           SetColor(  0,  0,  0); l[3][COLOR_KEYWORD].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_KEYWORD_PARTIAL].   SetColor(  0,  0,  0); l[3][COLOR_KEYWORD_PARTIAL].effects |= 0;
    l[3][COLOR_KEYWORD_MSCGEN].    SetColor( 50, 50, 50); l[3][COLOR_KEYWORD_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[3][COLOR_ATTRNAME].          SetColor(  0,  0,  0); l[3][COLOR_ATTRNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_ATTRNAME_PARTIAL].  SetColor(  0,  0,  0); l[3][COLOR_ATTRNAME_PARTIAL].effects |= 0;
    l[3][COLOR_ATTRNAME_MSCGEN].   SetColor( 50, 50, 50); l[3][COLOR_ATTRNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[3][COLOR_OPTIONNAME].        SetColor(  0,  0,  0); l[3][COLOR_OPTIONNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_OPTIONNAME_PARTIAL].SetColor(  0,  0,  0); l[3][COLOR_OPTIONNAME_PARTIAL].effects |= 0;
    l[3][COLOR_OPTIONNAME_MSCGEN]. SetColor( 50, 50, 50); l[3][COLOR_OPTIONNAME_MSCGEN].effects |= COLOR_FLAG_BOLD | COLOR_FLAG_ITALICS;
    l[3][COLOR_EQUAL].             SetColor(  0,  0,  0); l[3][COLOR_EQUAL].effects |= 0;
    l[3][COLOR_SEMICOLON].         SetColor(  0,  0,  0); l[3][COLOR_SEMICOLON].effects |= 0;
    l[3][COLOR_COLON].             SetColor(  0,  0,  0); l[3][COLOR_COLON].effects |= 0;
    l[3][COLOR_BRACE].             SetColor(  0,  0,  0); l[3][COLOR_BRACE].effects |= 0;
    l[3][COLOR_BRACKET].           SetColor(  0,  0,  0); l[3][COLOR_BRACKET].effects |= 0;
    l[3][COLOR_PARENTHESIS].       SetColor(  0,  0,  0); l[3][COLOR_BRACKET].effects |= 0;
    l[3][COLOR_SYMBOL].            SetColor( 20, 20,  0); l[3][COLOR_SYMBOL].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_SYMBOL].            SetColor( 80, 80,  0); l[3][COLOR_SYMBOL_MSCGEN].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_DESIGNNAME].        SetColor( 50,  0,  0); l[3][COLOR_DESIGNNAME].effects |= 0;
    l[3][COLOR_STYLENAME].         SetColor( 50,  0,  0); l[3][COLOR_STYLENAME].effects |= 0;
    l[3][COLOR_COLORNAME].         SetColor( 50,  0,  0); l[3][COLOR_COLORNAME].effects |= 0;
    l[3][COLOR_ENTITYNAME].        SetColor(  0, 50,  0); l[3][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_ENTITYNAME_PARTIAL].SetColor( 25, 50, 25); l[3][COLOR_ENTITYNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_ENTITYNAME_FIRST].  SetColor(  0,  0,  0); l[3][COLOR_ENTITYNAME_FIRST].effects |= COLOR_FLAG_BOLD|COLOR_FLAG_UNDERLINE;
    l[3][COLOR_MARKERNAME].        SetColor(  0,  0, 50); l[3][COLOR_MARKERNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_MARKERNAME_PARTIAL].SetColor( 25, 25, 50); l[3][COLOR_MARKERNAME_PARTIAL].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_PROCNAME].          SetColor(  0,  0,  0); l[3][COLOR_PROCNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_INCLUDEFILE].       SetColor(  0,  0,  0); l[3][COLOR_INCLUDEFILE].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_PARAMNAME].         SetColor(  0,  0, 50); l[3][COLOR_PARAMNAME].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_ATTRVALUE].         SetColor(  0,  0,  0); l[3][COLOR_ATTRVALUE].effects |= 0;
    l[3][COLOR_ATTRVALUE_EMPH].    SetColor(  0,  0,  0); l[3][COLOR_ATTRVALUE_EMPH].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_COLORDEF].          SetColor(  0,  0,  0); l[3][COLOR_COLORDEF].effects |= 0;
    l[3][COLOR_LABEL_TEXT].        SetColor(  0,  0,  0); l[3][COLOR_LABEL_TEXT].effects |= 0;
    l[3][COLOR_LABEL_ESCAPE].      SetColor(  0,  0,  0); l[3][COLOR_LABEL_ESCAPE].effects |= COLOR_FLAG_BOLD;
    l[3][COLOR_COMMENT].           SetColor(100,100,100); l[3][COLOR_COMMENT].effects |= COLOR_FLAG_ITALICS;
    l[3][COLOR_ERROR].             SetColor(255,  0,  0); l[3][COLOR_ERROR].effects |= COLOR_FLAG_UNDERLINE;
    l[3][COLOR_NO_ERROR].          SetColor(  0,  0,  0); l[3][COLOR_NO_ERROR].effects |= 0;
    l[3][COLOR_ERROR].mask = COLOR_FLAG_UNDERLINE | COLOR_FLAG_COLOR;
    l[3][COLOR_NO_ERROR].mask = COLOR_FLAG_UNDERLINE | COLOR_FLAG_COLOR;
}

/** State of coloring */
struct CurrentState {
    unsigned effects;    ///<What effects are in effect
    ColorType color;  ///<What is the color
    CurrentState() : effects(0) {}
    void Apply(const ColorSyntaxAppearance &appearance);  ///<Apply coloring for a language element type to us.
    string Print(bool fakeDash=true) const;                  ///<Print a set of string formatting escapes to represent the status. Do \\377 (octal 255) instead of slashes if fakeDash is true.
    bool operator == (const CurrentState &other) const
    {return effects == other.effects && color == other.color;}
};

void CurrentState::Apply(const ColorSyntaxAppearance &appearance)
{
    effects &= ~appearance.mask;
    effects |= appearance.effects & appearance.mask;
    if (appearance.mask & COLOR_FLAG_COLOR) {
        color.r = appearance.r;
        color.g = appearance.g;
        color.b = appearance.b;
        color.a = 255;
        color.type = ColorType::COMPLETE;
    }
}

string CurrentState::Print(bool fakeSlash) const
{
    //Insert \377 (octal 255) instead of a \ to distinguish between \s already there
    string ret = fakeSlash ? ESCAPE_STRING_TEMP_SLASH "B" ESCAPE_STRING_TEMP_SLASH "I" ESCAPE_STRING_TEMP_SLASH "U" : "\\B\\I\\U";
    if (!(effects & COLOR_FLAG_BOLD)) ret += fakeSlash ? ESCAPE_STRING_TEMP_SLASH "b" : "\\b";
    if (!(effects & COLOR_FLAG_ITALICS)) ret += fakeSlash ? ESCAPE_STRING_TEMP_SLASH "i" : "\\i";
    if (!(effects & COLOR_FLAG_UNDERLINE)) ret += fakeSlash ? ESCAPE_STRING_TEMP_SLASH "u" : "\\u";
    if (color.type!=ColorType::INVALID) ret += (fakeSlash ? ESCAPE_STRING_TEMP_SLASH "c" : "\\c") + color.Print();
    return ret;
}

/** Turn a chart text into another chart text, which when drawn produces the colored version of the original chart text.
 *
 * This is done by creating no entities, but a series of labels each representing a
 * line in the original chart.
 * @param [in] input The original chart text in UTF-8
 * @param [in] cshList The identified colored language elements. Positions refer to character indexes, not bytes.
 * @param [in] cshStyle The number of the coloring scheme to use.
 * @param [in] textformat Some formatting escapes defining the default font to use,
 */
string Cshize(std::string_view input, const CshListType &cshList, unsigned cshStyle,
              std::string_view textformat)
{
    map<int, CurrentState> textState;  ///maps byte_index->state changes

    //Set default to COLOR_NORMAL (we know this is fully specified, mask contains all COLOR_FLAG_*s)
    textState[-1].Apply(MscCshAppearanceList[cshStyle][COLOR_NORMAL]);

    for (auto i = cshList.begin(); i!=cshList.end(); i++) {
        auto j = textState.lower_bound(i->end_pos.byte_index); //j points at or after end_pos
        j--; //now j is at the appearance just before i->end_pos
        CurrentState state2revert2 = j->second;
        // if there are state changes between (i->first_pos and j], apply appearance to them
        if (i->first_pos.byte_index <= j->first) {
            j++;
            for (auto jj = textState.lower_bound(i->first_pos.byte_index); jj!=j; jj++)
                jj->second.Apply(MscCshAppearanceList[cshStyle][i->color]);
        }
        //Apply the new appearance from first pos. First copy any previous state to a new state at first pos...
        j = textState.lower_bound(i->first_pos.byte_index);
        j--;
        textState[i->first_pos.byte_index-1] = j->second;
        //...then modify that with the appearance
        textState[i->first_pos.byte_index-1].Apply(MscCshAppearanceList[cshStyle][i->color]);
        //Finally revert at the end to the original state
        textState[i->end_pos.byte_index] = state2revert2;
    }
    //Remove initial state at -1. Re-insert it if there is no state at 0.
    if (textState.find(0) == textState.end())
        textState[0] = textState[-1];
    textState.erase(-1);

    size_t byte_offset = 0, char_offset = 0;
    string ret;
    CurrentState lastState;
    string format;
    for (auto i = textState.begin(); i!=textState.end(); i++) {
        if (lastState == i->second) continue;
        size_t byte_from = byte_offset;
        //skip past to the character marked by i->first
        while ((int)char_offset < i->first && byte_offset < input.length()) {
            byte_offset += UTF8TrailingBytes(input[byte_offset]) + 1;
            char_offset++;
        }
        ret.append(input.begin()+byte_from, input.begin()+std::min(byte_offset, input.length()));
        if (byte_offset >= input.length()) break; // color codes past the end of the string
        ret.append(i->second.Print());
        lastState = i->second;
    }
    //append remaining bytes
    if (byte_offset<input.size())
        ret.append(input.begin()+byte_offset, input.end());

    //replace "\n" to "\\n"
    string::size_type pos = ret.find("\n");
    while (pos != string::npos) {
        ret.replace(pos, 1, ESCAPE_STRING_TEMP_SLASH "n");
        pos = ret.find("\n", pos+2);
    }
    //add escape for {}[];#" if they are not yet escaped
    pos = ret.find_first_of("{}[];#\\\"");
    while (pos != string::npos) {
        string::size_type pos2 = pos;
        while (pos2>0 && ret[pos2-1]=='\\') pos2--;
        //if odd number of \s
        if ((pos-pos2+1)%2)
            ret.insert(pos, "\\");
        pos = ret.find_first_of("{}[];#\\\"", pos+2);
    }
    //replace spaces with escaped ESCAPE_CHAR_SPACE
    for (unsigned i = 0; i<ret.length(); i++)
        if (ret[i] ==' ')
            ret.replace(i, 1, "\\" ESCAPE_STRING_SPACE);

    //replace octal 377 back to '\'
    for (int ipos = int(ret.length())-1; ipos>=0; ipos--)
        if (ret[ipos]==ESCAPE_STRING_TEMP_SLASH[0]) ret[ipos] = '\\';

    //make it a label msc-generator can draw
    string preamble = "hscale=auto;[_wide=yes]:\\pl";
    if (textformat.length()) preamble += textformat;
    preamble.append(ret).push_back(';');
    return preamble;
}

/** Calculates, how many substitutions, insertions, deletions or transpositions needed
 * to change one string to another. Returns -1 if a or b contains non letters, digits or -_. */
int DamerauLevenshteinDistance(std::string_view a, std::string_view b) {
    /** Index to vald function names.*/
    static constexpr signed char x[128] = {
        -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, //0-15
        -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, //16-31
        -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 0,+1,-1, //32-47, 45:-, 46:.
         2, 3, 4, 5, 6, 7, 8, 9,10,11,-1,-1,-1,-1,-1,-1, //48-57: digits, 58-63: not ok
        -1,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26, //64:@ not ok, 65-79: A-O
        27,28,29,30,31,32,33,34,35,36,37,-1,-1,-1,-1,38, //80-90:P-Z, 91-94: not ok, 95:_
        -1,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53, //96:' not ok, 97-11:a-o
        54,55,56,57,58,59,60,61,62,63,64,-1,-1,-1,-1,-1, //112-122:p-z
    };

    constexpr size_t MAX = std::ranges::max(x);

    struct Arr2D { //2D array indexed from -1
        const int bl;
        std::vector<int> d;
        Arr2D(int al_, int bl_) : bl(bl_+2), d((al_+2)*(bl_+2)) {}
        int& operator[] (std::pair<int, int> p) {
            _ASSERT((p.first + 1)*bl + p.second + 1 < (int)d.size());
            _ASSERT(0 <= (p.first + 1)*bl + p.second + 1);
            return d[(p.first + 1)*bl + p.second + 1];
        }
    } d(a.length(), b.length());
    std::array<int, MAX+1> da;
    std::ranges::fill(da, 0);
    const int maxdist = a.length() + b.length();
    d[{-1, -1}] = maxdist;
    for (int i = 0; i <= (int)a.length(); i++) {
        d[{i, -1}] = maxdist;
        d[{i, 0}] = i;
        if (i && i < (int)a.length() && ((signed char)(a[i])<0 || x[(unsigned char)a[i]] == -1)) return -1;
    }
    for (int j = 0; j <= (int)b.length(); j++) {
        d[{-1, j}] = maxdist;
        d[{0, j}] = j;
        if (j && j < (int)b.length() && ((signed char)(b[j])<0 || x[(unsigned char)b[j]] == -1)) return -1;
    }
    for (int i = 1; i <= (int)a.length(); i++) {
        int db = 0;
        for (int j = 1; j <= (int)b.length(); j++) {
            int k = da[x[(unsigned char)b[j - 1]]];
            int e = db;
            int cost;
            if (a[i-1]==b[j-1]) cost = 0, db = j;
            else                cost = 1;
            d[{i, j}] = std::min({d[{i-1, j-1}] + cost,                    //substitution
                                  d[{i, j-1}] + 1,                         //insertion
                                  d[{i-1, j}] + 1,                         //deletion
                                  d[{k-1, e-1}] + (i-k-1)+1 + (j - e-1)}); //transposition
        }
        da[x[(unsigned char)a[i-1]]] = i;
    }
    return d[{a.length(), b.length()}];
}

/** Return how well 'under_cursor' matches 'hint'.
 * Higher values are better matches.
 * Retuns something proportional to a number of characters in difference.*/
int FuzzyMatchHint(std::string_view hint, std::string_view under_cursor) {
    if (under_cursor.empty()) return 0;
    if (under_cursor.length()>hint.length()) return -INT_MAX;
    //We return the distance minus the length diff.
    //Since the distance includes needed insertions, if 'under_cursor' is
    //a substring of 'hint', we return 0.
    //We also return negative,so that larger is better => 0 will be best.
    return (hint.size()- under_cursor.size()) - DamerauLevenshteinDistance(under_cursor, hint);
}

/** Return a substring of 'under_cursor' contained in 'hint'.
 * The returned substring is part of 'hint' and may differ in letter case from the corresponding
 * substring of 'under_cursor'.*/
std::string_view SubStrMatchHint(std::string_view hint, std::string_view under_cursor) {
    if (under_cursor.empty()) return {};
    if (under_cursor.length()>hint.length()) return {};
    for (size_t l = under_cursor.length(); l; l--)
        for (size_t off = 0; off<=under_cursor.length()-l; off++)
            if (auto pos = CaseInsensitiveContains(hint, under_cursor.substr(off, l)))
                return hint.substr(*pos, l);
    return {};
}


/** A version of std::string which compares case-insensitive.*/
struct istring : public std::string
{
    bool operator < (const istring& o) const { return CaseInsensitiveSmaller(*this, o); }
};

auto sort_projection_by_type(const CshHint &h) { return std::tie(h.type, h.sort, static_cast<const istring&>(h.decorated)); }
auto sort_projection_by_applicability(const CshHint& h) { return std::tuple(!h.selectable ,h.sort, h.type, static_cast<const istring&>(h.plain)); }  //sort unselectable hints to the end

////////////////////////////////////////////////////////////////////

void CshHintStore::clear()
{
    Hints.clear();
    was_partial = false;
    allow_anything = false;
    hintSource = EHintSourceType::KEYWORD; //just to have a value
    hadEscapeHint = false;
    hadFileHint = false;
    hintStatus = HINT_NONE;
    hintAttrName.clear();
}

void CshHintStore::CopyHintsFrom(const CshHintStore& csh) {
    Hints = csh.Hints;
    was_partial = csh.was_partial;
    allow_anything = csh.allow_anything;
    hintSource = csh.hintSource;
    hadEscapeHint = csh.hadEscapeHint;
    hadFileHint = csh.hadFileHint;
    hintStatus = csh.hintStatus;
    hintAttrName = csh.hintAttrName;
    hintedStringPos = csh.hintedStringPos;
}

void Csh::clear()
{
    CshHintStore::clear();
    CshList.clear();
    ColonLabels.clear();
    Instructions.clear();
    BracePairs.clear();
    SqBracketPairs.clear();
    IfThenElses.clear();
    CshErrors.clear();
    FullDesigns.clear();
    PartialDesigns.clear();
    InLines.clear();
}



/** Post-process the list of hints.
 *
 * Fill in the 'size' and 'plain' members.
 * Remove hints ending in an asterisk (like 'line.*') - these are just included to
 * attach a description to a compacted group of hints. Compact means here that
 * entries having the same beginning up to a dot will be combined to a single hint.
 * Then fill and compact the list (attach descriptions to compacted items).
 * For example, if we have two hints "line.width" and "line.color" and the user has so
 * far typed "lin" we compact the two into "line.*". If, however, the user has already
 * typed "line.w" we do not compact (but perhaps filter line.color away if filtering is on).
 * Hints are also sorted (in a case-insensitive manner).
 * @param [in] canvas A canvas to use at size calculation.
 * @param [in] orig_format The text format to use at size calculation.
 * @param [in] orig_uc The string under the cursor. Used as prefix for filtering and grouping.
 * @param [in] filter_by_uc How shall we filter the hint list?
 * @param [in] compact_same We do compaction as described above if true.
 */
void Csh::ProcessHints(Canvas &canvas, StringFormat *orig_format, std::string_view orig_uc,
                       EHintFilter filter_by_uc, bool compact_same)
{
    const std::string_view uc = hadEscapeHint
        ? orig_uc.substr(0, orig_uc.find('(')) //remove the () part from the string under cursor.
        : orig_uc;
    std::ranges::sort(Hints, {}, [](const CshHint& a) { return sort_projection_by_type(a); });
    Hints.erase(std::unique(Hints.begin(), Hints.end()), Hints.end());
    //create a copy if we may filter away everything
    std::vector<CshHint> saved_Hints;
    if (filter_by_uc!=EHintFilter::None && uc.length())
        saved_Hints = Hints;
    //First separate hints ending in an asterisk (except for ESCAPE hints)
    std::vector<CshHint> group_hints;
    //In case of dot compression, move the elements ending in * to a separate list
    for (size_t i = 0; i<Hints.size(); /*none*/)
        if (Hints[i].decorated.size() && WantsDotCompress(Hints[i].type) &&
            Hints[i].decorated[Hints[i].decorated.size()-1]=='*') {
            group_hints.push_back(Hints[i]);
            Hints.erase(Hints.begin()+i);
        } else
            i++;

    //For escape hints we may only have groups for '\p' and '\m'. We shall display
    //all hints for any other escape char.
    if (hadEscapeHint && (uc.length()<2 || (uc[1]!='p' && uc[1]!='m')))
        filter_by_uc = EHintFilter::None;
    if (hadEscapeHint && (filter_by_uc==EHintFilter::Substr || filter_by_uc==EHintFilter::Fuzzy))
        filter_by_uc = EHintFilter::StartsWith;

    //Fill in 'plain' and filter/sort the hints is needed
    StringFormat f;
    f.Default();
    StringFormat* format = orig_format ? orig_format : &f;
    ShapeCollection dummy_shapes;
    std::vector<Label> labels;
    labels.reserve(Hints.size());
    for (size_t i = 0; i<Hints.size(); /*nope*/) {
        labels.emplace_back(Hints[i].decorated, canvas, pShapes ? *pShapes : dummy_shapes, *format);
        Hints[i].plain = labels.back(); //operator std::string converts to plain
        bool drop_this = false;
        if (uc.size()) switch (filter_by_uc) {
        case EHintFilter::Substr:
            if (const std::string_view match = SubStrMatchHint(Hints[i].plain, uc)
                    ; uc.length()<match.size()+2) {
                const size_t match_index = match.data()-Hints[i].plain.data();
                if (Hints[i].selectable)
                    Hints[i].highlight = {uint16_t(match_index), uint16_t(match_index + match.length())};
                Hints[i].sort = {Hints[i].selectable ? -(int)match.length() : 0, (int)match_index, (int)Hints[i].plain.length(), Hints[i].sort[3]};
                if (match.empty()) Hints[i].can_autoselect = false;
            } else
                drop_this = true;
            break;
        case EHintFilter::Fuzzy:
            if (auto pos = CaseInsensitiveContains(Hints[i].plain, uc)) {
                if (Hints[i].selectable)
                    Hints[i].highlight = {uint16_t(*pos), uint16_t(*pos+uc.length())};
                Hints[i].sort = { 0, (int)*pos, (int)Hints[i].plain.length(), Hints[i].sort[3]};
            } else {
                const int diff = -FuzzyMatchHint(Hints[i].plain, uc);
                Hints[i].sort = { diff, 0, (int)Hints[i].plain.length(), Hints[i].sort[3]}; //sort by less differences (of a sort), then user pref
                if ((int)uc.length()<diff+2)
                    drop_this = true;
            }
            break;
        default:
        case EHintFilter::None:
        case EHintFilter::StartsWith:
            if (CaseInsensitiveBeginsWith(Hints[i].plain, uc)) {
                Hints[i].highlight = {0, Hints[i].selectable ? uint16_t(uc.length()) : 0};
                Hints[i].sort = {Hints[i].selectable ? -(int)uc.length() : 0, (int)i, (int)Hints[i].plain.length(), Hints[i].sort[3]};
            } else if (filter_by_uc == EHintFilter::StartsWith)
                drop_this = true;
            else {
                Hints[i].can_autoselect = false;
                Hints[i].sort = {0, (int)i, (int)Hints[i].plain.length(), Hints[i].sort[3]};
            }
            break;
        }
        if (drop_this) {
            Hints.erase(Hints.begin()+i);
            labels.pop_back();
        } else
            i++;
    }
    std::ranges::sort(Hints, {}, [](const CshHint& a) { return sort_projection_by_type(a); });

    //Now compact the hints
    CshHint start("", nullptr, EHintType::ENTITY); //empty start
    unsigned start_len = 0;
    unsigned start_counter = 0;
    for (size_t i = 0; i<Hints.size(); /*none*/) {
        string::size_type dot_pos;
        /* if compacting is on we combine all hints with the same prefix into a xxx.*-like hint
          There are two kinds of compression, dot_compression and escape compression

          With dot compression we may have multiple levels of compression, e.g.,
          tag.line.width can be compressed into either tag.* or tag.line.* depending
          on what is the string under the cursor, that is, if it begins with
          'tag.' we have to use the latter, else the former.
          With dot compression we add hints for both of these cases (with individual
          descriptions), but remove them to 'group_hints' above. So when we create
          the the grouping, we actually create the group hint (only the right one,
          so only one of tag.* or tag.line.*) and add it to Hints in this for cycle.
          Then later, at the end of this function we match 'group_hints' with
          'Hints' and if we find a match, we copy the description to the entry in
          'Hints'.

          With escape hints, the situation is different. Only one level of
          compress may happen and not along dots. Specialty is that only \\p and \\m
          escapes can be grouped. In this case we simply drop the non-grouped
          hint from Hints if we need the grouped one and vice versa.
         */
        if (compact_same && WantsDotCompress(Hints[i].type)) {
            dot_pos = Hints[i].plain.find('.', Hints[i].highlight.second);
            if (start_len) {
                if (dot_pos != string::npos) {
                    if (CaseInsensitiveCommonPrefixLen(Hints[i].plain, start.plain)>=dot_pos
                            && Hints[i].callback==start.callback
                            && Hints[i].selectable == start.selectable) {
                        //OK, we need to be merged with start
                        //just erase us
                        start_counter++;
                    drop_this:
                        Hints.erase(Hints.begin()+i);
                        labels.erase(labels.begin()+i);
                        continue;
                    }
                }
                //Here we need to close combining, put start back to Hints
                if (start_counter) {
                    string::size_type pos_in_dec = start.decorated.find(std::string_view(start.plain).substr(start_len));
                    start.decorated.replace(pos_in_dec, start.plain.length()-start_len, ".*");
                    start.plain.erase(start_len+1);
                    start.keep = true;
                }
                labels.emplace(labels.begin()+i, start.decorated, canvas, pShapes ? *pShapes : dummy_shapes, *format);
                Hints.insert(Hints.begin()+i, std::move(start));
                i++;
                start_len = 0;
            }
            //OK, now start is empty, see if i can start a new compacting run
            //it must have a dot and we shall be able to tweak the decorated string, too
            if (dot_pos != string::npos && Hints[i].decorated.find(std::string_view(Hints[i].plain).substr(dot_pos)) != string::npos) {
                start_len = unsigned(dot_pos);
                start_counter = 0;
                start = std::move(Hints[i]);
                goto drop_this;
            }
        }
        //if compacting is on and we process escape hints, we combine all hints with
        //the same prefix into a '\p*'-like hint
        if (Hints[i].type == EHintType::ESCAPE) {
            if (compact_same) {
                //We only compress \p* and \m*
                if ((Hints[i].plain[1]=='p' || Hints[i].plain[1]=='m')) {
                    //if uc holds the same character as this entry, we delete the group version, else
                    //individual versions
                    if (uc.size()>=2 && (uc[1]==Hints[i].plain[1]) == (Hints[i].plain[2]=='*'))
                        goto drop_this;
                    if (Hints[i].plain[2]=='*') {
                        Hints[i].keep = true;
                        Hints[i].replaceto = Hints[i].plain.substr(0, 2);
                    }
                }
            } else if (Hints[i].plain[2]=='*') //if we are not compressing, remove compressed hints
                goto drop_this;
        }
        i++;
    }
    //If we have unfinished compact, flush it
    if (start_len) {
        if (start_counter) {
            string::size_type pos_in_dec = start.decorated.find(start.plain.substr(start_len));
            start.decorated.replace(pos_in_dec, start.plain.length()-start_len, ".*");
            start.plain.erase(start_len+1);
            start.keep = true;
        }
        labels.emplace_back(start.decorated, canvas, pShapes ? *pShapes : dummy_shapes, *format);
        Hints.push_back(std::move(start));
    }
    _ASSERT(Hints.size() == labels.size());
    //Fill in sizes and states
    for (size_t i = 0; i<Hints.size(); i++) {
        //OK, either we do no compacting or this one is not even a candidate for compacting
        const XY xy = labels[i].getTextWidthHeight();
        Hints[i].x_size = int(xy.x);
        Hints[i].y_size = int(xy.y);
        Hints[i].state = HINT_ITEM_NOT_SELECTED;

    }
    //Now check if any one of the resulting hints ended up being the same as one of the group
    //hints separated at the beginning and if so, copy the description.
    for (auto &hint : Hints)
        if (hint.decorated.size() &&
            hint.decorated[hint.decorated.size()-1] == '*')
            for (const auto &ghint : group_hints)
                if (hint.decorated == ghint.decorated) {
                    hint.description = ghint.description;
                    break;
                }
    //If we filtered away everything go back and keep everything
    if (filter_by_uc!=EHintFilter::None && uc.length() && Hints.size()==0 && saved_Hints.size()) {
        //restore hints and re-call ourselves with no filtering
        Hints.swap(saved_Hints);
        return ProcessHints(canvas, orig_format, orig_uc, EHintFilter::None, compact_same);
    }
    //Sort results so that the most applicable is first
    //(up to this point we have sorted by type for grouping to happen)
    std::ranges::sort(Hints, {}, [](const CshHint& a) { return sort_projection_by_applicability(a); });
}

/////////////////////////////////////////////////////


Csh::Csh(const Context &defaultDesign, FileListProc proc, const LanguageCollection* languages):
    cursor_pos{ .char_index = -1, .byte_index = -1},
    params(std::make_shared<CshParams>()), //Allocate own params. If added to LanguageCollection, it will be overwritten by that of the collection.
    file_list_proc(proc),
    languages(languages),
    addRefNamesAtEnd(false),
    hintsForcedOnly(false)
{
    default_styles = defaultDesign.GetACopyOfStyles();
    ForbiddenStyles = defaultDesign.GetStyleNames();
    ForbiddenStyles.erase("weak");
    ForbiddenStyles.erase("strong");
    PushContext(true, EContextParse::NORMAL);
    Contexts.back().SetToDesign(defaultDesign);
    FullDesigns.emplace("plain", Contexts.back());
}

/** Finds a text in a collection - or its prefix if no exact match.
@returns 0 if no match, 1 if text is a prefix of one of the entries,
2 if text is one of the entries.*/
unsigned Csh::FindPrefix(const std::set<std::string> &set, std::string_view text) noexcept {
    auto i = set.lower_bound(std::string(text));
    if (i==set.end()) return 0;
    if (*i == text) return 2;
    //no exact match, see if text is a prefix of the next one
//    i++;
    if (i==set.end()) return 0;
    return CaseInsensitiveBeginsWith(i->c_str(), text);
}

/** Finds a text in a collection - or its prefix if no exact match.
@returns 0 if no match, 1 if text is a prefix of one of the entries,
2 if text is one of the entries.*/
unsigned Csh::FindPrefix(const StyleNameSet &set, std::string_view text) noexcept {
    auto i = set.lower_bound(text);
    if (i==set.end()) return 0;
    if (*i == text) return 2;
    //no exact match, see if text is a prefix of the next one
//    i++;
    if (i==set.end()) return 0;
    return CaseInsensitiveBeginsWith(i->c_str(), text);
}

/** Moves the current set of hints (based on the 'decorated' part, as 'plain'
* is not readily available just after a call to 'AddToHints') and their description
* to the collection. Used when charts register options and attributes.*/
void Csh::MoveHintsToNameCollection(std::set<std::string> &c)
{
    for (auto &h: Hints) {
        string str(h.decorated);
        StringFormat::ConvertToPlainText(str);
        c.insert(str);
    }
    Hints.clear();
}

bool Csh::NoteInliningLanguage(std::string_view lang_name, const CshPos& pos) {
    last_inline_type = lang_name;
    last_inline_pos = pos;
    return languages && languages->GetLanguage(lang_name);
}


bool Csh::AddShapeName(std::string_view n, std::vector<std::string>* ports)
{
    if (GetShapeNum(n)>=0) return false;
    if (ports)
        shape_names.emplace_back(n, std::move(*ports));
    else
        shape_names.emplace_back(n, std::vector<std::string>{});
    return true;
}

int Csh::GetShapeNum(std::string_view n) const
{
    if (pShapes) {
        const int ret = pShapes->GetShapeNo(n);
        if (ret>=0) return ret;
    }
    const auto i = std::find_if(shape_names.begin(), shape_names.end(), [n](auto &s) {return s.first==n; });
    if (shape_names.end()==i) return -1;
    return int((pShapes ? pShapes->ShapeNum() : 0) + (i-shape_names.begin()));
}


void Csh::AddCSH(const CshPos&pos, EColorSyntaxType i)
{
    _ASSERT(pos.IsValid());
    if (pos.empty()) return;
    const auto r = std::ranges::partition(MultiElements, [pos](const MultiEntry& e) {return !pos.IsWithin(e); });
    if (!r.empty()) [[unlikely]] {
        std::vector v(r.begin(), r.end());
        MultiElements.erase(r.begin(), r.end());
        for (auto e : v)
            AddCSH(e, e.is_param ? COLOR_PARAMNAME : i);
        return;
    }
    if (MscCshAppearanceList[params->color_scheme][i] ==
        MscCshAppearanceList[params->color_scheme][COLOR_NORMAL])
        return;
    CshEntry e;
    e.first_pos = pos.first_pos;
    e.end_pos = pos.end_pos;
    e.color = i;
    CshList.AddToBack(e);
}

void Csh::AddCSH_ErrorAfter(const CshPos&pos, std::string &&text)
{
    CshPos pos2;
    pos2.first_pos = pos.end_pos;
    pos2.end_pos = pos.end_pos;
    //Now advance pos2.end_pos by one character
    pos2.end_pos += CharByteIndex::from(input_text.substr(pos2.end_pos.byte_index-first_pos.byte_index), 1);
    CshErrors.Add(pos2, std::move(text));
}

void Csh::AddCSH_AttrValue_CheckAndAddEscapeHint_Solo(const CshPos& pos, std::string_view value)
{
    //Add escape symbols
    //If the string was specified as a quoted string, we need to add 1
    //to the location as the 'value' does not include the quotation marks
    const bool quoted = IsQuotedString(pos);
    const EEscapeHintType hint =
        StringFormat::ExtractCSH(pos.first_pos.ShiftASCII(quoted), value, *this);
    if (hint != HINTE_NONE) {
        AddEscapesToHints(hint);
        hintSource = EHintSourceType::ESCAPE;
        hintStatus = HINT_READY;
        hintsForcedOnly = false;
    }
}


/** Extract CSH entries for attribute values.
 * At pos there is an attribute value.
 * If the attribute name indicates a label, color the escapes, too.
 * Not used for colon labels.
 * returns true, if the cursor is at a place to add escape hints.*/
void Csh::AddCSH_AttrValue_CheckAndAddEscapeHint(const CshPos& pos, std::string_view value, std::string_view name)
{
    if (name.length()==0 ||
        CaseInsensitiveEqual(name, "label") ||
        CaseInsensitiveEqual(name, "tag") ||
        CaseInsensitiveEqual(name, "text.format") ||
        CaseInsensitiveEqual(name, "numbering.format") ||
        CaseInsensitiveEqual(name, "numbering.append") ||
        CaseInsensitiveEqual(name, "numbering.pre") ||
        CaseInsensitiveEqual(name, "numbering.post")) {
        //This is a label or text.format attr/option
        //If it was a multi_string, do it piecewise
        const auto r = std::ranges::partition(MultiElements, [pos](const MultiEntry& e) {return !pos.IsWithin(e); });
        if (r.empty()) [[likely]] {
            AddCSH_AttrValue_CheckAndAddEscapeHint_Solo(pos, value);
        } else {
            std::vector v(r.begin(), r.end());
            MultiElements.erase(r.begin(), r.end());
            for (auto e : v)
                if (e.is_param) AddCSH(e, COLOR_PARAMNAME);
                else AddCSH_AttrValue_CheckAndAddEscapeHint_Solo(e, e.content);
        }
    } else {
        // No match - regular attribute value
        AddCSH(pos, COLOR_ATTRVALUE);
        //Register reference names
        if (CaseInsensitiveEqual(name, "refname"))
            RefNames.emplace(value);
    }
}

void Csh::AddCSH_AttrColorValue(const CshPos& pos)
{
    AddCSH(pos, COLOR_ATTRVALUE);
}

/** Parse a colon-label for CSH entries and checks if we shall provide escape hints.
 * This is called for a colon followed by a (quoted or unquoted) label.
 * (if 'two_colons' is true, 'value' starts with two colons, not one (graph language)
 * if unquoted is true, search for @# comments and color them so.
 * (False for quoted colon strings.)
 * returns true, if the cursor is at a position to provide escape hints.*/
void Csh::AddCSH_ColonString_CheckAndAddEscapeHint(const CshPos& pos, std::string_view value, bool unquoted, bool two_colons)
{
    EEscapeHintType ret = HINTE_NONE;
    if (two_colons) _ASSERT(value.starts_with("::"));
    else _ASSERT(value.starts_with(":") && (value.length() == 1 || value[1] != ':'));
    CshPos colon{ pos.first_pos, pos.first_pos.ShiftASCII(two_colons ? 2 : 1) }; //1 colon character=1 byte
    AddCSH(colon, COLOR_COLON);
    if (unquoted) {
        auto beginning = value.begin() + (two_colons ? 2 : 1);
        if (beginning != value.end() && *beginning) {
            auto hash = beginning;
            while (hash!=value.end() && *hash) {
                //search for #
                while (hash!=value.end() && *hash && *hash!='#') hash++;
                if (hash==value.end() || *hash==0) break;
                //if we hit a # count the \s before
                auto bkslash = std::prev(hash);
                //string starts with colon, so we are limited by that
                while (*bkslash == '\\') bkslash--;
                //if even number of backslashes (odd number off difference) then mark as comment till end of line
                if ((hash-bkslash)%2 == 1) {
                    //'hash' points to the # symbol starting the comment
                    CshPos txt;
                    txt.first_pos = pos.first_pos + CharByteIndex(int(UTF8len(value.substr(0, beginning - value.begin()))), beginning - value.begin());
                    txt.end_pos = pos.first_pos + CharByteIndex(int(UTF8len(value.substr(0, hash - value.begin()))), hash - value.begin());
                    if (!txt.empty())
                        ret |= StringFormat::ExtractCSH(txt.first_pos, std::string_view(beginning, hash), *this);
                    //move to end of comment
                    auto endcomment = hash;
                    while (endcomment !=value.end() && *endcomment!=0 && *endcomment !=0x0d && *endcomment !=0x0a)
                        endcomment++;
                    CshPos comment;
                    comment.first_pos = pos.first_pos + CharByteIndex(int(UTF8len(value.substr(0, hash - value.begin()))), hash - value.begin());
                    comment.end_pos = comment.first_pos + CharByteIndex(int(UTF8len(value.substr(hash - value.begin(), endcomment - hash))), endcomment - hash);
                    AddCSH(comment, COLOR_COMMENT);
                    //step over (potential) newlines
                    while (endcomment !=value.end() && (*endcomment==0x0d || *endcomment==0x0a)) endcomment++;
                    hash = beginning = endcomment;
                } else
                    hash++; //odd number: an escaped # symbol: step over the escaped #
            }
            //'hash' points to the char after the last one (or terminating zero)
            //Color the remainder as a label (no comments in it)
            CshPos txt;
            txt.first_pos = pos.first_pos + CharByteIndex(int(UTF8len(value.substr(0, beginning - value.begin()))), beginning - value.begin());
            txt.end_pos = txt.first_pos + CharByteIndex(int(UTF8len(value.substr(beginning - value.begin(), hash - beginning))), hash - beginning);
            ret |= StringFormat::ExtractCSH(txt.first_pos, std::string_view(beginning, hash), *this);
        }
    } else {
        //This is a quoted string
        CshPos p(pos);
        //search for heading quotation mark
        while (value.front() && value.front()!='"') {
            value.remove_prefix(1);
            p.first_pos.char_index++;  //We assume all characters before are ASCII (whitespace mostly)
            p.first_pos.byte_index++;
        }
        _ASSERT(value.length()); //we should not reach end - this function must be called like this only for qouted strings
        //check trailing quotation mark (may be missing)
        if (value.length() && value.back()=='"')
            value.remove_suffix(1);
        ret = StringFormat::ExtractCSH(p.first_pos, value, *this); //omit the colon(s) and quotation marks
    }
    if (ret != HINTE_NONE) {
        AddEscapesToHints(ret);
        hintSource = EHintSourceType::ESCAPE;
        hintStatus = HINT_READY;
        hintsForcedOnly = false;
    }
}

/** At this point it is either a keyword or an entity - decide and add appropriate coloring.
 *
 * This is called when a string is at the beginning of the line and is not part
 * of a valid option (e.g., has no '=' after it): it can either be a command or an entity definition
 * we give KEYWORD or KEYWORD_PARTIAL coloring for full or partial keyword matches;
 * ENTITYNAME if no keyword match, but an entity does and ENTITYNAME_FIRST otherwise.
 * Option names are not searched.
 * All-in-all partial matches are only given if the cursor is just after the
 * string in question. In this case we also store the partial match in
 * Csh::partial_at_cursor_pos
 * @param [in] pos The range to color
 * @param [in] name The content of the range: the supposed keyword or entity.*/
void Csh::AddCSH_KeywordOrEntity(const CshPos&pos, std::string_view name)
{
    EColorSyntaxType type = COLOR_KEYWORD;
    unsigned match_result = FindPrefix(keyword_names, name);
    unsigned match_result_options = FindPrefix(option_names, name);
    //If options fit better, we switch to them
    if (match_result_options > match_result) {
        type = COLOR_OPTIONNAME;
        match_result = match_result_options;
    }
    //Full match
    if (match_result == 2) {
        AddCSH(pos, type);
        return;
    }
    //Partial match but currently typing...
    if (pos.end_pos.char_index == cursor_pos.char_index && match_result == 1) {
        AddCSH(pos, EColorSyntaxType(type+1));
        was_partial = true;
        return;
    }
    //if no keyword or option match, we assume an entity name
    AddCSH_EntityName(pos, name);
    return;
}

void Csh::AddCSH_AttrName(const CshPos&pos, std::string_view name, EColorSyntaxType color)
{
    std::set<std::string> *array;
    std::set<std::string> empty_names;
    if (color == COLOR_OPTIONNAME) array = &option_names;
    else if (color == COLOR_ATTRNAME) array = &attribute_names;
    else array = &empty_names;
    unsigned match_result = FindPrefix(*array, name);

    //Honor partial matches only if cursor is right after
    if (pos.end_pos.char_index != cursor_pos.char_index && match_result == 1)
        match_result = 0;
    switch (match_result) {
    case 2: AddCSH(pos, color); return;
    case 0: AddCSH_Error(pos, color == COLOR_OPTIONNAME ? "Unknown chart option." : "Unknown attribute."); return;
    case 1:
        AddCSH(pos, EColorSyntaxType(color+1));
        was_partial = true;
    }
}

//This is called when a string is at the beginning of where an attribute
//is expected and there is no '=' following.
//It can either be a to-be typed attribute or a style name
// we give ATTRNAME or ATTRNAME_PARTIAL for full or partial attr name matches
// and STYLE for no matched
//All-in-all partial matches are only given if the cursor is just after the
//string in question. In this case we also store the partial match in
// Csh::partial_at_cursor_pos
void Csh::AddCSH_StyleOrAttrName(const CshPos&pos, std::string_view name)
{
    //Since Styles are among attribute names, we start checking these
    const unsigned match_result_style = FindPrefix(Contexts.back().StyleNames, name);
    if (match_result_style==2) {
        AddCSH(pos, COLOR_STYLENAME);
        return;
    }
    const unsigned match_result_attr = FindPrefix(attribute_names, name);
    if (match_result_attr == 2) {
        AddCSH(pos, COLOR_ATTRNAME);
        return;
    }
    if (pos.end_pos.char_index == cursor_pos.char_index && match_result_attr == 1) {
        AddCSH(pos, COLOR_ATTRNAME_PARTIAL);
        was_partial = true;
        return;
    }
    //assume all styles valid when parsing a proc def
    if (Contexts.back().SkipContent()) {
        AddCSH(pos, COLOR_STYLENAME);
        return;
    }
    if (match_result_style==1 && pos.end_pos==cursor_pos) {
        AddCSH(pos, COLOR_ATTRVALUE);
        was_partial = true;
        return;
    }
    //If we do not type or is it a prefix it add error
    AddCSH_Error(pos, "Unknown attribute or style name.");
}

void Csh::AddCSH_EntityName(const CshPos&pos, std::string_view name)
{
    if (Contexts.back().SkipContent()) {
        //When we are parsing procedures, do not collect entity names
        AddCSH(pos, COLOR_ENTITYNAME);
        return;
    }
    const unsigned u = FindPrefix(EntityNames, name);
    if (u==2) {
        AddCSH(pos, COLOR_ENTITYNAME);
        return;
    }
    //We know that there has been no such entity yet
    //If we are currently typing it and it is a prefix
    //of an entity, use entity color, else
    //the one designated for first use of entities
    //In the latter case insert to entity name database
    if (u==1 && pos.end_pos==cursor_pos) {
        AddCSH(pos, COLOR_ENTITYNAME);
        was_partial = true;
        return;
    }
    if (CursorIn(pos)>CURSOR_AFTER && EntityNames.find(name)==EntityNames.end())
        //We are inside this particular name block.
        //exclude from providing hints if not yet defined
        exclude_entity_hint = name;
    EntityNames.emplace(name);
    AddCSH(pos, COLOR_ENTITYNAME_FIRST);
}

void Csh::AddCSH_AllCommentBeyond(const CshPos& pos) { 
    const std::string_view remaining_text = std::string_view(input_text).substr(pos.end_pos.byte_index - first_pos.byte_index); //includes the last char of 'pos'
    const CshPos comment{ pos.end_pos, pos.end_pos + CharByteIndex::from(remaining_text) };
    AddCSH(comment, COLOR_COMMENT); 
} 


void Csh::AddCSH_ParamOrCond(const CshPos& pos, const multi_segment_string& str) {
    if (str.had_error) return;
    //If this is a quoted string or a multi-string, color as a label, else as an attribute value
    if (str.multi || IsQuotedString(pos))
        AddCSH_AttrValue_CheckAndAddEscapeHint(pos, str.view(), {});
    else
        AddCSH(pos, COLOR_ATTRVALUE);
}

void Csh::ReplaceCSH(const CshPos & pos, EColorSyntaxType from, EColorSyntaxType to)
{
    for (auto &c : CshList)
        if (c.IsWithin(pos) && c.color==from)
            c.color = to;
}

/** Remove all CSH entries that overlap with 'pos'*/
void Csh::RemoveAllTouchingCSH(const CshPos & pos)
{
    CshList.erase(
        std::remove_if(CshList.begin(), CshList.end(),
                       [pos](const CshEntry &e) {return e.Overlaps(pos); }),
        CshList.end());
}

void Csh::AddInLine(std::string_view text, const CshPos &pos, bool pedantic) {
    if (last_inline_type.empty())
        AddCSH_Error(CshPos(pos.first_pos, pos.first_pos + CharByteIndex::from(text,1)), "Unspecified language for inlined text.");
    else if (!languages)
        AddCSH_Error(last_inline_pos, "Cannot inline here, no languages available.");
    else if (auto* l = languages ? languages->GetLanguage(last_inline_type) : nullptr) {
        //Trim the whitespace from the last line of the chunk, if the last line is wsp only.
        //This will help when indenting the last line. (If the last line is only whitespace followed
        //by the INLINE_CLOSE_STR, we need to indent that as part of the parent chart.)
        CshPos inline_pos(pos);
        for (std::string_view txt = text; txt.size(); txt.remove_suffix(1)) {
            switch (txt.back()) {
            case ' ':
            case '\t':
                continue;
            case '\n': //last line is whitespace only: the INLINE_CLOSE_STR is on a separate line. Trim inlined text.
                inline_pos.end_pos.char_index -= text.size() - txt.size(); //1 whitespace byte = 1 character
                inline_pos.end_pos.byte_index -= text.size() - txt.size(); 
                text = txt;
                FALLTHROUGH;
            default: //last line has non wsp character and INLINE_CLOSE_STR follows without a newline
                //Add the (potentially trimmed) text, whose new position is inline_pos.
                //'pos' holds the text strictly between INLINE_OPEN_STR and INLINE_CLOSE_STR
                auto pCsh = l->pCsh->Clone();
                pCsh->FileName = FileName;
                pCsh->ParseText(std::string(text), inline_pos.first_pos, cursor_pos, pedantic);
                //Copy entries
                CshList.reserve(CshList.size() + pCsh->CshList.size());
                for (CshEntry p : pCsh->CshList)
                    CshList.AddToBack(p); //this asserts overlaps
                CshErrors.error_ranges.reserve(CshErrors.error_ranges.size() + pCsh->CshErrors.error_ranges.size());
                for (CshError e : pCsh->CshErrors.error_ranges)
                    CshErrors.error_ranges.push_back(std::move(e));
                if (inline_pos.IsWithin(cursor_pos))
                    CopyHintsFrom(*pCsh);
                InLines.emplace_back(inline_pos, std::move(pCsh), pos);
            }
            break;
        } //if for cycle ran without breaking: Entire chart is whitespace. Do not add anything.
    } else
        AddCSH_Error(last_inline_pos, StrCat("Unrecognized language '", last_inline_type, "'."));
    last_inline_type = {};
}


/**After parsing return the range if character 'char_index' is in
* one of the 'range' elements. If include after is set,
* we return true even if the caret is directly after the range.
* (Used for smart ident.)*/
const CshPos *Csh::IsInRange(const CshPosList &range, int char_index, bool include_after) const
{
    for (auto &p : range)
        if (p.first_pos.char_index<=char_index && char_index<p.end_pos.char_index+include_after)
            return &p;
    return nullptr;
}

/**After parsing return the range if character 'char_index' is inside
* one of the 'range' elements, meaning not at its start or end.
* (Used for smart ident.)*/
const CshPos *Csh::IsInsideRange(const CshPosList &range, int char_index) const
{
    for (auto &p : range)
        if (p.first_pos.char_index<char_index && char_index+1<p.end_pos.char_index)
            return &p;
    return nullptr;
}

const CshPos *Csh::IsAtEndOfRange(const CshPosList &range, int char_index) const
{
    for (auto &p : range)
        if (p.end_pos.char_index == char_index+1)
            return &p;
    return nullptr;
}




/** Line indentation rules are as follows.
 We call an arc definition, option, command etc. (all that ends in a semicolon) an _instruction_.
 Examples are given, '->' marks lines before, '=>' marks the line we aim to indent, '.'s mark the
 inserted spaces.
 We have four configurable values:
 - II: instruction indent(=4): how much an instruction is indented inside a {} block
 - IB: block indent(=0): how much a block of a block series is indented compared to the first one.
 - IM: middle indent(=4): how much non-attribute elements inside an instruction are indented compared to the first
       line of the instruction
 - IA: attribute indent (=1): how much attributes are indented compared to the [
 So the rules are as follows:
 - A new instruction shall be indented as the beginning of the instruction before.
          -> a->b;
          => c->d:
 - If there is a { before, it has to be indented
    - II+the indentation of the { if the { stands alone in its line
          -> {
          => ....a->b;
    - II+the indentation of the instruction of the { (and not the previous line)
          -> a--b {                           -> a--b [attributes,
          => ....c->d;               OR       ->       attributes] {
                                              => ....c->d
 - A { at the front of a line starting a content block for boxes or pipes shall be indented at the beginning of
   the box or pipe instruction.
          -> a--b [attributes]
          => {
 - A { at the beginning of the line in a box/pipe series representing a continued block shall be indented
   at the indentation of the instruction + IB.
          -> a--b [attributes] {
          ->     xxxx; }
          => ..{
 - A { in the middle of a parallel block series shall be indented as IB+the first line of this parallel block series
          -> {                                               -> {
          ->     xx;        OR      -> { a->b; }       OR    ->     xx;}    OR -> [attributes]
          -> }                      => ..{ c->d; }           => ..{            => ..{
          => ..{
 - A } at the beginning of the line shall be indented as the first line of the instruction it is part of
  - Additional box/pipe series elements shall be indented as IB+the first one
          -> a--b [attributes]           -> a--b [attributes] {
          => ..c--d [attributes];    OR  ->     xx;
                                         -> }
                                         => ..c--d [attributes];
 - A [ at the beginning of the line shall be indented IM + the instruction it is part of
          -> a->b                     -> a->b: label
          => ....[attributes];     OR ->       text here
                                      => ....[attributes];
 - A ] at the beginning of the line shall be indented as its opening pair ([)
          -> a->b                     -> a->b                      -> a->b [attributes,        -> a->b [
          ->     [attributes,     OR  ->     [                 OR  ->       attributes     OR  ->       attributes
          ->      attributes          ->         attributes        =>      ]                   =>      ]
          =>     ]                    =>     ]
 - Attributes at the front of the line shall be indented
    - IA+the indent of the opening [ if that is at the end of the line
          -> a->b                      -> a->b [
          ->     [             OR      =>      .attributes
          =>     .attributes
    - At the first attribute after the opening [ if that is not at the end of its line
          -> a->b                    -> a->b [ attributes,
          ->     [   attributes,    OR  =>   ..attributes
          =>     ....attributes
 - Text after a line break inside a colon label shall be indented
    - As the first character of the text of the colon label if the text to indent is not the first char
          -> a->b: label text
          => ......continues here;
    - IM+the indentation of the line of the colon if the text to indent is the first char of the text of the label
          -> a->b:                                -> a->b [attributes,
          => ....label starts here;          OR   ->       attributes]:
                                                  =>       ....labels starts here;
*/

/* Some additional rules:
 * Instructions must be registered for all language constructs in the file we want indented.
 * If braced_instruction-lists are standing by themselves - they must be registered as an
 * instruction, too.*/

/* Find the first character of the line 'pos' is in. If 'pos' is at a
 * newline, we return the first char of the line the newline terminates.
 * Assumes 'pos' is before (or at) the terminating zero, but after 'first_pos'.
 * Assumes 'pos' is before (or just after) the length of 'input_text'
 * If the file ends with a newline we may return the position of the terminating zero.
 * @param [in] pos A position somewhere in the line we talk about - indicates
 *                 the character the cursor is before.
 * @param [in] include_after If true, we indent the character just after a label as if part of the label.
 * @returns the zero-based BYTE and 1-based CHARACTER index of the first char of the line.*/
CharByteIndex Csh::FindLineBeginFrom(CharByteIndex pos) const
{
    _ASSERT(pos.IsValid());
    _ASSERT(first_pos <= pos);
    _ASSERT(pos.byte_index - first_pos.byte_index <= (int)input_text.size());
    //search backwards for a newline using 'byte_index'
    CharByteIndex ret = pos;
    if (first_pos.byte_index <= ret.byte_index) //if we stand on a \n, we seek the line begin of this \n
        ret.byte_index--;
    while (first_pos.byte_index <= ret.byte_index && input_text[ret.byte_index - first_pos.byte_index] != '\n')
        ret.byte_index--;
    //now we stand on a byte *before* the first char of the line (either before 'first_pos' or on a \n)
    ret.byte_index++;
    //Now update char_index
    ret.char_index -= UTF8len(input_text.substr(ret.byte_index - first_pos.byte_index, pos.byte_index - ret.byte_index));
    return ret;
}

/* Find the character indent of the pos - how far is it from the beginning of its line in CHARACTERs.
 * Assumes 'pos' is before (or at) the terminating zero, but after 'first_pos'.
 * Assumes 'pos' is before (or just after) the length of 'input_text'
 * @param [in] char_index A char position somewhere in the line we talk about - zero based, indicates
 *                        the character the cursor is before.
 * @returns the (zero-based) CHARACTER indent of this character in its line*/
int Csh::ColOf(CharByteIndex pos) const
{
    return pos.char_index-FindLineBeginFrom(pos).char_index;
}

/** Return the indent (offset from the head of the line) of the first non-whitespace within the line.
 * @param [in] line_begin The index of the first char of the line
 * @returns -1 if all the line is whitespace
 *          else the offset of the first non-whitespace character (equals to the byte offset of it)*/
int Csh::FirstNonWhitespaceIndent(CharByteIndex line_begin) const
{
    int i = line_begin.byte_index - first_pos.byte_index;
    while (input_text[i]==' ' || input_text[i]=='\t') i++;
    return input_text[i]=='\n' || input_text[i]=='\r' || input_text[i]==0 ? -1 : i - (line_begin.byte_index - first_pos.byte_index);
}

/** Finds the CHARACTER indentation of the first non-whitespace after 'pos'.
 * The indent is counted from the beginning of the line of 'pos'.
 * We assume 'pos' is after a ':', '{'  or '[' char, that is just after the
 * beginning of a colon label, a braced or bracketed construct.
 * @param pos The character index
 * @return If no such character in that line or # comes, we return -1, else the
 *         CHARACTER indent of that char. */
int Csh::FindIndentOfNonWhitespaceAfter(CharByteIndex pos) const
{
    //only these characters can be here, when the function is called
    _ASSERT(input_text[pos.byte_index - first_pos.byte_index]==':' 
         || input_text[pos.byte_index - first_pos.byte_index]=='{' 
         || input_text[pos.byte_index - first_pos.byte_index]=='[');
    pos = pos.ShiftASCII(1); //step over the colon, brace or bracket
    //test if we have any other char left than whitespace (except comments)
    std::string_view after = std::string_view(input_text).substr(pos.byte_index - first_pos.byte_index);
    while (after.size() && after.front() && after.front() != '#' && after.front() != '\n' && after.front() != '\r') {
        if (after.front() !=' ' && after.front() !='\t')
            return pos.char_index - FindLineBeginFrom(pos).char_index; //yes: we indent to this non-whitespace char
        const auto l = 1 + UTF8TrailingBytes(after.front());
        pos.char_index++;
        pos.byte_index += l;
        if ((int)after.size() <= l) return -1; //all spaces
        after.remove_prefix(l);
    }
    //no: only whitespace or comment or newline
    return -1;
}


/** Find what is the current indentation for a line (how many whitespace characters at its start).
 * @param [in] pos A character position somewhere in the line we talk about - indicates
 *                 the character the cursor is before.
 * @returns the number of spaces proper at the head of this line */
int Csh::FirstCurrentLineIndent(CharByteIndex pos) const
{
    return FirstNonWhitespaceIndent(FindLineBeginFrom(pos));
}

/** Find what is the proper indentation for a line
 * Considers Inlined charts, Instructions, BracePairs, SqBracketPairs, ColonLabels, IfThenElses, which must be
 * up-to-date.
 * @param [in] pos A character position somewhere in the line we talk about - zero based, indicates
 *                 the character the cursor is before.
 * @param [in] include_after If true, we indent the character just after a label as if part of the label.
 * @param [out] current_indent If non-null, we return the current number of leading whitespace in the line of 'char_index'.
 *                             Tab characters are counted as 1.
 * @param [out] line_begin If non-null, we return the character and byte positions of the beginning of the
 *                         line containing 'char_index'
 * @param [in] first_line_indent The indent of the first line. This is 0 if 'input_text' is a full file. But if it
 *                               is really an inlined chart (part of another chart's text) then the first line shall
 *                               indent below the inlining instruction in the parent chart.
 * @returns the number of spaces proper at the head of this line */
int Csh::FindProperLineIndent(CharByteIndex pos, bool include_after, int* current_indent, CharByteIndex* line_begin, int first_line_indent) const
{
    //We count indentation values from 0: zero indentation is right at the beginning of the line

    //Local variables named like 'pos' refer to byte_index

    //Find out, what situation are we in at the front of the line.
    //First, find our instruction
    const CharByteIndex beginning_of_line = FindLineBeginFrom(pos);
    if (line_begin)
        *line_begin = beginning_of_line;
    //line_indent is valid in both characters and bytes (as spaces and tabs are all 1 byte chars)
    const int line_indent = FirstNonWhitespaceIndent(beginning_of_line);
    if (current_indent)
        *current_indent = line_indent;

    const int first_nonws_char_of_line = beginning_of_line.char_index + std::max(0, line_indent);
    //This way we will test if the cursor is directly before or within an instruction.
    //'After' does not count, except if we have pressed enter.
    const CshPos* current_instr = IsInRange(Instructions, first_nonws_char_of_line, include_after);

    //Check if the first character of the line is inside an inlined chart
    //Or at its end: in this case the line starts with the INLINE_CLOSE_STR token, 
    //which shall be indented as the whole inline instruction
    const auto inline_chart = std::ranges::find_if(InLines, [beginning_of_line](const CshInLine& i) { return i.full_pos.IsWithin(beginning_of_line) || i.full_pos.end_pos==beginning_of_line; });
    if (inline_chart != InLines.end()) {
        //Find the indent of the instruction (or line in its absence) including the inline
        const CharByteIndex beginning_of_line_inline = FindLineBeginFrom(current_instr ? current_instr->first_pos : inline_chart->pos.first_pos);
        const int line_indent_inline_instruction = FirstNonWhitespaceIndent(beginning_of_line_inline);
        if (inline_chart->pos.IsWithin(beginning_of_line))
            //The beginning of the line the cursor is in belongs to an inline chart. Do lookup there.
            //Use the indentation of the instruction that includes the inlined chart, plus one level of
            //"intra-instruction indent" as the indent for the first line.
            return inline_chart->csh->FindProperLineIndent(pos, include_after, nullptr, nullptr, line_indent_inline_instruction + params->m_II);
        //We are indenting the last line of the inlined chart: the INLINE_CLOSE_STR token is the first non-whitespace char.
        //Indent it to the instruction as if it were a closing brace.
        return line_indent_inline_instruction;
    }

    //if we are in no instruction at all (not even in a braced arclist), we shall indent to zero
    //(or whatever our first line shall indent to) (or maybe the indent above us, but that is more
    //complicated) - this must be an empty line
    if (current_instr==nullptr) return first_line_indent;
    //Set up some variables
    //This below contains the indent of the first line of the current instruction.
    const int current_instr_indent = FirstCurrentLineIndent(current_instr->first_pos);
    //Now test if the first character of the line is part of a colon label
    //We need to catch this if the cursor is inside, but not directly after or before
    //the colon label. So we subscript one from pos to avoid matching if 'pos'
    //is before and handle the after case later.
    const CshPos* colon_label = IsInRange(ColonLabels, first_nonws_char_of_line, false);
    //fires if cursor is inside or directly after (hence -OFF).
    //Then exclude directly after case, we don't align a trailing [{; as if it were part of the label.
    //but do not exclude it after pressing an Enter - then we continue typing the label
    if (colon_label &&
        (include_after || first_nonws_char_of_line!=colon_label->end_pos.char_index))
    {
        const int i = FindIndentOfNonWhitespaceAfter(colon_label->first_pos);
        //if the colon is the last non-whitespace on its line, align us to current + IM
        //also, do this if we have not enabled special indent for labels
        if (i<0 || !params->m_bSIText)
            return current_instr_indent + params->m_IM;
        return i;
    }
    //Then, check for []{} at the beginning of a line without staring a new instruction
    if (line_indent>=0 && current_instr->first_pos.char_index != beginning_of_line.char_index+line_indent) {
        //we tested above that the line is not empty and that no instruction starts here
        //if not so, apply special rules to []{}
        if (input_text[beginning_of_line.byte_index+line_indent - first_pos.byte_index] == '{')
            return current_instr_indent + params->m_IB; //So, we return IB+indent of the instruction
        if (input_text[beginning_of_line.byte_index+line_indent - first_pos.byte_index] == '[')
            return current_instr_indent + params->m_IM; //So, we return IB+indent of the instruction
        //We are here sure that the closing braces, which could normally be part of a colon label,
        //are in fact, not part of one.
        if (input_text[beginning_of_line.byte_index+line_indent - first_pos.byte_index] == '}') {
            const CshPos *br = IsAtEndOfRange(BracePairs, first_nonws_char_of_line);
            if (br) {
                //find out if our brace pair is the first in the instruction or not.
                //We search all brace pairs and select one that is totally before 'br', but fully
                //within 'current_instruction'
                for (const auto &br2 : BracePairs)
                    if (br2.end_pos<=br->first_pos && br2.first_pos>=current_instr->first_pos)
                        return current_instr_indent + params->m_IB; //found one: we are subsequent brace-pairs
                return current_instr_indent; //not found one - we are first brace-pairs
            }
            //if we do not find this range - there is no associated opening brace,
            //so treat as any other char (fall through out of this if construct)
        } else if (input_text[beginning_of_line.byte_index+line_indent - first_pos.byte_index] == ']')
            if (const CshPos* br = IsAtEndOfRange(SqBracketPairs, first_nonws_char_of_line))
                return ColOf(br->first_pos); //return the indent of the opening bracket
            //if we do not find this range - there is no associated opening square bracket,
            //so treat as any other char (fall through out of this if construct)
    }
    //OK, here either the line is empty or it does not begin with one of '[]{}' (except if these start a new instruction)
    //and in any case not part of a colon label.
    //Next, check if we are part of an attribute list
    const CshPos *attr_sq = IsInsideRange(SqBracketPairs, first_nonws_char_of_line);
    if (attr_sq) {
        const int i = FindIndentOfNonWhitespaceAfter(attr_sq->first_pos);
        //if the [ is the last non-whitespace on its line, align us to current + IA
        //also, do this if we have not enabled special indent for attributes
        if (i<0 || !params->m_bSIAttr)
            return ColOf(attr_sq->first_pos) + params->m_IA;
        return i;
    }
    //Check if we are a solo instruction of a 'then' or else 'branch'
    //or an else or in a braced list, etc.
    const CshPos *ifthen_instr = IsInRange(IfThenElses, first_nonws_char_of_line,  false);
    const CshPos *brace_around = IsInsideRange(BracePairs, first_nonws_char_of_line);

    if (ifthen_instr && ifthen_instr->first_pos==current_instr->first_pos &&
        CaseInsensitiveBeginsWith(std::string_view(input_text).substr(beginning_of_line.byte_index+line_indent - first_pos.byte_index), "else")) {
        //OK, we are directly in the ifthenelse itself (and not in the instructions in either of the branches)
        //and the line starts as "else". This must be the else keyword => align to the first char in the line of the if
        return FirstNonWhitespaceIndent(FindLineBeginFrom(ifthen_instr->first_pos));
    }
    //If the current instruction is not the ifthenelse, but ifthenelse is around it,
    //and the surrounding brace pair is enveloping the ifthenelse itself...
    if (ifthen_instr && ifthen_instr->first_pos!=current_instr->first_pos &&
        (brace_around==nullptr || brace_around->first_pos<ifthen_instr->first_pos)) {
        //...We are an unbraced then or else branch.
        //If we are in the first line of it..
        //==>we need to align to the line of the if
        if (current_instr->first_pos.char_index == first_nonws_char_of_line)
            return ColOf(ifthen_instr->first_pos) + params->m_II;
        else //=>we need to align to the if, plus IM
            return ColOf(ifthen_instr->first_pos) + params->m_II+ params->m_IM;
    }
    //here either we are not in an ifthenelse; or
    //we are in the braced branch of one (such as if $t then {XXhereXX} else {XXorhereXX};)
    if (brace_around) {
        //find the opening bracket's instruction
        //if we are directly in the brace of the ifthenelse branch, we should take the ithenelse
        //but if we are in a braced arclist within the braced arclist of the ifthen branch, we
        //should not. Hence we search for the char before the brace (-1)
        const CshPos *before_brace = IsInRange(Instructions, brace_around->first_pos.char_index-1, false);
        const CshPos *align_to = ifthen_instr && before_brace &&
                                 before_brace->first_pos == ifthen_instr->first_pos ?
                                     ifthen_instr :
                                     IsInRange(Instructions, brace_around->first_pos.char_index, false);
        //if no instruction, we return the opening brace (can happen with msc {...} construct, because that is
        //not added as an instruction.
        if (align_to==nullptr)
            return first_line_indent;
        //find out if our brace pair is the first in the instruction or not.
        //We search all brace pairs and select one that is totally before 'brace_around', but fully
        //within 'align_to'
        int ib_off = 0;
        for (const auto &br2 : BracePairs)
            if (br2.end_pos<=brace_around->first_pos && br2.first_pos>=align_to->first_pos)
                ib_off = params->m_IB; //found one: we are in a subsequent brace-pairs
        //not found one - we are in the first brace-pairs
        //If the bracket's instruction is the same as ours, we are in fact in between
        //instructions in a bracket list. The offset we return will anchor to the
        //instruction of the bracket.
        if (align_to==current_instr)
            return current_instr_indent + params->m_II + ib_off;
        //If the bracket's instruction is not the same as ours, we are truly inside an instruction,
        //which either has no content (other instructions inside {}) - or we are not in the content part
        //Next see if we are in a subsequent line of our instruction
        if (current_instr->first_pos.char_index != first_nonws_char_of_line)
            return current_instr_indent + params->m_IM;
        //OK, we are the first line of our instruction - align to the instruction of the brace
        return FirstCurrentLineIndent(align_to->first_pos) + params->m_II + ib_off;
    }
    //Finally, the case, when we are not part of a brace
    //Next see if we are in a subsequent line of our instruction
    if (current_instr->first_pos.char_index != first_nonws_char_of_line)
        return current_instr_indent + params->m_IM;
    //if not, then we are the first line of a top-level instruction: return zero (or whatever our first line shall indent to)
    return first_line_indent;
}

void CshContext::SetToDesign(const Context& design)
{
    is_full = true;
    for (auto i = design.colors.begin(); i!=design.colors.end(); i++)
        Colors[i->first] = i->second;
    StyleNames.merge(design.GetStyleNames());
}

/** Set the chart design.
*
* Used when an msc= or msc+= option is found during csh parse
* @param [in] design The name of the design.
* @param [in] full True if the option was msc=, false if it was msc+=
* @returns An error message if the design is not found or not of appropriate type. Empty if OK. */
std::string Csh::SetDesignTo(std::string_view design, bool full)
{
    bool found_full = true;
    auto i = FullDesigns.find(design);
    if (i==FullDesigns.end()) {
        i = PartialDesigns.find(design);
        if (i==PartialDesigns.end()) {
            return StrCat("Design '", design, "' not defined earlier.");
        }
        found_full = false;
    }
    Contexts.back() += i->second;
    if (found_full == full) return "";
    if (found_full) return StrCat("Design '", design, "' is a full design. Use 'msc = ' instead of 'msc += '.");
    return StrCat("Design '", design, "' is a partial design. Use 'msc += ' instead of 'msc = '.");
}

ECursorRelPosType Csh::CursorIn(int first_char, int end_char) const noexcept
{
    if (cursor_pos.char_index <  first_char) return CURSOR_BEFORE;
    if (cursor_pos.char_index == first_char) return CURSOR_AT_BEGINNING;
    if (cursor_pos.char_index < end_char) return CURSOR_IN;
    if (cursor_pos.char_index == end_char) return CURSOR_AT_END;
    return CURSOR_AFTER;
}
/** Return true if the cursor is at the beginning of a line or inside or just after the first keyword on a line.*/
bool Csh::CalculateIsCursorAtLineBegin() const
{
    const int cursor_in_input_text = cursor_pos.byte_index - first_pos.byte_index; 
    if (cursor_in_input_text < 0)
        return false; 
    if (cursor_in_input_text == 0)
        return true; //at the very beginning (just before the first char)- obviously we are at the line begin
    if (unsigned(cursor_in_input_text)>input_text.length())
        return false; //beyond the text - something went wrong or we are an inlined chart with the cursor after.
    //Search for a position backwards, where we are *after* a '\n' or at cursor_in_input_text==0.
    int line_begin = cursor_in_input_text;
    while (line_begin>0 && input_text[line_begin-1]!='\n')
        line_begin--;
    //now line_begin is the first char in the line
    //Walk up to just before cursor_in_input_text and see that we are before, inside or just after the first keyword
    //the keyword must start with an alpha char and may contain numbers and a single dot.
    bool in_keyword = false;
    bool had_dot = false;
    for (int i = line_begin; i < cursor_in_input_text; i++) {
        if (input_text[i]==' ' || input_text[i]=='\t' || input_text[i]=='\r') {
            if (in_keyword) return false;
            else continue;
        }
        if ('0'<=input_text[i] && input_text[i]<='9') {
            if (in_keyword) continue;
            else return false;
        }
        if ('.'==input_text[i]) {
            if (had_dot || !in_keyword) return false;
            had_dot = true;
            continue;
        }
        if (('a'<=input_text[i] && input_text[i]<='z') ||
            ('A'<=input_text[i] && input_text[i]<='Z') ||
            (128<=(unsigned char)(input_text[i]))) {
            in_keyword = true;
            continue;
        }
        //any other char means we are not in the first keyword
        return false;
    }
    return true;
}

/** This is called when a hint is found. Descendants can override.
* Always returns true - so that it can be called in a return statement.*/
bool Csh::DoHintLocated(EHintSourceType hsource, std::string_view a_name)
{
    //We don't act if we have already located the hint.
    //This is because the deeper rule probably had a better hint.
    if (hintStatus==HINT_NONE) {
        hintStatus = HINT_LOCATED;
        hintsForcedOnly = false;
        hintSource = hsource;
        hintAttrName = a_name;
    }
    return true;
}

std::string Csh::HintPrefix(EColorSyntaxType t) const
{
    CurrentState state;
    state.Apply(MscCshAppearanceList[params->color_scheme][t]);
    return state.Print(false);
}

/** Callback for drawing a symbol before attribute names in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForAttributeNames(Canvas *canvas, CshHintGraphicParam /*p*/, CshHintStore &)
{
    if (!canvas) return false;
    const double w = 0.4*HINT_GRAPHIC_SIZE_X;
    const double h = 0.08*HINT_GRAPHIC_SIZE_Y;
    const double off = 0.35*HINT_GRAPHIC_SIZE_Y;
    ColorType color(0, 0, 0);
    LineAttr line;
    line.radius = 3;
    const FillAttr fill = FillAttr::Solid(color);
    canvas->Fill(XY((HINT_GRAPHIC_SIZE_X-w)/2, off), XY((HINT_GRAPHIC_SIZE_X+w)/2, off+h), line, fill);
    canvas->Fill(XY((HINT_GRAPHIC_SIZE_X-w)/2, HINT_GRAPHIC_SIZE_Y-off-h), XY((HINT_GRAPHIC_SIZE_X+w)/2, HINT_GRAPHIC_SIZE_Y-off), line, fill);
    return true;
}

/** Callback for drawing a symbol before color names in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForColors(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    const int size = HINT_GRAPHIC_SIZE_Y-3;
    const int off_x = (HINT_GRAPHIC_SIZE_X - size)/2;
    const int off_y = 1;
    ColorType color((unsigned int)p);
    Block b(XY(off_x, off_y), XY(off_x+size, off_y+size));
    b.Round();
    if (color.a<255) {
        FillAttr fill = FillAttr::Solid(ColorType::white());
        canvas->Fill(b.Centroid(), b.UpperLeft(), fill);
        canvas->Fill(b.Centroid(), b.LowerRight(), fill);
        fill.color = ColorType(196, 196, 196);
        canvas->Fill(b.Centroid(), b.UpperRight(), fill);
        canvas->Fill(b.Centroid(), b.LowerLeft(), fill);
    }
    canvas->Fill(b, FillAttr::Solid(color));
    b.Expand(0.5);
    canvas->Line(b, LineAttr(ELineType::SOLID, ColorType(0, 0, 0), 1, ECornerType::NONE, 0));
    return true;
}


/** This function is called before every color syntax parse.
 * Since Csh and descendants exist in only one copy, here we should
 * completely initialize every member that changes from parse to parse.
 * @param [in] input The text to parse. Will be moved to 'input_text'.
 * @param [in] first_pos The position of the first character of 'input' in its file.
 *        If 'input' is a whole file, this shall be {0,0}, if it is a fragment, 
 *        byte and char indices are from the start of the file. This cannot be invalid.
 * @param [in] cursor_p The position of the character the cursor is before in the file of 'input',
 *        so both byte and character indices are from the beginning of the file. This can be 
 *        invalid if there is no cursor, in which case we assume a cursor just before the beginning
 *        of the file. This is because Csh in general is not prepared to handle no cursor,
 *        so we simply place it outside the file.*/
void Csh::BeforeYaccParse(std::string&& input, CharByteIndex first_p, CharByteIndex cursor_p)
{
    _ASSERT(first_p.IsValid());
    input_text = std::move(input);
    cursor_pos = cursor_p.IsValid() ? cursor_p : CharByteIndex{-1,-1};
    first_pos = first_p;
    hadEscapeHint = false;
    hadFileHint = false;
    addRefNamesAtEnd = false;
    addEntityNamesAtEnd.clear();
    exclude_entity_hint.clear();
    CshList.clear();
    ColonLabels.clear();
    Instructions.clear();
    BracePairs.clear();
    SqBracketPairs.clear();
    IfThenElses.clear();
    CshErrors.clear();
    EntityNames.clear();
    RefNames.clear();
    shape_names.clear();
    Contexts.clear();
    InLines.clear();

    auto i = FullDesigns.find(params->ForcedDesign);
    if (!params->ForcedDesign.empty() && i != FullDesigns.end())
        Contexts.push_back(i->second);
    else {
        i = FullDesigns.find("plain");
        _ASSERT(i != FullDesigns.end());
        if (i != FullDesigns.end())
            Contexts.push_back(i->second);
        else
            Contexts.emplace_back(true, EContextParse::NORMAL);
    }
    hintStatus = HINT_NONE;
    hintedStringPos.first_pos = cursor_p; //zero_length at the position of the cursor
    hintedStringPos.end_pos = cursor_p;
    hintAttrName.clear();
    Hints.clear();
    allow_anything = false;
    cursor_at_line_begin = CalculateIsCursorAtLineBegin();
}


/** This function is called just after every color syntax parse.
 * Here we can complete delayed actions, resolve loose ends and tidy up. */
void Csh::AfterYaccParse()
{
    _ASSERT(MultiElements.empty());
    if (addRefNamesAtEnd) {
        hintStatus = HINT_FILLING;
        for (auto &n : RefNames)
            AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + n,
                "Reference names defined via the 'refname' attributes.", EHintType::ATTR_VALUE,
                true));
        hintStatus = HINT_READY;
    }
    if (addEntityNamesAtEnd.size()) {
        hintStatus = HINT_FILLING;
        for (auto n : EntityNames) {
            //Skip adding this entity if its name is under definition
            if (n == exclude_entity_hint)
                continue;
            std::string desc = addEntityNamesAtEnd;
            auto at = desc.find("%s");
            if (at!=std::string::npos)
                desc.replace(at, 2, n);
            AddToHints(CshHint(HintPrefix(COLOR_ENTITYNAME) + n,
                               std::move(desc), EHintType::ATTR_VALUE, true));
        }
        hintStatus = HINT_READY;
    }
    if (hintStatus == HINT_FILLING) hintStatus = HINT_READY;
    //If the cursor is not at the front of the line, kill LINE_START hints
    if (hintSource == EHintSourceType::LINE_START && !IsCursorAtLineBegin())
        Hints.clear();
    _ASSERT(hintStatus==HINT_READY || Hints.size()==0);
}


/** Insert a hint to the list of hints.*/
void Csh::AddToHints(CshHint &&h)
{
    if (hintStatus == HINT_READY) return; //we add no more
    if (h.callback==nullptr && h.type == EHintType::ATTR_NAME) {
        h.callback = CshHintGraphicCallbackForAttributeNames;
        h.param = 0;
    }
    Hints.push_back(std::move(h));
    if (h.type == EHintType::ESCAPE)
        hadEscapeHint = true;
    else {
        //when we add an escape hint, we should have only hints of escape type
        _ASSERT(!hadEscapeHint);
    }
    if (h.type == EHintType::FILES)
        hadFileHint = true;
    else {
        //when we add an escape hint, we should have only hints of escape type
        _ASSERT(!hadFileHint);
    }
}

/** Append a bunch of hints to the hint list.
*
* @param [in] names_descriptions The text of the hints and descriptions (alternating) in a char
*                                pointer array. The last hint shall be "".
* @param [in] prefix A string to prepend to each hint.
* @param [in] t The type of the hints.
* @param [in] c The callback function to use. The index of the hints in 'names' will be passed as parameter to the callback.
* @param [in] in_order If true, elements will be attached a 'sort' key in the order they are added. (for HINT_ESCAPEs which do not sort alphabetically.)
* @param [in] selectable If true, the added hints will be selectable, else not.*/
void Csh::AddToHints(const char * const * names_descriptions,
                     std::string_view prefix, EHintType t,
                     CshHintGraphicCallback c, bool in_order, bool selectable)
{
    //index==0 is usually "invalid"
    for (unsigned i = 2; names_descriptions[i][0]; i += 2)
        AddToHints(CshHint(StrCat(prefix, names_descriptions[i]), names_descriptions[i+1],
                           t, selectable, c, CshHintGraphicParam(i/2), in_order ? i/2 : 0));
}

/** Append a bunch of hints to the hint list.
*
* @param [in] names_descriptions The text of the hints and descriptions (alternating) in a char
*                                pointer array. The last hint shall be "".
* @param [in] prefix A string to prepend to each hint.
* @param [in] t The type of the hints.
* @param [in] c The callback function to use.
* @param [in] p The parameter to pass to the callback.
* @param [in] selectable If true, the added hints will be selectable, else not.*/
void Csh::AddToHints(const char * const * names_descriptions,
                     std::string_view prefix, EHintType t,
                     CshHintGraphicCallback c, CshHintGraphicParam p, bool selectable)
{
    //index==0 is usually "invalid"
    for (unsigned i = 2; names_descriptions[i][0]; i += 2)
        AddToHints(CshHint(StrCat(prefix, names_descriptions[i]), names_descriptions[i+1],
                           t, selectable, c, p));
}

/** Append a bunch of hints to the hint list.
*
* @param [in] names_descriptions The text of the hints and descriptions (alternating) in a char
*                                pointer array. The last hint shall be "".
* @param [in] prefix A string to prepend to each hint.
* @param [in] t The type of the hints.
* @param [in] c The callback function to use.
* @param [in] p The parameter to pass to the callback.
* @param [in] selectable If true, the added hints will be selectable, else not.*/
void Csh::AddToHints(const std::map<std::string, std::string> &names_descriptions,
                     std::string_view prefix, EHintType t,
                     CshHintGraphicCallback c, CshHintGraphicParam p, bool selectable)
{
    for (auto &i : names_descriptions)
        AddToHints(CshHint(StrCat(prefix, i.first), i.second.c_str(), t, selectable, c, p));
}

/** Append a bunch of hints to the hint list.
*
* @param [in] names The text of the hints in a 2D char
*                   array. The last hint shall be "".
* @param [in] descriptions The descriptions of the hints and in a char
*                          pointer array. At least as many as in 'names'.
* @param [in] prefix A string to prepend to each hint.
* @param [in] t The type of the hints.
* @param [in] c The callback function to use. The index of the hints in 'names' will be passed as parameter to the callback.
* @param [in] selectable If true, the added hints will be selectable, else not.*/
void Csh::AddToHints(const char names[][ENUM_STRING_LEN], const char * const descriptions[],
                     std::string_view prefix, EHintType t,
                     CshHintGraphicCallback c, bool selectable)
{
    //index==0 is usually "invalid"
    for (unsigned i = 1; names[i][0]; i++)
        AddToHints(CshHint(StrCat(prefix, names[i]), descriptions ? descriptions[i] : nullptr,
                           t, selectable, c, CshHintGraphicParam(i)));
}

/** Append a bunch of hints to the hint list.
*
* @param [in] names The text of the hints in a 2D char
*                   array. The last hint shall be "".
* @param [in] descriptions The descriptions of the hints and in a char
*                          pointer array. At least as many as in 'names'.
* @param [in] prefix A string to prepend to each hint.
* @param [in] t The type of the hints.
* @param [in] c The callback function to use.
* @param [in] p The parameter to pass to the callback.
* @param [in] selectable If true, the added hints will be selectable, else not.*/
void Csh::AddToHints(const char names[][ENUM_STRING_LEN], const char * const descriptions[],
                     std::string_view prefix, EHintType t,
                     CshHintGraphicCallback c, CshHintGraphicParam p, bool selectable)
{
    //index==0 is usually "invalid"
    for (unsigned i = 1; names[i][0]; i++)
        AddToHints(CshHint(StrCat(prefix, names[i]), descriptions ? descriptions[i] : nullptr,
                           t, selectable, c, p));
}


/** Checks if 'one_text' starts with an alpha numeric character .
* For empty texts we return true.
* Descendant classes can overload this to redefine what counts as alphanumeric.*/
bool Csh::StartsInAlpha(const CshPos & one) const
{
    if (one.end_pos.byte_index <= first_pos.byte_index) return false; //nothing at the beginning of the file is not alpha
    //if 'one' starts beyond the file, we say it does not start with alpha
    if ((int)input_text.length() < one.first_pos.byte_index) return false; //beyond the end of file
    const char c = input_text[one.first_pos.byte_index - first_pos.byte_index];
    return
        ('a'<=c && c<='z') || //if a capital letter (or a capital letter followed by a dot) ...
        ('A'<=c && c<='Z') || //if a small letter (or a small letter followed by a dot) ...
        ('0'<=c && c<='9') || //if a number (or a number followed by a dot) ...
        (127<=(unsigned char)c); //if a UTF-8 char (or a UTF-8 char followed by a dot) ...
}

/** Checks if 'one' ends with an alpha numeric character (or a single dot).
* For empty texts we return true.
* Descendant classes can overload this to redefine what counts as alphanumeric.*/
bool Csh::EndsInAlpha(const CshPos &one) const
{
    //if 'one' ends before the start of the file, we say it is not ending in alpha
    if (one.end_pos.byte_index<=first_pos.byte_index) return false; //nothing at the beginning of the file is not alpha
    size_t off = one.end_pos.byte_index - first_pos.byte_index - 1;
    if (input_text.size() <= off) return false; //beyond the end of the file
    if (input_text[off]=='.') {
        if (off==0) return false; //a single dot at the beginning of the file is not alpha
        off--;
    }
    char c = input_text[off];
    return
        ('a'<=c && c<='z') || //if a capital letter (or a capital letter followed by a dot) ...
        ('A'<=c && c<='Z') || //if a small letter (or a small letter followed by a dot) ...
        ('0'<=c && c<='9') || //if a number (or a number followed by a dot) ...
        (127<=(unsigned char)c); //if a UTF-8 char (or a UTF-8 char followed by a dot) ...
}


/** Mark hint status to HINT_LOCATED if cursor is before a range, but after the
 * preceding non-whitespace token.
 * This function is used when we want to hint something at the beginning of a token.
 * @param [in] one The range, we use only first_pos of it.
 * @param [in] hsource The source type at the position in the file.
 * @param [in] a_name The name of the attribute if the hint type is HINT_ATTRVALUE
 * @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
 *                   we do not do anything if the cursor is directly at the end of
 *                   'one' or at the beginning of the next token and these end/start
 *                   with an alphanumeric char, respectively.
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckHintBefore(const CshPos &one, EHintSourceType hsource, std::string_view a_name, bool alpha)
{
    if (one.first_pos < first_pos) return false; //token starts before start of file - nothing before
    //find end of the previous token
    const int start_offset = one.first_pos.byte_index - first_pos.byte_index - 1;
    int prev_last = start_offset;
    while (prev_last>=0 && (input_text[prev_last]==' ' || input_text[prev_last]=='\t'||
                            input_text[prev_last]=='\r'|| input_text[prev_last]=='\n'))
        prev_last--;
    CharByteIndex before = one.first_pos.ShiftASCII(prev_last - start_offset); //if no ws before 'one' this points to 'one.first_pos'
    return CheckHintBetween(CshPos{before,before}, one, hsource, a_name, alpha);
}


/** Mark hint status to HINT_LOCATED if cursor is inside, just before or just after a range.
 *
 * Checks if the cursor is inside, just before or just after a range.
 * If so, it applies the hinttype with status HINT_LOCATED. hintedStringPos
 * is set to 'one'.
 * @param [in] one The range
 * @param [in] hsource The source type at the position in the file.
 * @param [in] a_name The name of the attribute if the hint type is HINT_ATTRVALUE
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckHintAt(const CshPos &one, EHintSourceType hsource, std::string_view a_name)
{
    if (CursorIn(one)<=CURSOR_AFTER) return false;
    hintedStringPos = one;
    return DoHintLocated(hsource, a_name);
}



/** Mark hint status to HINT_LOCATED if cursor is after a range, but before the
 * next non-whitespace token.
 * This function is used when we want to hint something at the end of a token.
 * @param [in] one The range, we use only last_pos of it.
 * @param [in] hsource The source type at the position in the file.
 * @param [in] a_name The name of the attribute if the hint type is HINT_ATTRVALUE
 * @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
 *                   we do not do anything if the cursor is directly at the end of
 *                   'one' or at the beginning of the next token and these end/start
 *                   with an alphanumeric char, respectively.
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckHintAfter(const CshPos &one, EHintSourceType hsource, std::string_view a_name, bool alpha)
{
    //find beginning of next non-whitespace char
    int off = one.end_pos.byte_index - first_pos.byte_index;
    _ASSERT(0 <= off);
    _ASSERT(off <= (int)input_text.size());
    std::string_view after = std::string_view(input_text).substr(off);
    size_t next = after.find_first_not_of(" \t\r\n");
    if (next == std::string::npos) next = after.length(); //only whitespace remains
    const CharByteIndex two = one.end_pos.ShiftASCII(next); //1 whitespace char = 1 byte
    return CheckHintBetween(one, CshPos{ two, two }, hsource, a_name, alpha);
}

/** Mark hint status to HINT_LOCATED if cursor is between two ranges
*
* Checks if the cursor is between the two ranges and if so, it applies
* the hinttype with status HINT_LOCATED. It sets hintsForcedOnly to false.
* If the cursor is immediately at the beginning of the second range we do nothing.
* @param [in] one The first range
* @param [in] two The second range
* @param [in] hsource The source type at the position in the file.
* @param [in] a_name The name of the attribute if the hint type is HINT_ATTRVALUE
* @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
*                   we do not do anything if the cursor is directly at the end of
*                   'one' or at the beginning of 'two' and these end/start
*                   with an alphanumeric char, respectively.
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckHintBetween(const CshPos &one, const CshPos &two,
                           EHintSourceType hsource, std::string_view a_name, bool alpha)
{
    if (one.end_pos==two.first_pos && cursor_pos==two.first_pos) {
        //an empty range and cursor is in it
        if (alpha && (EndsInAlpha(one) || StartsInAlpha(two)))
            return false;
        //signal hint for alpha==true only if none after or before cursor is alpha
        return DoHintLocated(hsource, a_name);
    }
    switch (CursorIn(CshPos(one.end_pos, two.first_pos))) {
    default:
    case CURSOR_BEFORE:
    case CURSOR_AFTER:
        return false;
    case CURSOR_AT_BEGINNING:
        if (alpha && EndsInAlpha(one)) return false;
        break;
    case CURSOR_AT_END:
        if (alpha && StartsInAlpha(two)) return false;
        break;
    case CURSOR_IN:
        break;
    }
    return DoHintLocated(hsource, a_name);
}


/** Check if the cursor is anywhere before (or at the beginning of) 'pos'
 * and in heading whitespace of a line.
 * If so, mark hint status as HINT_FILLING and hint type as HINT_LINE_START
 * except if we have already closed the hints via HINT_READY
 *
 * Checks if the cursor is well before or just before a range.
 * If so, it applies the hinttype with status HINT_FILLING. hintedStringPos
 * is set to 'pos' only if the cursor is at its beginning.
 * This function is used to check if the cursor is before any characters in the file.
 * @param [in] pos The range to check
 * @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
 *                   we do not do anything if the cursor is directly at the beginning of
 *                   'pos'.
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckLineStartHintBefore(const CshPos &pos, bool alpha)
{
    if (hintStatus != HINT_READY && IsCursorAtLineBegin() &&
        CheckHintBefore(pos, EHintSourceType::LINE_START, {}, alpha)) {
        hintStatus = HINT_FILLING;
        return true;
    }
    return false;
}


/** Mark hint type to HINT_ENTITY and status to HINT_FILLING if cursor is before a range.
*
* This function is used when we want to hint an entity before a yacc rule.
* @param [in] one The first range
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckEntityHintBefore(const CshPos &one)
{
    if (!CheckHintBefore(one, EHintSourceType::ENTITY, {}, true))
        return false;
    AddEntitiesToHints();
    hintStatus = HINT_READY;
    return true;
}



/** Check if the cursor is anywhere in 'pos'.
 * If so, mark hint status as HINT_FILLING and hint type as HINT_LINE_START
 * except if we have already closed the hints via HINT_READY
 *
 * Checks if the cursor just before, in or just after a range.
 * If so, it applies the hinttype with status HINT_FILLING. hintedStringPos
 * is set to 'pos'.
 * @param [in] pos The range to check
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckLineStartHintAt(const CshPos &pos)
{
    if (hintStatus != HINT_READY && IsCursorAtLineBegin() &&
        CheckHintAt(pos, EHintSourceType::LINE_START, {})) {
        hintStatus = HINT_FILLING;
        hintedStringPos = pos;
        return true;
    }
    return false;
}

/** Check if the cursor is after 'pos'.
* If so, mark hint status as HINT_FILLING and hint type as HINT_LINE_START
* except if we have already closed the hints via HINT_READY
*
* Checks if the cursor just before, in or just after a range.
* If so, it applies the hinttype with status HINT_FILLING. hintedStringPos
* is set to 'pos'.
* @param [in] pos The range to check
* @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
*                   we do not do anything if the cursor is directly at the end of
*                   'one' or at the beginning of the next token and these end/start
*                   with an alphanumeric char, respectively.
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckLineStartHintAfter(const CshPos & pos, bool alpha)
{
    if (hintStatus != HINT_READY && IsCursorAtLineBegin() &&
        CheckHintAfter(pos, EHintSourceType::LINE_START, {}, alpha)) {
        hintStatus = HINT_FILLING;
        return true;
    }
    return false;
}

/** Check if the cursor is between 'pos' and 'pos2'.
* If so, mark hint status as HINT_FILLING and hint type as HINT_LINE_START
* except if we have already closed the hints via HINT_READY
*
* Checks if the cursor just before, in or just after a range.
* If so, it applies the hinttype with status HINT_FILLING. hintedStringPos
* is set to 'pos'.
* @param [in] pos The range after to check
* @param [in] pos2 The range before to check
* @param [in] alpha If true, we assume this hint will be alphanumeric. In this case
*                   we do not do anything if the cursor is directly at the end of
*                   'one' or at the beginning of the next token and these end/start
*                   with an alphanumeric char, respectively.
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckLineStartHintBetween(const CshPos & pos, const CshPos & pos2, bool alpha)
{
    if (hintStatus != HINT_READY && IsCursorAtLineBegin() &&
        CheckHintBetween(pos, pos2, EHintSourceType::LINE_START, {}, alpha)) {
        hintStatus = HINT_FILLING;
        return true;
    }
    return false;
}

/** Mark hint type to HINT_FILLING and status to HINT_FILLING if cursor is inside, just before or just after a range.
 *
 * Checks if the cursor is inside, just before or just after a range.
 * If so, sets hint type to
 * HINT_ENTITY and status to HINT_FILLING and adds the entities collected so far to the hints.
 * hintedStringPos
 * is set to 'one'. hintsForcedOnly is set to true if the cursor is just before one
 * or is at the end of one
 * @param [in] one The range
 * @returns True if the cursor is in this hintable place.*/
bool Csh::CheckEntityHintAt(const CshPos &one)
{
    if (!CheckHintAt(one, EHintSourceType::ENTITY, {}))
        return false;
    AddEntitiesToHints();
    hintStatus = HINT_FILLING;
    return true;
}


/** Mark hint type to HINT_ENTITY and status to HINT_FILLING if cursor is after a range.
*
* This function is used when we want to hint an entity at the end of a token.
* @param [in] one The first range
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckEntityHintAfter(const CshPos &one)
{
    if (!CheckHintAfter(one, EHintSourceType::ENTITY, {}, true))
        return false;
    AddEntitiesToHints();
    hintStatus = HINT_READY;
    return true;
}


/** Mark hint type to HINT_ENTITY and status to HINT_FILLING if cursor is between two ranges.
*
* Checks if the cursor is between the two ranges. If so, it sets hint type to
* HINT_ENTITY and status to HINT_READY and adds the entities collected so far to the hints.
* If cursor is inside two, hintedStringPos
* is set to two. hintsForcedOnly is set to true iff the cursor is truly before two
* or is at the end of two.
* @param [in] one The first range
* @param [in] two The second range
* @returns True if the cursor is in this hintable place.*/
bool Csh::CheckEntityHintBetween(const CshPos &one, const CshPos &two)
{
    if (!CheckHintBetween(one, two, EHintSourceType::ENTITY, {}, true))
        return false;
    AddEntitiesToHints();
    hintStatus = HINT_FILLING;
    return true;
}

/** Check if a hint has been previously located with specific properties.
 *
 * If the hint had been located and its location is fully inside the "location_to_check"
 * and its type equals to "ht" we set its status to HINT_FILLING and return true.
 * After this one can add the hints.*/
bool Csh::CheckHintLocated(EHintSourceType hsource, const CshPos &location_to_check)
{
    return location_to_check.IsWithin(hintedStringPos) && CheckHintLocated(hsource);
}



/** Check if a hint has been previously located with specific properties.
 *
 * If the hint had been located and its type equals to "ht" we set its status to
 * HINT_FILLING and return true. After this one can add the hints.*/
bool Csh::CheckHintLocated(EHintSourceType hsource)
{
    if (hintStatus!=HINT_LOCATED || hintSource!=hsource)
        return false;
    hintStatus = HINT_FILLING;
    return true;
}


/** Check if a hint has been previously located with specific properties.
 *
 * If the hint had been located and its location is fully inside the "location_to_check"
 * we set its status to HINT_FILLING and return true.
 * After this one can add the hints.*/
bool Csh::CheckHintLocated(const CshPos &location_to_check)
{
    //If hintedString is fully inside the location_to_check only then do we signal a located hint.
    //If the hinted string is empty then hintedStringPos.first equals hintedStringPos.last+1.
    //In this case if the last is within (perhaps at the end) of "location_to_check", we are
    //still OK and shall give hints.
    if (!location_to_check.IsWithin(hintedStringPos))
        return false;
    if (hintStatus!=HINT_LOCATED)
        return false;
    hintStatus = HINT_FILLING;
    return true;
}

/** Add colors available at the cursor to the list of hints.
 * @param [in] define If true we hint after a 'defcolor' command.
 *                    Else we hint at a value of a 'color' attr.
 *                    In the second case we add explanatory hints
 *                    on color syntax definition. In the first
 *                    case we add an explanatory hint to say any new
 *                    color name can be used. */
void Csh::AddColorValuesToHints(bool define)
{
     if (define) {
         AddToHints(CshHint(HintPrefixNonSelectable()+"new color name to define",
             "You can specify a new color name here if you want to define a new color.",
             EHintType::ATTR_VALUE, false));
         AllowAnything();
     } else {
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"red,green,blue\">",
             "You can specify the three components of an RGB color. They can be either integers between "
             "0..255 or floating point numbers between [0..1].",
             EHintType::ATTR_VALUE, false));
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"red,green,blue,opacity\">",
             "You can specify the three components of an RGB color, plus an opacity value. They can be either integers between "
             "0..255 or floating point numbers between [0..1]. Opacity of zero means full transparency - nothing visible.",
             EHintType::ATTR_VALUE, false));
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"++red,green,blue,opacity\">",
             "Using the '++' prefix you can specify a translucent overlay color, which will be overlaid on top of "
             "an existing color.",
             EHintType::ATTR_VALUE, false));
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"color name,opacity\">",
             "You can make an existing color transparent by specifying an opacity value separated by a comma. "
             "It can be either an integer between [0..255] or a floating point number between [0..1]. "
             "Opacity of zero means full transparency - nothing visible.",
             EHintType::ATTR_VALUE, false));
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"++color name,opacity\">",
             "Using the '++' prefix you can specify a translucent overlay color, which will be overlaid on top of "
             "an existing color.",
             EHintType::ATTR_VALUE, false));
         AddToHints(CshHint(HintPrefixNonSelectable()+"<\"color name+-brightness%\">",
             "You can take an existing color and make it lighter/darker like this.",
             EHintType::ATTR_VALUE, false));
     }
    CshHint hint("", define ? "Redefine this color." : "Apply this color.", 
                 EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForColors, 0);
    for (auto i=Contexts.back().Colors.begin(); i!=Contexts.back().Colors.end(); i++) {
        hint.decorated = HintPrefix(COLOR_ATTRVALUE) + i->first;
        hint.param = i->second.ConvertToUnsigned();
        AddToHints(hint);
    }
}

/** Callback for drawing a symbol before design names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForDesigns(Canvas *canvas, CshHintGraphicParam /*p*/, CshHintStore &)
{
    if (!canvas || canvas->cannot_draw(false)) return false;
    const XY ul(0.2*HINT_GRAPHIC_SIZE_X, 0.2*HINT_GRAPHIC_SIZE_Y);
    const XY br(0.8*HINT_GRAPHIC_SIZE_X, 0.8*HINT_GRAPHIC_SIZE_Y);
    ColorType color(0, 0, 0);
    LineAttr line;
    line.radius = 2;
    canvas->Clip(ul, br, line);
    cairo_pattern_t *pattern = cairo_pattern_create_linear(ul.x, ul.y, br.x, br.y);
    cairo_pattern_add_color_stop_rgb(pattern, 0.0, 255/255., 255/255., 255/255.);  //white
    cairo_pattern_add_color_stop_rgb(pattern, 0.1, 128/255., 128/255.,   0/255.);  //brown
    cairo_pattern_add_color_stop_rgb(pattern, 0.2, 255/255.,   0/255.,   0/255.);  //red
    cairo_pattern_add_color_stop_rgb(pattern, 0.3, 255/255., 255/255.,   0/255.);  //yellow
    cairo_pattern_add_color_stop_rgb(pattern, 0.4,   0/255., 255/255.,   0/255.);  //green
    cairo_pattern_add_color_stop_rgb(pattern, 0.5,   0/255., 255/255., 255/255.);  //cyan
    cairo_pattern_add_color_stop_rgb(pattern, 0.6,   0/255.,   0/255., 255/255.);  //blue
    cairo_pattern_add_color_stop_rgb(pattern, 0.7, 255/255.,   0/255., 255/255.);  //magenta
    cairo_pattern_add_color_stop_rgb(pattern, 0.8, 255/255.,   0/255.,   0/255.);  //red
    cairo_pattern_add_color_stop_rgb(pattern, 0.9, 255/255., 255/255.,   0/255.);  //yellow
    cairo_pattern_add_color_stop_rgb(pattern, 1.0, 255/255., 255/255., 255/255.);  //white
    cairo_set_source(canvas->GetContext(), pattern);
    cairo_rectangle(canvas->GetContext(), ul.x, ul.y, br.x, br.y);
    cairo_fill(canvas->GetContext());
    //Over a white rectangle to lower saturation
    cairo_set_source_rgba(canvas->GetContext(), 1, 1, 1, 0.5);
    cairo_rectangle(canvas->GetContext(), ul.x, ul.y, br.x, br.y);
    cairo_fill(canvas->GetContext());
    canvas->UnClip();
    //canvas->Line(ul, br, line);
    cairo_pattern_destroy(pattern);
    return true;
}

/** Add design available at the cursor to the list of hints.
 * @param [in] full Decides if full or partial design names shall be added.*/
void Csh::AddDesignsToHints(bool full)
{
    for (auto i= (full ? FullDesigns : PartialDesigns).begin(); i!=(full ? FullDesigns : PartialDesigns).end(); i++)
        Hints.push_back(CshHint(HintPrefix(COLOR_ATTRVALUE) + i->first,
                                full ? "Apply this full design to the chart." : "Apply this partial design to the chart.",
                                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForDesigns));
}

/** Callback for drawing a symbol before style names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForStyles(Canvas *canvas, CshHintGraphicParam, CshHintStore &)
{
    if (!canvas) return false;
    LineAttr line(ELineType::SOLID, ColorType(0,0,0), 1, ECornerType::ROUND, 1);
    FillAttr fill(ColorType(0,255,0), EGradientType::UP);
    ShadowAttr shadow(ColorType(0,0,0));
    shadow.offset = 2;
    shadow.blur = 0;

    Block b(HINT_GRAPHIC_SIZE_X*0.1, HINT_GRAPHIC_SIZE_X*0.5, HINT_GRAPHIC_SIZE_Y*0.1, HINT_GRAPHIC_SIZE_Y*0.5);
    canvas->Fill(b, line, fill);
    canvas->Line(b, line);

    b.Shift(XY(HINT_GRAPHIC_SIZE_X*0.15, HINT_GRAPHIC_SIZE_X*0.15));
    fill.color = ColorType(255,0,0);
    canvas->Fill(b, fill);
    canvas->Line(b, line);

    b.Shift(XY(HINT_GRAPHIC_SIZE_X*0.15, HINT_GRAPHIC_SIZE_X*0.15));
    fill.color = ColorType(0,0,255);
    canvas->Shadow(b, shadow);
    canvas->Fill(b, fill);
    canvas->Line(b, line);
    return true;
}

/** Callback for drawing a symbol before style names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForStyles2(Canvas *canvas, CshHintGraphicParam, CshHintStore &)
{
    if (!canvas) return false;
    constexpr Block b(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_X*0.8,
                      HINT_GRAPHIC_SIZE_Y*0.3, HINT_GRAPHIC_SIZE_Y*0.7);
    SingleArrowHead ah(EArrowType::SOLID, true, XY(ArrowScale(EArrowSize::INVALID), ArrowScale(EArrowSize::INVALID))); //for very small
    LineAttr line(ELineType::SOLID, ColorType(0, 0, 0));
    FillAttr fill(ColorType(0, 255, 0), EGradientType::UP);
    ShadowAttr shadow;

    //calculate arrow contour
    Contour area = ah.BlockContour(b.x.till, b.y.from, b.y.till, line, false);
    auto who = ah.BlockWidthHeight(b.y.Spans(), line, false);
    area += Block(b.x.from, b.x.till-who.body_connect_before, b.y.from, b.y.till);

    //draw
    canvas->Clip(XY(1, 1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    area.Expand(line.LineWidth()/2); //get midline
    canvas->Shadow(area, shadow);
    canvas->Fill(area.CreateExpand(line.Spacing()), fill);
    canvas->Line(area, line);
    canvas->UnClip();
    return true;
}

/** Add styles available at the cursor to the list of hints.
 * @param [in] include_forbidden If true, we include the forbidden styles, too.
 *                               These include all default and refinement styles (like 'arrow' and '->'),
 *                               but not user-defined styles or 'strong' and 'weak'.
 * @param [in] define If true we formulate description appropriate for defstyle commands, else
 *                    as for when applying a style.*/
void Csh::AddStylesToHints(bool include_forbidden, bool define)
{
    for (auto i=Contexts.back().StyleNames.begin(); i!=Contexts.back().StyleNames.end(); i++)
        if (include_forbidden || ForbiddenStyles.find(*i) == ForbiddenStyles.end())
            AddToHints(CshHint(HintPrefix(COLOR_STYLENAME) + *i,
                               define ? "Change this style." : "Apply this style.",
                               EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForStyles));
}

/** Port names for a shape, negative numbers OK (invalid shape) but don't add anything.
 * If the shape number is larger than the ones in pShape, we continue looking into
 * shape_names.*/
void Csh::AddPortsToHints(int shape)
{
    if (shape<0) return;
    const size_t pShapeNum = pShapes ? pShapes->ShapeNum() : 0;
    //Add ports for a specific shape
    if (size_t(shape)<pShapeNum) {
        unsigned port = 0;
        auto pShape = pShapes->GetShape(shape);
        for (auto &s : pShape->GetPorts())
            AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + s.first,
                               StrCat("Use this port of shape '", pShape->name, "'."),
                               EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForPorts,
                               CshHintGraphicParam(shape*ShapePortCshHintGraphicCallbackMultiplier + port++)));
    } else if (size_t(shape) < pShapeNum + shape_names.size()) {
        for (auto &s : shape_names[shape-pShapeNum].second)
            AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + s,
                                StrCat("Use this port of shape '", shape_names[shape-pShapeNum].first, "'."),
                                EHintType::ATTR_VALUE, true));
    }
}

void Csh::AddCompassPointsToHints()
{
    const char *compass_points_descr[] =
    { "North","North-East","East","South-East","South","South-West","West","North-West",
        "Center - no specific direction","Center - no specific direction",
        "Perpendicular to the contour of the block.", "" };
    for (unsigned u = 0; compass_points_descr[u] && Chart::compass_points[u][0]; u++)
        AddToHints(CshHint(StrCat(HintPrefix(COLOR_ATTRVALUE)+Chart::compass_points[u]), compass_points_descr[u], EHintType::ENTITY));
}


void Csh::AddShapesToHints()
{
    if (pShapes)
        pShapes->AttributeValues(*this);
    for (const auto &s : shape_names)
        AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE)+s.first,
            nullptr,
            EHintType::ATTR_VALUE, true));
}

void Csh::AddYesNoToHints()
{
    AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE)+"yes", nullptr, EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, CshHintGraphicParam(1)));
    AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE)+"no", nullptr, EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, CshHintGraphicParam(0)));
}

/** Adds include file names to the hints.
 * Used after an 'include' command. 
 * @param [in] text The text typed by the user so far. We assume it is 
 *     the prefix of the filename the user seeks to enter. If there are directories
 *     in it, we limit the hints to the members of that directory.
 * @param [in] pos The location of 'text' in the input file.*/
void Csh::AddIncludeFilesToHints(std::string_view text, const CshPos &pos)
{
    was_partial = false;
    //if pos is badly filled, assume all files.
    if (!pos.IsWithin(cursor_pos))
        text = {};
    if (text.length())
        hintedStringPos = pos;
    else
        hintedStringPos = {cursor_pos, cursor_pos};
    if (file_list_proc) {
        std::string prefix;
        if (text.length())
            prefix.assign(text.substr(0, cursor_pos.byte_index - pos.first_pos.byte_index));
        std::vector<std::string> files = file_list_proc(prefix, FileName.c_str());
        std::string format = HintPrefix(COLOR_INCLUDEFILE);
        for (auto &s: files) {
            for (string::size_type i = 0; (i = s.find("\\", i)) != string::npos;) {
                s.replace(i, 1, "\\\\\\\\");
                i += 4;
            }
            AddToHints(CshHint(format+"\\\""+s+"\\\"", nullptr, EHintType::FILES, true, nullptr, 0, s.back()!='\\'));
        }
    }
    hintStatus = HINT_READY;
}

void Csh::AddLanguagesToHints(std::string_view what) {
    for (std::string_view l : languages->GetLanguages())
        AddToHints(CshHint(StrCat(HintPrefix(COLOR_KEYWORD), l),
                           StrCat("Inlines an entire chart of this language. Supply the chart text after ", what, 
                                  " enclosed between the '" INLINE_OPEN_STR "' and '" INLINE_CLOSE_STR"' tokens."),
                           EHintType::KEYWORD, true));
}

/** Callback for drawing a symbol before keywords in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForKeywords(Canvas *canvas, CshHintGraphicParam, CshHintStore &)
{
    if (!canvas) return false;
    ColorType color(128, 64, 64);
    canvas->Clip(Contour(XY(HINT_GRAPHIC_SIZE_X/2, HINT_GRAPHIC_SIZE_Y/2), HINT_GRAPHIC_SIZE_Y*0.4));
    canvas->Fill(XY(0, 0), XY(HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y), FillAttr(color, EGradientType::DOWN));
    canvas->UnClip();
    return true;
}

/** Add entities defined up to now to the list of hints. */
void Csh::AddEntitiesToHints()
{
    for (auto &e : EntityNames)
        if (e!=exclude_entity_hint)
            AddToHints(CshHint(HintPrefix(COLOR_ENTITYNAME) + e, nullptr, EHintType::ENTITY, true));
}


/** Add text escape sequences to hints.*/
void Csh::AddEscapesToHints(EEscapeHintType hint)
{
    switch (hint) {
    default: _ASSERT(0); FALLTHROUGH;
    case HINTE_NONE:
        return;
    case HINTE_ESCAPE:
        StringFormat::EscapeHints(*this, string());
        break;
    case HINTE_PARAM_COLOR:
        AddColorValuesToHints(false);
        break;
    case HINTE_PARAM_STYLE:
        AddStylesToHints(false, false);
        break;
    case HINTE_PARAM_SHAPE:
        AddShapesToHints();
        break;
    case HINTE_PARAM_FONT:
        for (const auto &str : params->fontnames)
            if (str.length() && str[0]!='@')
                AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE)+str, nullptr,
                                    EHintType::ATTR_VALUE));
        break;
    case HINTE_PARAM_REF:
        addRefNamesAtEnd = true;
        break;
    case HINTE_PARAM_NUMBER:
        AddToHints(CshHint(HintPrefixNonSelectable()+"<number in pixels>", nullptr,
                           EHintType::ATTR_VALUE, false));
        break;
    case HINTE_PARAM_PARAM:
        //TODO: Add current parameter names to hints
        break;
    case HINTE_PARAM_LINK:
        const bool empty = hintedStringPos.empty();
        const string prefix = empty ?
            HintPrefix(COLOR_LABEL_ESCAPE) : HintPrefixNonSelectable();
        AddToHints(CshHint(prefix+"http://",
            "You can enter a valid URL here.",
            EHintType::ATTR_VALUE, empty));
        AddToHints(CshHint(prefix+"\\\\ref ",
            "Use this to refer to documented items, when the chart is a part of Doxygen documentation.",
            EHintType::ATTR_VALUE, empty));
        break;
    }
}

void Csh::AddLineBeginToHints()
{
    static const char * const keyword_names[] = {"invalid", nullptr,
        "defcolor", "Define or change the meaning of a color name.",
        "defproc", "Define a new procedure that can be re-played later.",
        "defstyle", "Define or change a style.",
        "defdesign", "Define or change a design.",
        "defshape", "Define a new shape.",
        "set", "Define or set the value of a variable or change the value of a parameter inside a procedure.",
        "include", "Include the text of another file in parsing.",
        "if", "Start a conditional statement. Useful only inside procedures.",
        "replay", "Re-plays a previously stored procedure.",
        ""
    };
    AddToHints(keyword_names, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
        CshHintGraphicCallbackForKeywords);
}

void Csh::AddDesignLineBeginToHints()
{
    static const char * const keyword_names[] = {"invalid", nullptr,
        "defcolor", "Define or change the meaning of a color name.",
        "defproc", "Define a new procedure that can be re-played later.",
        "defstyle", "Define or change a style.",
        "include", "Include the text of another file in parsing.",
        ""
    };
    AddToHints(keyword_names, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
               CshHintGraphicCallbackForKeywords);
}

void Csh::AttributeNamesForStyle(std::string_view style)
{
    default_styles->AttributeNamesForStyle(*this, style);
}

bool Csh::AttributeValuesForStyle(std::string_view attr, const std::string &style)
{
    return default_styles->AttributeValuesForStyle(attr, *this, style);
}

void Csh::CleanupAfterDesignlib()
{
    //Now clean it up. Keep only FullDesigns, PartialDesigns,
    //the outermost context and forbidden styles
    //1.clear hinted errors, actual csh entries, hints
    CshList.clear();
    ColonLabels.clear();
    Instructions.clear();
    BracePairs.clear();
    SqBracketPairs.clear();
    IfThenElses.clear();
    CshErrors.clear();
    Hints.clear();
    //2. clear entity names, marker names, all but the top of the the context stack
    _ASSERT(EntityNames.size()==0);
    _ASSERT(RefNames.size()==0);
    _ASSERT(Contexts.size()==1);
    RefNames.clear();
    Contexts.clear();
    //3. clear shape names, too: these will be added via pShapes (in parsed form)
    shape_names.clear();
    //4. clear all miscellaneous info that just takes up memory
    hintAttrName.clear();
}

CharByteIndex Csh::PosOfChar(int char_index) const {
    if (char_index < first_pos.char_index)
        return CharByteIndex::invalid();
    _ASSERT(std::ranges::is_sorted(CshList));
    auto i = std::ranges::lower_bound(CshList, char_index, {}, [](const CshEntry& e) { return e.first_pos.char_index; });
    if (i != CshList.end() && i->first_pos.char_index == char_index)
        return i->first_pos;
    if (i!=CshList.begin() && CshList.size()) { //end() can be decremented for non-empty lists
        --i;
        int byte_index = GetUTF8ByteIndex(std::string_view(input_text).substr(i->first_pos.byte_index), char_index - i->first_pos.char_index);
        if (byte_index < 0)
            return CharByteIndex::invalid();
        return CharByteIndex{ .char_index = char_index, .byte_index = i->first_pos.byte_index + byte_index };
    } //else: either no CSH list or 'char_index' is before the first entry
    int byte_index = GetUTF8ByteIndex(input_text, char_index - first_pos.char_index);
    if (byte_index < 0)
        return CharByteIndex::invalid();
    return CharByteIndex{ .char_index = char_index, .byte_index = first_pos.byte_index + byte_index };    
}

void Csh::TrimHintedStringPos(int char_index) {
    if (hintedStringPos.first_pos.char_index <= char_index && char_index < hintedStringPos.end_pos.char_index) 
        if (first_pos.char_index <=char_index) {
            std::string_view text_at = std::string_view(input_text).substr(hintedStringPos.first_pos.byte_index - first_pos.byte_index);
            hintedStringPos.end_pos.byte_index = hintedStringPos.first_pos.byte_index + GetUTF8ByteIndex(text_at, char_index - hintedStringPos.first_pos.char_index);
            hintedStringPos.end_pos.char_index = char_index;
        }
}

Csh::EntityNameUnderResult Csh::EntityNameUnderInLined(long char_index) const {
    for (auto& i : InLines)
        if (i.pos.IsWithinChar(char_index)) {
            auto ret = i.csh->EntityNameUnder(char_index);
            if (ret && ret.pos_inline.empty())
                ret.pos_inline = i.pos;
            return ret;
        }
    return {};
}

Csh::EntityNameUnderResult Csh::EntityNameUnder(long char_index) const {
    if (auto res = EntityNameUnderInLined(char_index)) return res;
    for (auto &p : CshList)
        if ((p.color == COLOR_ENTITYNAME || p.color == COLOR_ENTITYNAME_FIRST) && p.IsWithinChar(char_index) ) {
            const std::string_view entityname = std::string_view(input_text).substr(p.first_pos.byte_index, p.end_pos.byte_index - p.first_pos.byte_index);
            return EntityNameUnderResult{.full_entity_name=entityname, .byte_pos=std::max(0, (int)GetUTF8ByteIndex(entityname, char_index-p.first_pos.char_index)), .csh=this};
        }
    return {};
}

std::string Csh::ReplaceEntityName(std::string_view full_entity_name, int /*char_index*/, std::string_view replace_to,
                                   long lStart, long lEnd) const {
    const size_t entity_name_len_in_bytes = UTF8len(full_entity_name);
    if (!EntityNames.contains(full_entity_name)) return {input_text};
    if (lStart == lEnd) {
        lStart = first_pos.char_index;
        lEnd = INT_MAX;  //We do not care about end_pos here, just something very large
    }
    //The positions in 'CshList' are in csh units: the first char is
    //indexed 1, and for single character ranges first_pos==last_pos
    //Collect potential places to replace, where the CSH range
    //- is within the selection
    //- denotes an entity
    //- is of the same length as 'full_entity_name' (same number of characters)
    std::vector<size_t> poses; //these are byte indices
    for (auto &p : CshList)
        if ((p.color == COLOR_ENTITYNAME || p.color == COLOR_ENTITYNAME_FIRST)
                && lStart<=p.first_pos.char_index && p.end_pos.char_index <= lEnd
                && entity_name_len_in_bytes == unsigned(std::max(0, p.byte_length()))) {
            _ASSERT(first_pos.byte_index <= p.first_pos.byte_index);
            _ASSERT(p.end_pos.byte_index <= first_pos.byte_index + (int)input_text.length());
            poses.push_back(p.first_pos.byte_index - first_pos.byte_index);
        }
    std::ranges::reverse(poses); //helps sorting
    std::ranges::sort(poses, std::greater<>()); //sort reverse
    std::string text{input_text};
    for (size_t p : poses) //replace backwards: keeps indices that are not yet processed valid
        text.replace(p, full_entity_name.length(), replace_to);
    return text;
}


///////////////////////////////////////////////////////////////////////////////

DummyCsh::DummyCsh(const LanguageCollection* languages) :
    Csh(ContextBase<Style>(true, EContextParse::NORMAL, EContextCreate::PLAIN, FileLineCol()), nullptr, languages)
{
}
