gvcode

package module
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: May 10, 2025 License: MIT Imports: 32 Imported by: 1

README

gvcode

gvcode is a Gio based text editor component for code editing.

Key Features:

  • Uses a PieceTable backed text buffer for efficient text editing.
  • Optimized undo/redo operations with built-in support in the PieceTable.
  • Supports both hard and soft tabs, ensuring alignment with tab stops.
  • Lines can be unwrapped, with horizontal scrolling supported.
  • Syntax highlighting is available by applying text styles.
  • Built-in line numbers for better readability.
  • Auto-complete of bracket pairs and quote pairs.
  • Auto-indent new lines.
  • Bracket auto-indent.
  • Increase or descease indents of multi-lines using Tab key and Shift+Tab.
  • Expanded shortcuts support via command registry.
  • Flexible auto-completion via the Completion API, a built-in implementation is provided as an Add-On.
  • Large file rendering(Planned).

Why another code editor?

I ported Gio's editor component to gioview and added a few features to support basic code editing, but it is far from a good code editor. Keep expanding the original editor seems a wise decision, but the design of the original editor makes it hard to adding more features. And it is also not performant enough for large file editing/rendering. Most importantly it lacks tab/tab stop support. When I tried to add tab stop support to it, I found it needs a new overall design, so there is gvcode.

Key Design

Gio's text shaper layout the whole document in one pass, although internally the document is processed by paragraphs. After the shaping flow, there is a iterator style API to get the shaped glyphs one by one. This is what Gio's editor does when layouting the texts.

Gvcode has chosen another way. gvcode read and layout in a paragraph by paragrah manner. The outcomes are joined together to assemble the final document view. This gives up the oppertunity to process the text only visible in the viewport, making incremental shaping possible. Besides that we can also process tab expanding & tab stops at line level, because we have full control of the paragraph layout. To achive that goal, gvcode implemented its own line wrapper.

Screenshots

Example editor

How To Use

Gvcode exports simple APIs to ease the integration with your project. Here is a basic example:

    state := &editor.Editor{}
    var ops op.Ops

    for {
        e := ed.window.Event()

        switch e := e.(type) {
        case app.DestroyEvent:
            return e.Err
        case app.FrameEvent:
            gtx := app.NewContext(&ops, e)
            
            es := gvcode.NewEditor(th, state)
            es.Font.Typeface = "monospace"
            es.TextSize = unit.Sp(12)
            es.LineHeightScale = 1.5
            es.Layout(gtx)

            e.Frame(gtx.Ops)
        }
    }

For a full working example, please see code in the folder ./example.

Configuration

gvcode uses EditorOption to configure the various part of the editor. Here's how you can do that:

    editor := &Editor{}
    editor.WithOptions(
        gvcode.WithSoftTab(true),
        gvcode.WithQuotePairs(quotePairs),
        gvcode.WithBracketPairs(bracketPairs),
        gvcode.WithAutoCompletion(completion),
    )

Some of the configurable properties might be missing from the EditorOption APIs, in this case you can set it directly to the editor. Besides that, a EditorStyle object is provided to easy the initial setup.

Some of the notable options are illustrated below.

  • WithWordSeperators: This is an optional configuration that set the word seperators. gvcode uses common word seperators as default value if there is no custom seperators. Please be aware that unicode whitespaces are always accounted, so there is no need to add them.
  • WithQuotePairs: This configures the characters treated as quotes. Configured quote characters can be auto-completed if the left character is typed. This option is also optional if there is no extra requirements.
  • WithBracketPairs: This configures the characters treated as brackets. Configured brackets characters can be auto-completed if the left character is typed. This option is also optional if there is no extra requirements.
  • WrapLine: This configuration affects how the lines of text are layouted. If setting to true, a line of text will be broken into multiple visual lines when reaching the maximum width of the editor. If it is disabled, the editor make the text scrollable in the horizontal direction.
  • WithAutoCompletion: This configures the auto-completion component. Details are illustrated in the section below.
  • AddBeforePasteHook: This configres a hook to transform the text before pasting text.
Hooks

Hooks are used to intercept various operations and apply custom logic to the data. There is only one hook at this time.

  • BeforePasteHook: Transform the text before pasting text.
