typedef __SIZE_TYPE__    size_t;
typedef __PTRDIFF_TYPE__ ssize_t;
typedef __WCHAR_TYPE__   wchar_t;
typedef __PTRDIFF_TYPE__ ptrdiff_t;
typedef __PTRDIFF_TYPE__ intptr_t;
typedef __SIZE_TYPE__    uintptr_t;

#if __STDC_VERSION__ >= 201112L
typedef union { long long __ll; long double __ld; } max_align_t;
#endif

#ifndef NULL
#define NULL ((void*)0)
#endif

#undef offsetof
#define offsetof(type, field) __builtin_offsetof(type, field)

typedef signed char        int8_t;
typedef unsigned char      uint8_t;
typedef short              int16_t;
typedef unsigned short     uint16_t;
typedef int                int32_t;
typedef unsigned           uint32_t;
typedef long long          int64_t;
typedef unsigned long long uint64_t;

typedef __builtin_va_list va_list;
#define va_start __builtin_va_start
#define va_arg __builtin_va_arg
#define va_copy __builtin_va_copy
#define va_end __builtin_va_end

/* fix a buggy dependency on GCC in libio.h */
typedef va_list __gnuc_va_list;
#define _VA_LIST_DEFINED

// Basic CRT protos.
int memcmp(const void* _Buf1, const void* _Buf2, size_t _Size);
void* memcpy(void* _Dst, const void* _Src, size_t _Size);
void* memset(void* _Dst, int _Val, size_t _Size);

