Skip to content

The Command Interaction Refactor

Shalabh Chaturvedi edited this page Oct 24, 2019 · 9 revisions
Status merged
Version 0.7
Author shalabhc
PR #506

This page describes the new interaction and command line system. Currently implemented in the command-interaction-refactor branch and not merged in master. It is very close to being done.

Motivation

The goal of the refactoring is to clean the API and simplify the implementation of interactions and the command line. Currently the implementation is complicated (see command_line.moon) and the API can be complex (for example having both factory and handler APIs - two different ways of implementing command line interactions). There are also design problems, e.g. when using the handler API, the interaction is invoked with a finish function only and window.command_line.text refers to the current top-most command line activity and not necessarily the specific activity that is associated with the factory calling that code.

The interact module is tightly coupled with the command line. The interact.select interaction, which is heavily used is growing with more features but still not capable of handling nested lists, as is common with many interactions.

The New Design

Overview

The command line internals and API have been completely reimplemented.

All existing functionality has been reimplemented, but some commands work differently. E.g. the buffer-grep and buffer-replace commands now use the same code and both show a list of matches in the bottom panel.

The interact Module

The interact module is now just a registry of functions with no dependency on the command line.

The Command Line

  • The core command line API is vastly simpler, and the complexity is spread out over other modules.

  • The factory vs. handler dual API is gone. The new API is more factory like.

  • Simple interactions such as reading some text are implemented directly on top of the command line API.

  • The complex interactions such as variable setting, file exploring, external commands and such are grouped into three distinct styles of interaction:

    • Exploring nested lists (file explorer and config variable setting)
    • Search and replace with preview (buffer search and buffer replace)
    • Command invocation (external commands and howl commands)

    Interactions that fall into one of the above styles are implemented using custom APIs for each style (for example, as an 'explorer API' for exploring nested lists). A view component (e.g. ExplorerView) binds the custom API and displays the interaction in the command line. These are described more below.

Implementation and Core API

CommandPanel and CommandLine

The core command line API uses two classes - the CommandPanel (there is a single instance of this for each window) and the CommandLine (there are zero or more instances for each CommandPanel).

The CommandPanel is always available at howl.window.command_panel. The command_panel\run(view) method creates a new CommandLine, displays it, waits until it is finished and returns the result.

If a second run is invoked before the first is finished a new CommandLine is pushed onto the command panel stack. In this way the stack is represented as an array of CommandLine objects, each with its own set of widgets, including the main text widget. The displayed widgets are only from the topmost CommandLine object.

CommandLine Views

The command_panel\run(view) method must be passed a view object. The view object determines what is displayed in the command line and how it responds to user events. The view object must implement init, on_text_changed and optionally on_finish methods. It can also define a keymap. The view\init(command_line) is called once and passed the instance of the associated CommandLine. The view can store this and call methods on the command line such as command_line.text, command_line.prompt. A special method command_line\finish must be called if the view wants to end the interaction. While the view is active (i.e. before finish is called), the text widget for this command line is displayed and as the user types text, the view\on_text_changed method is invoked. When the view is being closed, either if it called finish itself or cancelled externally, the view\on_finish method is called. A method command_line\add_widget allows the view to add any widgets above the command line. These widgets are automatically removed when this view is disabled or finished.

Simple interactions, such as reading some text from the user are implemented directly as command line views, see text_entry. The coupling looks like this:

CommandLine <-> TextReader

More complex interactions are implemented as generic views. This means the view is further customized when the view object is instantiated. There are three such views currently implemented - ExplorerView, ConsoleView and SearchView, described below. Note that from the perspective of the CommandLine there is no difference between a simple and complex interaction - the both implement the view API. The three custom views only make it easy to create views that implement specific interaction styles.

ExplorerView

The ExplorerView is a generic command line view used to present and allow the user to operate on any nested list structure. The ExplorerView is initialized with a special object called the explorer that implements an 'explorer API'. This view displays the explorer in the command line using list widgets and handles automatic traversing, up and down, of nested objects. The coupling looks like this:

 CommandLine <-> ExplorerView -> Explorer

The Explorer object itself has no knowledge of the command line, but only implements the explorer API methods such as display_items() (which returns a list of items, each of which can also implement the explorer API). Note that the explorer does not call into the view, all calls are one way. All the explorer style interactions are implemented as Explorer instances, for example DirectoryExplorer implements the Explorer API on top of the file system. One useful feature of ExplorerViews is they can refresh themselves when you press ctrl_r. This lets the user see newly added files and buffers to an already open explorer.

The above is a simplified description. The ExplorerView works with an array of Explorer objects not just a single one. The array represents a path from a root object to the currently displayed node. Pressing backspace goes up the path to the previous node. Navigating down by selection adds a new node to this stack. Some API features make the ExplorerView jump to a completely new stack (e.g. while file browsing, typing ~/ goes to the users home directory, irrespective of where your current location is). The ExplorerView is the most complex of all the generic views. It also supports previews of the currently selected item. However it is a very common style of interaction and used by a lot of interactions. Simple lists are just special cases of nested lists.

Because Explorer objects have no dependency on the command line itself, they could be used in the future in other contexts. E.g. maybe we can display them in special ActionBuffers or ListBuffers.

ConsoleView

The ConsoleView is a generic command line view that allows the user to type commands on a prompt and provide some auto completion. The ConsoleView is initialized with a special console object that that implements a 'console API'. The view uses the command line to present that console. It is used by the howl command picker and the external command picker.

SearchView

The SearchView is a generic command line view that allows the user to search or replace chunks of text. It is initialized with a search and optionally, a replace function. The search function must return an iterator of Chunk objects. These are displayed by the search view in a condensed list on the bottom and previewed via highlights in a buffer on the top. The replace function, if provided allows preview based replacement for each Chunk. The view allows hiding the bottom list using alt_s.

The buffer-search and buffer-replace command use the SearchView.

Contextual Help

The help system that shows a popup when you press f1 was embedded in the command line module. This is now a completely separate module and can be used in other places. The HelpContext is an object that stores help text and keystroke help. The get_buffer() method returns a nicely rendered ActionBuffer that contains all the help.

These HelpContext objects can be passed around and merged. This is used by the command module and some command line interactions. Merging is useful when multiple shortcut keys are active from different parts of code that are not tightly coupled. The help from the outer code can be passed into the inner code where it can be merged with a local help object.

The command Module

The command module has been adjusted to use the new interaction and command panel system. There is a slight change in the command registration API. E.g. see changes to app_commands.moon. The input function now takes an opts table where the text, prompt and help fields are passed explicitly. There were previously implicit which made the command module tightly coupled to the command line. This way the input implementation is free to choose any method to display the text and prompt. All current implementations just pass these through to the command line, via the interaction.

The input function returns a single argument only, and the handler now only accepts a single argument only. The only affected command is exec which has been adjusted to use a single table with two fields rather than two parameters.

Command History

There is a new function get_input_text that can be provided by the registered command. This should convert the final result (returned by input and fed into the handler) to a text representation. This is used by the command module to determine the text representation of the input. This input_text is then recorded along with the command name in the command history. Some results such as Files and strings are automatically convertible to text. See get_input_text. This makes the history decoupled form the command line itself, though it still handles the common cases where some text or a file is returned by the command line.

Tests

Test coverage of the code has been improved - most of the new modules have tests. Using the explorer API, decoupled from the command line, makes explorer objects easier to test.

Conclusion

While I think the new system overall is only slightly less complex, I feel the complexity is much better factored and the design is much cleaner. The core command line API is simpler, the explorer implementations are decoupled from the actual display API. It is clear which generic view could be use for any interaction, or new view is needed. We are free to add more generic views if necessary.