Internal state for the Editor GenServer.
Field categories
EditorState fields fall into three categories:
Workspace fields live in state.workspace (MingaEditor.Workspace.State)
and are saved/restored when switching tabs. Each tab carries a snapshot
of the workspace so switching tabs restores the full editing context.
Shell fields live in state.shell_state (MingaEditor.Shell.Traditional.State)
and hold presentation concerns: chrome, overlays, transient UI state.
The active shell module is state.shell. See MingaEditor.Shell for the
behaviour definition.
Global fields are shared across all tabs and never snapshotted:
port_manager, theme, render_timer, focus_stack,
tab_bar, capabilities, layout, modeline_click_regions,
tab_bar_click_regions, agent, picker_ui, whichkey.
Composed sub-structs
MingaEditor.Workspace.State— per-tab editing context (buffers, windows, vim, etc.)MingaEditor.Shell.Traditional.State— presentation state (nav_flash, hover, dashboard, etc.)MingaEditor.State.Picker— picker instance, source, restore indexMingaEditor.State.WhichKey— which-key popup node, timer, visibilityMingaEditor.State.Registers— named registers and active register selection
Summary
Types
Display metadata derived from the active window's content type.
A document highlight range from the LSP server.
Line number display style.
Functions
Returns the active buffer index.
Returns display metadata for the active window's content.
Returns the active window struct, or nil if windows aren't initialized.
Returns the active window's viewport, falling back to state.workspace.viewport
when no window is active. Use this for scroll commands that need to
read/write the viewport of the focused window (not the terminal-level
viewport).
Adds a new buffer and makes it the active buffer for the current window.
Pure variant of add_buffer/2. Returns {state, effects} instead of
performing side effects directly.
Applies render pipeline mutations back to the editor state.
Returns the active buffer pid.
Returns the buffer list.
Builds a complete per-tab context for an agent tab.
Pure variant of remove_dead_buffer/2. Returns {state, effects} instead
of performing side effects directly.
Finds the agent chat window in the windows map.
Returns the index of the buffer whose file path matches file_path, or nil.
Switches focus to the given window, saving the current cursor to the outgoing window and restoring the target window's stored cursor.
Invalidates render caches for all windows.
Monitors a buffer pid so the Editor receives :DOWN when it dies.
Monitors a list of buffer pids. Convenience wrapper around monitor_buffer/2.
Updates the active window's viewport. Falls back to updating
state.workspace.viewport when no window is active.
Rebuilds state.agent from the Session process when switching to an agent tab. The Session is the source of truth for status, pending approval, and error. The editor's agent field is a rendering cache, not a source of truth.
Removes a dead buffer pid from all state locations.
Writes a tab context back into the live editor state.
Returns the appropriate keymap scope for the active window's content type.
Derives the keymap scope from a window's content type.
Returns the screen rect for layout computation, excluding the global minibuffer row and reserving space for the file tree panel when open.
Scrolls the agent chat window's viewport by delta lines and updates
pinned state. Delegates to Window.scroll_viewport/3.
Sets the context for the next add_buffer call.
Returns true if the given tool should NOT be prompted for installation.
Captures the current per-tab fields into a context map.
Returns true if the editor has more than one window.
Starts a new buffer under the buffer supervisor for the given file path.
Switches to the buffer at idx, making it active for the current window.
Switches to the tab with target_id.
Pure variant of switch_tab/2. Returns {state, effects} instead of
performing side effects directly.
Syncs the active window's buffer reference with state.workspace.buffers.active.
Snapshots the active buffer's cursor into the active window struct.
Transitions the editor to a new vim mode.
Returns the screen rect for the file tree panel, or nil if closed.
Applies a function to the shell state and returns the updated state.
Updates the window struct for the given window id via a mapper function.
Applies a function to the workspace and returns the updated state.
Types
@type backend() :: :tui | :native_gui | :headless
@type content_context() :: %{ type: :buffer | :agent, display_name: String.t(), directory: String.t(), dirty: boolean(), filetype: atom() }
Display metadata derived from the active window's content type.
Used by title, modeline, and any other subsystem that needs to answer "what is the user looking at?" without assuming a buffer is active.
@type document_highlight() :: Minga.LSP.DocumentHighlight.t()
A document highlight range from the LSP server.
@type line_number_style() :: :hybrid | :absolute | :relative | :none
Line number display style.
@type t() :: %MingaEditor.State{ backend: backend(), buffer_add_context: MingaEditor.Shell.buffer_add_context(), buffer_monitors: %{required(pid()) => reference()}, capabilities: MingaEditor.Frontend.Capabilities.t(), editing_model: :vim | :cua, face_override_registries: %{ required(pid()) => MingaEditor.UI.Face.Registry.t() }, focus_stack: [module()], font_registry: MingaEditor.UI.FontRegistry.t(), git_remote_op: {msg_ref :: reference(), task_monitor :: reference(), {git_root :: String.t(), success_msg :: String.t(), error_prefix :: String.t()}} | nil, last_cursor_line: non_neg_integer() | nil, last_test_command: {String.t(), String.t()} | nil, layout: MingaEditor.Layout.t() | nil, lsp: MingaEditor.State.LSP.t(), message_store: MingaEditor.UI.Panel.MessageStore.t(), parser_status: MingaEditor.Shell.Traditional.Modeline.parser_status(), pending_quit: :quit | :quit_all | nil, port_manager: GenServer.server() | nil, render_timer: reference() | nil, session: MingaEditor.State.Session.t(), shell: module(), shell_state: MingaEditor.Shell.Traditional.State.t() | MingaEditor.Shell.Board.State.t(), space_leader_pending: boolean(), space_leader_timer: reference() | nil, stashed_board_state: MingaEditor.Shell.Board.State.t() | nil, theme: MingaEditor.UI.Theme.t(), workspace: MingaEditor.Workspace.State.t() }
Functions
@spec active_buffer(t()) :: non_neg_integer()
Returns the active buffer index.
@spec active_content_context(t()) :: content_context()
Returns display metadata for the active window's content.
Buffer windows return file/buffer metadata. Agent chat windows return agent-specific display info. Falls back to buffer metadata when the active window is nil or unrecognized.
@spec active_tab(t()) :: MingaEditor.State.Tab.t() | nil
@spec active_tab_kind(t()) :: MingaEditor.State.Tab.kind()
@spec active_window_struct(t()) :: MingaEditor.Window.t() | nil
Returns the active window struct, or nil if windows aren't initialized.
@spec active_window_viewport(t()) :: MingaEditor.Viewport.t()
Returns the active window's viewport, falling back to state.workspace.viewport
when no window is active. Use this for scroll commands that need to
read/write the viewport of the focused window (not the terminal-level
viewport).
Adds a new buffer and makes it the active buffer for the current window.
Thin wrapper around add_buffer_pure/3 that applies effects inline.
@spec add_buffer_pure(t(), pid(), keyword()) :: {t(), [MingaEditor.effect()]}
Pure variant of add_buffer/2. Returns {state, effects} instead of
performing side effects directly.
Generic concerns (buffer pool) are handled here. Shell-specific
presentation logic (tab bar, card routing) is dispatched through
shell.on_buffer_added/4. The only effect returned is {:monitor, pid}.
The buffer-add context is read from state.buffer_add_context (set by
picker preview) or overridden via opts[:context]. After dispatch the
field is reset to :open.
@spec agent(t()) :: MingaEditor.State.Agent.t()
@spec apply_render_output(t(), MingaEditor.RenderPipeline.Input.t()) :: t()
Applies render pipeline mutations back to the editor state.
The render pipeline updates window caches (invalidation tracking, context fingerprints), click regions, and layout during rendering. This function writes those mutations back after the pipeline completes.
The render_output is a RenderPipeline.Input struct with the mutated
fields. Only windows, shell_state, and layout carry meaningful
changes; other fields are unchanged.
@spec bottom_panel(t()) :: MingaEditor.BottomPanel.t()
Returns the active buffer pid.
Returns the buffer list.
@spec build_agent_tab_defaults(t(), MingaEditor.State.Windows.t(), pid() | nil) :: MingaEditor.State.Tab.context()
Builds a complete per-tab context for an agent tab.
Used by agent tab creation paths to ensure all @per_tab_fields are
populated. Accepts a pre-built Windows struct for the agent chat
window and the agent buffer pid.
@spec close_buffer_pure(t(), pid()) :: {t(), [MingaEditor.effect()]}
Pure variant of remove_dead_buffer/2. Returns {state, effects} instead
of performing side effects directly.
Removes the pid from the buffer list, clears it from special buffer slots, switches to another buffer if the active one died, and cleans up the monitor ref. This function is already pure (no process calls), so the effects list is always empty.
@spec dashboard(t()) :: MingaEditor.Dashboard.state() | nil
@spec find_agent_chat_window(t()) :: {MingaEditor.Window.id(), MingaEditor.Window.t()} | nil
Finds the agent chat window in the windows map.
Returns {win_id, window} or nil if no agent chat window exists.
@spec find_buffer_by_path(t() | map(), String.t()) :: non_neg_integer() | nil
Returns the index of the buffer whose file path matches file_path, or nil.
Catches :exit for each buffer in case a process has died but not yet been
removed from the buffer list.
@spec find_tab_by_buffer(t(), pid()) :: MingaEditor.State.Tab.t() | nil
@spec focus_window(t(), MingaEditor.Window.id()) :: t()
Switches focus to the given window, saving the current cursor to the outgoing window and restoring the target window's stored cursor.
No-op if target_id is already the active window or windows aren't set up.
@spec git_status_panel(t()) :: MingaEditor.Frontend.Protocol.GUI.git_status_data() | nil
@spec hover_popup(t()) :: MingaEditor.HoverPopup.t() | nil
Invalidates render caches for all windows.
Call when the screen layout changes (file tree toggle, agent panel toggle) because cached draws contain baked-in absolute coordinates that become wrong when column offsets shift.
Monitors a buffer pid so the Editor receives :DOWN when it dies.
Idempotent: if the pid is already monitored, returns state unchanged.
Monitors a list of buffer pids. Convenience wrapper around monitor_buffer/2.
@spec picker_ui(t()) :: MingaEditor.State.Picker.t()
@spec prompt_ui(t()) :: MingaEditor.State.Prompt.t()
@spec put_active_window_viewport(t(), MingaEditor.Viewport.t()) :: t()
Updates the active window's viewport. Falls back to updating
state.workspace.viewport when no window is active.
@spec rebuild_agent_from_session(t(), MingaEditor.State.Tab.t()) :: t()
Rebuilds state.agent from the Session process when switching to an agent tab. The Session is the source of truth for status, pending approval, and error. The editor's agent field is a rendering cache, not a source of truth.
Removes a dead buffer pid from all state locations.
Called from the Editor's :DOWN handler. Removes the pid from the buffer
list, clears it from special buffer slots (messages, warnings, help), and
switches to another buffer if the active one died. Also cleans up the
monitor ref.
Thin wrapper around close_buffer_pure/2 that applies effects inline.
@spec restore_tab_context(t(), MingaEditor.State.Tab.context()) :: t()
Writes a tab context back into the live editor state.
The context carries workspace fields as a flat map. Empty context means a brand-new tab; we build defaults with the current active buffer and viewport dimensions.
Backward compatibility: old contexts with nested structure are migrated
to the new flat format via maybe_migrate_legacy_context/2.
Returns the appropriate keymap scope for the active window's content type.
Used when leaving the file tree (toggle, close, navigate right) to restore the correct scope. Returns :agent for agent chat windows, :editor otherwise.
@spec scope_for_content( MingaEditor.Window.Content.t(), Minga.Keymap.Scope.scope_name() ) :: Minga.Keymap.Scope.scope_name()
Derives the keymap scope from a window's content type.
Agent chat windows always use :agent scope. Buffer windows use
:editor when coming from :agent scope, and preserve the current
scope otherwise (e.g., :file_tree stays as :file_tree).
@spec screen_rect(t()) :: MingaEditor.WindowTree.rect()
Returns the screen rect for layout computation, excluding the global minibuffer row and reserving space for the file tree panel when open.
Scrolls the agent chat window's viewport by delta lines and updates
pinned state. Delegates to Window.scroll_viewport/3.
Returns the state unchanged if no agent chat window exists.
@spec set_agent(t(), MingaEditor.State.Agent.t()) :: t()
@spec set_bottom_panel(t(), MingaEditor.BottomPanel.t()) :: t()
@spec set_buffer_add_context(t(), MingaEditor.Shell.buffer_add_context()) :: t()
Sets the context for the next add_buffer call.
Used by picker preview to mark buffer additions as transient previews
rather than permanent opens. The context is consumed and reset to
:open by add_buffer_pure/3.
@spec set_dashboard(t(), MingaEditor.Dashboard.state()) :: t()
@spec set_hover_popup(t(), MingaEditor.HoverPopup.t()) :: t()
@spec set_picker_ui(t(), MingaEditor.State.Picker.t()) :: t()
@spec set_prompt_ui(t(), MingaEditor.State.Prompt.t()) :: t()
@spec set_tab_bar(t(), MingaEditor.State.TabBar.t() | nil) :: t()
@spec set_tab_session(t(), MingaEditor.State.Tab.id(), pid() | nil) :: t()
@spec set_whichkey(t(), MingaEditor.State.WhichKey.t()) :: t()
Returns true if the given tool should NOT be prompted for installation.
A tool is skipped when it's already declined this session, already installed, currently being installed, or already in the prompt queue.
@spec snapshot_tab_context(t()) :: MingaEditor.State.Tab.context()
Captures the current per-tab fields into a context map.
The returned map is stored in the outgoing tab so it can be restored when the user switches back.
Returns true if the editor has more than one window.
Starts a new buffer under the buffer supervisor for the given file path.
@spec switch_buffer(t(), non_neg_integer()) :: t()
Switches to the buffer at idx, making it active for the current window.
Centralizes Buffers.switch_to + window sync so callers don't need to
remember to call sync_active_window_buffer/1. Shell-specific
presentation logic (tab label updates, etc.) is dispatched through
shell.on_buffer_switched/2.
@spec switch_tab(t(), MingaEditor.State.Tab.id()) :: t()
Switches to the tab with target_id.
Snapshots the current tab's context, stores it, updates the tab bar's active pointer, and restores the target tab's saved context into the live editor state. Invalidates layout and window caches since the entire visual context changes.
Thin wrapper around switch_tab_pure/2 that applies effects inline.
@spec switch_tab_pure(t(), MingaEditor.State.Tab.id()) :: {t(), [MingaEditor.effect()]}
Pure variant of switch_tab/2. Returns {state, effects} instead of
performing side effects directly.
Snapshots the current tab's context, updates the tab bar pointer, restores the target tab's context, invalidates layout. Side effects (spinner stop/start, agent session rebuild) are returned as effects.
The returned effects list may include:
:stop_spinner— cancel the outgoing agent's spinner timer{:rebuild_agent_session, tab}— rebuild agent state from session process:start_spinner— conditionally restart spinner for incoming agent
Syncs the active window's buffer reference with state.workspace.buffers.active.
Call this after any operation that changes state.workspace.buffers.active to keep the
window tree consistent. No-op when windows aren't initialized.
Snapshots the active buffer's cursor into the active window struct.
Call this before rendering split views so inactive windows have a fresh cursor position for the active window when it becomes inactive later.
@spec tab_bar(t()) :: MingaEditor.State.TabBar.t() | nil
@spec transition_mode(t(), Minga.Mode.mode(), Minga.Mode.state() | nil) :: t()
Transitions the editor to a new vim mode.
Convenience wrapper around VimState.transition/3 that operates on
the full EditorState. This is the preferred API for call sites that
already have an EditorState.
Examples
# Simple transition (uses default mode_state):
EditorState.transition_mode(state, :normal)
EditorState.transition_mode(state, :insert)
# With explicit mode_state (required for visual, search, etc.):
EditorState.transition_mode(state, :visual, %VisualState{...})
@spec tree_rect(t()) :: MingaEditor.WindowTree.rect() | nil
Returns the screen rect for the file tree panel, or nil if closed.
@spec update_picker_ui(t(), (MingaEditor.State.Picker.t() -> MingaEditor.State.Picker.t())) :: t()
@spec update_shell_state(t(), (MingaEditor.Shell.Traditional.State.t() -> MingaEditor.Shell.Traditional.State.t())) :: t()
Applies a function to the shell state and returns the updated state.
@spec update_window(t(), MingaEditor.Window.id(), (MingaEditor.Window.t() -> MingaEditor.Window.t())) :: t()
Updates the window struct for the given window id via a mapper function.
@spec update_workspace(t(), (MingaEditor.Workspace.State.t() -> MingaEditor.Workspace.State.t())) :: t()
Applies a function to the workspace and returns the updated state.
@spec whichkey(t()) :: MingaEditor.State.WhichKey.t()