typedef enum EditorCommands {
  ED_SaveRequestSave, // (.cmd) Saves the current buffer if there are changes.
  ED_SaveRequest_END = 0xF,
  ED_CloseRequestClose, // (.cmd) Closes the current editor.
  ED_CloseRequest_END = 0x1F,
  ED_RequestClipboardCopy, // (.cmd) Copies selection to clipboard.  If no selection it will copy the content line.
  ED_RequestClipboardCut, // (.cmd) Copies with the same semantics as copy to clipboard, but also removes the content.
  ED_RequestClipboardPaste, // (.cmd) Inserts the content of the system clipboard into the current buffer.
  ED_RequestClipboardPasteBeforeCursor, // (.cmd) Inserts the content of the system clipboard into the buffer.  If it is a line copy, the line is pasted before the cursor line.
  ED_RequestClipboard_END = 0x2F,
  ED_FindRequestFind, // (.cmd) Opens the 'Find' widget.
  ED_FindRequestFindWithSeed, // (.cmd) Opens the 'Find' widget but seeds the search with the current selection or word under cursor.
  ED_FindRequest_END = 0x3F,
  ED_SelectionClearSelections, // (.cmd) Clears all selections.
  ED_Selection_END = 0x4F,
  ED_NavPageUp, // (.cmd,.flags) Moves cursor up by one page.
  ED_NavPageDown, // (.cmd,.flags) Moves cursor down by one page.
  ED_NavLeft, // (.cmd,.flags) Moves cursor to the left.  If at the beginning of the line, cursor will advance to end of line above.
  ED_NavRight, // (.cmd,.flags) Moves cursor to the right.  If at the end of the line, cursor will advance to the beginning of the line below.
  ED_NavRightNoNLAdvance, // (.cmd,.flags) Moves cursor to the right.
  ED_NavLineDown, // (.cmd,.flags) Moves cursor down a line maintaining column.
  ED_NavLineUp, // (.cmd,.flags) Moves cursor up a line maintaining column.
  ED_NavCursorTopScreen, // (.cmd,.flags) Moves cursor to top of viewport.
  ED_NavCursorBottomScreen, // (.cmd,.flags) Moves cursor to bottom of viewport.
  ED_NavCursorCenterScreen, // (.cmd,.flags) Moves cursor to middle of viewport.
  ED_NavBeginningOfLine, // (.cmd,.flags) Moves cursor to beginning of the line.
  ED_NavFirstNonemptyOfLine, // (.cmd,.flags) Moves cursor to first non-whitespace character of line.
  ED_NavEndOfLine, // (.cmd,.flags) Moves cursor to end of the line.
  ED_NavContentEnd, // (.cmd,.flags) Moves cursor to the end of the content.
  ED_NavContentBeginning, // (.cmd,.flags) Moves cursor to beginning of content.
  ED_NavMatchingEncloser, // (.cmd,.flags) Snaps cursor to nearest enclosing symbol.  If at an enclosing symbol, cursor will move to opposing enclosing symbol.
  ED_NavWordRight, // (.cmd,.flags) Moves cursor one word to the right.
  ED_NavChunkRight, // (.cmd,.flags) Moves cursor one word chunk to the right.
  ED_NavWordLeft, // (.cmd,.flags) Moves cursor one word to the left.
  ED_NavChunkLeft, // (.cmd,.flags) Moves cursor one word chunk to the left.
  ED_NavRequestGotoLine, // (.cmd,.flags) Opens go to line widget.
  ED_NavCenterCameraCursor, // (.cmd,.flags) Moves the camera view such that the primary cursor is in mid screen.
  ED_NavEmptyBlockUp, // (.cmd,.flags) Moves cursor to nearest empty line above.
  ED_NavEmptyBlockDown, // (.cmd,.flags) Moves cursor to nearest empty line below.
  ED_NavCursorHistoryBack, // (.cmd,.flags) Moves cursor to its previous position.
  ED_NavCursorHistoryForward, // (.cmd,.flags) Moves cursor to its next future position.
  ED_NavMoveCursorTo, // (.cmd,.flags,.byte_offsets) Moves cursor to a specific byte position in the buffer.
  ED_Nav_END = 0x4FF,
  ED_MCDupCursorDown, // (.cmd) Creates a new multi-cursor one line below.
  ED_MCDupCursorUp, // (.cmd) Creates a new multi-cursor one line above.
  ED_MCInsertGroup, // (.cmd,.buffers) Performs a unique insert per-cursor.
  ED_MCDropCursors, // (.cmd) Deletes all multi-cursors.
  ED_MCSelectionToCursor, // (.cmd) Creates a selection at the current word.  If selection exists, creates a new multi-cursor at the next identical selection.
  ED_MCCreateCursors, // (.cmd,.byte_offsets) Creates multi-cursors at the specified byte positions.
  ED_MC_END = 0x50F,
  ED_InsInsert, // (.cmd,.buf) Inserts a buffer at the cursor.
  ED_InsReplace, // (.cmd,.buf) Inserts a buffer at cursor, overwriting characters overlapping the buffer.  Does not advance cursor.
  ED_InsOverwriteInsertBuf, // (.cmd,.buf) Inserts a buffer at cursor, overwriting characters overlapping the buffer.
  ED_InsOpenLineBelow, // (.cmd) Opens a new content line below cursor.
  ED_InsOpenLineAbove, // (.cmd) Opens a new content line above cursor.
  ED_InsTab, // (.cmd) Inserts a tap (may be expanded to spaces).
  ED_Ins_END = 0x90F,
  ED_SpecJoinLineBelow, // (.cmd) Joins the content line below with the current line at cursor.
  ED_SpecTrimLineEndings, // (.cmd) Trims trailing spaces at all lines.
  ED_SpecTab, // (.cmd) Tabs a line.
  ED_SpecUntab, // (.cmd) Untabs a line.
  ED_Spec_END = 0xA0F,
  ED_DelDeleteLine, // (.cmd) Deletes the line of content at cursor.
  ED_DelDeleteChar, // (.cmd) Deletes a single character at the cursor.  If a selection is active, the selection is deleted.
  ED_DelDeleteWord, // (.cmd) Deletes a word at the cursor.  If a selection is active, the selection is deleted.
  ED_DelDeleteChunk, // (.cmd) Deletes a word chunk at cursor.  If a selection is active, the selection is deleted.
  ED_DelBackspaceWord, // (.cmd) Deletes a word before the cursor.  If a selection is active, the selection is deleted.
  ED_DelBackspaceChunk, // (.cmd) Deletes a word chunk before the cursor.  If a selection is active, the selection is deleted.
  ED_Del_END = 0xF0F,
  ED_URTryUndo, // (.cmd) Attemps an undo operation.
  ED_URTryRedo, // (.cmd) Attemps a redo operation.
  ED_UR_END = 0xF1F,
} EditorCommands;

