input

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 10, 2023 License: MPL-2.0 Imports: 1 Imported by: 12

Documentation

Overview

Package input contains the types that vidar uses to process user input. The big beefy chunk is the input handler, which is the glue between the key strokes, key bindings, and commands.

When Action types are bound to Binder types, there is a specific order in which actions are bound and their corresponding methods are called. The quick summary is:

1. Handler types are bound.

2. Operation types are bound.

3. Hook types are bound, and they are passed to their corresponding Operation types' SetHook methods.

4. SetupOperation types have their Setup method called.

5. Command types are bound.

At each stage, extra checks are made based on any other types that the Action implements - for example, PickyAction types will have their Valid method called, and HookedOperation types will have their Empty method called (and the empty version will be bound).

This means that a Command that relies on some setup in an Operation may implement PickyAction and check on the Operation it relies on. This is particularly useful for Command types that rely on capabilities in the connected language server - the LSP SetupOperation may set up its connection to the server during Setup, and then the Command may check capabilities in Valid.

Index

Constants

View Source
const HandlerName = "input-handler"

HandlerName is used as the name for the primary Handler for a Binder. It is fine to store extra Handler values at other names (for example: if they should not be the primary Handler _yet_, but some event may make them the primary Handler later), but vidar's core code uses this name to look up the Handler to send events to.

Variables

This section is empty.

Functions

func AppendHook

func AppendHook[T Hook](l []T, h Hook) []T

AppendHook is sugar for appending a Hook type to a slice of Hook types, but only if it implements T.

This is provided for use in a HookedOperation's SetHook method to reduce the temptation to use a type switch. A single Hook may implement multiple types, so using a type switch is naive and can cause difficult-to-diagnose problems when a Hook type is only treated as one of the types it implements.

func Get

func Get[T any](ctx context.Context, b Binder, a Action, key any) (T, bool)

Get is sugar for retrieving a value from b using b.Get, but returning a specific type. Get returns false if the value didn't exist or the value was not of type T.

This is equivalent to:

v, ok := b.Get(ctx, a, key)
if !ok {
    // handle not ok
}
t, ok := v.(T)
if !ok {
    // handle not ok
}

Types

type Action

type Action interface {
	// Name must be a unique name identifying this Action.  In rare
	// occasions, some plugins MAY choose to override an existing
	// Action by naming one of their Action types the same as the
	// Action to be overridden.  They MUST NOT rely on their
	// overriding Action to be bound, however, as the user may
	// choose to disable their action.
	Name(context.Context) string
}

Action is any type which can perform actions against vidar. The Action type is a base type, and all Action types are expected to also implement one of Operation, Command, or Hook.

type Actions

type Actions interface {
	// Named looks for and returns the Action with a Name() matching name.
	// Returns nil if no matching Action was found.
	Named(_ context.Context, name string) Action
}

Actions is a group of Action values that has some helper methods.

type Binder

type Binder interface {
	// Set sets some state on the Binder bound to a given Action type. Plugins
	// that need to track state over time that isn't directly implemented by the
	// binder in question may store their state here.
	//
	// The Action argument ensures that the value is set on the Binder that the
	// Action was bound to, especially in the case of PickyAction types.
	//
	// Returns false if the no Binder can be found for the Action argument.
	//
	// Typically, key will be an unexported local empty struct type. Think of
	// this like context.WithValue, except that the Binder is modified in-place
	// instead of returning a copy with the new value added.
	//
	// Set and Get are goroutine-safe.
	Set(_ context.Context, _ Action, key, value any) bool

	// Get retrieves state previously stored on the Binder bound to the Action.
	// See Set for more info.
	//
	// If no value has been set, the return value will be (nil, true).
	Get(_ context.Context, _ Action, key any) (any, bool)

	// Bound returns the Actions that are bound to the Binder and all of the
	// Binder's parents. Any Actions that are bound to one of the Binder's
	// parents but not to the Binder will execute against the parent instead
	// when they are passed to Run.
	Bound(context.Context) Actions

	// Run will run the Action passed in. Only core Operation types (which have
	// access to internal packages) are able to *directly* manipulate the
	// Binder's state, so anything that is *not* a core operation should
	// manipulate state by looking up other Operations bound to the Binder and
	// passing them to this method.
	Run(context.Context, Action) error
}

Binder is any element which can have ops or commands bound to it. It is implemented mostly by the core, internal types that make up vidar.

It is often reasonable to use type assertions to read data from the Binder, but nothing outside of vidar's core operations should directly manipulate the state of a Binder.

type Command