Command

Commands are specific key bindings and their handlers when the keys are pressed. They can be used extend the functionality of the editor. Please note that built-in key bindings are not yet open to customize.

Here is how to use it to add a custom key binding.

    // register a command.
    editor.RegisterCommand(tag, key.Filter{Name: key.NameUpArrow, Optional: key.ModShift},
        func(gtx layout.Context, evt key.Event) gvcode.EditorEvent {
            // do something with the event.
            return nil
        },
    )

    // commands can also be removed at runtime
    editor.RemoveCommands(tag)

Commands are managed by groups, and you have to provide a tag (usually a pointer to the widget) when registering. The tag indicates the owner of the commands. If a key is already registered, the newly added one will "replace" the old one, and keyboard events are only delivered to the newly registered command handler until it is removed.

In the case of a overlay widget, this enables us to handle keyboard events without loosing focus of the editor. This is how the completion popup works behind the scene.

Auto-Completion

gvcode provides extensible auto-completion component. The central component is the Completion interface which acts as the scheduler between the editor and the completion algorithm, and the popup widget.

To setup an auto-completion component, we have to provide the following parts:

  1. Trigger: Tell the editor in what condition can it activate the completion. An activated completion shows a popup box.

    • Characters: This activates the completion if the user typed the required characters.
    • KeyTrigger: This activates the competion if the required key binding is pressed.
    • Other: This activates the completion if when any of the allowed characters are typed.
  2. Completor: Completors provide completion algorithms to the completion component. The completion component accepts multiple completors so you can have different completors for different sources.

  3. CompletionPopup: A CompletionPopup shows completion candidates to the user, and user can navigate through them to select the desired one. 4 Completion: The completion component itself.

The code from the example editor shows the usage:

	// Setting up auto-completion.
	cm := &completion.DefaultCompletion{Editor: editorApp.state}

	// set popup widget to let user navigate the candidates.
	popup := completion.NewCompletionPopup(editorApp.state, cm)
	popup.Theme = th
	popup.TextSize = unit.Sp(12)

	cm.AddCompletor(&goCompletor{editor: editorApp.state}, popup)

Cautions

gvcode is not intended to be a drop-in replacement for the official Editor widget as it dropped some features such as single line mode, truncator and max lines limit.

This project is a work in progress, and the APIs may change as development continues. Please use it at your own risk.

Contributing

See the contribution guide for details.

Acknowledgments

This project uses code from the Gio project, which is licensed under the Unlicense OR MIT License.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetDebug added in v0.1.0

func SetDebug(enable bool)

SetDebug enable or disable the debug mode. In debug mode, internal buffer state is printed.

Types

type BeforePasteHook added in v0.1.0

type BeforePasteHook func(text string) string

BeforePasteHook defines a hook to be called before pasting text to transform the text.

type ChangeEvent added in v0.0.2

type ChangeEvent struct{}

A ChangeEvent is generated for every user change to the text.

type CommandHandler added in v0.2.0

type CommandHandler func(gtx layout.Context, evt key.Event) EditorEvent

CommandHandler defines a callback function for the specific key event. It returns an EditorEvent if there is any.

type Completion added in v0.2.0

type Completion interface {
	// AddCompletors adds Completors to Completion. Completors should run independently and return
	// candidates to Completion. A popup is also required to present the cadidates to user.
	AddCompletor(completor Completor, popup CompletionPopup) error

	// OnText update the completion context. If there is no ongoing session, it should start one.
	OnText(ctx CompletionContext)
	// OnConfirm set a callback which is called when the user selected the candidates.
	OnConfirm(idx int)
	// Cancel cancels the current completion session.
	Cancel()
	// IsActive reports if the completion popup is visible.
	IsActive() bool

	// Offset returns the offset used to locate the popup when painting.
	Offset() image.Point
	// Layout layouts the completion selection box as popup near the caret.
	Layout(gtx layout.Context) layout.Dimensions
}

Completion is the main auto-completion interface for the editor. A Completion object schedules flow between the editor, the visual popup widget and completion algorithms(the Completor).

type CompletionCandidate added in v0.2.1

