Repository: neovim
Issue: Lua: Position / Range abstraction (vim.pos.Pos)
Labels: enhancement,api,lua

# Problem

- There are various different ways of representing a position (e.g. `:help api-indexing`).
    - https://github.com/neovim/neovim/issues/25272#issuecomment-1729294143
- Comparing or reasoning about positions requires boilerplate / utility functions such as:
    - [get_range_from_mark](https://github.com/neovim/neovim/pull/25301#discussion_r1334298996)
    - [make_line_range_params](https://github.com/neovim/neovim/pull/25194#discussion_r1328985155)
    - [compare_position](https://github.com/vscode-neovim/vscode-neovim/pull/1490/files#diff-356c71d28d1ad83654bec96ec39481938dd06e6589a05d781ed2ab028d9296b6R70-R84)

Copying @mfussenegger summary:

> cursor/position related functions:
> 
> * `vim.region(..., pos1, pos2, ...)`
>   * pos1 & pos2: `integer[] (line, column)` `(0,0)` indexed byte positions
> * `vim.inspect_pos(..., row, col, ...)` 0 based
> * `vim.show_pos(..., row, col, ...)` 0 based
> * `vim.highlight.range(... start, finish, ...)` - `{line, col}` tuple, doesn't mention if `(0, 0)` or something else?
> * `vim.treesitter.get_node_text(node, ...)`
> * `vim.fn.cursor(lnum, col, off)` `(1, 1)` - With `0` meaning stay in current line or column
> * `vim.fn.getpos(expr)` - result is `[bufnr, lnum, col, off]` (1, 1) based;
> * `vim.fn.getcurpos([{winid}])` like ^ but with an extra `curswant` in the result.
> * `vim.fn.getline(lnum, end)` - 1 based
> * `nvim_win_set_cursor(win, pos)` -  `pos` is `(row, col)` tuple  with `(1, 0)` indexing
> * `nvim_win_get_cursor(win)` - Mirrors `nvim_win_set_cursor`
> * `nvim_buf_get_lines(..., start, end, ...)` - 0 based, end-exclusive
> * `nvim_buf_get_lines(..., start_row, start_col, end_row, end_col...)` - 0 based, row indices end-inclusive, column indices end-exclusive
> 
> LSP structures:
> 
> ```
> Position
>    line: integer 0 based
>    character: integer 0 base  (but encoding dependent
> ```
> 
> ```
> Range
>     start: Position
>     end: Position
> ```

# Proposal

Implement `vim.pos` module for representing and working with normalized positions.

- `vim.pos(row, col)` creates a `vim.pos.Pos` position using "normalized" indexing (TBD).
    - `vim.pos(expr: string)` takes a [getpos()-like expression](https://neovim.io/doc/user/builtin.html#line()).
- `vim.pos.Pos` structure is defined as:
  ```
  vim.pos.Pos: {
    kind?: 'mark' | 'extmark' | 'cursor' | ...
    col: number
    row: number
    buf?: number -- Buffer id.

    -- TODO: do we need these fields?
    -- row_offset?: number
    -- col_offset?: number
    -- vcol?: number  -- virtual column
    -- vrow?: number  -- virtual row
  }
  ```
- `vim.pos.cmp()` compares positions (analogous to `vim.version.cmp()`)
- `vim.pos.relative(kind:'cursor'|'editor'|'laststatus'|'mouse'|'tabline'|'win')` gets raw positions (exposes internals of `nvim_open_win(…,{relative=…})`
- For integration with legacy interfaces it can _accept_ and _produce_ any kind of position. Interface proposal:
  ```
  vim.pos.mark(row,col)       -- Creates a position from a mark-like row/col.
  vim.pos.extmark(row,col)    -- Creates a position from a extmark-like row/col.
  vim.pos.cursor(row,col)     -- Creates a position from a cursor-like row/col.
  vim.pos.mark(pos)           -- Converts `pos` to a mark-like position.
  vim.pos.extmark(pos)        -- Converts `pos` to a extmark-like position.
  vim.pos.cursor(pos)         -- Converts `pos` to a cursor-like position.
  ```