type Command interface {
	Action

	// Type is required to be implemented by the Command so that it cannot also
	// implement Operation or Hook. This is done to encourage decoupling among
	// Actions.
	Type(Command)

	// Menu returns the toolbar menu that this Command should be listed under.
	Menu(context.Context) string

	// Op looks up and returns the Operation value that should be executed when
	// this Command is triggered. If the Command needs to execute multiple
	// Operations, look up and use 'multi-operation' on the Binder (see
	// core/op/multi.go).
	Op(context.Context, Binder) (Operation, error)
}

Command is any type which is triggered by key bindings. It is required to return an Operation to execute rather than actually performing work itself in order to ensure that any Hook values which may be bound to the Operation are also executed.

type Dialog

type Dialog interface {
	// Inform is used to show information to the user.
	//
	// The returned function must be called to clear the message. While I
	// would prefer to use a sum type as an argument to inform the dialog
	// how long messages should be displayed for (either a time.Duration or
	// a context.Context, for instance), go doesn't have sum types. This is
	// the cleanest alternative that I could think of.
	//
	// It is the responsibility of the caller to ensure that the function
	// will eventually be called.
	Inform(_ context.Context, uiElement any) (done func(), err error)

	// Prompt is used to request information from the user. Text input will
	// be processed by whatever input handler is loaded (note: it is
	// possible for a different input handler to load for the dialog than
	// for the main editor); once the input handler has received a signal
	// that the user is done, it will be returned.
	//
	// The uiMessage argument should be the prompt. The Dialog
	// implementation will take care of creating a text box to read back
	// user input, because the Dialog needs to know which element to
	// focus/unfocus and process events for.
	Prompt(_ context.Context, uiMessage any) (string, error)
}

Dialog is a type that can display information to a user and get input from a user. Since we are modeling vidar after vim- and emacs-like editors, this is typically a persistent UI element rather than a separate window that pops up.

type Edit

type Edit struct {
	At  int
	Len int
	New []rune
}

Edit is a type containing details about edited text.

Multiple edits (e.g. in a slice) will be applied in order, with each edit being applied to a new state of the text. This means that when applying multiple edits, each edit's At should take the previous edit into account.

In rough code terms, this is how a slice of edits will be applied:

state := ed.State(ctx)
for _, e := range []Edit{e1, e2, ... eN} {
    state = apply(state, e)
}

In order for e2.At to be correct, it must take into account e1.Len and len(e1.New).

type Handler

type Handler interface {
	// Name implements Action, allowing input handlers to be bound to Binders as
	// normal action values so that they can be looked up via Bound().Named()
	//
	// Normally, this should return HandlerName; but in some circumstances, it
	// may be okay to provide extra Handler implementations that do not directly
	// override the primary Handler.
	Name(context.Context) string

	// SetDefaultKeys takes an action and attempts to set defaults for the
	// action in this Handler's preferences.
	//
	// The set of Actions bound to various Binders may change frequently, and
	// each time they change they should pass new (and possibly existing) Action
	// values to this method. Handler implementations must be ready for that,
	// and avoid overriding any already-bound Actions in their preferences.
	SetDefaultKeys(context.Context, Binder, Action)

	//KeyPressed will be called for every key down that
	// is received through the UI library's keyboard events.
	KeyPressed(context.Context, Binder, KeyEvent)

	// CharInput will be called for each character that is received through
	// system text input.
	CharInput(context.Context, Binder, rune)
}

Handler is a type which maps commands to user input via key bindings and processes key strokes. It is up to the Handler to decide whether a key stroke should trigger a command or write text to the focused element.

If you want vim-style key bindings, this is the type that your plugin should implement.

type Hook

type Hook interface {
	Action

	// Type is required to be implemented by the Hook so that it
	// cannot also implement Command or Operation.  This is done
	// to encourage decoupling among Actions.
	Type(Hook)

	// OpNames returns a list of operation names which this Hook
	// must be bound to.
	//
	// While it's much more common for a Hook to bind to a single
	// Operation than to multiple, implementing branching logic
	// for a SingleOpHook vs MultiOpHook is much more trouble
	// than it's worth.
	OpNames(context.Context) []string
}

Hook is any type which attaches itself to one or more Operation values, executing whenever the Operation performs certain work. Examples include calling gofmt/goimports before a file is saved or triggering syntax highlighting to update when text changes.

type HookedOperation

type HookedOperation interface {
	Operation

	// Empty returns an empty copy of this HookedOperation, with no hooks bound.
	// This is important in case extra hooks need to be bound with a specific
	// type of file (e.g. syntax highlighting hooks) - the returned
	// HookedOperation *must* be in an empty state.
	Empty(context.Context) HookedOperation

	// SetHook sets the Hook in the HookedOperation, so that when certain
	// actions are performed by the Operation it will trigger the actions on the
	// Hook.
	//
	// Each HookedOperation may support as many Hook interface types as it would
	// like. See the documentation for the specific Operation to see which
	// interface type(s) it supports.
	SetHook(context.Context, Hook) error
}