typedef enum EditorCommandFlags {
  ED_FLG_None = 0, // No options.
  ED_FLG_UpdateSelection = 1 << 0, // If the cursor moves, add to the selection.
  ED_FLG_ResetCamera = 1 << 1, // Make the camera follow the cursor after command.
} EditorCommandFlags;

// --- fred plugin API ---
// --- Infra structures ---
// Core string struct.
typedef struct String8 {
  // String pointer (null termination not necessary).
  char* str;
  // Size of string (not including any null terminator).
  uint64_t size;
} String8;

// Collection of strings.
typedef struct String8Array {
  // Pointer to strings.
  String8* strs;
  // Size of array.
  uint64_t size;
} String8Array;

// Structure storing a collection of offsets into the buffer.
typedef struct EditorOffsetArray {
  // Pointer to offsets.
  uint64_t* array;
  // Size of array.
  uint64_t size;
} EditorOffsetArray;

// Structure storing a pair of buffer offset ranges.
typedef struct EditorOffsetRange {
  // Offset to the first byte.
  uint64_t first_off;
  // Last byte offset of the range (not inclusive).
  uint64_t last_off;
} EditorOffsetRange;

// Structure storing cursor byte offsets.
typedef struct EditorCursorRange {
  // Byte offset of the cursor.
  uint64_t cursor_off;
  // Will be non-empty if there is an active selection.
  EditorOffsetRange sel;
} EditorCursorRange;

// Structure storing a collection of cursor ranges.
typedef struct EditorCursorArray {
  // Pointer to cursor ranges.
  EditorCursorRange* array;
  // Size of array.
  uint64_t size;
} EditorCursorArray;

// Structure storing a collection of search result ranges.
typedef struct EditorFindResults {
  // Pointer to search result ranges.
  EditorOffsetRange* array;
  // Size of array.
  uint64_t size;
} EditorFindResults;

// --- Editor batching API ---
// Structure for working with batch edits.
typedef struct EditorBatchEdit {
  // Private data.
  void* pvt[2];
} EditorBatchEdit;

// Structure storing a collection of batch offset and string buffer.
typedef struct EditorInsertData {
  // Offset to the byte to insert at.
  uint64_t off;
  // Buffer to insert.
  String8 buf;
} EditorInsertData;

// Structure storing a collection of batch offset and string buffer.
typedef struct EditorReplaceData {
  // Range of bytes to remove and replace with 'buf'.
  EditorOffsetRange range;
  // Buffer to replace at 'range'.
  String8 buf;
} EditorReplaceData;

// Structure storing a collection of batch insertions.
typedef struct EditorBatchInsert {
  // Pointer to insert operations.
  EditorInsertData* array;
  // Size of array.
  uint64_t size;
} EditorBatchInsert;

// Structure storing a collection of batch removals.
typedef struct EditorBatchRemove {
  // Pointer to removal operations.
  EditorOffsetRange* array;
  // Size of array.
  uint64_t size;
} EditorBatchRemove;

// Structure storing a collection of batch replacements.
typedef struct EditorBatchReplace {
  // Pointer to replacement operations.
  EditorReplaceData* array;
  // Size of array.
  uint64_t size;
} EditorBatchReplace;

// --- Context structures ---
// Opaque: Pointer to an allocating arena.
typedef struct Arena Arena;
// Opaque: Pointer to plugin manager instance.
typedef struct PluginManager PluginManager;
// Opaque: Pointer to editor instance.
typedef struct Editor Editor;
// Context structure for editor interactions.
typedef struct EditorCtx {
  // Plugin manager.
  PluginManager* mgr;
  // Calling editor.
  Editor* editor;
} EditorCtx;

// --- Command structures ---
// Structure for queuing editor commands.
typedef struct EditorCmd {
  // Command.
  uint32_t cmd;
  // Flags applied to command.
  uint32_t flags;
  // Buffer for command.
  String8 buf;
  // Buffer collection for bulk operations.
  String8Array buffers;
  // Byte offsets into the buffer.
  EditorOffsetArray byte_offsets;
} EditorCmd;