type CompletionCandidate struct {
	// Label is a short text shown to user to indicate
	// what the candicate looks like.
	Label string
	// TextEdit is the real text with range info to be
	// inserted into the editor.
	TextEdit TextEdit
	// A short description of the candicate.
	Description string
	// Kind of the candicate, for example, function,
	// class, keywords etc.
	Kind string
}

CompletionCandidate are results returned from Completor, to be presented to the user to select from.

type CompletionContext added in v0.2.0

type CompletionContext struct {
	// The last key input.
	Input string
	// // Prefix is the text before the caret.
	// Prefix string
	// // Suffix is the text after the caret.
	// Suffix string
	// Coordinates of the caret. Scroll off will change after we update the position,
	// so we use doc view position instead of viewport position.
	Coords image.Point
	// The position of the caret in line/column and selection range.
	Position Position
}

type CompletionPopup added in v0.2.0

type CompletionPopup interface {
	Layout(gtx layout.Context, items []CompletionCandidate) layout.Dimensions
}

type Completor added in v0.2.0

type Completor interface {
	Trigger() Trigger
	Suggest(ctx CompletionContext) []CompletionCandidate
}

Completor defines a interface that each of the delegated completor must implement.

type EditRange added in v0.2.2

type EditRange struct {
	Start Position
	End   Position
}

type Editor added in v0.0.2

type Editor struct {
	// LineNumberGutter specifies the gap between the line number and the main text.
	LineNumberGutter unit.Dp
	// Color used to paint text
	TextMaterial op.CallOp
	// Color used to highlight the selections.
	SelectMaterial op.CallOp
	// Color used to highlight the current paragraph.
	LineMaterial op.CallOp
	// Color used to paint the line number
	LineNumberMaterial op.CallOp
	// Color used to highlight the text snippets, such as search matches.
	TextHighlightMaterial op.CallOp
	// contains filtered or unexported fields
}

Editor implements an editable and scrollable text area.

func (*Editor) CaretCoords added in v0.0.2

func (e *Editor) CaretCoords() f32.Point

CaretCoords returns the coordinates of the caret, relative to the editor itself.

func (*Editor) CaretPos added in v0.0.2

func (e *Editor) CaretPos() (line, col int)

CaretPos returns the line & column numbers of the caret.

func (*Editor) ClearSelection added in v0.0.2

func (e *Editor) ClearSelection()

ClearSelection clears the selection, by setting the selection end equal to the selection start.

func (*Editor) ConvertPos added in v0.2.2

func (e *Editor) ConvertPos(line, col int) int

ConvertPos convert a line/col position to rune offset.

func (*Editor) Delete added in v0.0.2

func (e *Editor) Delete(graphemeClusters int) (deletedRunes int)

Delete runes from the caret position. The sign of the argument specifies the direction to delete: positive is forward, negative is backward.

If there is a selection, it is deleted and counts as a single grapheme cluster.

func (*Editor) DeleteLine added in v0.1.0

func (e *Editor) DeleteLine() (deletedRunes int)

DeleteLine delete the current line, and place the caret at the start of the next line.

func (*Editor) GetCompletionContext added in v0.2.0

func (e *Editor) GetCompletionContext() CompletionContext

GetCompletionContext returns a context from the current caret position. This is usually used in the condition of a key triggered completion.

func (*Editor) Insert added in v0.0.2

func (e *Editor) Insert(s string) (insertedRunes int)

func (*Editor) InsertLine added in v0.1.0

func (e *Editor) InsertLine(s string) (insertedRunes int)

InsertLine insert a line of text before the current line, and place the caret at the start of the current line.

This single line insertion is mainly for paste operation after copying/cutting the current line(paragraph) when there is no selection, but it can also used outside of the editor to insert a entire line(paragraph).

func (*Editor) Layout added in v0.0.2

func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper) layout.Dimensions

func (*Editor) Len added in v0.0.2

func (e *Editor) Len() int

Len is the length of the editor contents, in runes.

func (*Editor) MoveCaret added in v0.0.2

func (e *Editor) MoveCaret(startDelta, endDelta int)