HookedOperation is any Operation type which additionally allows Hook types to bind to it.

type Key

type Key int

Key is a keycode from a keyboard.

type KeyEvent

type KeyEvent struct {
	// Code is the key code that triggered the event.
	Code Key

	// Mods is a list of keys that were held down at the time of the event.
	//
	// Vidar passes *all* keys that were down at the time of the event in this
	// slice, ordered by key code (i.e. key code 12 will be before key code 113,
	// etc). This is intended to allow implementations of input.Handler to bind
	// things like c+a in non-input modes, if desired.
	//
	// Unfortunately, this slice makes the KeyEvent non-hashable, so creating a
	// map requires some translation back and forth. We make sure that the Mods
	// slice has a consistent order to make this easier.
	//
	// See the reference Handler implementation (core/input.Handler) for a very
	// simple (albeit not super fast) method of translating events to strings.
	Mods []Key
}

KeyEvent is a single keyboard event, triggered on key down. It contains the key that caused the event, along with any modifier keys that were held down at the time.

Various UI libraries contain constants for key codes. We're not redefining them here in order to keep this package free of imports; it's expected that the Handler implementation will use whichever libraries it needs in order to translate key codes to user-friendly formats and back.

type LineEdit

type LineEdit struct {
	StartLine, StartCol int
	EndLine, EndCol     int
	New                 []rune
}

LineEdit is a type containing line/column information about edited text.

This operates the same as Edit, except that it uses start and end line/column positions instead of a raw character offset. This type is supported as a first party type for integration with third party tools, mostly the language server protocol.

type NamedConstructor

type NamedConstructor struct {
	// Name must be the name of the Handler that would be returned.
	Name string

	// Construct must initalize the Handler, launching any required goroutines
	// and setting up any initial state.
	//
	// The ui.Creator is passed in to provide access to the UI goroutine for
	// applying text edits.
	Construct func() (Handler, error)
}

NamedConstructor is a type that must be used to wrap up Handler constructors.

type Operation

type Operation interface {
	Action

	// Type is required to be implemented by the Operation so that it cannot
	// also implement Command or Hook. This is done to encourage decoupling
	// among Actions.
	Type(Operation)

	// Run runs the Operation as configured against the Binder. Plugins will
	// *almost always* need to look up other Operations and call
	// Binder.Run(Operation) in order to manipulate state. Methods which
	// manipulate Binders' state are almost always unexported to ease
	// development of hooks that need to be triggered every time the editor's
	// state changes.
	Run(context.Context, Binder) error
}

Operation is any type which performs core operations, like moving the caret or opening a new file.

type PickyAction

type PickyAction interface {
	Action

	// Valid returns nil if the Action can/should be bound to b, or a non-nil
	// error that explains the reason it cannot be bound if not.
	//
	// Note: once an Action has been bound to a Binder, it cannot be un-bound.
	// This means that if Valid checks for bound actions, it should be aware
	// of the order in which actions are bound to Binder types. See the package
	// documentation for details.
	Valid(context.Context, Binder) error
}

PickyAction is picky about the input.Binder types which it can be bound to. Implement this if your Action type should only be bound to specific types of input.Binders.

Note that Action types are bound in a specific order. See package documentation for details.

For example:

func (c *someGoCommand) Valid(b Binder) error {
    ed, ok := b.(Pather)
    if !ok {
        return errWrongBinderType
    }
    path, ok := ed.Path()
    if !ok || !strings.HasSuffix(ed, ".go") {
        return errWrongFileType
    }
    return nil
}

type SetupOperation

type SetupOperation interface {
	Operation

	// Setup will be called after all Hook and Operation types are bound, but
	// before any Command types are bound.
	Setup(context.Context, Binder) error
}

SetupOperation is a type of operation that needs to perform some setup after it and its Hooks are bound, but before Commands are bound.

This was added to support language servers, so that the following use cases will work:

  • Some operations add new client capabilities to the editor and register them using hooks when a file is opened.
  • Some commands rely on server capabilities in the attached language server and need to know if a capability is supported before they're bound.
  • The language server needs to know about the client capabilities during startup, and can't provide its server capabilities until it has started.

In this case, the language server operation implements SetupOperation so that after hooks have registered client capabilities, it can launch the language server and store the server capabilities. Then the commands can implement PickyAction and ask the language server operation about the connected server's capabilities and error if they should not be bound.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL