// slang-command-options.cpp #include "slang-command-options.h" #include "slang-string-util.h" #include "slang-char-util.h" #include "slang-byte-encode-util.h" namespace Slang { SlangResult CommandOptions::_addName(LookupKind kind, const UnownedStringSlice& name, Index targetIndex) { NameKey nameKey; nameKey.kind = kind; nameKey.nameIndex = (Index)m_pool.add(name); if (m_nameMap.tryGetValueOrAdd(nameKey, targetIndex)) { SLANG_ASSERT(!"Option is already added!"); return SLANG_FAIL; } return SLANG_OK; } SlangResult CommandOptions::_addOptionName(const UnownedStringSlice& name, Flags flags, Index targetIndex) { SLANG_RETURN_ON_FAIL(_addName(LookupKind::Option, name, targetIndex)); // Add to prefix flags if (flags & (Flag::CanPrefix | Flag::IsPrefix)) { const auto length = name.getLength(); SLANG_ASSERT(length < 32); m_prefixSizes |= uint32_t(1) << length; } return SLANG_OK; } SlangResult CommandOptions::_addValueName(const UnownedStringSlice& name, Index categoryIndex, Index optionIndex) { return _addName(LookupKind(categoryIndex), name, optionIndex); } SlangResult CommandOptions::_addUserValue(LookupKind kind, UserValue userValue, Index targetIndex) { // If it's invalid we don't need to add it if (userValue == kInvalidUserValue) { return SLANG_OK; } UserValueKey userValueKey; userValueKey.kind = kind; userValueKey.userValue = userValue; if (m_userValueMap.tryGetValueOrAdd(userValueKey, targetIndex)) { SLANG_ASSERT(!"UserValue is already used for this kind!"); return SLANG_FAIL; } return SLANG_OK; } UnownedStringSlice CommandOptions::_addString(const char* text) { if (text == nullptr) { return UnownedStringSlice(); } return _addString(UnownedStringSlice(text)); } UnownedStringSlice CommandOptions::_addString(const UnownedStringSlice& slice) { const auto length = slice.getLength(); const char* dst = m_arena.allocateString(slice.begin(), length); return UnownedStringSlice(dst, length); } Index CommandOptions::_addOption(const UnownedStringSlice& name, const Option& inOption) { return _addOption(&name, 1, inOption); } Index CommandOptions::_addOption(const UnownedStringSlice* names, Count namesCount, const Option& inOption) { SLANG_ASSERT(namesCount > 0); SLANG_ASSERT(inOption.categoryIndex >= 0); if (namesCount <= 0 || inOption.categoryIndex < 0) { return -1; } auto& cat = m_categories[inOption.categoryIndex]; // If there are already options associated with this category, we have to be in the run of the last ones added if (cat.optionStartIndex != cat.optionEndIndex) { // If we aren't at the end then this is an error if (cat.optionEndIndex != m_options.getCount()) { return -1; } } else { // Move to the end of the option list cat.optionStartIndex = m_options.getCount(); cat.optionEndIndex = cat.optionStartIndex; } Option option(inOption); const Index optionIndex = m_options.getCount(); if (cat.kind == CategoryKind::Option) { for (Index i = 0; i < namesCount; ++i) { if (SLANG_FAILED(_addOptionName(names[i], inOption.flags, optionIndex))) { return -1; } } if (SLANG_FAILED(_addUserValue(LookupKind::Option, inOption.userValue, optionIndex))) { return -1; } } else { for (Index i = 0; i < namesCount; ++i) { _addValueName(names[i], inOption.categoryIndex, optionIndex); } if (SLANG_FAILED(_addUserValue(LookupKind(inOption.categoryIndex), inOption.userValue, optionIndex))) { return -1; } } if (namesCount == 1) { // We already have storage on the slice option.names = m_pool.addAndGetSlice(names[0]); } else { // Put all of the names in the list StringBuilder buf; StringUtil::join(names, namesCount, ',', buf); // Allocate storage no in the pool option.names = _addString(buf.getUnownedSlice()); } m_options.add(option); // Set the end index cat.optionEndIndex = optionIndex + 1; return optionIndex; } static void _handlePostFix(UnownedStringSlice& ioSlice, CommandOptions::Flags& ioFlags) { if (ioSlice.endsWith(toSlice("..."))) { if (ioSlice.endsWith(toSlice("?..."))) { ioFlags |= CommandOptions::Flag::CanPrefix; ioSlice = ioSlice.head(ioSlice.getLength() - 4); } else { ioFlags |= CommandOptions::Flag::IsPrefix; ioSlice = ioSlice.head(ioSlice.getLength() - 3); } } } void CommandOptions::add(const char* inName, const char* usage, const char* description, UserValue userValue) { UnownedStringSlice nameSlice(inName); Option option; option.categoryIndex = m_currentCategoryIndex; option.usage = _addString(usage); option.description = _addString(UnownedStringSlice(description)); option.userValue = userValue; option.flags = 0; if (nameSlice.indexOf(',') >= 0) { List names; StringUtil::split(nameSlice, ',', names); for (auto& name : names) { _handlePostFix(name, option.flags); } _addOption(names.getBuffer(), names.getCount(), option); } else { _handlePostFix(nameSlice, option.flags); _addOption(&nameSlice, 1, option); } } void CommandOptions::add(const UnownedStringSlice* names, Count namesCount, const char* usage, const char* description, UserValue userValue, Flags flags) { Option option; option.categoryIndex = m_currentCategoryIndex; option.usage = _addString(usage); option.description = _addString(UnownedStringSlice(description)); option.flags = flags; option.userValue = userValue; _addOption(names, namesCount, option); } Index CommandOptions::_addValue(const UnownedStringSlice& name, const Option& inOption) { SLANG_ASSERT(m_currentCategoryIndex >= 0); SLANG_ASSERT(m_categories[m_currentCategoryIndex].kind == CategoryKind::Value); return _addOption(name, inOption); } void CommandOptions::addValues(const ValuePair* pairs, Count pairsCount) { for (auto& pair : makeConstArrayView(pairs, pairsCount)) { addValue(pair.name, pair.description); } } void CommandOptions::addValues(const ConstArrayView& values) { for (const auto& value : values) { addValue(value.name, UserValue(value.value)); } } void CommandOptions::addValues(const ConstArrayView& values) { for (const auto& value : values) { addValue(value.names, UserValue(value.value)); } } void CommandOptions::addValues(const ConstArrayView& values) { for (const auto& value : values) { addValue(value.names, value.description, UserValue(value.value)); } } void CommandOptions::addValuesWithAliases(const ConstArrayView& inValues) { List values; values.addRange(inValues.getBuffer(), inValues.getCount()); values.sort([](const NameValue& a, const NameValue& b) -> bool { return a.value < b.value; }); List names; const Count count = values.getCount(); Index i = 0; while (i < count) { names.clear(); const auto value = values[i].value; names.add(UnownedStringSlice(values[i++].name)); for (; i < count && values[i].value == value; ++i) { names.add(UnownedStringSlice(values[i].name)); } addValue(names.getBuffer(), names.getCount(), UserValue(value)); } } void CommandOptions::addValue(const UnownedStringSlice& name, UserValue userValue) { Option option; option.categoryIndex = m_currentCategoryIndex; option.userValue = userValue; _addValue(name, option); } void CommandOptions::addValue(const UnownedStringSlice& name, const UnownedStringSlice& description, UserValue userValue) { Option option; option.categoryIndex = m_currentCategoryIndex; option.description = _addString(description); option.userValue = userValue; _addValue(name, option); } void CommandOptions::addValue(const UnownedStringSlice* names, Count namesCount, UserValue userValue) { Option option; option.categoryIndex = m_currentCategoryIndex; option.userValue = userValue; SLANG_ASSERT(m_currentCategoryIndex >= 0); SLANG_ASSERT(m_categories[m_currentCategoryIndex].kind == CategoryKind::Value); _addOption(names, namesCount, option); } void CommandOptions::addValue(const char* inName, const char* description, UserValue userValue) { const UnownedStringSlice name(inName); if (description) { addValue(name, UnownedStringSlice(description), userValue); } else { addValue(name, userValue); } } void CommandOptions::addValue(const char* name, UserValue userValue) { addValue(UnownedStringSlice(name), userValue); } Index CommandOptions::addCategory(CategoryKind kind, const char* name, const char* description, UserValue userValue) { const UnownedStringSlice nameSlice(name); const auto categoryIndex = m_categories.getCount(); if (SLANG_FAILED(_addName(LookupKind::Category, nameSlice, categoryIndex))) { return -1; } if (userValue != kInvalidUserValue) { _addUserValue(LookupKind::Category, userValue, categoryIndex); } Category cat; cat.kind = kind; cat.name = _addString(nameSlice); cat.description = _addString(description); cat.userValue = userValue; m_currentCategoryIndex = categoryIndex; m_categories.add(cat); return categoryIndex; } void CommandOptions::setCategory(const char* name) { const UnownedStringSlice nameSlice(name); for (Index i = 0; i < m_categories.getCount(); ++i) { auto& cat = m_categories[i]; if (cat.name == nameSlice) { m_currentCategoryIndex = i; return; } } SLANG_ASSERT(!"Category not found"); m_currentCategoryIndex = -1; } Index CommandOptions::findTargetIndexByName(LookupKind kind, const UnownedStringSlice& name) const { const auto nameIndex = m_pool.findIndex(name); // If the name isn't in the pool then there isn't a category with this name if (nameIndex < 0) { return -1; } NameKey key; key.kind = kind; key.nameIndex = nameIndex; if (auto ptr = m_nameMap.tryGetValue(key)) { return *ptr; } return -1; } Index CommandOptions::findTargetIndexByUserValue(LookupKind kind, UserValue userValue) const { UserValueKey key; key.kind = kind; key.userValue = userValue; if (auto ptr = m_userValueMap.tryGetValue(key)) { return *ptr; } return -1; } Index CommandOptions::findCategoryByCaseInsensitiveName(const UnownedStringSlice& slice) const { const Count count = m_categories.getCount(); for (Index i = 0; i < count; ++i) { const auto& cat = m_categories[i]; if (cat.name.caseInsensitiveEquals(slice)) { return i; } } return -1; } Index CommandOptions::findOptionByCategoryUserValue(UserValue categoryUserValue, const UnownedStringSlice& name) const { Index categoryIndex = findTargetIndexByUserValue(LookupKind::Category, categoryUserValue); if (categoryIndex < 0) { return -1; } return findValueByName(categoryIndex, name); } Index CommandOptions::findOptionByName(const UnownedStringSlice& name) const { { auto index = findTargetIndexByName(LookupKind::Option, name); if (index >= 0) { return index; } } // We need to search for partials auto prefixSizes = m_prefixSizes; while (prefixSizes) { auto prefixSize = ByteEncodeUtil::calcMsb32(prefixSizes); if (prefixSize < name.getLength()) { // Look it up const auto index = findTargetIndexByName(LookupKind::Option, name.head(prefixSize)); if (index >= 0) { auto& option = m_options[index]; // If the option accepts prefixes, we return the index if (option.flags & (Flag::CanPrefix | Flag::IsPrefix)) { return index; } } } // Remove the bit prefixSizes &= ~(uint32_t(1) << prefixSize); } // Was not found return -1; } ConstArrayView CommandOptions::getOptionsForCategory(Index categoryIndex) const { const auto& cat = m_categories[categoryIndex]; return makeConstArrayView(m_options.getBuffer() + cat.optionStartIndex, cat.optionEndIndex - cat.optionStartIndex); } void CommandOptions::getCategoryOptionNames(Index categoryIndex, List& outNames) const { outNames.clear(); for (const auto& option : getOptionsForCategory(categoryIndex)) { StringUtil::appendSplit(option.names, ',', outNames); } } void CommandOptions::findCategoryIndicesFromUsage(const UnownedStringSlice& slice, List& outCategories) const { const auto* cur = slice.begin(); const auto* end = slice.end(); while (cur < end) { // Find < while (cur < end && *cur != '<') cur++; // If we found it look for the end if (cur < end && *cur == '<') { ++cur; auto start = cur; while (cur < end && (CharUtil::isAlphaOrDigit(*cur) || *cur == '-' || *cur == '_') && *cur != '>') { cur++; } // If we hit closing > we want to lookup if (cur < end && *cur == '>') { const UnownedStringSlice categoryName(start, cur); Index categoryIndex = findCategoryByName(categoryName); if (categoryIndex >= 0 && outCategories.indexOf(categoryIndex) < 0) { outCategories.add(categoryIndex); } } cur++; } } } Count CommandOptions::getOptionCountInRange(Index categoryIndex, UserValue start, UserValue nonInclEnd) const { const UserIndex startIndex = UserIndex(start); const UserIndex endIndex = UserIndex(nonInclEnd); Count count = 0; for (auto& opt : getOptionsForCategory(categoryIndex)) { const auto val = opt.userValue; if (val == kInvalidUserValue) { continue; } const auto valIndex = UserIndex(val); count += Index(valIndex >= startIndex && valIndex < endIndex); } return count; } Count CommandOptions::getOptionCountInRange(LookupKind kind, UserValue start, UserValue nonInclEnd) const { Index count = 0; if (kind == LookupKind::Category) { const UserIndex startIndex = UserIndex(start); const UserIndex endIndex = UserIndex(nonInclEnd); for (auto& cat : m_categories) { if (cat.userValue != kInvalidUserValue) { const auto valIndex = UserIndex(cat.userValue); count += Index(valIndex >= startIndex && valIndex < endIndex); } } } if (kind == LookupKind::Option) { // If we are lookup up options, then we iterate over all option categories const auto catCount = m_categories.getCount(); for (Index categoryIndex = 0; categoryIndex < catCount; ++categoryIndex) { if (m_categories[categoryIndex].kind == CategoryKind::Option) { count += getOptionCountInRange(categoryIndex, start, nonInclEnd); } } } else if (Index(kind) >= 0) { // It's a regular category count = getOptionCountInRange(Index(kind), start, nonInclEnd); } return count; } bool CommandOptions::hasContiguousUserValueRange(LookupKind kind, UserValue start, UserValue nonInclEnd) const { const Count rangeCount = Count(nonInclEnd) - Count(start); SLANG_ASSERT(rangeCount >= 0); if (rangeCount <= 0) { return true; } const Count count = getOptionCountInRange(kind, start, nonInclEnd); return rangeCount == count; } /* !!!!!!!!!!!!!!!!!!!!!!!!!!! CommandOptionsWriter !!!!!!!!!!!!!!!!!!!!!!!!!!! */ void CommandOptionsWriter::appendDescriptionForCategory(const CommandOptions& options, Index categoryIndex) { const auto& categories = options.getCategories(); const auto& category = categories[categoryIndex]; // Header { const auto count = m_builder.getLength(); if (category.kind == CategoryKind::Value) { m_builder << "<" << category.name << ">"; } else { m_builder << category.name; } const auto length = m_builder.getLength() - count; m_builder << "\n"; m_builder.appendRepeatedChar('=', length); m_builder << "\n\n"; // If there is a description output it if (category.description.getLength() > 0) { _appendText(0, category.description); m_builder << "\n"; } } for (auto& option : options.getOptionsForCategory(categoryIndex)) { m_builder << m_indentSlice; if (option.usage.getLength()) { m_builder << option.usage; } else { List names; StringUtil::split(option.names, ',', names); _appendWithWrap(1, names, toSlice(", ")); } if (option.description.getLength() == 0) { m_builder << "\n"; continue; } m_builder << ": "; _appendText(2, option.description); if (option.usage.getLength()) { List usageCategoryIndices; options.findCategoryIndicesFromUsage(option.usage, usageCategoryIndices); for (auto usageCategoryIndex : usageCategoryIndices) { auto& usageCat = categories[usageCategoryIndex]; m_builder << m_indentSlice << m_indentSlice; m_builder << "<" << usageCat.name << "> can be: "; List optionNames; options.getCategoryOptionNames(usageCategoryIndex, optionNames); _appendWithWrap(2, optionNames, toSlice(", ")); m_builder << "\n"; } } } m_builder << "\n"; } void CommandOptionsWriter::appendDescription(const CommandOptions& options) { // Go through categories in order const auto& categories = options.getCategories(); for (Index categoryIndex = 0; categoryIndex < categories.getCount(); ++categoryIndex) { appendDescriptionForCategory(options, categoryIndex); } } void CommandOptionsWriter::_appendText(Count indentCount, const UnownedStringSlice& text) { List lines; StringUtil::calcLines(text, lines); // Remove very last line if it's empty if (lines.getCount() > 1 && lines.getLast().trim().getLength() == 0) { lines.removeLast(); } _appendWithWrap(indentCount, lines); } Count CommandOptionsWriter::_getCurrentLineLength() { // Work out the current line length const char* start = m_builder.begin(); const char* cur = m_builder.end(); Count lineLength = 0; if (cur > start) { for (--cur; cur > start; --cur) { const auto c = *cur; if (c == '\n' || c == '\r') { ++cur; break; } } lineLength = Count(ptrdiff_t(m_builder.end() - cur)); } return lineLength; } void CommandOptionsWriter::_requireIndent(Count indentCount) { const auto length = m_builder.getLength(); if (length) { const auto c = m_builder[length - 1]; if (c == '\n' || c == '\r') { for (Index j = 0; j < indentCount; j++) { m_builder.append(m_indentSlice); } } } } void CommandOptionsWriter::_appendWithWrap(Count indentCount, List& lines) { List words; for (auto line : lines) { if (line.trim().getLength() == 0 || line.startsWith(toSlice(" "))) { // Append the line as is after the indent _requireIndent(indentCount); m_builder << line << "\n"; } else { words.clear(); StringUtil::split(line, ' ', words); _requireIndent(indentCount); _appendWithWrap(indentCount, words, toSlice(" ")); m_builder << "\n"; } } } void CommandOptionsWriter::_appendWithWrap(Count indentCount, List& slices, const UnownedStringSlice& delimit) { Count lineLength = _getCurrentLineLength(); const auto count = slices.getCount(); for (Index i = 0; i < count; ++i) { auto slice = slices[i]; auto sliceLength = slice.getLength(); if (i < count - 1) { sliceLength += delimit.getLength(); } // If out of space onto the next line if (lineLength + sliceLength > m_lineLength) { m_builder.append("\n"); lineLength = indentCount * m_indentSlice.getLength(); for (Index j = 0; j < indentCount; j++) { m_builder.append(m_indentSlice); } } m_builder.append(slice); if (i < count - 1) { m_builder.append(delimit); } lineLength += sliceLength; } } } // namespace Slang