MoveCaret moves the caret (aka selection start) and the selection end relative to their current positions. Positive distances moves forward, negative distances moves backward. Distances are in grapheme clusters, which closely match what users perceive as "characters" even when the characters are multiple code points long.

func (*Editor) ReadOnly added in v0.0.2

func (e *Editor) ReadOnly() bool

func (*Editor) ReadUntil added in v0.2.2

func (e *Editor) ReadUntil(direction int, seperator func(r rune) bool) string

ReadUntil reads in the specified direction from the current caret position until the seperator returns false. It returns the read text.

func (*Editor) RegisterCommand added in v0.2.0

func (e *Editor) RegisterCommand(srcTag any, filter key.Filter, handler CommandHandler)

RegisterCommand register an extra command handler responding to key events. If there is an existing handler, it appends to the existing ones. Only the last key filter is checked during event handling. This method is expected to be invoked dynamically during layout.

func (*Editor) RemoveCommands added in v0.2.0

func (e *Editor) RemoveCommands(tag any)

RemoveCommands unregister command handlers from tag.

func (*Editor) ReplaceAll added in v0.0.2

func (e *Editor) ReplaceAll(texts []TextRange, newStr string) int

ReplaceAll replaces all texts specifed in TextRange with newStr. It returns the number of occurrences replaced.

func (*Editor) ScrollByRatio added in v0.0.2

func (e *Editor) ScrollByRatio(gtx layout.Context, ratio float32)

func (*Editor) SelectedText added in v0.0.2

func (e *Editor) SelectedText() string

SelectedText returns the currently selected text (if any) from the editor.

func (*Editor) Selection added in v0.0.2

func (e *Editor) Selection() (start, end int)

Selection returns the start and end of the selection, as rune offsets. start can be > end.

func (*Editor) SelectionLen added in v0.0.2

func (e *Editor) SelectionLen() int

SelectionLen returns the length of the selection, in runes; it is equivalent to utf8.RuneCountInString(e.SelectedText()).

func (*Editor) SetCaret added in v0.0.2

func (e *Editor) SetCaret(start, end int)

SetCaret moves the caret to start, and sets the selection end to end. start and end are in runes, and represent offsets into the editor text.

func (*Editor) SetHighlights added in v0.0.2

func (e *Editor) SetHighlights(highlights []TextRange)

SetHighlights sets the texts to be highlighted.

func (*Editor) SetText added in v0.0.2

func (e *Editor) SetText(s string)

func (*Editor) TabStyle added in v0.1.1

func (e *Editor) TabStyle() (TabStyle, int)

func (*Editor) Text added in v0.0.2

func (e *Editor) Text() string

Text returns the contents of the editor.

func (*Editor) Update added in v0.0.2

func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool)

Update the state of the editor in response to input events. Update consumes editor input events until there are no remaining events or an editor event is generated. To fully update the state of the editor, callers should call Update until it returns false.

func (*Editor) UpdateTextStyles added in v0.0.2

func (e *Editor) UpdateTextStyles(styles []*TextStyle)

func (*Editor) ViewPortRatio added in v0.0.2

func (e *Editor) ViewPortRatio() (float32, float32)

returns start and end offset ratio of viewport

func (*Editor) WithOptions added in v0.1.0

func (e *Editor) WithOptions(opts ...EditorOption)

WithOptions applies various options to configure the editor.

type EditorEvent added in v0.0.2

type EditorEvent interface {
	// contains filtered or unexported methods
}

type EditorOption added in v0.1.0

type EditorOption func(*Editor)

EditorOption defines a function to configure the editor.

func AddBeforePasteHook added in v0.1.0

func AddBeforePasteHook(hook BeforePasteHook) EditorOption

func ReadOnlyMode added in v0.1.0

func ReadOnlyMode(enabled bool) EditorOption

ReadOnlyMode controls whether the contents of the editor can be altered by user interaction. If set to true, the editor will allow selecting text and copying it interactively, but not modifying it.

func WithAutoCompletion added in v0.2.0

func WithAutoCompletion(completor Completion) EditorOption

func WithBracketPairs added in v0.1.1

func WithBracketPairs(bracketPairs map[rune]rune) EditorOption

WithBracketPairs configures a set of bracket pairs that can be auto-completed when the left half is entered.