// --- Arenas ---
// Fetches a scratch arena that does not conflict with 'conflict'.
Arena* arena_pull_scratch(PluginManager* mgr, Arena* conflict);
// Bumps arena pointer, allocating space for the caller.
void* arena_push(Arena* a, uint64_t size, uint64_t align, uint32_t zero);
// Gets the arena position.
uint64_t arena_pos(Arena* a);
// Pops arena to specific position.
void arena_pop_to(Arena* a, uint64_t pos);

// --- Message feed ---
// Emit an info message.
void feed_queue_info_internal(String8* str);
// Emit an warning message.
void feed_queue_warning_internal(String8* str);
// Emit an error message.
void feed_queue_error_internal(String8* str);

// --- Editor ---
// Queries.
// Populates each cursor selection (empty or not) to the 'result' array.
void ed_cursor_selections(Arena* a, EditorCtx* ctx, String8Array* result);
// Gets the cursors in the current editor.
void ed_cursor_ranges(Arena* a, EditorCtx* ctx, EditorCursorArray* result);
// Gets a string at the specified byte range.
void ed_string_at_range(Arena* a, EditorCtx* ctx, EditorOffsetRange* rng, String8* result);
// Gets the line at a given byte offset in the buffer.
uint64_t ed_line_at_offset(EditorCtx* ctx, uint64_t off);
// Gets the byte range for a specific line.  Note: Lines are 1-indexed.
void ed_byte_range_at_line(EditorCtx* ctx, uint64_t line, EditorOffsetRange* result);
// Gets the byte range for a specific line including the newline character.  Note: Lines are 1-indexed.
void ed_byte_range_at_line_with_newline(EditorCtx* ctx, uint64_t line, EditorOffsetRange* result);
// Gets the number of lines for the buffer.
uint64_t ed_line_count(EditorCtx* ctx);
// Gets the total content length for the buffer.
uint64_t ed_content_byte_count(EditorCtx* ctx);
// Performs an immediate incremental search for 'str' over the current buffer.
void ed_find_all(Arena* a, EditorCtx* ctx, String8* str, uint32_t ignore_case, EditorFindResults* result);
// Batching.
// Starts a new batch edit.  WARNING: You must not queue new commands until the 'ed_edit_batch_end' is called.
void ed_edit_batch_begin(EditorCtx* ctx, EditorBatchEdit* batch);
// Completes the batch and recomputes internal editor state.
void ed_edit_batch_end(EditorCtx* ctx, EditorBatchEdit* batch);
// Performs a batch insert.  After calling this, you must recompute content length for subsequent batch edits.
void ed_edit_batch_insert(EditorBatchEdit* batch, EditorBatchInsert* ops);
// Performs a batch remove.  After calling this, you must recompute content length for subsequent batch edits.
void ed_edit_batch_remove(EditorBatchEdit* batch, EditorBatchRemove* ops);
// Performs a batch range replacement.  After calling this, you must recompute content length for subsequent batch edits.
void ed_edit_batch_replace(EditorBatchEdit* batch, EditorBatchReplace* ops);
// Commands.
// Pushes a new editor command.
void ed_push_command(EditorCtx* ctx, EditorCmd* cmd);

// --- Utility ---
// Formats a string printf-style and allocates result to specified arena.  There is a unique specifier '%S' for String8.
void str8_fmt_internal(Arena* a, String8* result, const char* fmt, va_list vlst);
// Performs a lexicographic comparison on input strings.
int32_t str8_compare_internal(String8* a, String8* b);
// Returns 1 if the input string is an integral value of the specified radix.
uint32_t str8_is_integer_internal(String8* str, uint32_t radix);
// Converts the input string to an integral value of the specified radix.
uint64_t u64_from_str8_internal(String8* str, uint32_t radix);
// Tries to convert the intput string to a floating-point value.  Returns 0 if the conversion failed.
uint32_t try_f64_from_str8_internal(String8* str, double* result);

// --- fred API version info ---
// The version of this fred API.  If there is a mismatch, you should regenerate the API.
const uint32_t api_version = 1;

typedef struct Temp {
  void* arena;
  uint64_t pos;
} Temp;

Temp arena_temp_begin(void* arena) {
  uint64_t pos =  arena_pos(arena);
  Temp t = { .arena = arena, .pos = pos };
  return t;
}

void arena_temp_end(Temp t) {
  arena_pop_to(t.arena, t.pos);
}

// Helpers for walking.
#define EachIndex(it, count) (uint64_t it = 0; it < (count); it += 1)
#define EachLine(it, count) (uint64_t it = 1; it <= (count); it += 1)

#define FRED_Min(A, B) (((A)<(B))?(A):(B))
#define FRED_Max(A,B) (((A)>(B))?(A):(B))
#define FRED_AlignOf(T) __alignof(T)
#define push_array_no_zero_aligned(a, T, c, align) (T *)arena_push((a), sizeof(T)*(c), (align), (0))
#define push_array_aligned(a, T, c, align) (T *)arena_push((a), sizeof(T)*(c), (align), (1))
#define push_array_no_zero(a, T, c) push_array_no_zero_aligned(a, T, c, FRED_Max(8, FRED_AlignOf(T)))

#define push_array(a, T, c) push_array_aligned(a, T, c, FRED_Max(8, FRED_AlignOf(T)))

// Scratch arena creation.  You pass in a 'conflict' arena when you don't want one scratch arena to overwrite another when nested functions are involved.
#define scratch_begin(conflict) arena_temp_begin(arena_pull_scratch(ctx->mgr, (conflict)))
#define scratch_end(scratch) arena_temp_end(scratch)

// Feed.
void feed_queue_info(String8 msg) {
  feed_queue_info_internal(&msg);
}

void feed_queue_warning(String8 msg) {
  feed_queue_warning_internal(&msg);
}

void feed_queue_error(String8 msg) {
  feed_queue_error_internal(&msg);
}

// Strings.
typedef struct String8Slice {
  uint64_t off;
  uint64_t len;
} String8Slice;

String8 str8(char* s, uint64_t size) {
  String8 r = { .str = s, .size = size };
  return r;
}

#define str8_lit(S) str8((char*)(S), sizeof(S) - 1)

String8 str8_substr_impl(String8 str, String8Slice slice) {
  slice.off = FRED_Min(str.size, slice.off);
  slice.len = FRED_Min(str.size - slice.off, slice.len);
  str.str += slice.off;
  str.size = slice.len;
  return str;
}

#define str8_substr(str, ...) str8_substr_impl(str, (String8Slice){ .off = 0, .len = (uint64_t)-1, __VA_ARGS__ })

String8 str8_fmt(void* a, const char* fmt, ...) {
  String8 result = {0};
  va_list va;
  va_start(va, fmt);
  str8_fmt_internal(a, &result, fmt, va);
  va_end(va);
  return result;
}

int32_t str8_compare(String8 a, String8 b) {
  return str8_compare_internal(&a, &b);
}

String8 str8_copy(void* a, String8 str) {
  String8 cpy = {0};
  cpy.size = str.size;
  cpy.str = push_array_no_zero(a, char, str.size);
  memcpy(cpy.str, str.str, str.size);
  return cpy;
}

// String conversions.
// Returns 1 if the input string is an integer of the desired radix.
uint32_t str8_is_integer(String8 str, uint32_t radix) {
  return str8_is_integer_internal(&str, radix);
}

uint64_t u64_from_str8(String8 str, uint32_t radix) {
  return u64_from_str8_internal(&str, radix);
}

// Returns 0 if the conversion failed.
uint32_t try_f64_from_str8(String8 str, double* result) {
  return try_f64_from_str8_internal(&str, result);
}

// API definition hook.
typedef void(*HotkeyPluginEditorFn)(EditorCtx*);

typedef struct HotkeyPluginEditorHook {
  const char* name;
  const char* description;
  HotkeyPluginEditorFn fn;
} HotkeyPluginEditorHook;

// This is the 'ED' group.
#define DEF_PLUGIN_EDITOR_HOOK(ui_name, desc, fn_name)                                                                 \
  void ED_ ## fn_name ## _impl_fn(EditorCtx*);                                                                         \
  HotkeyPluginEditorHook ED_ ## fn_name = { .name = ui_name, .description = desc, .fn = &ED_ ## fn_name ## _impl_fn }; \
  void ED_ ## fn_name ## _impl_fn(EditorCtx* ctx)

