From a71ac7d06d020dffcc810f00b57d9aae4ab92d1b Mon Sep 17 00:00:00 2001 From: linrongbin16 Date: Thu, 19 Sep 2024 09:08:52 +0800 Subject: [PATCH] chore: Update docs and dependencies (#245) Co-authored-by: linrongbin16 --- .github/workflows/ci.yml | 89 +++-- .markdownlint.jsonc | 14 + README.md | 215 ++++++------ lua/gitlinker/commons/_json.lua | 557 ------------------------------ lua/gitlinker/commons/api.lua | 114 ------ lua/gitlinker/commons/fileio.lua | 132 +++---- lua/gitlinker/commons/json.lua | 29 -- lua/gitlinker/commons/logging.lua | 3 +- lua/gitlinker/commons/num.lua | 3 +- lua/gitlinker/commons/path.lua | 13 +- lua/gitlinker/commons/ringbuf.lua | 221 ++++++++++++ lua/gitlinker/commons/str.lua | 6 +- lua/gitlinker/commons/tbl.lua | 6 +- lua/gitlinker/commons/version.lua | 54 +-- lua/gitlinker/commons/version.txt | 2 +- 15 files changed, 481 insertions(+), 977 deletions(-) create mode 100644 .markdownlint.jsonc delete mode 100644 lua/gitlinker/commons/_json.lua delete mode 100644 lua/gitlinker/commons/api.lua delete mode 100644 lua/gitlinker/commons/json.lua create mode 100644 lua/gitlinker/commons/ringbuf.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab4565df..e7650faa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: - master concurrency: group: ${{ github.ref }}-${{ github.workflow }} - cancel-in-progress: ${{ !contains(github.ref, 'master') }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} jobs: commits: name: Commits @@ -69,7 +69,7 @@ jobs: - lint strategy: matrix: - nvim_version: [stable, nightly, v0.7.0] + nvim_version: [stable, nightly, v0.9.0] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -80,60 +80,59 @@ jobs: version: ${{ matrix.nvim_version }} - uses: leafo/gh-actions-lua@v10 with: - luaVersion: "luajit-2.1.0-beta3" + # luaVersion: "luajit-2.1.0-beta3" + luaVersion: "luajit-openresty" - uses: leafo/gh-actions-luarocks@v4 - name: Run Test Cases shell: bash run: | luarocks install vusted vusted ./spec - code_coverage: - name: Code Coverage - needs: - - lint - strategy: - matrix: - nvim_version: [stable] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: rhysd/action-setup-vim@v1 - id: vim - with: - neovim: true - version: ${{ matrix.nvim_version }} - - uses: leafo/gh-actions-lua@v10 - with: - luaVersion: "luajit-2.1.0-beta3" - - uses: leafo/gh-actions-luarocks@v4 - - name: Generate Coverage Reports - shell: bash - run: | - luarocks install luacov - luarocks install vusted - vusted --coverage ./spec - - name: Upload Coverage Reports - shell: bash - run: | - echo "ls ." - ls -l . - echo "run luacov" - luacov - echo "ls ." - ls -l . - echo "cat ./luacov.report.out" - cat ./luacov.report.out - - uses: codecov/codecov-action@v3 - with: - files: luacov.report.out - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # code_coverage: + # name: Code Coverage + # needs: + # - lint + # strategy: + # matrix: + # nvim_version: [stable] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: rhysd/action-setup-vim@v1 + # id: vim + # with: + # neovim: true + # version: ${{ matrix.nvim_version }} + # - uses: leafo/gh-actions-lua@v10 + # with: + # # luaVersion: "luajit-2.1.0-beta3" + # luaVersion: "luajit-openresty" + # - uses: leafo/gh-actions-luarocks@v4 + # - name: Generate Coverage Reports + # run: | + # luarocks --lua-version=5.1 install luacov + # luarocks --lua-version=5.1 install luacov-reporter-lcov + # luarocks --lua-version=5.1 install vusted + # vusted --coverage ./spec + # echo "ls ." + # ls -l . + # echo "run luacov" + # luacov + # echo "ls ." + # ls -l . + # echo "tail ./luacov.report.out" + # tail -n 10 ./luacov.report.out + # - uses: codecov/codecov-action@v4 + # with: + # files: luacov.report.out + # env: + # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} release: name: Release if: ${{ github.ref == 'refs/heads/master' }} needs: - unit_test - - code_coverage + # - code_coverage runs-on: ubuntu-latest steps: - uses: google-github-actions/release-please-action@v3 diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 00000000..952f9387 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,14 @@ +{ + // Heading levels should only increment by one level at a time + "MD001": false, + // Line length + "MD013": false, + // Fenced code blocks should have a language specified + "MD040": false, + // Inline HTML + "MD033": false, + // First line in a file should be a top-level heading + "MD041": false, + // Bare URL used + "MD034": false, +} diff --git a/README.md b/README.md index b32ead37..5702e6e1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ - - # gitlinker.nvim

-Neovim +Neovim commons.nvim luarocks ci.yml @@ -20,11 +18,11 @@ https://github.com/linrongbin16/gitlinker.nvim/assets/6496887/d3e425a5-cf08-487f For now supported platforms are: -- [github.com](https://github.com/) -- [gitlab.com](https://gitlab.com/) -- [bitbucket.org](https://bitbucket.org/) -- [codeberg.org](https://codeberg.org/) -- [git.samba.org](https://git.samba.org/) +- https://github.com/ +- https://gitlab.com/ +- https://bitbucket.org/ +- https://codeberg.org/ +- https://git.samba.org/ PRs are welcomed for other git host websites! @@ -35,6 +33,9 @@ PRs are welcomed for other git host websites! - [Installation](#installation) - [Usage](#usage) - [Command](#command) + - [Multiple Remotes](#multiple-remotes) + - [Relative File Path](#relative-file-path) + - [Commit ID](#commit-id) - [API](#api) - [Recommended Key Mappings](#recommended-key-mappings) - [Configuration](#configuration) @@ -52,18 +53,22 @@ PRs are welcomed for other git host websites! - Provide `GitLink` command instead of default key mappings. 2. New Features: - Windows (+wsl2) support. + - Blame support. + - Full [git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) support. - Respect ssh host alias. - Add `?plain=1` for markdown files. - - Support blame url. - - Full [git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) support. 3. Improvements: - - Use stderr from git command as error message. + - Use git `stderr` output as error message. - Async child process IO via coroutine and `uv.spawn`. - - Drop off `plenary` dependency. + - No third-party dependencies. ## Requirements -- Neovim ≥ 0.7. +> [!NOTE] +> +> This plugin keeps update with the latest stable Neovim version, supports until the last legacy version, while earlier versions are dropped for maintenance reason. For example at the time of writing (2024-08-20), stable is 0.10, last legacy is 0.9. Thus this plugin supports 0.9+. + +- Neovim ≥ 0.9. - [git](https://git-scm.com/). - [ssh](https://www.openssh.com/) (optional for resolve ssh host alias). - [wslview](https://github.com/wslutilities/wslu) (optional for open browser from Windows wsl2). @@ -109,12 +114,16 @@ return require('pckr').add( ### Command -You can use the user command `GitLink` to generate git permlink: +You can use the user command `GitLink` to generate a (perm)link to the git host website: -- `GitLink(!)`: copy the `/blob` url to clipboard (use `!` to open in browser). -- `GitLink(!) blame`: copy the `/blame` url to clipboard (use `!` to open in browser). -- `GitLink(!) default_branch`: copy the `/main` or `/master` url to clipboard (use `!` to open in browser). -- `GitLink(!) current_branch`: copy the current branch url to clipboard (use `!` to open in browser). +- `GitLink`: copy the `/blob` url to clipboard. +- `GitLink blame`: copy the `/blame` url to clipboard. +- `GitLink default_branch`: copy the `/main` or `/master` url to clipboard. +- `GitLink current_branch`: copy the current branch url to clipboard. + +> [!NOTE] +> +> Add `!` after the command (`GitLink!`) to directly open the url in browser. There're several **router types**: @@ -125,33 +134,45 @@ There're several **router types**: > [!NOTE] > -> A router type is a general collection of router implementations binding on different git hosts, thus it can work for any git hosts, for example for [bitbucket.org](https://bitbucket.org/): +> A router type is a collection of multiple implementations binding on different git host websites, it works for any git hosts. For example the [bitbucket.org](https://bitbucket.org/): > -> - `browse` generate the `/src` url (default): https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14. -> - `blame` generate the `/annotate` url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/annotate/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14. -> - `default_branch` generate the `/main` or `/master` url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/master/.gitignore#lines-9:14. -> - `current_branch` generate the current branch url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/feat-dev/.gitignore#lines-9:14. +> - `browse` generates the `/src` url (default): https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14. +> - `blame` generates the `/annotate` url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/annotate/dbf3922382576391fbe50b36c55066c1768b08b6/.gitignore#lines-9:14. +> - `default_branch` generates the `/main` or `/master` url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/master/.gitignore#lines-9:14. +> - `current_branch` generates the current branch url: https://bitbucket.org/gitlinkernvim/gitlinker.nvim/src/feat-dev/.gitignore#lines-9:14. -To specify the remote when there're multiple git remotes, add `remote=xxx` parameter, for example: +#### Multiple Remotes + +When there are multiple git remotes, please specify the remote with `remote=xxx` parameter. For example: - `GitLink remote=upstream`: copy url for the `upstream` remote. - `GitLink! blame remote=upstream`: open blame url for the `upstream` remote. +> [!NOTE] +> > By default `GitLink` will use the first detected remote (usually it's `origin`). -To specify the relative file path when current buffer's file path is not a normal file name, add `file=xxx` parameter, for example: +#### Relative File Path + +When the current buffer name is not the file name you want, please specify the target file path with `file=xxx` parameter. For example: - `GitLink file=lua/gitlinker.lua`: copy url for the `lua/gitlinker.lua` file. - `GitLink! blame file=README.md`: open blame url for the `README.md` file. -> By default `GitLink` will use the current buffer's file name. +> [!NOTE] +> +> By default `GitLink` will use the current buffer's name. + +#### Commit ID -To specify the git commit ID when current repository's commit ID is not on your needs, add `rev=xxx` parameter, for example: +When the current git repository's commit ID is not that one you want, please specify the target commit ID with `rev=xxx` parameter. For example: - `GitLink rev=00b3f9a1`: copy url for the `00b3f9a1` commit ID. - `GitLink! blame rev=00b3f9a1`: open blame url for the `00b3f9a1` commit ID. -> By default `GitLink` will use the current repository's commit ID. +> [!NOTE] +> +> By default `GitLink` will use the current git repository's commit ID. ### API @@ -160,7 +181,7 @@ To specify the git commit ID when current repository's commit ID is not on your > Highly recommend reading [Customize Urls](#customize-urls) before this section, which helps understanding the router design of this plugin.

-Click here to see lua api +Click here to see the details.
You can also use the `link` API to generate git permlink: @@ -173,40 +194,40 @@ You can also use the `link` API to generate git permlink: require("gitlinker").link(opts) ``` +> The `GitLink` is actually just a user command wrapper on this API. + **Parameters:** - `opts`: (Optional) lua table that contains below fields: - - `router_type`: Which router type should this API use. By default is `nil`, means `browse`. It has below builtin options: + - `router_type`: Which router type should use. By default is `browse` when not specified. It has below options: - `browse` - `blame` - `default_branch` - `current_branch` - - `router`: Which router implementation should this API use. By default is `nil`, it uses the configured router implementations while this plugin is been setup (see [Configuration](#configuration)). You can **_dynamically_** overwrite the generate behavior by pass a router in this field. + - `router`: Which router implementation should use. By default it uses the configured implementations when this plugin is been setup (see [Configuration](#configuration)). You can overwrite the configured behavior by passing your implementation to this field. Please see [`gitlinker.Router`](#gitlinkerrouter) for more details. - > Once set this field, you will get full control of generating the url, and `router_type` field will no longer take effect. + > [!NOTE] > - > Please refer to [`gitlinker.Router`](#gitlinkerrouter) for more details. - - - `action`: What action should this API behave. By default is `nil`, this API will copy the generated link to clipboard. It has below builtin options: + > Once set this field, you will get full control of generating the url, and `router_type` field will no longer take effect. - - `require("gitlinker.actions").clipboard`: Copy generated link to clipboard. - - `require("gitlinker.actions").system`: Open generated link in browser. + - `action`: What action should do. By default it will copy the generated link to clipboard. It has below options, please see [`gitlinker.Action`](#gitlinkeraction) for more details. - > Please refer to [`gitlinker.Action`](#gitlinkeraction) for more details. + - `require("gitlinker.actions").clipboard`: Copy url to clipboard. + - `require("gitlinker.actions").system`: Open url in browser. - - `lstart`/`lend`: Visual selected line range, e.g. start & end line numbers. By default both are `nil`, it will automatically try to find user selected line range. You can also overwrite these two fields to force the line numbers in generated url. - - `message`: Whether print message in nvim command line. By default it uses the configured value while this plugin is been setup (see [Configuration](#configuration)). You can also overwrite this field to change the configured behavior. - - `highlight_duration`: How long (milliseconds) to highlight the line range. By default it uses the configured value while this plugin is been setup (see [Configuration](#configuration)). You can also overwrite this field to change the configured behavior. - - `remote`: Specify the git remote. By default is `nil`, it uses the first detected git remote (usually it's `origin`). - - `file`: Specify the relative file path. By default is `nil`, it uses the current buffer's file name. - - `rev`: Specify the git commit ID. By default is `nil`, it uses the current repository's git commit ID. + - `lstart`/`lend`: Line range, i.e. start and end line numbers. By default it uses the current line or visual selections. You can also overwrite them to specify the line numbers. + - `message`: Whether print message in command line. By default it uses the configured value while this plugin is been setup (see [Configuration](#configuration)). You can overwrite the configured behavior by passing your option to this field. + - `highlight_duration`: How long (in milliseconds) to highlight the line range. By default it uses the configured value while this plugin is been setup (see [Configuration](#configuration)). You can overwrite the configured behavior by passing your option to this field. + - `remote`: Specify the git remote. By default it uses the first detected git remote (usually it's `origin`). + - `file`: Specify the relative file path. By default it uses the current buffer's name. + - `rev`: Specify the git commit ID. By default it uses the current git repository's commit ID. -##### `gitlinker.Router` +#### `gitlinker.Router` -`gitlinker.Router` is a lua function that implements a router for a git host. It use below function signature: +A lua function that implements a router for a git host website. It uses below function signature: ```lua function(lk:gitlinker.Linker):string? @@ -214,18 +235,16 @@ function(lk:gitlinker.Linker):string? **Parameters:** -- `lk`: Lua table that presents the `gitlinker.Linker` data type. It contains all the information (fields) you need to generate a git link, e.g. the `protocol`, `host`, `username`, `path`, `rev`, etc. - - > Please refer to [Customize Urls - Lua Function](#lua-function) for more details. +- `lk`: A lua table that presents the `gitlinker.Linker` data type. It contains all the information (fields) you need to generate a git link, e.g. the `protocol`, `host`, `username`, `path`, `rev`, etc. Please see [Customize Urls - Lua Function](#lua-function) for more details. **Returns:** -- It returns the generated link as a `string` type, if success. -- It returns `nil`, if failed. +- Returns the generated link as a `string` type, if success. +- Returns `nil`, if failed. -##### `gitlinker.Action` +#### `gitlinker.Action` -`gitlinker.Action` is a lua function that do some operations with a generated git link. It use below function signature: +A lua function that does some operations with the generated url. It uses below function signature: ```lua function(url:string):any @@ -233,14 +252,14 @@ function(url:string):any **Parameters:** -- `url`: The generated git link. For example: https://codeberg.org/linrongbin16/gitlinker.nvim/src/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L4-L7. +- `url`: The generated url. For example: https://codeberg.org/linrongbin16/gitlinker.nvim/src/commit/a570f22ff833447ee0c58268b3bae4f7197a8ad8/LICENSE#L4-L7. For now we have below builtin actions: - `require("gitlinker.actions").clipboard`: Copy url to clipboard. - `require("gitlinker.actions").system`: Open url in browser. -If you only need to get the generated url, instead of do some actions, you can pass a callback function to accept the url: +If you only need to print the generated url, you can pass a callback function to consume: ```lua require("gitlinker").link({ @@ -250,14 +269,12 @@ require("gitlinker").link({ }) ``` -> The `link` API is running in async mode and cannot directly returns the generated link, because it uses lua coroutine to avoid blocking IO. -
### Recommended Key Mappings
-Click here to see key mappings with vim command +Click here to see mappings with user commands.
```lua @@ -320,7 +337,7 @@ vim.keymap.set(
-Click here to see key mappings with lua api +Click here to see mappings with lua apis.
```lua @@ -419,63 +436,63 @@ For complete default options, please see `Defaults` in [configs.lua](https://git > [!NOTE] > -> Please refer to [Git Protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) and [giturlparser](https://github.com/linrongbin16/giturlparser.lua?tab=readme-ov-file#features) for better understanding git url. +> Recommend reading [Git Protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols) and [giturlparser](https://github.com/linrongbin16/giturlparser.lua?tab=readme-ov-file#features) for better understanding git urls. #### String Template > [!NOTE] > -> Please refer to `Defaults.router` in [configs.lua](https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/configs.lua) for more examples about string template. +> Please see `Defaults.router` in [configs.lua](https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/configs.lua) for more examples. -To create customized urls for other git hosts, please bind the target git host name with a new router. A router simply constructs the url string from below components (upper case with prefix `_A.`): +To create customized urls for other git hosts, please bind the target git host name with a new implementation, which simply constructs the url string from below components (upper case with prefix `_A.`): -- `_A.PROTOCOL`: Network protocol before `://` delimiter, for example: +- `_A.PROTOCOL`: Network protocol before `://` delimiter. For example: - `https` in `https://github.com`. - `ssh` in `ssh://github.com`. -- `_A.USERNAME`: Optional user name component before `@` delimiter, for example: +- `_A.USERNAME`: Optional user name component before `@` delimiter. For example: - `git` in `ssh://git@github.com/linrongbin16/gitlinker.nvim.git`. - - `myname` in `myname@github.com:linrongbin16/gitlinker.nvim.git` (**Note:** the ssh protocol `ssh://` can be omitted). -- `_A.PASSWORD`: Optional password component after `_A.USERNAME`, for example: + - `myname` in `myname@github.com:linrongbin16/gitlinker.nvim.git` (**Note:** the ssh protocol `ssh://` is omitted in this case). +- `_A.PASSWORD`: Optional password component after `_A.USERNAME`. For example: - `mypass` in `myname:mypass@github.com:linrongbin16/gitlinker.nvim.git`. - `mypass` in `https://myname:mypass@github.com/linrongbin16/gitlinker.nvim.git`. -- `_A.HOST`: The host component, for example: - - `github.com` in `https://github.com/linrongbin16/gitlinker.nvim` (**Note:** for http/https protocol, host ends with `/`). - - `127.0.0.1` in `git@127.0.0.1:linrongbin16/gitlinker.nvim` (**Note:** for omitted ssh protocol, host ends with `:`, and cannot have `_A.PORT` component). -- `_A.PORT`: Optional port component after `_A.HOST` (**Note:** omitted ssh protocols cannot have `_A.PORT` component), for example: +- `_A.HOST`: The host component. For example: + - `github.com` in `https://github.com/linrongbin16/gitlinker.nvim` (**Note:** for http/https protocol, the host ends with `/`). + - `127.0.0.1` in `git@127.0.0.1:linrongbin16/gitlinker.nvim` (**Note:** for _omitted_ ssh protocol, the host ends with `:`, and it cannot have `_A.PORT` component). +- `_A.PORT`: Optional port component after `_A.HOST` (**Note:** omitted ssh protocols cannot have `_A.PORT` component). For example: - `22` in `https://github.com:22/linrongbin16/gitlinker.nvim`. - `123456` in `https://127.0.0.1:123456/linrongbin16/gitlinker.nvim`. -- `_A.PATH`: All the other parts in the output of the `git remote get-url origin`, for example: +- `_A.PATH`: Path component, i.e. all the other parts in the output of the `git remote get-url origin`. For example: - `/linrongbin16/gitlinker.nvim.git` in `https://github.com/linrongbin16/gitlinker.nvim.git`. - - `linrongbin16/gitlinker.nvim.git` in `git@github.com:linrongbin16/gitlinker.nvim.git`. -- `_A.REV`: Git commit, for example: + - `linrongbin16/gitlinker.nvim.git` in `git@github.com:linrongbin16/gitlinker.nvim.git` (**Note:** for ssh protocol, the `:` before the path component doesn't belong to it). +- `_A.REV`: Git commit ID. For example: - `a009dacda96756a8c418ff5fa689999b148639f6` in `https://github.com/linrongbin16/gitlinker.nvim/blob/a009dacda96756a8c418ff5fa689999b148639f6/lua/gitlinker/git.lua?plain=1#L3`. -- `_A.FILE`: Relative file path, for example: - - The `lua/gitlinker/routers.lua` in `https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua`. -- `_A.LSTART`/`_A.LEND`: Start/end line number, for example: +- `_A.FILE`: Relative file path. For example: + - `lua/gitlinker/routers.lua` in `https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua`. +- `_A.LSTART`/`_A.LEND`: Start/end line number. For example: - `5`/`13` in `https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua#L5-L13`. There're 2 more sugar components derived from `_A.PATH`: -- `_A.REPO`: The last part after the last slash (`/`) in `_A.PATH`, with around slashes been removed (and the `.git` suffix is been removed for easier writing), for example: +- `_A.REPO`: The last part after the last slash (`/`) in `_A.PATH` (around slashes are removed, and the `.git` suffix is been removed for easier writing). For example: - `gitlinker.nvim` in `https://github.com/linrongbin16/gitlinker.nvim.git`. - `neovim` in `git@192.168.0.1:path/to/the/neovim.git`. -- `_A.ORG`: All the other parts before `_A.REPO`, with around slashes been removed, for example: +- `_A.ORG`: All the previous parts before `_A.REPO` (around slashes are removed). For example: - `linrongbin16` in `https://github.com/linrongbin16/gitlinker.nvim.git`. - `path/to/the` in `https://github.com/path/to/the/repo.git`. > [!IMPORTANT] > -> The `_A.ORG` component can be empty when the `_A.PATH` contains only 1 slash (`/`), for example: the `_A.ORG` in `ssh://git@host.xyz/repo.git` is empty. +> The `_A.ORG` component can be empty if `_A.PATH` only contains 1 slash (`/`). For example `_A.ORG` in `ssh://git@host.xyz/repo.git` is empty, while `_A.REPO` is `repo`. There're 2 more sugar components for git branches: -- `_A.DEFAULT_BRANCH`: Default branch retrieved from `git rev-parse --abbrev-ref origin/HEAD`, for example: +- `_A.DEFAULT_BRANCH`: Default branch retrieved from `git rev-parse --abbrev-ref origin/HEAD`. For example: - `master` in `https://github.com/ruifm/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua#L37-L156`. - `main` in `https://github.com/linrongbin16/commons.nvim/blob/main/lua/commons/uv.lua`. -- `_A.CURRENT_BRANCH`: Current branch retrieved from `git rev-parse --abbrev-ref HEAD`, for example: - - `feat-router-types`. +- `_A.CURRENT_BRANCH`: Current branch retrieved from `git rev-parse --abbrev-ref HEAD`. For example: + - `feat-router-types` in `https://github.com/ruifm/gitlinker.nvim/blob/feat-router-types/lua/gitlinker/routers.lua#L37-L156`. -For example you can customize the line numbers in form `?&line=1&lines-count=2` like this: +With above components, you can customize the line numbers (for example) in form `?&line=1&lines-count=2` like this: ```lua require("gitlinker").setup({ @@ -493,15 +510,15 @@ require("gitlinker").setup({ }) ``` -The template string use curly braces `{}` to contain lua scripts, and evaluate via [luaeval()](https://neovim.io/doc/user/lua.html#lua-eval), while the error message can be confusing if there's any syntax issue. +The template string use curly braces `{}` to contain lua scripts, and evaluate via [luaeval()](https://neovim.io/doc/user/lua.html#lua-eval) (the error message can be confusing if there's any syntax issue). #### Lua Function > [!NOTE] > -> Please refer to [routers.lua](https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua) for more examples about function-based routers. +> Please see [routers.lua](https://github.com/linrongbin16/gitlinker.nvim/blob/master/lua/gitlinker/routers.lua) for more examples. -You can also bind a lua function to the git host, the function accepts only 1 lua table as its parameter, which contains the same fields as string template, but in lower case, without the prefix `_A.`: +You can also implement the router with a lua function. The function accepts only 1 lua table as its parameter, which contains the same fields as string template, but in lower case, without the prefix `_A.`: - `protocol` - `username` @@ -513,17 +530,17 @@ You can also bind a lua function to the git host, the function accepts only 1 lu - `file` - `lstart`/`lend` -The 2 derived components are: +The 2 sugar components derived from `path` are: - `org` -- `repo`: **Note:** the `.git` suffix is not omitted. +- `repo` (**Note:** the `.git` suffix is not omitted) -The 2 branch components are: +The 2 git branch components are: - `default_branch` - `current_branch` -Recall to previous use case, e.g. customize the line numbers in form `?&line=1&lines-count=2`, you can implement the router with below function: +Recall to previous use case (customize the line numbers in form `?&line=1&lines-count=2`), you can implement the router with below function: ```lua --- @param s string @@ -567,15 +584,15 @@ require("gitlinker").setup({ }) ``` -There are some pre-defined lua apis in `gitlinker.routers` that you can use: +There are some pre-defined APIs in `gitlinker.routers` that you can use: -- `github_browse`/`github_blame`: for github.com. -- `gitlab_browse`/`gitlab_blame`: for gitlab.com. -- `bitbucket_browse`/`bitbucket_blame`: for bitbucket.org. -- `codeberg_browse`/`codeberg_blame`: for codeberg.org. -- `samba_browse`: for git.samba.org (blame not support). +- `github_browse`/`github_blame`: for https://github.com/. +- `gitlab_browse`/`gitlab_blame`: for https://gitlab.com/. +- `bitbucket_browse`/`bitbucket_blame`: for https://bitbucket.org/. +- `codeberg_browse`/`codeberg_blame`: for https://codeberg.org/. +- `samba_browse`: for https://git.samba.org/ (blame not support). -For example if you need to bind a github enterprise domain, you can use: +If you need to bind a github enterprise host, please use: ```lua require('gitlinker').setup({ @@ -592,7 +609,7 @@ require('gitlinker').setup({ ### Create Your Own Router -You can even create your own router (e.g. use the same engine with `browse`/`blame`), for example create the `file_only` router type (generate link without line numbers): +You can even create your own router (with the same engine). For example let's create the `file_only` router type, it generates url without line numbers: ```lua require("gitlinker").setup({ @@ -608,7 +625,7 @@ require("gitlinker").setup({ }) ``` -Then use it just like `browse`: +Use it just like `browse`: ```vim GitLink file_only diff --git a/lua/gitlinker/commons/_json.lua b/lua/gitlinker/commons/_json.lua deleted file mode 100644 index 42e27aa2..00000000 --- a/lua/gitlinker/commons/_json.lua +++ /dev/null @@ -1,557 +0,0 @@ -local type = type -local next = next -local error = error -local tonumber = tonumber -local tostring = tostring -local table_concat = table.concat -local table_sort = table.sort -local string_char = string.char -local string_byte = string.byte -local string_find = string.find -local string_match = string.match -local string_gsub = string.gsub -local string_sub = string.sub -local string_format = string.format -local setmetatable = setmetatable -local getmetatable = getmetatable -local huge = math.huge -local tiny = -huge - -local utf8_char -local math_type - -if _VERSION == "Lua 5.1" or _VERSION == "Lua 5.2" then - local math_floor = math.floor - function utf8_char(c) - if c <= 0x7f then - return string_char(c) - elseif c <= 0x7ff then - return string_char(math_floor(c / 64) + 192, c % 64 + 128) - elseif c <= 0xffff then - return string_char( - math_floor(c / 4096) + 224, - math_floor(c % 4096 / 64) + 128, - c % 64 + 128 - ) - elseif c <= 0x10ffff then - return string_char( - math_floor(c / 262144) + 240, - math_floor(c % 262144 / 4096) + 128, - math_floor(c % 4096 / 64) + 128, - c % 64 + 128 - ) - end - error(string_format("invalid UTF-8 code '%x'", c)) - end - - function math_type(v) - if v >= -2147483648 and v <= 2147483647 and math_floor(v) == v then - return "integer" - end - return "float" - end -else - utf8_char = utf8.char - math_type = math.type -end - -local json = {} - -json.supportSparseArray = true - -local objectMt = {} - -function json.createEmptyObject() - return setmetatable({}, objectMt) -end - -function json.isObject(t) - if t[1] ~= nil then - return false - end - return next(t) ~= nil or getmetatable(t) == objectMt -end - -if debug and debug.upvalueid then - -- Generate a lightuserdata - json.null = debug.upvalueid(json.createEmptyObject, 1) -else - json.null = function () end -end - --- json.encode -- -local statusVisited -local statusBuilder - -local encode_map = {} - -local encode_escape_map = { - ["\""] = "\\\"", - ["\\"] = "\\\\", - ["/"] = "\\/", - ["\b"] = "\\b", - ["\f"] = "\\f", - ["\n"] = "\\n", - ["\r"] = "\\r", - ["\t"] = "\\t", -} - -local decode_escape_set = {} -local decode_escape_map = {} -for k, v in next, encode_escape_map do - decode_escape_map[v] = k - decode_escape_set[string_byte(v, 2)] = true -end - -for i = 0, 31 do - local c = string_char(i) - if not encode_escape_map[c] then - encode_escape_map[c] = string_format("\\u%04x", i) - end -end - -local function encode(v) - local res = encode_map[type(v)](v) - statusBuilder[#statusBuilder+1] = res -end - -encode_map["nil"] = function () - return "null" -end - -local function encode_string(v) - return string_gsub(v, '[%z\1-\31\\"]', encode_escape_map) -end - -function encode_map.string(v) - statusBuilder[#statusBuilder+1] = '"' - statusBuilder[#statusBuilder+1] = encode_string(v) - return '"' -end - -local function convertreal(v) - local g = string_format("%.16g", v) - if tonumber(g) == v then - return g - end - return string_format("%.17g", v) -end - -if string_match(tostring(1 / 2), "%p") == "," then - local _convertreal = convertreal - function convertreal(v) - return string_gsub(_convertreal(v), ",", ".") - end -end - -function encode_map.number(v) - if v ~= v or v <= tiny or v >= huge then - error("unexpected number value '"..tostring(v).."'") - end - if math_type(v) == "integer" then - return string_format("%d", v) - end - return convertreal(v) -end - -function encode_map.boolean(v) - if v then - return "true" - else - return "false" - end -end - -function encode_map.table(t) - local first_val = next(t) - if first_val == nil then - if getmetatable(t) == objectMt then - return "{}" - else - return "[]" - end - end - if statusVisited[t] then - error("circular reference") - end - statusVisited[t] = true - if type(first_val) == "string" then - local keys = {} - for k in next, t do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types: "..k) - end - keys[#keys+1] = k - end - table_sort(keys) - do - local k = keys[1] - statusBuilder[#statusBuilder+1] = '{"' - statusBuilder[#statusBuilder+1] = encode_string(k) - statusBuilder[#statusBuilder+1] = '":' - encode(t[k]) - end - for i = 2, #keys do - local k = keys[i] - statusBuilder[#statusBuilder+1] = ',"' - statusBuilder[#statusBuilder+1] = encode_string(k) - statusBuilder[#statusBuilder+1] = '":' - encode(t[k]) - end - statusVisited[t] = nil - return "}" - elseif json.supportSparseArray then - local max = 0 - for k in next, t do - if math_type(k) ~= "integer" or k <= 0 then - error("invalid table: mixed or invalid key types: "..k) - end - if max < k then - max = k - end - end - statusBuilder[#statusBuilder+1] = "[" - encode(t[1]) - for i = 2, max do - statusBuilder[#statusBuilder+1] = "," - encode(t[i]) - end - statusVisited[t] = nil - return "]" - else - if t[1] == nil then - error("invalid table: sparse array is not supported") - end - if jit and t[0] ~= nil then - -- 0 is the first index in luajit - error("invalid table: mixed or invalid key types: "..0) - end - statusBuilder[#statusBuilder+1] = "[" - encode(t[1]) - local count = 2 - while t[count] ~= nil do - statusBuilder[#statusBuilder+1] = "," - encode(t[count]) - count = count + 1 - end - if next(t, count - 1) ~= nil then - local k = next(t, count - 1) - if type(k) == "number" then - error("invalid table: sparse array is not supported") - else - error("invalid table: mixed or invalid key types: "..k) - end - end - statusVisited[t] = nil - return "]" - end -end - -local function encode_unexpected(v) - if v == json.null then - return "null" - else - error("unexpected type '"..type(v).."'") - end -end -encode_map["function"] = encode_unexpected -encode_map["userdata"] = encode_unexpected -encode_map["thread"] = encode_unexpected - -function json.encode(v) - statusVisited = {} - statusBuilder = {} - encode(v) - return table_concat(statusBuilder) -end - -json._encode_map = encode_map -json._encode_string = encode_string - --- json.decode -- - -local statusBuf -local statusPos -local statusTop -local statusAry = {} -local statusRef = {} - -local function find_line() - local line = 1 - local pos = 1 - while true do - local f, _, nl1, nl2 = string_find(statusBuf, "([\n\r])([\n\r]?)", pos) - if not f then - return line, statusPos - pos + 1 - end - local newpos = f + ((nl1 == nl2 or nl2 == "") and 1 or 2) - if newpos > statusPos then - return line, statusPos - pos + 1 - end - pos = newpos - line = line + 1 - end -end - -local function decode_error(msg) - error(string_format("ERROR: %s at line %d col %d", msg, find_line()), 2) -end - -local function get_word() - return string_match(statusBuf, "^[^ \t\r\n%]},]*", statusPos) -end - -local function next_byte() - local pos = string_find(statusBuf, "[^ \t\r\n]", statusPos) - if pos then - statusPos = pos - return string_byte(statusBuf, pos) - end - return -1 -end - -local function consume_byte(c) - local _, pos = string_find(statusBuf, c, statusPos) - if pos then - statusPos = pos + 1 - return true - end -end - -local function expect_byte(c) - local _, pos = string_find(statusBuf, c, statusPos) - if not pos then - decode_error(string_format("expected '%s'", string_sub(c, #c))) - end - statusPos = pos -end - -local function decode_unicode_surrogate(s1, s2) - return utf8_char(0x10000 + (tonumber(s1, 16) - 0xd800) * 0x400 + (tonumber(s2, 16) - 0xdc00)) -end - -local function decode_unicode_escape(s) - return utf8_char(tonumber(s, 16)) -end - -local function decode_string() - local has_unicode_escape = false - local has_escape = false - local i = statusPos + 1 - while true do - i = string_find(statusBuf, '[%z\1-\31\\"]', i) - if not i then - decode_error "expected closing quote for string" - end - local x = string_byte(statusBuf, i) - if x < 32 then - statusPos = i - decode_error "control character in string" - end - if x == 34 --[[ '"' ]] then - local s = string_sub(statusBuf, statusPos + 1, i - 1) - if has_unicode_escape then - s = string_gsub(string_gsub(s - , "\\u([dD][89aAbB]%x%x)\\u([dD][c-fC-F]%x%x)", decode_unicode_surrogate) - , "\\u(%x%x%x%x)", decode_unicode_escape) - end - if has_escape then - s = string_gsub(s, "\\.", decode_escape_map) - end - statusPos = i + 1 - return s - end - --assert(x == 92 --[[ "\\" ]]) - local nx = string_byte(statusBuf, i + 1) - if nx == 117 --[[ "u" ]] then - if not string_match(statusBuf, "^%x%x%x%x", i + 2) then - statusPos = i - decode_error "invalid unicode escape in string" - end - has_unicode_escape = true - i = i + 6 - else - if not decode_escape_set[nx] then - statusPos = i - decode_error("invalid escape char '"..(nx and string_char(nx) or "").."' in string") - end - has_escape = true - i = i + 2 - end - end -end - -local function decode_number() - local num, c = string_match(statusBuf, "^([0-9]+%.?[0-9]*)([eE]?)", statusPos) - if not num or string_byte(num, -1) == 0x2E --[[ "." ]] then - decode_error("invalid number '"..get_word().."'") - end - if c ~= "" then - num = string_match(statusBuf, "^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},]", statusPos) - if not num then - decode_error("invalid number '"..get_word().."'") - end - end - statusPos = statusPos + #num - return tonumber(num) -end - -local function decode_number_zero() - local num, c = string_match(statusBuf, "^(.%.?[0-9]*)([eE]?)", statusPos) - if not num or string_byte(num, -1) == 0x2E --[[ "." ]] or string_match(statusBuf, "^.[0-9]+", statusPos) then - decode_error("invalid number '"..get_word().."'") - end - if c ~= "" then - num = string_match(statusBuf, "^([^eE]*[eE][-+]?[0-9]+)[ \t\r\n%]},]", statusPos) - if not num then - decode_error("invalid number '"..get_word().."'") - end - end - statusPos = statusPos + #num - return tonumber(num) -end - -local function decode_number_negative() - statusPos = statusPos + 1 - local c = string_byte(statusBuf, statusPos) - if c then - if c == 0x30 then - return -decode_number_zero() - elseif c > 0x30 and c < 0x3A then - return -decode_number() - end - end - decode_error("invalid number '"..get_word().."'") -end - -local function decode_true() - if string_sub(statusBuf, statusPos, statusPos + 3) ~= "true" then - decode_error("invalid literal '"..get_word().."'") - end - statusPos = statusPos + 4 - return true -end - -local function decode_false() - if string_sub(statusBuf, statusPos, statusPos + 4) ~= "false" then - decode_error("invalid literal '"..get_word().."'") - end - statusPos = statusPos + 5 - return false -end - -local function decode_null() - if string_sub(statusBuf, statusPos, statusPos + 3) ~= "null" then - decode_error("invalid literal '"..get_word().."'") - end - statusPos = statusPos + 4 - return json.null -end - -local function decode_array() - statusPos = statusPos + 1 - if consume_byte "^[ \t\r\n]*%]" then - return {} - end - local res = {} - statusTop = statusTop + 1 - statusAry[statusTop] = true - statusRef[statusTop] = res - return res -end - -local function decode_object() - statusPos = statusPos + 1 - if consume_byte "^[ \t\r\n]*}" then - return json.createEmptyObject() - end - local res = {} - statusTop = statusTop + 1 - statusAry[statusTop] = false - statusRef[statusTop] = res - return res -end - -local decode_uncompleted_map = { - [string_byte '"'] = decode_string, - [string_byte "0"] = decode_number_zero, - [string_byte "1"] = decode_number, - [string_byte "2"] = decode_number, - [string_byte "3"] = decode_number, - [string_byte "4"] = decode_number, - [string_byte "5"] = decode_number, - [string_byte "6"] = decode_number, - [string_byte "7"] = decode_number, - [string_byte "8"] = decode_number, - [string_byte "9"] = decode_number, - [string_byte "-"] = decode_number_negative, - [string_byte "t"] = decode_true, - [string_byte "f"] = decode_false, - [string_byte "n"] = decode_null, - [string_byte "["] = decode_array, - [string_byte "{"] = decode_object, -} -local function unexpected_character() - decode_error("unexpected character '"..string_sub(statusBuf, statusPos, statusPos).."'") -end -local function unexpected_eol() - decode_error("unexpected character ''") -end - -local decode_map = {} -for i = 0, 255 do - decode_map[i] = decode_uncompleted_map[i] or unexpected_character -end -decode_map[-1] = unexpected_eol - -local function decode() - return decode_map[next_byte()]() -end - -local function decode_item() - local top = statusTop - local ref = statusRef[top] - if statusAry[top] then - ref[#ref+1] = decode() - else - expect_byte '^[ \t\r\n]*"' - local key = decode_string() - expect_byte "^[ \t\r\n]*:" - statusPos = statusPos + 1 - ref[key] = decode() - end - if top == statusTop then - repeat - local chr = next_byte() - statusPos = statusPos + 1 - if chr == 44 --[[ "," ]] then - return - end - if statusAry[statusTop] then - if chr ~= 93 --[[ "]" ]] then decode_error "expected ']' or ','" end - else - if chr ~= 125 --[[ "}" ]] then decode_error "expected '}' or ','" end - end - statusTop = statusTop - 1 - until statusTop == 0 - end -end - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got "..type(str)) - end - statusBuf = str - statusPos = 1 - statusTop = 0 - local res = decode() - while statusTop > 0 do - decode_item() - end - if string_find(statusBuf, "[^ \t\r\n]", statusPos) then - decode_error "trailing garbage" - end - return res -end - -return json diff --git a/lua/gitlinker/commons/api.lua b/lua/gitlinker/commons/api.lua deleted file mode 100644 index 17d3f90c..00000000 --- a/lua/gitlinker/commons/api.lua +++ /dev/null @@ -1,114 +0,0 @@ -local NVIM_VERSION_0_8 = false -local NVIM_VERSION_0_9 = false - -do - NVIM_VERSION_0_8 = require("gitlinker.commons.version").ge({ 0, 8 }) - NVIM_VERSION_0_9 = require("gitlinker.commons.version").ge({ 0, 9 }) -end - -local M = {} - --- buffer { - ---- @param bufnr integer ---- @param name string ---- @return any -M.get_buf_option = function(bufnr, name) - if NVIM_VERSION_0_8 then - return vim.api.nvim_get_option_value(name, { buf = bufnr }) - else - return vim.api.nvim_buf_get_option(bufnr, name) - end -end - ---- @param bufnr integer ---- @param name string ---- @param value any -M.set_buf_option = function(bufnr, name, value) - if NVIM_VERSION_0_8 then - return vim.api.nvim_set_option_value(name, value, { buf = bufnr }) - else - return vim.api.nvim_buf_set_option(bufnr, name, value) - end -end - --- buffer } - --- window { - ---- @param winnr integer ---- @param name string ---- @return any -M.get_win_option = function(winnr, name) - if NVIM_VERSION_0_8 then - return vim.api.nvim_get_option_value(name, { win = winnr }) - else - return vim.api.nvim_win_get_option(winnr, name) - end -end - ---- @param winnr integer ---- @param name string ---- @param value any ---- @return any -M.set_win_option = function(winnr, name, value) - if NVIM_VERSION_0_8 then - return vim.api.nvim_set_option_value(name, value, { win = winnr }) - else - return vim.api.nvim_win_set_option(winnr, name, value) - end -end - --- window } - --- highlight { - ---- @param hl string ---- @return {fg:integer?,bg:integer?,[string]:any,ctermfg:integer?,ctermbg:integer?,cterm:{fg:integer?,bg:integer?,[string]:any}?} -M.get_hl = function(hl) - if NVIM_VERSION_0_9 then - return vim.api.nvim_get_hl(0, { name = hl, link = false }) - else - ---@diagnostic disable-next-line: undefined-field - local ok1, rgb_value = pcall(vim.api.nvim_get_hl_by_name, hl, true) - if not ok1 then - return vim.empty_dict() - end - ---@diagnostic disable-next-line: undefined-field - local ok2, cterm_value = pcall(vim.api.nvim_get_hl_by_name, hl, false) - if not ok2 then - return vim.empty_dict() - end - local result = vim.tbl_deep_extend("force", rgb_value, { - ctermfg = cterm_value.foreground, - ctermbg = cterm_value.background, - cterm = cterm_value, - }) - result.fg = result.foreground - result.bg = result.background - result.sp = result.special - result.cterm.fg = result.cterm.foreground - result.cterm.bg = result.cterm.background - result.cterm.sp = result.cterm.special - return result - end -end - ---- @param ... string? ---- @return {fg:integer?,bg:integer?,[string]:any,ctermfg:integer?,ctermbg:integer?,cterm:{fg:integer?,bg:integer?,[string]:any}?}, integer, string? -M.get_hl_with_fallback = function(...) - for i, hl in ipairs({ ... }) do - if type(hl) == "string" then - local hl_value = M.get_hl(hl) - if type(hl_value) == "table" and not vim.tbl_isempty(hl_value) then - return hl_value, i, hl - end - end - end - - return vim.empty_dict(), -1, nil -end - --- highlight } - -return M diff --git a/lua/gitlinker/commons/fileio.lua b/lua/gitlinker/commons/fileio.lua index c038277c..f76e2a4a 100644 --- a/lua/gitlinker/commons/fileio.lua +++ b/lua/gitlinker/commons/fileio.lua @@ -1,3 +1,5 @@ +local uv = vim.uv or vim.loop + local M = {} -- FileLineReader { @@ -15,7 +17,6 @@ local FileLineReader = {} --- @param batchsize integer? --- @return commons.FileLineReader? function FileLineReader:open(filename, batchsize) - local uv = require("gitlinker.commons.uv") local handler = uv.fs_open(filename, "r", 438) --[[@as integer]] if type(handler) ~= "number" then error( @@ -54,7 +55,6 @@ end --- @private --- @return integer function FileLineReader:_read_chunk() - local uv = require("gitlinker.commons.uv") local chunksize = (self.filesize >= self.offset + self.batchsize) and self.batchsize or (self.filesize - self.offset) if chunksize <= 0 then @@ -125,7 +125,6 @@ end -- Close the file reader. function FileLineReader:close() - local uv = require("gitlinker.commons.uv") if self.handler then uv.fs_close(self.handler) self.handler = nil @@ -197,80 +196,65 @@ M.readfile = function(filename, opts) return opts.trim and vim.trim(content) or content end +--- @alias commons.AsyncReadFileOnComplete fun(data:string?):any +--- @alias commons.AsyncReadFileOnError fun(step:string?,err:string?):any --- @param filename string ---- @param on_complete fun(data:string?):any ---- @param opts {trim:boolean?}? +--- @param on_complete commons.AsyncReadFileOnComplete +--- @param opts {trim:boolean?,on_error:commons.AsyncReadFileOnError?}? M.asyncreadfile = function(filename, on_complete, opts) - local uv = require("gitlinker.commons.uv") opts = opts or { trim = false } opts.trim = type(opts.trim) == "boolean" and opts.trim or false - local open_result, open_err = uv.fs_open(filename, "r", 438, function(open_complete_err, fd) - if open_complete_err then + if type(opts.on_error) ~= "function" then + opts.on_error = function(step1, err1) error( string.format( - "failed to complete open(r) file %s: %s", + "failed to read file(%s), filename:%s, error:%s", + vim.inspect(step1), vim.inspect(filename), - vim.inspect(open_complete_err) + vim.inspect(err1) ) ) + end + end + + local open_result, open_err = uv.fs_open(filename, "r", 438, function(open_complete_err, fd) + if open_complete_err then + opts.on_error("fs_open complete", open_complete_err) return end - uv.fs_fstat(fd --[[@as integer]], function(fstat_err, stat) - if fstat_err then - error( - string.format( - "failed to fstat file %s: %s", - vim.inspect(filename), - vim.inspect(fstat_err) - ) - ) + uv.fs_fstat(fd --[[@as integer]], function(fstat_complete_err, stat) + if fstat_complete_err then + opts.on_error("fs_fstat complete", fstat_complete_err) return end if not stat then - error( - string.format( - "failed to fstat file %s (empty stat): %s", - vim.inspect(filename), - vim.inspect(fstat_err) - ) - ) + opts.on_error("fs_fstat returns nil", fstat_complete_err) return end - uv.fs_read(fd --[[@as integer]], stat.size, 0, function(read_err, data) - if read_err then - error( - string.format( - "failed to read file %s: %s", - vim.inspect(filename), - vim.inspect(read_err) - ) - ) + uv.fs_read(fd --[[@as integer]], stat.size, 0, function(read_complete_err, data) + if read_complete_err then + opts.on_error("fs_read complete", read_complete_err) return end - uv.fs_close(fd --[[@as integer]], function(close_err) - on_complete((opts.trim and type(data) == "string") and vim.trim(data) or data) - if close_err then - error( - string.format( - "failed to close file %s: %s", - vim.inspect(filename), - vim.inspect(close_err) - ) - ) + uv.fs_close(fd --[[@as integer]], function(close_complete_err) + if opts.trim and type(data) == "string" then + local trimmed_data = vim.trim(data) + on_complete(trimmed_data) + else + on_complete(data) + end + + if close_complete_err then + opts.on_error("fs_close complete", close_complete_err) end end) end) end) end) - assert( - open_result ~= nil, - string.format( - "failed to open(read) file: %s, error: %s", - vim.inspect(filename), - vim.inspect(open_err) - ) - ) + if open_result == nil then + opts.on_error("fs_open", open_err) + end end --- @param filename string @@ -288,46 +272,43 @@ M.readlines = function(filename) return results end +--- @alias commons.AsyncReadLinesOnLine fun(line:string):any +--- @alias commons.AsyncReadLinesOnComplete fun(bytes:integer):any +--- @alias commons.AsyncReadLinesOnError fun(step:string?,err:string?):any --- @param filename string ---- @param opts {on_line:fun(line:string):any,on_complete:fun(bytes:integer):any,on_error:fun(err:string?):any,batchsize:integer?} +--- @param opts {on_line:commons.AsyncReadLinesOnLine,on_complete:commons.AsyncReadLinesOnComplete,on_error:commons.AsyncReadLinesOnError?,batchsize:integer?} M.asyncreadlines = function(filename, opts) assert(type(opts) == "table") assert(type(opts.on_line) == "function") - ---@diagnostic disable-next-line: undefined-field local batchsize = opts.batchsize or 4096 - local function _handle_error(err, msg) - ---@diagnostic disable-next-line: undefined-field - if type(opts.on_error) == "function" then - ---@diagnostic disable-next-line: undefined-field - opts.on_error(err) - else + if type(opts.on_error) ~= "function" then + opts.on_error = function(step1, err1) error( string.format( - "failed to async read file(%s): %s, error: %s", - vim.inspect(msg), + "failed to async read file by lines(%s), filename:%s, error:%s", + vim.inspect(step1), vim.inspect(filename), - vim.inspect(err) + vim.inspect(err1) ) ) end end - local uv = require("gitlinker.commons.uv") local open_result, open_err = uv.fs_open(filename, "r", 438, function(open_complete_err, fd) if open_complete_err then - _handle_error(open_complete_err, "fs_open complete") + opts.on_error("fs_open complete", open_complete_err) return end local fstat_result, fstat_err = uv.fs_fstat( fd --[[@as integer]], function(fstat_complete_err, stat) if fstat_complete_err then - _handle_error(fstat_complete_err, "fs_fstat complete") + opts.on_error("fs_fstat complete", fstat_complete_err) return end if stat == nil then - _handle_error("stat is nil", "fs_fstat complete") + opts.on_error("fs_fstat returns nil", fstat_complete_err) return end @@ -358,7 +339,7 @@ M.asyncreadlines = function(filename, opts) offset, function(read_complete_err, data) if read_complete_err then - _handle_error(read_complete_err, "fs_read complete") + opts.on_error("fs_read complete", read_complete_err) return end @@ -389,23 +370,21 @@ M.asyncreadlines = function(filename, opts) fd --[[@as integer]], function(close_complete_err) if close_complete_err then - _handle_error(close_complete_err, "fs_close complete") + opts.on_error("fs_close complete", close_complete_err) end - ---@diagnostic disable-next-line: undefined-field if type(opts.on_complete) == "function" then - ---@diagnostic disable-next-line: undefined-field opts.on_complete(fsize) end end ) if close_result == nil then - _handle_error(close_err, "fs_close") + opts.on_error("fs_close", close_err) end end end ) if read_result == nil then - _handle_error(read_err, "fs_read") + opts.on_error("fs_read", read_err) end end @@ -414,11 +393,11 @@ M.asyncreadlines = function(filename, opts) ) if fstat_result == nil then - _handle_error(fstat_err, "fs_fstat") + opts.on_error("fs_fstat", fstat_err) end end) if open_result == nil then - _handle_error(open_err, "fs_open") + opts.on_error("fs_open", open_err) end end @@ -442,7 +421,6 @@ end --- @param on_complete fun(bytes:integer?):any callback on write complete. --- 1. `bytes`: written data bytes. M.asyncwritefile = function(filename, content, on_complete) - local uv = require("gitlinker.commons.uv") uv.fs_open(filename, "w", 438, function(open_err, fd) if open_err then error( diff --git a/lua/gitlinker/commons/json.lua b/lua/gitlinker/commons/json.lua deleted file mode 100644 index 121cf1f7..00000000 --- a/lua/gitlinker/commons/json.lua +++ /dev/null @@ -1,29 +0,0 @@ -local M = {} - ---- @param t table? ---- @return string? -M.encode = function(t) - if t == nil then - return nil - end - if vim.json ~= nil and vim.is_callable(vim.json.encode) then - return vim.json.encode(t) - else - return require("gitlinker.commons._json").encode(t) - end -end - ---- @param j string? ---- @return table? -M.decode = function(j) - if j == nil then - return nil - end - if vim.json ~= nil and vim.is_callable(vim.json.decode) then - return vim.json.decode(j) - else - return require("gitlinker.commons._json").decode(j) - end -end - -return M diff --git a/lua/gitlinker/commons/logging.lua b/lua/gitlinker/commons/logging.lua index fc000f10..309869b8 100644 --- a/lua/gitlinker/commons/logging.lua +++ b/lua/gitlinker/commons/logging.lua @@ -1,4 +1,5 @@ local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0 +local uv = vim.uv or vim.loop local M = {} @@ -322,8 +323,6 @@ end function Logger:_log(dbg, lvl, msg) assert(type(lvl) == "number" and LogLevelNames[lvl] ~= nil) - local uv = require("gitlinker.commons.uv") - if lvl < self.level then return end diff --git a/lua/gitlinker/commons/num.lua b/lua/gitlinker/commons/num.lua index 5702f688..1c076873 100644 --- a/lua/gitlinker/commons/num.lua +++ b/lua/gitlinker/commons/num.lua @@ -155,7 +155,8 @@ end --- @param n integer? --- @return number M.random = function(m, n) - local rand_result, rand_err = require("gitlinker.commons.uv").random(4) + local uv = vim.uv or vim.loop + local rand_result, rand_err = uv.random(4) assert(rand_result ~= nil, rand_err) local bytes = { diff --git a/lua/gitlinker/commons/path.lua b/lua/gitlinker/commons/path.lua index c58ce12c..b002b1b5 100644 --- a/lua/gitlinker/commons/path.lua +++ b/lua/gitlinker/commons/path.lua @@ -1,4 +1,5 @@ local IS_WINDOWS = vim.fn.has("win32") > 0 or vim.fn.has("win64") > 0 +local uv = vim.uv or vim.loop local M = {} @@ -8,7 +9,7 @@ M.SEPARATOR = IS_WINDOWS and "\\" or "/" --- @return boolean M.exists = function(p) assert(type(p) == "string") - local result, _ = require("gitlinker.commons.uv").fs_lstat(p) + local result, _ = uv.fs_lstat(p) return result ~= nil end @@ -16,7 +17,7 @@ end --- @return boolean M.isfile = function(p) assert(type(p) == "string") - local result, _ = require("gitlinker.commons.uv").fs_lstat(p) + local result, _ = uv.fs_lstat(p) -- print( -- string.format( -- "|paths.isfile| p:%s, result:%s\n", @@ -31,7 +32,7 @@ end --- @return boolean M.isdir = function(p) assert(type(p) == "string") - local result, _ = require("gitlinker.commons.uv").fs_lstat(p) + local result, _ = uv.fs_lstat(p) -- print( -- string.format( -- "|paths.isdir| p:%s, result:%s\n", @@ -46,7 +47,7 @@ end --- @return boolean M.islink = function(p) assert(type(p) == "string") - local result, _ = require("gitlinker.commons.uv").fs_lstat(p) + local result, _ = uv.fs_lstat(p) -- print( -- string.format( -- "|paths.issymlink| p:%s, result:%s\n", @@ -95,7 +96,7 @@ end M.expand = function(p) assert(type(p) == "string") if string.len(p) >= 1 and string.sub(p, 1, 1) == "~" then - return require("gitlinker.commons.uv").os_homedir() .. string.sub(p, 2) + return uv.os_homedir() .. string.sub(p, 2) else return p end @@ -108,7 +109,7 @@ M.resolve = function(p) if not M.islink(p) then return p end - local result, _ = require("gitlinker.commons.uv").fs_realpath(p) + local result, _ = uv.fs_realpath(p) -- print( -- string.format( -- "|paths.resolve|-4 p:%s, result:%s\n", diff --git a/lua/gitlinker/commons/ringbuf.lua b/lua/gitlinker/commons/ringbuf.lua new file mode 100644 index 00000000..28ebb1a9 --- /dev/null +++ b/lua/gitlinker/commons/ringbuf.lua @@ -0,0 +1,221 @@ +local M = {} + +--- @class commons.RingBuffer +--- @field pos integer +--- @field queue any[] +--- @field size integer +--- @field maxsize integer +local RingBuffer = {} + +--- @param maxsize integer? +--- @return commons.RingBuffer +function RingBuffer:new(maxsize) + assert(type(maxsize) == "number" and maxsize > 0) + local o = { + pos = 0, + queue = {}, + size = 0, + maxsize = maxsize, + } + setmetatable(o, self) + self.__index = self + return o +end + +--- @param idx integer +--- @return integer +function RingBuffer:_inc(idx) + if idx == self.maxsize then + return 1 + else + return idx + 1 + end +end + +--- @param idx integer +--- @return integer +function RingBuffer:_dec(idx) + if idx == 1 then + return self.maxsize + else + return idx - 1 + end +end + +--- @param item any +--- @return integer +function RingBuffer:push(item) + assert(self.size >= 0 and self.size <= self.maxsize) + + if self.size < self.maxsize then + table.insert(self.queue, item) + self.pos = self:_inc(self.pos) + self.size = self.size + 1 + else + self.pos = self:_inc(self.pos) + self.queue[self.pos] = item + end + return self.pos +end + +--- @return any? +function RingBuffer:pop() + if self.size <= 0 then + return nil + end + + local old = self.queue[self.pos] + self.queue[self.pos] = nil + self.size = self.size - 1 + self.pos = self:_dec(self.pos) + return old +end + +--- @return any? +function RingBuffer:peek() + if self.size <= 0 then + return nil + end + return self.queue[self.pos] +end + +--- @return integer +function RingBuffer:clear() + local old = self.size + self.pos = 0 + self.queue = {} + self.size = 0 + return old +end + +-- RingBufferIterator { + +-- usage: +-- +-- ```lua +-- local it = ringbuf:iterator() +-- local item = nil +-- repeat +-- item = it:next() +-- if item then +-- -- consume item data +-- end +-- until item +-- ``` +-- +--- @class commons._RingBufferIterator +--- @field ringbuf commons.RingBuffer +--- @field index integer +--- @field initial_index integer +local _RingBufferIterator = {} + +--- @param ringbuf commons.RingBuffer +--- @param index integer +--- @return commons._RingBufferIterator +function _RingBufferIterator:new(ringbuf, index) + assert(type(ringbuf) == "table") + + local o = { + ringbuf = ringbuf, + index = index, + initial_index = index, + } + setmetatable(o, self) + self.__index = self + return o +end + +--- @return boolean +function _RingBufferIterator:has_next() + if self.ringbuf.size == 0 then + return false + end + if self.index <= 0 or self.index > self.ringbuf.size then + return false + end + if self.index ~= self.initial_index and self.ringbuf:_inc(self.index) == self.initial_index then + return false + end + + return true +end + +--- @return any? +function _RingBufferIterator:next() + assert(self:has_next()) + assert(self.index >= 1 and self.index <= self.ringbuf.maxsize) + local item = self.ringbuf.queue[self.index] + self.index = self.ringbuf:_inc(self.index) + return item +end + +-- RingBufferIterator } + +-- RingBufferRIterator { + +--- @class commons._RingBufferRIterator +--- @field ringbuf commons.RingBuffer +--- @field index integer +--- @field initial_index integer +local _RingBufferRIterator = {} + +--- @param ringbuf commons.RingBuffer +--- @param index integer +--- @return commons._RingBufferRIterator +function _RingBufferRIterator:new(ringbuf, index) + assert(type(ringbuf) == "table") + + local o = { + ringbuf = ringbuf, + index = index, + initial_index = index, + } + setmetatable(o, self) + self.__index = self + return o +end + +--- @return boolean +function _RingBufferRIterator:has_next() + if self.ringbuf.size == 0 then + return false + end + if self.index <= 0 or self.index > self.ringbuf.size then + return false + end + if self.index ~= self.initial_index and self.ringbuf:_dec(self.index) == self.initial_index then + return false + end + + return true +end + +--- @return any? +function _RingBufferRIterator:next() + assert(self:has_next()) + assert(self.index >= 1 and self.index <= self.ringbuf.maxsize) + + local item = self.ringbuf.queue[self.index] + self.index = self.ringbuf:_dec(self.index) + return item +end + +-- RingBufferRIterator } + +--- @return commons._RingBufferIterator +function RingBuffer:iterator() + if self.size < self.maxsize then + return _RingBufferIterator:new(self, 0) + else + return _RingBufferIterator:new(self, self:_inc(self.pos)) + end +end + +--- @return commons._RingBufferRIterator +function RingBuffer:riterator() + return _RingBufferRIterator:new(self, self.pos) +end + +M.RingBuffer = RingBuffer + +return M diff --git a/lua/gitlinker/commons/str.lua b/lua/gitlinker/commons/str.lua index 7d49599c..0512ae9d 100644 --- a/lua/gitlinker/commons/str.lua +++ b/lua/gitlinker/commons/str.lua @@ -1,7 +1,9 @@ local M = {} -local string_len, string_byte, string_sub, string_gsub = - string.len, string.byte, string.sub, string.gsub +local string_len = string.len +local string_byte = string.byte +local string_sub = string.sub +local string_gsub = string.gsub --- @param s any --- @return boolean diff --git a/lua/gitlinker/commons/tbl.lua b/lua/gitlinker/commons/tbl.lua index a9e4b10f..c91ef17a 100644 --- a/lua/gitlinker/commons/tbl.lua +++ b/lua/gitlinker/commons/tbl.lua @@ -16,8 +16,12 @@ end --- @param ... any --- @return any M.tbl_get = function(t, ...) + local args = { ... } + if #args == 0 then + return t + end local e = t --[[@as table]] - for _, k in ipairs({ ... }) do + for _, k in ipairs(args) do if type(e) == "table" and e[k] ~= nil then e = e[k] else diff --git a/lua/gitlinker/commons/version.lua b/lua/gitlinker/commons/version.lua index fa61ad54..e44c0490 100644 --- a/lua/gitlinker/commons/version.lua +++ b/lua/gitlinker/commons/version.lua @@ -1,21 +1,10 @@ local M = {} -M.HAS_VIM_VERSION = vim.is_callable(vim.version) -M.HAS_VIM_VERSION_EQ = M.HAS_VIM_VERSION - and type(vim.version) == "table" - and vim.is_callable(vim.version.eq) -M.HAS_VIM_VERSION_GT = M.HAS_VIM_VERSION - and type(vim.version) == "table" - and vim.is_callable(vim.version.gt) -M.HAS_VIM_VERSION_GE = M.HAS_VIM_VERSION - and type(vim.version) == "table" - and vim.is_callable(vim.version.ge) -M.HAS_VIM_VERSION_LT = M.HAS_VIM_VERSION - and type(vim.version) == "table" - and vim.is_callable(vim.version.lt) -M.HAS_VIM_VERSION_LE = M.HAS_VIM_VERSION - and type(vim.version) == "table" - and vim.is_callable(vim.version.le) +M.HAS_VIM_VERSION_EQ = type(vim.version) == "table" and vim.is_callable(vim.version.eq) +M.HAS_VIM_VERSION_GT = type(vim.version) == "table" and vim.is_callable(vim.version.gt) +M.HAS_VIM_VERSION_GE = type(vim.version) == "table" and vim.is_callable(vim.version.ge) +M.HAS_VIM_VERSION_LT = type(vim.version) == "table" and vim.is_callable(vim.version.lt) +M.HAS_VIM_VERSION_LE = type(vim.version) == "table" and vim.is_callable(vim.version.le) --- @param l integer[] --- @return string @@ -43,40 +32,19 @@ end --- @param ver string|integer[] --- @return boolean M.lt = function(ver) - if M.HAS_VIM_VERSION and M.HAS_VIM_VERSION_LT then - if type(ver) == "string" then - ver = M.to_list(ver) - end - return vim.version.lt(vim.version(), ver) - else - if type(ver) == "table" then - ver = M.to_string(ver) - end - ver = "nvim-" .. ver - return vim.fn.has(ver) < 1 + if type(ver) == "string" then + ver = M.to_list(ver) end + return vim.version.lt(vim.version(), ver) end --- @param ver string|integer[] --- @return boolean M.ge = function(ver) - if M.HAS_VIM_VERSION and M.HAS_VIM_VERSION_EQ and M.HAS_VIM_VERSION_GT then - if type(ver) == "string" then - ver = M.to_list(ver) - end - return vim.version.gt(vim.version(), ver) or vim.version.eq(vim.version(), ver) - elseif M.HAS_VIM_VERSION and M.HAS_VIM_VERSION_GE then - if type(ver) == "string" then - ver = M.to_list(ver) - end - return vim.version.ge(vim.version(), ver) - else - if type(ver) == "table" then - ver = M.to_string(ver) - end - ver = "nvim-" .. ver - return vim.fn.has(ver) > 0 + if type(ver) == "string" then + ver = M.to_list(ver) end + return vim.version.gt(vim.version(), ver) or vim.version.eq(vim.version(), ver) end return M diff --git a/lua/gitlinker/commons/version.txt b/lua/gitlinker/commons/version.txt index 2bbd2b4b..49e3587f 100644 --- a/lua/gitlinker/commons/version.txt +++ b/lua/gitlinker/commons/version.txt @@ -1 +1 @@ -15.0.1 +19.0.0