func WithQuotePairs added in v0.1.1

func WithQuotePairs(quotePairs map[rune]rune) EditorOption

WithQuotePairs configures a set of quote pairs that can be auto-completed when the left half is entered.

func WithShaperParams added in v0.1.0

func WithShaperParams(font font.Font, textSize unit.Sp, alignment text.Alignment, lineHeight unit.Sp, lineHeightScale float32) EditorOption

WithShaperParams set the basic shaping params for the editor.

func WithSoftTab added in v0.1.0

func WithSoftTab(enabled bool) EditorOption

WithSoftTab controls the behaviour when user try to insert a Tab character. If set to true, the editor will insert the amount of space characters specified by TabWidth, else the editor insert a \t character.

func WithTabWidth added in v0.1.0

func WithTabWidth(tabWidth int) EditorOption

WithTabWidth set how many spaces to represent a tab character. In the case of soft tab, this determines the number of space characters to insert into the editor. While for hard tab, this controls the maximum width of the 'tab' glyph to expand to.

func WithWordSeperators added in v0.1.0

func WithWordSeperators(seperators string) EditorOption

WithWordSeperators configures a set of characters that will be used as word separators when doing word related operations, like navigating or deleting by word.

func WrapLine added in v0.1.0

func WrapLine(enabled bool) EditorOption

WrapLine configures whether the displayed text will be broken into lines or not.

type Position added in v0.2.2

type Position struct {
	// Line number of the caret where the typing is happening.
	Line int
	// Column is the rune offset from the start of the line.
	Column int
	// Runes is the rune offset in the editor text of the input.
	Runes int
}

Position is a position in the eidtor. Line/column and Runes may not be set at the same time depending on the use cases.

type Region added in v0.0.2

type Region = lt.Region

Region describes the position and baseline of an area of interest within shaped text.

type SelectEvent added in v0.0.2

type SelectEvent struct{}

A SelectEvent is generated when the user selects some text, or changes the selection (e.g. with a shift-click), including if they remove the selection. The selected text is not part of the event, on the theory that it could be a relatively expensive operation (for a large editor), most applications won't actually care about it, and those that do can call Editor.SelectedText() (which can be empty).

type TabStyle added in v0.1.1

type TabStyle uint8
const (
	Tabs TabStyle = iota
	Spaces
)

func GuessIndentation added in v0.1.1

func GuessIndentation(text string) (TabStyle, bool, int)

GuessIndentation guesses which kind of indentation the editor is using, returing the kind, if mixed indent is used, and the indent size in the case if spaces indentation.

type TextEdit added in v0.2.2

type TextEdit struct {
	NewText   string
	EditRange EditRange
}

TextEdit is the text with range info to be inserted into the editor, used in auto-completion.

func NewTextEditWithPos added in v0.2.2

func NewTextEditWithPos(text string, start Position, end Position) TextEdit

func NewTextEditWithRuneOffset added in v0.2.2

func NewTextEditWithRuneOffset(text string, start, end int) TextEdit

type TextRange added in v0.0.2

type TextRange struct {
	// offset of the start rune in the document.
	Start int
	// offset of the end rune in the document.
	End int
}

TextRange contains the range of text of interest in the document. It can used for search, styling text, or any other purposes.

type TextStyle added in v0.0.2

type TextStyle struct {
	TextRange
	// Color of the text..
	Color op.CallOp
	// Background color of the painted text in the range.
	Background op.CallOp
}

TextStyle defines style for a range of text in the document.

type Trigger added in v0.2.0

type Trigger struct {
	// Characters that must be present before the caret to trigger the completion.
	// If it is empty, any character will trigger the completion.
	Characters []string

	// Trigger completion even the caret is in side of comment.
	Comment bool
	// Trigger completion even the caret is in side of string(quote pair).
	String bool

	// Special key binding triggers the completion.
	KeyBinding struct {
		Name      key.Name
		Modifiers key.Modifiers
	}
}

Trigger

func (Trigger) ActivateOnKey added in v0.2.2

func (tr Trigger) ActivateOnKey(evt key.Event) bool

Directories

Path Synopsis
addons
internal

Jump to

Keyboard shortcuts

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