diff --git a/src/command_manager.cc b/src/command_manager.cc index 267dc44e6..1a55a9571 100644 --- a/src/command_manager.cc +++ b/src/command_manager.cc @@ -267,7 +267,7 @@ void CommandManager::execute(const String& command_line, if (it->type() == Token::Type::OptionExpand) { const Option& option = context.options()[it->content()]; - params.push_back(option.as_string()); + params.push_back(option.get_as_string()); } if (it->type() == Token::Type::CommandSeparator) { diff --git a/src/commands.cc b/src/commands.cc index e143accad..6dce39869 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -466,7 +466,24 @@ void set_option(OptionManager& options, const CommandParameters& params, if (params.size() != 2) throw wrong_argument_count(); - options.set_option(params[0], Option(params[1])); + options.get_local_option(params[0]).set_from_string(params[1]); +} + +void declare_option(const CommandParameters& params, Context& context) +{ + if (params.size() != 2 and params.size() != 3) + throw wrong_argument_count(); + Option* opt = nullptr; + + if (params[0] == "int") + opt = &GlobalOptions::instance().declare_option(params[1], 0); + else if (params[0] == "str") + opt = &GlobalOptions::instance().declare_option(params[1], ""); + else + throw runtime_error("unknown type " + params[0]); + + if (params.size() == 3) + opt->set_from_string(params[2]); } template @@ -878,6 +895,7 @@ void register_commands() [](const Context& context, const String& prefix, ByteCount cursor_pos) { return context.window().options().complete_option_name(prefix, cursor_pos); } })); + cm.register_command("decl", declare_option); cm.register_commands({"ca", "colalias"}, define_color_alias); cm.register_commands({"name"}, set_client_name); diff --git a/src/completion.cc b/src/completion.cc index 2c7cea118..ad66ee3c9 100644 --- a/src/completion.cc +++ b/src/completion.cc @@ -36,7 +36,7 @@ CandidateList complete_filename(const Context& context, String dirprefix; String fileprefix = real_prefix; - boost::regex ignored_files = make_regex_ifp(context.options()["ignored_files"].as_string()); + boost::regex ignored_files = make_regex_ifp(context.options()["ignored_files"].get()); ByteCount dir_end = -1; for (ByteCount i = 0; i < real_prefix.length(); ++i) diff --git a/src/file.cc b/src/file.cc index 6d184349a..c2b8c1702 100644 --- a/src/file.cc +++ b/src/file.cc @@ -135,8 +135,8 @@ Buffer* create_buffer_from_file(const String& filename) Buffer* buffer = new Buffer(filename, Buffer::Flags::File, std::move(lines)); OptionManager& options = buffer->options(); - options.set_option("eolformat", Option(crlf ? "crlf" : "lf")); - options.set_option("BOM", Option(bom ? "utf-8" : "no")); + options.get_local_option("eolformat").set(crlf ? "crlf" : "lf"); + options.get_local_option("BOM").set(bom ? "utf-8" : "no"); return buffer; } @@ -159,7 +159,7 @@ static void write(int fd, const memoryview& data, const String& filename) void write_buffer_to_file(const Buffer& buffer, const String& filename) { - String eolformat = buffer.options()["eolformat"].as_string(); + String eolformat = buffer.options()["eolformat"].get(); if (eolformat == "crlf") eolformat = "\r\n"; else @@ -172,7 +172,7 @@ void write_buffer_to_file(const Buffer& buffer, const String& filename) throw file_access_error(filename, strerror(errno)); auto close_fd = on_scope_end([fd]{ close(fd); }); - if (buffer.options()["BOM"].as_string() == "utf-8") + if (buffer.options()["BOM"].get() == "utf-8") ::write(fd, "\xEF\xBB\xBF", 3); for (LineCount i = 0; i < buffer.line_count(); ++i) diff --git a/src/filters.cc b/src/filters.cc index 267144a9d..238de5e05 100644 --- a/src/filters.cc +++ b/src/filters.cc @@ -37,7 +37,7 @@ void cleanup_whitespaces(Buffer& buffer, Selection& selection, String& content) void expand_tabulations(Buffer& buffer, Selection& selection, String& content) { - const int tabstop = buffer.options()["tabstop"].as_int(); + const int tabstop = buffer.options()["tabstop"].get(); if (content == "\t") { int column = 0; diff --git a/src/highlighters.cc b/src/highlighters.cc index b406ab082..3afd50ce7 100644 --- a/src/highlighters.cc +++ b/src/highlighters.cc @@ -200,7 +200,7 @@ HighlighterAndId highlight_search_factory(const HighlighterParameters params) void expand_tabulations(const OptionManager& options, DisplayBuffer& display_buffer) { - const int tabstop = options["tabstop"].as_int(); + const int tabstop = options["tabstop"].get(); for (auto& line : display_buffer.lines()) { for (auto atom_it = line.begin(); atom_it != line.end(); ++atom_it) diff --git a/src/input_handler.cc b/src/input_handler.cc index 663a924db..16454499e 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -377,7 +377,7 @@ public: DisplayCoord menu_pos{ context().ui().dimensions().line, 0_char }; context().ui().menu_show(candidates, menu_pos, MenuStyle::Prompt); - bool use_common_prefix = context().options()["complete_prefix"].as_int(); + bool use_common_prefix = context().options()["complete_prefix"].get(); String prefix = use_common_prefix ? common_prefix(candidates) : String(); if (m_completions.end - m_completions.start > prefix.length()) prefix = line.substr(m_completions.start, diff --git a/src/main.cc b/src/main.cc index 0bc2603e0..2e8692bd2 100644 --- a/src/main.cc +++ b/src/main.cc @@ -176,7 +176,7 @@ void do_search(Context& context) RegisterManager::instance()['/'] = ex; context.push_jump(); } - else if (ex.empty() or not context.options()["incsearch"].as_int()) + else if (ex.empty() or not context.options()["incsearch"].get()) return; context.editor().select(std::bind(select_next_match, _1, ex), mode); @@ -338,7 +338,7 @@ void do_join(Context& context) void do_indent(Context& context) { - size_t width = context.options()["indentwidth"].as_int(); + size_t width = context.options()["indentwidth"].get(); String indent(' ', width); Editor& editor = context.editor(); @@ -351,7 +351,7 @@ void do_indent(Context& context) void do_deindent(Context& context) { - int width = context.options()["indentwidth"].as_int(); + int width = context.options()["indentwidth"].get(); Editor& editor = context.editor(); DynamicSelectionList sels{editor.buffer(), editor.selections()}; auto restore_sels = on_scope_end([&]{ editor.select((SelectionList)std::move(sels)); }); @@ -745,7 +745,7 @@ void register_env_vars() { return runtime_directory(); }); shell_manager.register_env_var("opt_.+", [](const String& name, const Context& context) - { return context.options()[name.substr(4_byte)].as_string(); }); + { return context.options()[name.substr(4_byte)].get_as_string(); }); shell_manager.register_env_var("reg_.+", [](const String& name, const Context& context) { return RegisterManager::instance()[name[4]].values(context)[0]; }); diff --git a/src/option_manager.cc b/src/option_manager.cc index 3857c78e8..f08a23e96 100644 --- a/src/option_manager.cc +++ b/src/option_manager.cc @@ -6,6 +6,54 @@ namespace Kakoune { +template +class TypedOption : public Option +{ +public: + TypedOption(OptionManager& manager, String name, const T& value) + : Option(manager, std::move(name)), m_value(value) {} + + void set(const T& value) + { + if (m_value != value) + { + m_value = value; + m_manager.on_option_changed(*this); + } + } + const T& get() const { return m_value; } + + String get_as_string() const override; + void set_from_string(const String& str) override; + + Option* clone(OptionManager& manager) const override + { + return new TypedOption{manager, name(), m_value}; + } +private: + T m_value; +}; + +Option::Option(OptionManager& manager, String name) + : m_manager(manager), m_name(std::move(name)) {} + +template const T& Option::get() const { return dynamic_cast*>(this)->get(); } +template void Option::set(const T& val) { return dynamic_cast*>(this)->set(val); } + +// TypedOption specializations; +template<> String TypedOption::get_as_string() const { return m_value; } +template<> void TypedOption::set_from_string(const String& str) { set(str); } +template class TypedOption; +template const String& Option::get() const; +template void Option::set(const String&); + +// TypedOption specializations; +template<> String TypedOption::get_as_string() const { return int_to_str(m_value); } +template<> void TypedOption::set_from_string(const String& str) { set(str_to_int(str)); } +template class TypedOption; +template const int& Option::get() const; +template void Option::set(const int&); + OptionManager::OptionManager(OptionManager& parent) : m_parent(&parent) { @@ -33,23 +81,33 @@ void OptionManager::unregister_watcher(OptionManagerWatcher& watcher) m_watchers.erase(it); } -void OptionManager::set_option(const String& name, const Option& value) +template +auto find_option(T& container, const String& name) -> decltype(container.begin()) { - Option old_value = m_options[name]; - m_options[name] = value; + using ptr_type = decltype(*container.begin()); + return find_if(container, [&name](const ptr_type& opt) { return opt->name() == name; }); +} - if (old_value != value) +Option& OptionManager::get_local_option(const String& name) +{ + auto it = find_option(m_options, name); + if (it != m_options.end()) + return **it; + else if (m_parent) { - for (auto watcher : m_watchers) - watcher->on_option_changed(name, value); + m_options.emplace_back((*m_parent)[name].clone(*this)); + return *m_options.back(); } + else + throw option_not_found(name); + } const Option& OptionManager::operator[](const String& name) const { - auto it = m_options.find(name); + auto it = find_option(m_options, name); if (it != m_options.end()) - return it->second; + return **it; else if (m_parent) return (*m_parent)[name]; else @@ -65,42 +123,60 @@ CandidateList OptionManager::complete_option_name(const String& prefix, result = m_parent->complete_option_name(prefix, cursor_pos); for (auto& option : m_options) { - if (option.first.substr(0, real_prefix.length()) == real_prefix and - not contains(result, option.first)) - result.push_back(option.first); + const auto& name = option->name(); + if (name.substr(0, real_prefix.length()) == real_prefix and + not contains(result, name)) + result.push_back(name); } return result; } -OptionManager::OptionMap OptionManager::flatten_options() const +OptionManager::OptionList OptionManager::flatten_options() const { - OptionMap res = m_parent ? m_parent->flatten_options() : OptionMap(); + OptionList res = m_parent ? m_parent->flatten_options() : OptionList{}; for (auto& option : m_options) - res.insert(option); + { + auto it = find_option(res, option->name()); + if (it != res.end()) + *it = option.get(); + else + res.emplace_back(option.get()); + } return res; } -void OptionManager::on_option_changed(const String& name, const Option& value) +void OptionManager::on_option_changed(const Option& option) { // if parent option changed, but we overrided it, it's like nothing happened - if (m_options.find(name) != m_options.end()) + if (&option.manager() != this and + find_option(m_options, option.name()) != m_options.end()) return; for (auto watcher : m_watchers) - watcher->on_option_changed(name, value); + watcher->on_option_changed(option); } GlobalOptions::GlobalOptions() : OptionManager() { - set_option("tabstop", Option(8)); - set_option("indentwidth", Option(4)); - set_option("eolformat", Option("lf")); - set_option("BOM", Option("no")); - set_option("shell", Option("sh")); - set_option("complete_prefix", Option(1)); - set_option("incsearch", Option(1)); - set_option("ignored_files", Option(R"(^(\..*|.*\.(o|so|a))$)")); + declare_option("tabstop", 8); + declare_option("indentwidth", 4); + declare_option("eolformat", "lf"); + declare_option("BOM", "no"); + declare_option("shell", "sh"); + declare_option("complete_prefix", 1); + declare_option("incsearch", 1); + declare_option("ignored_files", R"(^(\..*|.*\.(o|so|a)))$)"); + declare_option("filetype", ""); +} + +template +Option& GlobalOptions::declare_option(const String& name, const T& value) +{ + if (find_option(m_options, name) != m_options.end()) + throw runtime_error("option " + name + " already declared"); + m_options.emplace_back(new TypedOption{*this, name, value}); + return *m_options.back(); } } diff --git a/src/option_manager.hh b/src/option_manager.hh index 7b7f81c84..804433231 100644 --- a/src/option_manager.hh +++ b/src/option_manager.hh @@ -16,23 +16,27 @@ struct option_not_found : public runtime_error : runtime_error("option not found: " + name) {} }; +class OptionManager; + class Option { public: - Option() {} - explicit Option(int value) : m_value(int_to_str(value)) {} - explicit Option(const String& value) : m_value(value) {} + Option(OptionManager& manager, String name); + virtual ~Option() {} - Option& operator=(int value) { m_value = int_to_str(value); return *this; } - Option& operator=(const String& value) { m_value = value; return *this; } + template const T& get() const; + template void set(const T& val); - bool operator==(const Option& other) const { return m_value == other.m_value; } - bool operator!=(const Option& other) const { return m_value != other.m_value; } + virtual String get_as_string() const = 0; + virtual void set_from_string(const String& str) = 0; - int as_int() const { return str_to_int(m_value); } - String as_string() const { return m_value; } -private: - String m_value; + String name() const { return m_name; } + OptionManager& manager() const { return m_manager; } + + virtual Option* clone(OptionManager& manager) const = 0; +protected: + OptionManager& m_manager; + String m_name; }; class OptionManagerWatcher @@ -40,8 +44,7 @@ class OptionManagerWatcher public: virtual ~OptionManagerWatcher() {} - virtual void on_option_changed(const String& name, - const Option& option) = 0; + virtual void on_option_changed(const Option& option) = 0; }; class OptionManager : private OptionManagerWatcher @@ -51,29 +54,27 @@ public: ~OptionManager(); const Option& operator[] (const String& name) const; - - void set_option(const String& name, const Option& value); + Option& get_local_option(const String& name); CandidateList complete_option_name(const String& prefix, ByteCount cursor_pos); - typedef std::unordered_map OptionMap; - OptionMap flatten_options() const; + using OptionList = std::vector; + OptionList flatten_options() const; void register_watcher(OptionManagerWatcher& watcher); void unregister_watcher(OptionManagerWatcher& watcher); + void on_option_changed(const Option& option) override; private: OptionManager() : m_parent(nullptr) {} // the only one allowed to construct a root option manager friend class GlobalOptions; - OptionMap m_options; + std::vector> m_options; OptionManager* m_parent; - void on_option_changed(const String& name, const Option& value); - std::vector m_watchers; }; @@ -82,8 +83,10 @@ class GlobalOptions : public OptionManager, { public: GlobalOptions(); -}; + template + Option& declare_option(const String& name, const T& inital_value); +}; } diff --git a/src/rc/client.kak b/src/rc/client.kak index 6bb0cd05e..234c8353f 100644 --- a/src/rc/client.kak +++ b/src/rc/client.kak @@ -1,4 +1,4 @@ -setg termcmd %sh{ +decl str termcmd %sh{ if [[ -n "$TMUX" ]]; then echo "'tmux split-window -h'" else diff --git a/src/rc/grep.kak b/src/rc/grep.kak index b050f862c..53e2c2df9 100644 --- a/src/rc/grep.kak +++ b/src/rc/grep.kak @@ -1,4 +1,4 @@ -setg grepcmd 'grep -RHn' +decl str grepcmd 'grep -RHn' def -shell-params -file-completion \ grep %{ %sh{ diff --git a/src/rc/make.kak b/src/rc/make.kak index a2a6d10fe..269ef10b2 100644 --- a/src/rc/make.kak +++ b/src/rc/make.kak @@ -1,4 +1,4 @@ -setg makecmd make +decl str makecmd make def -shell-params make %{ %sh{ output=$(mktemp -d -t kak-make.XXXXXXXX)/fifo diff --git a/src/shell_manager.cc b/src/shell_manager.cc index 31a9d4c23..31ff0b1b3 100644 --- a/src/shell_manager.cc +++ b/src/shell_manager.cc @@ -119,7 +119,7 @@ String ShellManager::pipe(const String& input, ++it; } - String shell = context.options()["shell"].as_string(); + String shell = context.options()["shell"].get(); std::vector execparams = { shell.c_str(), "-c", cmdline.c_str() }; if (not params.empty()) execparams.push_back(shell.c_str()); diff --git a/src/window.cc b/src/window.cc index e38900a74..473fa5f56 100644 --- a/src/window.cc +++ b/src/window.cc @@ -30,7 +30,7 @@ Window::Window(Buffer& buffer) m_builtin_highlighters.append({"selections", [this](DisplayBuffer& db) { highlight_selections(selections(), db); }}); for (auto& option : m_options.flatten_options()) - on_option_changed(option.first, option.second); + on_option_changed(*option); } Window::~Window() @@ -177,9 +177,9 @@ DisplayCoord Window::display_position(const BufferIterator& iterator) return { 0, 0 }; } -void Window::on_option_changed(const String& name, const Option& option) +void Window::on_option_changed(const Option& option) { - String desc = name + "=" + option.as_string(); + String desc = option.name() + "=" + option.get_as_string(); Context hook_context{*this}; m_hooks.run_hook("WinSetOption", desc, hook_context); } diff --git a/src/window.hh b/src/window.hh index 9c62356f6..68276422b 100644 --- a/src/window.hh +++ b/src/window.hh @@ -51,7 +51,7 @@ public: private: Window(const Window&) = delete; - void on_option_changed(const String& name, const Option& option) override; + void on_option_changed(const Option& option) override; void scroll_to_keep_cursor_visible_ifn();