// Example plugins.
DEF_PLUGIN_EDITOR_HOOK("Says hello", "Says hello in each feed.", say_hello) {
  String8 str = str8_lit("Hello, world!");
  feed_queue_warning(str);
  feed_queue_error(str);
  Temp scratch = scratch_begin(NULL);
  String8 msg = str8_fmt(scratch.arena, "Hello! This is an int '%d'. This is a char '%c'. This is a float '%f'. This is a string '%S'", 42, 'C', 3.1415, str);
  feed_queue_info(msg);
  scratch_end(scratch);
}

DEF_PLUGIN_EDITOR_HOOK("Message demo", "Demos a formatted message to each feed.", message_demo) {
  Temp scratch = scratch_begin(NULL);
  String8 str = str8_lit("String");
  String8 msg = str8_fmt(scratch.arena, "Hello! This is an int '%d'. This is a char '%c'. This is a float '%f'. This is a string '%S'", 42, 'C', 3.1415, str);
  feed_queue_info(msg);
  feed_queue_warning(msg);
  feed_queue_error(msg);
  scratch_end(scratch);
}

DEF_PLUGIN_EDITOR_HOOK("Replace word with FRED", "Replaces the next word with 'FRED'", replace_w_fred) {
  EditorCmd cmd = {0};
  cmd.cmd = ED_NavWordRight;
  cmd.flags = ED_FLG_UpdateSelection;
  ed_push_command(ctx, &cmd);
  cmd.cmd = ED_InsInsert;
  cmd.flags = 0;
  cmd.buf = str8_lit("FRED");
  ed_push_command(ctx, &cmd);
}

DEF_PLUGIN_EDITOR_HOOK("Show selections", "Emits a message for each cursor selection", show_selections) {
  Temp scratch = scratch_begin(NULL);
  String8Array strings;
  ed_cursor_selections(scratch.arena, ctx, &strings);
  String8 msg = {0};
  for EachIndex(i, strings.size) {
    String8 str = strings.strs[i];
    if (str.size != 0) {
      msg = str8_fmt(scratch.arena, "[%d] = '%S'", i, str);
    }
    else {
      msg = str8_fmt(scratch.arena, "[%d] = empty", i);
    }
    feed_queue_info(msg);
  }
  scratch_end(scratch);
}

DEF_PLUGIN_EDITOR_HOOK("Change selection to snake case", "For each selection if it is PascalCase or camelCase, change it to snake_case", snake_case) {
  Temp scratch = scratch_begin(NULL);
  EditorCmd cmd = {0};
  String8Array strings;
  ed_cursor_selections(scratch.arena, ctx, &strings);
  // First, let's allocate our result array which will be inserted as a command.  The strings here will have lengths +1 for each capital letter.
  String8Array ins_buf = {0};
  ins_buf.size = strings.size;
  ins_buf.strs = push_array(scratch.arena, String8, strings.size);
  for EachIndex(i, strings.size) {
    String8 str = strings.strs[i];
    for EachIndex(j, str.size) {
      // Skip the first index because we only care about internal characters.
      if (j != 0
          && str.str[j] >= 'A'
          && str.str[j] <= 'Z') {
        ins_buf.strs[i].size += 2;
      }
      else {
        ins_buf.strs[i].size += 1;
      }
    }
  }
  // Now we can compute the result.
  const uint32_t delta = 'a' - 'A';
  char ins_char = 0;
  for EachIndex(i, strings.size) {
    String8 str = strings.strs[i];
    ins_buf.strs[i].str = push_array_no_zero(scratch.arena, char, ins_buf.strs[i].size);
    char* r_buf = ins_buf.strs[i].str;
    for EachIndex(j, str.size) {
      ins_char = str.str[j];
      if (j != 0
          && str.str[j] >= 'A'
          && str.str[j] <= 'Z') {
        *r_buf++ = '_';
        ins_char = str.str[j] + delta;
      }
      else {
          if (j == 0
              && str.str[j] >= 'A'
              && str.str[j] <= 'Z') {
            ins_char = str.str[j] + delta;
          }
          else {
            ins_char = str.str[j];
          }
      }
      *r_buf++ = ins_char;
    }
  }
  // Now we can compose the command.
  cmd.cmd = ED_MCInsertGroup;
  cmd.buffers = ins_buf;
  ed_push_command(ctx, &cmd);
  scratch_end(scratch);
}



