17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 23:23:30 +02:00
Files
maszyna/editor/editorIncludeInfo.cpp
maj00r 9474293af7 fix(editor): signals move along their track again, and the editor finds the descriptions of the templates beside the game data
- a press on the mast of a selected signal grabbed the gizmo, whose up axis runs along the mast, and lifted the signal
  instead of moving it along the track; the gizmo gives way over a signal and while one is dragged
- dragged beside its track, off every track, the signal follows the nearest place of the track it stands by instead of
  staying put
- a drop away from any track, and a signal in a file the editor doesn't change, say so instead of doing nothing
- a template without a //$e description of its own takes it from editor/templates/<its path in the scenery folder>; the
  header scripts write there with --sidecar, leaving the templates alone, so a check of the game files by the store
  doesn't take the descriptions away (the descriptions written into the templates on 8.10 and 11.10 were gone, and with
  them every signal and drive the editor knew)
2026-10-18 02:08:19 +02:00

860 lines
24 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "editor/editorIncludeInfo.hpp"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include <yaml-cpp/yaml.h>
#include <filesystem>
#include <random>
#include <sstream>
#include <tuple>
#include <unordered_map>
namespace
{
std::string const linemark{"//$e"};
// template file split into the description and the rest
struct template_text
{
std::string content; // whole file
std::string yaml; // description, with the line marks taken off; utf-8
std::vector<std::pair<std::size_t, std::size_t>> lines; // ranges of the content taken by the description lines, line ends included
std::string eol{"\r\n"};
std::size_t start{0}; // location of the first character past the byte order mark
bool utf8{false}; // the file is utf-8 encoded, rather than windows-1250 like most of the scenery files
};
// true if specified text is valid utf-8. HasExtended: set if the text has any characters outside of ascii
bool is_utf8(std::string const &Text, bool &HasExtended)
{
HasExtended = false;
for (std::size_t idx = 0; idx < Text.size();)
{
auto const lead{static_cast<unsigned char>(Text[idx])};
if (lead < 0x80)
{
++idx;
continue;
}
HasExtended = true;
auto const length{(lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 0};
if (length == 0 || idx + length > Text.size())
{
return false;
}
for (auto offset = 1; offset < length; ++offset)
{
if ((static_cast<unsigned char>(Text[idx + offset]) & 0xC0) != 0x80)
{
return false;
}
}
idx += length;
}
return true;
}
// counterpart of win1250_to_utf8(). characters outside of the supported set are replaced with question marks
std::string utf8_to_win1250(std::string const &Text)
{
// polish letters, as utf-8 sequences; the same set win1250_to_utf8() handles
static std::unordered_map<std::string, char> const charmap{{"\xC4\x84", '\xA5'}, {"\xC4\x86", '\xC6'}, {"\xC4\x98", '\xCA'}, {"\xC5\x81", '\xA3'}, {"\xC5\x83", '\xD1'}, {"\xC3\x93", '\xD3'},
{"\xC5\x9A", '\x8C'}, {"\xC5\xB9", '\x8F'}, {"\xC5\xBB", '\xAF'}, {"\xC4\x85", '\xB9'}, {"\xC4\x87", '\xE6'}, {"\xC4\x99", '\xEA'},
{"\xC5\x82", '\xB3'}, {"\xC5\x84", '\xF1'}, {"\xC3\xB3", '\xF3'}, {"\xC5\x9B", '\x9C'}, {"\xC5\xBA", '\x9F'}, {"\xC5\xBC", '\xBF'}};
std::string output;
for (std::size_t idx = 0; idx < Text.size();)
{
auto const lead{static_cast<unsigned char>(Text[idx])};
if (lead < 0x80)
{
output += Text[idx++];
continue;
}
auto const length{std::min<std::size_t>((lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 1, Text.size() - idx)};
auto const lookup{charmap.find(Text.substr(idx, length))};
output += (lookup != charmap.end() ? lookup->second : '?');
idx += length;
}
return output;
}
bool read_template(std::string const &File, template_text &Text, std::string &Error)
{
std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary};
if (false == input.is_open())
{
Error = "can't open file \"" + File + "\"";
return false;
}
auto &content{Text.content};
content.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
auto const bom{content.compare(0, 3, "\xEF\xBB\xBF") == 0};
Text.start = bom ? 3 : 0;
bool extended;
Text.utf8 = bom || (is_utf8(content, extended) && extended);
if (auto const linebreak{content.find('\n')}; linebreak != std::string::npos)
{
Text.eol = (linebreak > 0 && content[linebreak - 1] == '\r') ? "\r\n" : "\n";
}
for (auto linebegin{Text.start}; linebegin < content.size();)
{
auto const linebreak{content.find('\n', linebegin)};
auto const next{linebreak == std::string::npos ? content.size() : linebreak + 1};
if (content.compare(linebegin, linemark.size(), linemark) == 0)
{
auto line{content.substr(linebegin + linemark.size(), next - linebegin - linemark.size())};
line.erase(line.find_last_not_of("\r\n") + 1);
// a single space separates the mark from the document; the rest is indentation, which carries meaning
if (false == line.empty() && line.front() == ' ')
{
line.erase(0, 1);
}
Text.yaml += line + '\n';
Text.lines.emplace_back(linebegin, next);
}
linebegin = next;
}
// a template without a description of its own may have one beside the game data, in editor/templates, where the
// scripts of tools/editor write it with --sidecar: a check of the game files doesn't take it away
if (Text.yaml.empty())
{
std::ifstream beside{"editor/templates/" + File, std::ios_base::binary};
if (beside.is_open())
Text.yaml.assign(std::istreambuf_iterator<char>(beside), std::istreambuf_iterator<char>());
}
if (false == is_utf8(Text.yaml, extended))
{
Text.yaml = win1250_to_utf8(Text.yaml);
}
return true;
}
// number of the parameter if specified text holds (pN) placeholder at specified location, 0 otherwise. Length: size of the placeholder
int placeholder(std::string const &Text, std::size_t const Location, std::size_t &Length)
{
if (Text.compare(Location, 2, "(p") != 0)
{
return 0;
}
auto idx{Location + 2};
auto id{0};
while (idx < Text.size() && std::isdigit(static_cast<unsigned char>(Text[idx])) && id < 1000)
{
id = id * 10 + (Text[idx] - '0');
++idx;
}
if (idx == Location + 2 || idx >= Text.size() || Text[idx] != ')')
{
return 0;
}
Length = idx + 1 - Location;
return id;
}
// number of the parameter if the token is nothing but its placeholder, 0 otherwise
int exact_placeholder(std::string const &Token)
{
std::size_t length{0};
auto const id{placeholder(Token, 0, length)};
return length == Token.size() ? id : 0;
}
// number of the first parameter whose placeholder is a part of the token, 0 if there's none
int first_placeholder(std::string const &Token)
{
std::size_t length{0};
for (auto location{Token.find("(p")}; location != std::string::npos; location = Token.find("(p", location + 1))
{
if (auto const id{placeholder(Token, location, length)}; id != 0)
{
return id;
}
}
return 0;
}
// tokens of scenery text, in lower case and with the comments left out
std::vector<std::string> tokenize(std::string const &Text)
{
std::vector<std::string> tokens;
std::string token;
auto const flush = [&]() {
if (false == token.empty())
{
tokens.emplace_back(ToLower(token));
token.clear();
}
};
for (std::size_t idx = 0; idx < Text.size(); ++idx)
{
if (Text.compare(idx, 2, "//") == 0)
{
flush();
idx = Text.find('\n', idx);
if (idx == std::string::npos)
{
break;
}
}
else if (Text.compare(idx, 2, "/*") == 0)
{
flush();
idx = Text.find("*/", idx + 2);
if (idx == std::string::npos)
{
break;
}
++idx;
}
else if (Text[idx] == ' ' || Text[idx] == '\t' || Text[idx] == '\n' || Text[idx] == '\r' || Text[idx] == ';')
{
flush();
}
else
{
token += Text[idx];
}
}
flush();
return tokens;
}
// reads description held by specified template text
bool parse(template_text const &Text, include_info &Info, std::string &Error)
{
Info = include_info();
auto const &text{Text};
try
{
auto const document{YAML::Load(text.yaml)};
if (document.IsNull())
{
return true;
}
if (false == document.IsMap())
{
Error = "the description isn't a list of named values";
return false;
}
Info.name = document["name"].as<std::string>("");
Info.category = document["category"].as<std::string>("");
Info.description = document["description"].as<std::string>("");
if (auto const drive{document["switch"]}; drive.IsDefined() && drive.IsMap())
{
Info.switch_hand = drive["hand"].as<std::string>("");
Info.switch_drive = drive["drive"].as<std::string>("");
Info.switch_default = drive["default"].as<bool>(false);
Info.switch_height = drive["height"].as<double>(0.0);
}
if (auto const signal{document["signal"]}; signal.IsDefined() && signal.IsMap())
{
Info.signal_kind = signal["kind"].as<std::string>("");
Info.signal_mount = signal["mount"].as<std::string>("");
Info.signal_lamps = signal["lamps"].as<std::string>("");
Info.signal_lean = signal["lean"].as<std::string>("");
Info.signal_read = signal["read"].as<std::string>("");
}
if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap())
{
Info.turntable_length = table["length"].as<double>(0.0);
Info.turntable_angles = table["angles"].as<int>(0);
Info.turntable_positions = table["positions"].as<int>(0);
}
if (auto const parameters{document["params"]}; parameters.IsDefined() && parameters.IsSequence())
{
for (auto const &entry : parameters)
{
auto const id{entry.IsMap() ? entry["id"].as<int>(0) : 0};
if (id <= 0)
{
continue;
}
auto &parameter{Info.parameter(id)};
parameter.role = entry["role"].as<std::string>("free");
parameter.label = entry["label"].as<std::string>("");
parameter.value = entry["default"].as<std::string>("");
}
}
}
catch (YAML::Exception const &exception)
{
Error = std::string{"the description is malformed: "} + exception.what();
return false;
}
return true;
}
// highest number of (pN) placeholder used by specified template text
int count_parameters(template_text const &Text)
{
// the description lines may mention parameters the template doesn't use
auto content{Text.content};
for (auto line{Text.lines.rbegin()}; line != Text.lines.rend(); ++line)
{
content.erase(line->first, line->second - line->first);
}
auto count{0};
std::size_t length{0};
for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1))
{
count = std::max(count, placeholder(content, location, length));
}
return count;
}
} // namespace
include_parameter &include_info::parameter(int const Id)
{
auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })};
if (lookup == std::end(parameters) || lookup->id != Id)
{
lookup = parameters.emplace(lookup);
lookup->id = Id;
}
return *lookup;
}
namespace editor_includes
{
std::vector<std::string> const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"};
bool load(std::string const &File, include_info &Info, std::string &Error)
{
template_text text;
return read_template(File, text, Error) && parse(text, Info, Error);
}
bool save(std::string const &File, include_info const &Info, std::string &Error)
{
template_text text;
if (false == read_template(File, text, Error))
{
return false;
}
std::string yaml;
try
{
// start with the existing description, to keep the entries we don't know
auto document{YAML::Load(text.yaml)};
if (false == document.IsNull() && false == document.IsMap())
{
Error = "the existing description isn't a list of named values";
return false;
}
if (false == document["version"].IsDefined())
{
document["version"] = 1;
}
auto const set = [&](char const *Key, std::string const &Value) {
if (false == Value.empty())
{
document[Key] = Value;
}
else
{
document.remove(Key);
}
};
set("name", Info.name);
set("category", Info.category);
set("description", Info.description);
YAML::Node parameters{YAML::NodeType::Sequence};
for (auto const &parameter : Info.parameters)
{
YAML::Node entry{YAML::NodeType::Map};
if (auto const existing{document["params"]}; existing.IsDefined() && existing.IsSequence())
{
for (auto const &candidate : existing)
{
if (candidate.IsMap() && candidate["id"].as<int>(0) == parameter.id)
{
entry = YAML::Clone(candidate);
break;
}
}
}
entry["id"] = parameter.id;
entry["role"] = parameter.role.empty() ? std::string{"free"} : parameter.role;
if (false == parameter.label.empty())
{
entry["label"] = parameter.label;
}
else
{
entry.remove("label");
}
if (false == parameter.value.empty())
{
entry["default"] = parameter.value;
}
else
{
entry.remove("default");
}
entry.SetStyle(YAML::EmitterStyle::Flow);
parameters.push_back(entry);
}
if (parameters.size() > 0)
{
document["params"] = parameters;
}
else
{
document.remove("params");
}
YAML::Emitter emitter;
emitter << document;
yaml = emitter.c_str();
}
catch (YAML::Exception const &exception)
{
Error = std::string{"the existing description is malformed, fix or remove it first: "} + exception.what();
return false;
}
if (false == text.utf8)
{
yaml = utf8_to_win1250(yaml);
}
std::string block;
std::istringstream lines{yaml};
for (std::string line; std::getline(lines, line);)
{
block += linemark + (line.empty() ? "" : " " + line) + text.eol;
}
// the description replaces the old one, or goes to the top of the file if there was none
auto content{text.content};
auto location{text.lines.empty() ? text.start : text.lines.front().first};
for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line)
{
content.erase(line->first, line->second - line->first);
}
content.insert(location, block);
auto const filepath{Global.asCurrentSceneryPath + File};
std::ofstream output{filepath + ".tmp", std::ios_base::binary | std::ios_base::trunc};
output << content;
output.close();
std::error_code error;
if (output.fail())
{
std::filesystem::remove(filepath + ".tmp", error);
Error = "can't write file \"" + File + "\"";
return false;
}
// the first save of a file leaves its original content next to it
if (false == std::filesystem::exists(filepath + ".bak", error))
{
std::filesystem::copy_file(filepath, filepath + ".bak", error);
}
std::filesystem::rename(filepath + ".tmp", filepath, error);
if (error)
{
Error = "can't replace file \"" + File + "\": " + error.message();
std::filesystem::remove(filepath + ".tmp", error);
return false;
}
return true;
}
int parameter_count(std::string const &File)
{
template_text text;
std::string error;
return read_template(File, text, error) ? count_parameters(text) : 0;
}
void suggest(std::string const &File, include_info &Info)
{
template_text text;
std::string error;
if (false == read_template(File, text, error))
{
return;
}
auto const tokens{tokenize(text.content)};
auto const assign = [&](std::size_t const Index, char const *Role, bool const Exact = true) {
if (Index >= tokens.size())
{
return;
}
auto const id{Exact ? exact_placeholder(tokens[Index]) : first_placeholder(tokens[Index])};
if (id != 0 && Info.parameter(id).role == "free")
{
Info.parameter(id).role = Role;
}
};
for (std::size_t idx = 0; idx < tokens.size(); ++idx)
{
auto const &token{tokens[idx]};
if (token == "origin")
{
assign(idx + 1, "pos.x");
assign(idx + 2, "pos.y");
assign(idx + 3, "pos.z");
}
else if (token == "rotate")
{
assign(idx + 1, "rot.x");
assign(idx + 2, "rot.y");
assign(idx + 3, "rot.z");
}
else if (token == "node" && idx + 4 < tokens.size())
{
// node <max> <min> <name> <type> <x> <y> <z> ...
assign(idx + 3, "name", false);
auto const &type{tokens[idx + 4]};
if (type == "model" || type == "memcell" || type == "eventlauncher" || type == "sound")
{
assign(idx + 5, "pos.x");
assign(idx + 6, "pos.y");
assign(idx + 7, "pos.z");
}
if (type == "model")
{
assign(idx + 8, "rot.y");
assign(idx + 9, "model");
assign(idx + 10, "texture");
}
if (type == "memcell")
{
assign(idx + 11, "track");
}
}
}
}
std::string const directive_mark{"include "};
bool automatic(std::string const &Role)
{
return Role == "name" || Role.starts_with("pos.") || Role.starts_with("rot.");
}
bool complete(include_info const &Info, int const Parameters, std::string *Issue)
{
std::string issue;
if (Info.name.empty())
{
issue = "the template needs a name";
}
for (auto id = 1; id <= Parameters && issue.empty(); ++id)
{
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
if (lookup == std::end(Info.parameters) || (false == automatic(lookup->role) && lookup->value.empty()))
{
issue = "(p" + std::to_string(id) + ") needs a default value, or a role filled in by the editor (name, pos.*, rot.*)";
}
}
if (false == issue.empty() && Issue != nullptr)
{
*Issue = issue;
}
return issue.empty();
}
std::string number(double const Value)
{
std::ostringstream converter;
converter.imbue(std::locale::classic());
converter << std::fixed << std::setprecision(3) << Value;
return converter.str();
}
int parameter_with_role(include_info const &Info, std::string const &Role)
{
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [&](include_parameter const &Parameter) { return Parameter.role == Role; })};
return lookup != std::end(Info.parameters) ? lookup->id : 0;
}
bool parse_directive(std::string const &Directive, std::string &File, std::vector<std::string> &Values)
{
// the same rules the scenery parser goes by, except the text is taken as it is written
std::vector<std::string> tokens;
std::string token;
auto const flush = [&]() {
if (false == token.empty())
{
tokens.emplace_back(token);
token.clear();
}
};
for (std::size_t idx = 0; idx < Directive.size(); ++idx)
{
auto const character{Directive[idx]};
if (character == '\"')
{
flush();
auto const end{Directive.find('\"', idx + 1)};
if (end == std::string::npos)
{
return false;
}
tokens.emplace_back(Directive, idx + 1, end - idx - 1);
idx = end;
}
else if (Directive.compare(idx, 2, "//") == 0)
{
flush();
idx = Directive.find('\n', idx);
if (idx == std::string::npos)
{
break;
}
}
else if (Directive.compare(idx, 2, "/*") == 0)
{
flush();
idx = Directive.find("*/", idx + 2);
if (idx == std::string::npos)
{
break;
}
++idx;
}
else if (character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == ';')
{
flush();
}
else
{
token += character;
}
}
flush();
if (tokens.size() < 3 || ToLower(tokens.front()) != "include" || tokens.back() != "end")
{
return false;
}
File = tokens[1];
Values.assign(std::begin(tokens) + 2, std::end(tokens) - 1);
return true;
}
std::string compose_directive(std::string const &File, std::vector<std::string> const &Values)
{
auto const quoted = [](std::string const &Text) { return Text.find_first_of(" \t;") != std::string::npos ? '\"' + Text + '\"' : Text; };
std::string text{"include " + quoted(File)};
for (auto const &value : Values)
{
// a parameter can't be left out, as the ones after it would take its place
text += ' ' + (value.empty() ? std::string{"none"} : quoted(value));
}
return text + " end";
}
std::string substitute(std::string const &Text, std::vector<std::string> const &Values)
{
std::string result;
std::size_t length{0};
for (std::size_t idx = 0; idx < Text.size();)
{
auto const id{placeholder(Text, idx, length)};
if (id > 0 && id <= static_cast<int>(Values.size()))
{
result += Values[id - 1];
idx += length;
}
else
{
result += Text[idx++];
}
}
return result;
}
std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional<float> Yaw, std::string const &Track, glm::dvec2 const &Tilt, std::map<std::string, std::string> const &Values)
{
std::vector<std::string> values;
for (auto id = 1; id <= Parameters; ++id)
{
auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
auto const role{lookup != std::end(Info.parameters) ? lookup->role : std::string{"free"}};
auto value{lookup != std::end(Info.parameters) ? lookup->value : std::string{}};
if (auto const given{Values.find("#" + std::to_string(id))}; given != Values.end())
{
value = given->second;
}
else if (auto const given{Values.find(role)}; given != Values.end())
{
value = given->second;
}
else if (role == "pos.x")
{
value = number(Location.x);
}
else if (role == "pos.y")
{
value = number(Location.y);
}
else if (role == "pos.z")
{
value = number(Location.z);
}
else if (role == "rot.y" && Yaw)
{
value = number(*Yaw);
}
else if (role == "rot.x" && Tilt.x != 0.0)
{
value = number(Tilt.x);
}
else if (role == "rot.z" && Tilt.y != 0.0)
{
value = number(Tilt.y);
}
else if (role.starts_with("rot.") && value.empty())
{
value = "0";
}
else if (role == "track" && false == Track.empty())
{
value = Track;
}
else if (role == "name")
{
// names have to be unique in the scenery. the default value, or the name of the template, makes them recognizable
static std::mt19937 engine{std::random_device{}()}; // NOSONAR
char suffix[16];
std::snprintf(suffix, sizeof(suffix), "_%08x", static_cast<unsigned int>(engine()));
value = (value.empty() ? std::filesystem::path(File).stem().string() : value) + suffix;
}
values.emplace_back(value);
}
return compose_directive(File, values);
}
} // namespace editor_includes
editorIncludeBank EditorIncludes;
namespace
{
include_entry examine(std::string const &File)
{
include_entry entry;
entry.file = File;
entry.statement = std::make_shared<std::string>(editor_includes::directive_mark + File);
template_text text;
if (false == read_template(File, text, entry.issue))
{
return entry;
}
entry.described = false == text.yaml.empty();
if (false == entry.described)
{
entry.issue = "the template has no description";
return entry;
}
include_info info;
if (false == parse(text, info, entry.issue))
{
return entry;
}
entry.name = info.name;
entry.category = info.category;
entry.complete = editor_includes::complete(info, count_parameters(text), &entry.issue);
return entry;
}
} // namespace
void editorIncludeBank::scan()
{
// the node bank may hold one of the templates selected, the statements stay in place across the scans
std::unordered_map<std::string, std::shared_ptr<std::string>> statements;
for (auto &entry : m_entries)
{
statements.emplace(entry.file, entry.statement);
}
m_entries.clear();
m_scanned = true;
std::error_code error;
auto const root{std::filesystem::path(Global.asCurrentSceneryPath)};
// NOTE: on an error the iterator turns into the end one
for (std::filesystem::recursive_directory_iterator file{root, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error))
{
if (false == file->is_regular_file(error))
{
continue;
}
// the scenery refers to its files with lower case names, relative to the scenery directory
std::string name;
try
{
name = ToLower(file->path().lexically_relative(root).generic_string());
}
catch (std::exception const &)
{
// the name can't be expressed in the encoding the scenery files use, so nothing can include the file anyway
continue;
}
if (false == name.ends_with(".inc"))
{
continue;
}
m_entries.emplace_back(examine(name));
if (auto const lookup{statements.find(name)}; lookup != statements.end())
{
m_entries.back().statement = lookup->second;
}
}
std::sort(std::begin(m_entries), std::end(m_entries), [](include_entry const &Left, include_entry const &Right) { return Left.file < Right.file; });
index();
}
void editorIncludeBank::index()
{
m_ready.clear();
for (std::size_t idx = 0; idx < m_entries.size(); ++idx)
{
if (m_entries[idx].complete)
{
m_ready.emplace_back(idx);
}
}
std::sort(std::begin(m_ready), std::end(m_ready), [this](std::size_t const Left, std::size_t const Right) {
auto const &left{m_entries[Left]};
auto const &right{m_entries[Right]};
return std::tie(left.category, left.name, left.file) < std::tie(right.category, right.name, right.file);
});
++m_revision;
}
void editorIncludeBank::update(std::string const &File)
{
auto const lookup{std::find_if(std::begin(m_entries), std::end(m_entries), [&](include_entry const &Entry) { return Entry.file == File; })};
if (lookup == std::end(m_entries))
{
m_entries.insert(std::upper_bound(std::begin(m_entries), std::end(m_entries), File, [](std::string const &Name, include_entry const &Entry) { return Name < Entry.file; }), examine(File));
}
else
{
// the node bank may hold the template selected, keep its statement
auto const statement{lookup->statement};
*lookup = examine(File);
lookup->statement = statement;
}
index();
}