DEF_PLUGIN_EDITOR_HOOK("Page up and center", "Moves the screen a page up and centers the screen", page_up_center) {
  EditorCmd cmd = {0};
  cmd.cmd = ED_NavPageUp;
  ed_push_command(ctx, &cmd);
  cmd.cmd = ED_NavCenterCameraCursor;
  ed_push_command(ctx, &cmd);
}

DEF_PLUGIN_EDITOR_HOOK("Page down and center", "Moves the screen a page down and centers the screen", page_down_center) {
  EditorCmd cmd = {0};
  cmd.cmd = ED_NavPageDown;
  ed_push_command(ctx, &cmd);
  cmd.cmd = ED_NavCenterCameraCursor;
  ed_push_command(ctx, &cmd);
}

DEF_PLUGIN_EDITOR_HOOK("Trim file and save", "", trim_and_save) {
  EditorCmd cmd = {0};
  cmd.cmd = ED_SpecTrimLineEndings;
  ed_push_command(ctx, &cmd);
  cmd.cmd = ED_SaveRequestSave;
  ed_push_command(ctx, &cmd);
}

DEF_PLUGIN_EDITOR_HOOK("Report line info", "Displays info about the current line.", line_info) {
    Temp scratch = scratch_begin(NULL);
    // Get the cursors first.
    EditorCursorArray cursors = {0};
    EditorCursorRange cursor_rng;
    uint64_t line;
    EditorOffsetRange line_rng;
    String8 msg;
    ed_cursor_ranges(scratch.arena, ctx, &cursors);
    for EachIndex(i, cursors.size) {
        cursor_rng = cursors.array[i];
        line = ed_line_at_offset(ctx, cursor_rng.cursor_off);
        ed_byte_range_at_line(ctx, line, &line_rng);
        msg = str8_fmt(scratch.arena, "Cursor{%I64d} | Line{%I64d} | LineRng{%I64d, %I64d}",
                        cursor_rng.cursor_off, line, line_rng.first_off, line_rng.last_off);
        feed_queue_info(msg);
    }
    scratch_end(scratch);
}

DEF_PLUGIN_EDITOR_HOOK("Replace selection with numbers", "Replaces the current selection with ascending numbers for each subsequent occurrence.", repl_all_nums) {
  Temp scratch = scratch_begin(NULL);
  // Get the cursors first.
  EditorCursorArray cursors = {0};
  ed_cursor_ranges(scratch.arena, ctx, &cursors);
  String8 repl_str = {0};
  int good = 1;
  if (cursors.size != 1) {
    String8 msg = str8_lit("Operation only supported with a single cursor");
    feed_queue_warning(msg);
    good = 0;
  }

  // The range containing the original selection.
  EditorOffsetRange off_rng;
  if (good) {
    EditorCursorRange rng = cursors.array[0];
    off_rng.first_off = rng.sel.first_off;
    off_rng.last_off = rng.sel.last_off;
    ed_string_at_range(scratch.arena, ctx, &off_rng, &repl_str);
    good = repl_str.size != 0;
  }

  // Now we're going to try and extract a 'starting' point for the numbering based on the selection.
  // We do this by the following format:
  // SELECTION_STARTNUM
  // e.g.: 'FOOBAR_10'
  // This would replace every instance of 'FOOBAR' with an ascending number starting at 10.
  // Since the first instance will have extra characters, we save the first replacement string with
  // 'initial_repl_str'.
  String8 initial_repl_str = repl_str;
  String8 sliced;
  uint64_t count_start = 0;
  if (good) {
    // First, let's see if there's an '_'.
    uint32_t found_int = 0;
    for EachIndex(i, repl_str.size) {
      if (repl_str.str[i] == '_') {
        sliced = str8_substr(repl_str, .off = i + 1);
        // Only support base-10 for now.
        if (sliced.size != 0 && str8_is_integer(sliced, 10)) {
          found_int = 1;
          break;
        }
      }
    }
    if (found_int) {
      // Also remove '1' for the '_' character.
      repl_str = str8_substr(repl_str, .len = repl_str.size - sliced.size - 1);
      count_start = u64_from_str8(sliced, 10);
    }
  }

  if (good) {
    EditorBatchEdit batch;
    // First, find every instance of the string above.
    EditorFindResults find_results = {0};
    ed_find_all(scratch.arena, ctx, &repl_str, 0, &find_results);
    // Allocate enough space for the batch.
    EditorBatchReplace repl_op = {0};
    repl_op.size = find_results.size;
    repl_op.array = push_array(scratch.arena, EditorReplaceData, repl_op.size);
    // Fill.
    for EachIndex(i, repl_op.size) {
      // Since the first selection can be different, we will special case it.
      if (find_results.array[i].first_off == off_rng.first_off) {
        repl_op.array[i].range = off_rng;
      }
      else {
        repl_op.array[i].range = find_results.array[i];
      }
      repl_op.array[i].buf = str8_fmt(scratch.arena, "%I64d", count_start);
      ++count_start;
    }
    // Iterate the lines and perform the batch replacement.
    ed_edit_batch_begin(ctx, &batch);
    ed_edit_batch_replace(&batch, &repl_op);
    ed_edit_batch_end(ctx, &batch);
  }
  scratch_end(scratch);
}

DEF_PLUGIN_EDITOR_HOOK("Toggle C-style comment line(s)", "Adds a C-style comment to selected lines or current line.", toggle_comment) {
  Temp scratch = scratch_begin(NULL);
  // Get the cursors first.
  EditorCursorArray cursors = {0};
  ed_cursor_ranges(scratch.arena, ctx, &cursors);
  String8 repl_str = {0};
  int good = 1;
  if (cursors.size != 1) {
    String8 msg = str8_lit("Operation only supported with a single cursor");
    feed_queue_warning(msg);
    good = 0;
  }

  uint64_t start_line;
  uint64_t end_line;
  uint32_t add_comment = 0;
  if (good) {
    // Find the line at the cursor selection.  Note: The selection might also be empty, but that's OK.
    start_line = ed_line_at_offset(ctx, cursors.array[0].sel.first_off);
    end_line = ed_line_at_offset(ctx, cursors.array[0].sel.last_off);
    // Pull the first line range and decide if we need to add a comment or remove one.
    EditorOffsetRange line_rng;
    ed_byte_range_at_line(ctx, start_line, &line_rng);
    String8 line_txt;
    ed_string_at_range(scratch.arena, ctx, &line_rng, &line_txt);
    uint32_t slash_count = 0;
    for (uint64_t i = 0; i < line_txt.size && i < 2; ++i) {
      slash_count += line_txt.str[i] == '/';
    }
    add_comment = slash_count != 2;

    // Create batch to iterate lines which either removes the comment or adds one.
    // Note: For selected lines which do not already have a comment, we will leave
    // them alone.
    EditorBatchEdit batch;
    ed_edit_batch_begin(ctx, &batch);
    if (add_comment) {
      // Note: Lines are inclusive ranges.
      EditorBatchInsert ins = {0};
      ins.size = (end_line - start_line) + 1;
      ins.array = push_array(scratch.arena, EditorInsertData, ins.size);
      uint64_t idx = 0;
      for (uint64_t line = start_line; line <= end_line; ++line) {
        ed_byte_range_at_line(ctx, line, &line_rng);
        ins.array[idx].off = line_rng.first_off;
        ins.array[idx].buf = str8_lit("//");
        ++idx;
      }
      ed_edit_batch_insert(&batch, &ins);
    }
    else {
      // Note: Lines are inclusive ranges.
      EditorBatchRemove rm = {0};
      rm.size = (end_line - start_line) + 1;
      rm.array = push_array(scratch.arena, EditorOffsetRange, rm.size);
      uint64_t idx = 0;
      for (uint64_t line = start_line; line <= end_line; ++line) {
        ed_byte_range_at_line(ctx, line, &line_rng);
        ed_string_at_range(scratch.arena, ctx, &line_rng, &line_txt);
        if (line_txt.size > 1 && line_txt.str[0] == '/' && line_txt.str[1] == '/') {
          rm.array[idx].first_off = line_rng.first_off;
          rm.array[idx].last_off = line_rng.first_off + 2;
        }
        ++idx;
      }
      ed_edit_batch_remove(&batch, &rm);
    }
    ed_edit_batch_end(ctx, &batch);
  }
  scratch_end(scratch);
}