Skip to content

ref!: major refactor #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
4d39989
chore(doc): auto generate docs
github-actions[bot] Jun 6, 2024
40a5d00
chore: shuffle all autocmd functions into the event module
xiaoshihou514 Jun 6, 2024
f19c4fd
chore: remove deprecated methods
xiaoshihou514 Jun 6, 2024
7399998
doc: add example of how to write custom formatting logic
xiaoshihou514 Jun 6, 2024
c9b9488
doc: promote advanced.md in README
xiaoshihou514 Jun 6, 2024
1b0d339
fix: typo
xiaoshihou514 Jun 6, 2024
8ad2cd4
chore: less verbose alias
xiaoshihou514 Jun 7, 2024
e0b7039
wip: basic outlinen of do_fmt
xiaoshihou514 Jun 7, 2024
d373d6d
wip: formatting module
xiaoshihou514 Jun 8, 2024
3cf35c1
fix: "async" execution using coroutines
xiaoshihou514 Jun 10, 2024
547149c
chore: remove version checks
xiaoshihou514 Jun 10, 2024
c35b0e1
feat: formatting module working correctly
xiaoshihou514 Jun 11, 2024
5be58dd
fix: autocmd test
xiaoshihou514 Jun 11, 2024
df79ed6
wip: mess with changedtick
xiaoshihou514 Jun 11, 2024
c96d016
fix: incorrect changedtick
xiaoshihou514 Jun 11, 2024
651d53b
chore: more rigorous error handling
xiaoshihou514 Jun 11, 2024
0a8e6b2
wip: lsp formatting
xiaoshihou514 Jun 12, 2024
ab5b673
fix: potential race condition
xiaoshihou514 Jun 12, 2024
4ec630e
fix: lint module
xiaoshihou514 Jun 12, 2024
248e533
feat: enhancements for linter module
xiaoshihou514 Jun 13, 2024
7950c42
doc: add nice looking demos :)
xiaoshihou514 Jun 13, 2024
434dc80
doc: formatting
xiaoshihou514 Jun 13, 2024
341b579
fix: save view
xiaoshihou514 Jun 13, 2024
b149d11
fix: remove deepcopies
xiaoshihou514 Jun 13, 2024
639ef8e
fix: tbl_filter and exepath
xiaoshihou514 Jun 13, 2024
527696e
chore: use custom table copy
xiaoshihou514 Jun 16, 2024
19a96d7
fix: existing tests
xiaoshihou514 Jun 17, 2024
3c1caff
test: toolcopy
xiaoshihou514 Jun 17, 2024
59c4527
chore: apply changes
xiaoshihou514 Jun 17, 2024
b3e42b0
doc: advanced linter customization
xiaoshihou514 Jun 17, 2024
6f6fe0f
Merge branch 'ref-api' of github.com:xiaoshihou514/guard.nvim into re…
xiaoshihou514 Jun 17, 2024
79bd3fa
fix: link
xiaoshihou514 Jun 17, 2024
8b496bf
fix: emoji
xiaoshihou514 Jun 17, 2024
e41c0e2
doc: fix missing line
xiaoshihou514 Jun 18, 2024
59c3977
chore: add types
xiaoshihou514 Jun 19, 2024
d425c0a
fix: types
xiaoshihou514 Jun 19, 2024
4e8f7d6
fix: use fnamemodify
xiaoshihou514 Jun 19, 2024
b3fa4c7
chore: refine code
xiaoshihou514 Jun 26, 2024
fb98efd
fix: lsp warning
xiaoshihou514 Jul 1, 2024
2fde1a6
chore: add executable check for linters
xiaoshihou514 Jul 2, 2024
7e2b579
chore: check executable in setup instead
xiaoshihou514 Jul 2, 2024
3bb6227
chore: remove redundant bufid check
xiaoshihou514 Jul 2, 2024
5f01630
fix: typo
xiaoshihou514 Jul 3, 2024
b322cfc
feat: more elegant checking and corresponding tests
xiaoshihou514 Jul 3, 2024
b2b900d
chore: rename due to namespace conflict
xiaoshihou514 Jul 3, 2024
eec417e
chore: ensure non nil
xiaoshihou514 Jul 3, 2024
475d970
chore: consistent executable checks
xiaoshihou514 Jul 3, 2024
3b79f3a
fix: incorrect copy and paste
xiaoshihou514 Jul 3, 2024
da9d6e2
chore: use vim.iter more
xiaoshihou514 Jul 3, 2024
9ae2f34
test: test for multiple formatters
xiaoshihou514 Jul 3, 2024
1835101
chore: ignore patterns should support single string
xiaoshihou514 Jul 3, 2024
939f6f4
feat: exepath patch for windows
xiaoshihou514 Jul 7, 2024
b051387
fix: exe check warning msg
xiaoshihou514 Jul 7, 2024
e377b9a
fix: update plugin/guard.lua
xiaoshihou514 Jul 10, 2024
787dac2
feat: tests for commands
xiaoshihou514 Jul 12, 2024
d31e457
fix: lsp attach autocmd
xiaoshihou514 Jul 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[*.lua]
indent_style = space
indent_size = 2
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ jobs:
version: latest
args: --check .


docs:
runs-on: ubuntu-latest
name: pandoc to vimdoc
Expand All @@ -28,7 +27,7 @@ jobs:
treesitter: true
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: 'chore(doc): auto generate docs'
commit_message: "chore(doc): auto generate docs"
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
Expand Down
189 changes: 186 additions & 3 deletions ADVANCED.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,187 @@
# Advanced tips

## Special case formatting logic

With the introduction of `vim.system` in neovim 0.10, it is now easy to write custom formatting logic. Let's work through an example of how you could do so.

`prettierd` does not work well with guard because of it's error mechanism. Guard follows a reasonable unix standard when it comes to determining exit status, that is, assuming the program would exit with non-zero exit code and print some reasonable error message in stderr:

```lua
if exit_code ~= 0 and num_stderr_chunks ~= 0 then
-- failed
else
-- success
end
```

However, `prettierd` prints error messages to stdout, so guard will fail to detect an error and proceed to replace your code with its error message :cry:

But fear not! You can create your custom logic by passing a function in the config table, let's do this step by step:

```lua
local function prettierd_fmt(buf, range, acc)
local co = assert(coroutine.running())
end
```

Guard runs the format function in a coroutine so as not to block the UI, to achieve what we want we have to interact with the current coroutine.

We can now go on to mimic how we would call `prettierd` on the cmdline:

```
cat test.js | prettierd test.js
```

```lua
-- previous code omitted
local handle = vim.system({ "prettierd", vim.api.nvim_buf_get_name(buf) }, {
stdin = true,
}, function(result)
if result.code ~= 0 then
-- "returns" the error
coroutine.resume(co, result)
else
-- "returns" the result
coroutine.resume(co, result.stdout)
end
end)
```

We get the unformatted code, then call `vim.system` with 3 arguments

- the cmd, which is of the form `prettierd <file>`
- the option table, here we only specified that we wish to write to its stdin, but you can refer to `:h vim.system` for more options
- the `at_exit` function, which takes in a result table (again, check out `:h vim.system` for more details)

Now we can do our custom error handling, here we simply return if `prettierd` failed. But if it succeeded we replace the range with the formatted code and save the file.

Finally we write the unformatted code to stdin

```lua
-- previous code omitted
handle:write(acc)
handle:write(nil) -- closes stdin
return coroutine.yield() -- this returns either the error or the formatted code we returned earlier
```

Whoa la! Now we can tell guard to register our formatting function

```lua
ft("javascript"):fmt({
fn = prettierd_fmt
})
```

[demo](https://github.com/xiaoshihou514/guard.nvim/assets/108414369/56dd35d4-8bf6-445a-adfd-8786fb461021)

You can always refer to [spawn.lua](https://github.com/nvimdev/guard.nvim/blob/main/lua/guard/spawn.lua).

## Custom logic with linters

`clippy-driver` is a linter for rust, because it prints diagnostics to stderr, you cannot just specify it the usual way. But guard allows you to pass a custom function, which would make it work :)

Let's start by doing some imports:

```lua
local ft = require("guard.filetype")
local lint = require("guard.lint")
```

The lint function is a simple modification of the one in [spawn.lua](https://github.com/nvimdev/guard.nvim/blob/main/lua/guard/spawn.lua).

```lua
local function clippy_driver_lint(acc)
local co = assert(coroutine.running())
local handle = vim.system({ "clippy-driver", "-", "--error-format=json", "--edition=2021" }, {
stdin = true,
}, function(result)
-- wake coroutine on exit, omit error checking
coroutine.resume(co, result.stderr)
end)
-- write contents to stdin and close it
handle:write(acc)
handle:write(nil)
-- sleep until awakened after process finished
return coroutine.yield()
end
```

We register it via guard:

```lua
ft("rust"):lint({
fn = clippy_driver_lint,
stdin = true,
parse = clippy_diagnostic_parse, -- TODO!
})
```

To write the lint function, we inspect the output of `clippy-driver` when called with the arguments above:

<details>
<summary>full output</summary>

```
❯ cat test.rs
fn main() {
let _ = 'a' .. 'z';
if 42 > i32::MAX {}
}
❯ cat test.rs | clippy-driver - --error-format=json --edition=2021
{"$message_type":"diagnostic","message":"almost complete ascii range","code":{"code":"clippy::almost_complete_range","explanation":null},"level":"warning","spans":[{"file_name":"<anon>","byte_start":24,"byte_end":34,"line_start":2,"line_end":2,"column_start":13,"column_end":23,"is_primary":true,"text":[{"text":" let _ = 'a' .. 'z';","highlight_start":13,"highlight_end":23}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_range","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"`#[warn(clippy::almost_complete_range)]` on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null},{"message":"use an inclusive range","code":null,"level":"help","spans":[{"file_name":"<anon>","byte_start":28,"byte_end":30,"line_start":2,"line_end":2,"column_start":17,"column_end":19,"is_primary":true,"text":[{"text":" let _ = 'a' .. 'z';","highlight_start":17,"highlight_end":19}],"label":null,"suggested_replacement":"..=","suggestion_applicability":"MaybeIncorrect","expansion":null}],"children":[],"rendered":null}],"rendered":"warning: almost complete ascii range\n --> <anon>:2:13\n |\n2 | let _ = 'a' .. 'z';\n | ^^^^--^^^^\n | |\n | help: use an inclusive range: `..=`\n |\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_range\n = note: `#[warn(clippy::almost_complete_range)]` on by default\n\n"}
{"$message_type":"diagnostic","message":"this comparison involving the minimum or maximum element for this type contains a case that is always true or always false","code":{"code":"clippy::absurd_extreme_comparisons","explanation":null},"level":"error","spans":[{"file_name":"<anon>","byte_start":43,"byte_end":56,"line_start":3,"line_end":3,"column_start":8,"column_end":21,"is_primary":true,"text":[{"text":" if 42 > i32::MAX {}","highlight_start":8,"highlight_end":21}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"because `i32::MAX` is the maximum value for this type, this comparison is always false","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"`#[deny(clippy::absurd_extreme_comparisons)]` on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"error: this comparison involving the minimum or maximum element for this type contains a case that is always true or always false\n --> <anon>:3:8\n |\n3 | if 42 > i32::MAX {}\n | ^^^^^^^^^^^^^\n |\n = help: because `i32::MAX` is the maximum value for this type, this comparison is always false\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons\n = note: `#[deny(clippy::absurd_extreme_comparisons)]` on by default\n\n"}
{"$message_type":"diagnostic","message":"aborting due to 1 previous error; 1 warning emitted","code":null,"level":"error","spans":[],"children":[],"rendered":"error: aborting due to 1 previous error; 1 warning emitted\n\n"}
```

</details>

That's a lot of output! But we can see three main blocks: the first two diagnostics and the last one an overview. We only need the first two:

```lua
local clippy_diagnostic_parse =
parse = lint.from_json({
get_diagnostics = function(line)
local json = vim.json.decode(line)
-- ignore overview json which does not have position info
if not vim.tbl_isempty(json.spans) then
return json
end
end,
lines = true,
attributes = { ... } -- TODO
})
```

Now our diagnostics are transformed into a list of json, we just need to get the attributes we need: the positions, the message and the error level. That's what the attributes field does, it extracts them from the json table:

```lua
attributes = {
-- it is json turned into a lua table
lnum = function(it)
-- clippy has really weird indexes
return math.ceil(tonumber(it.spans[1].line_start) / 2)
end,
lnum_end = function(it)
return math.ceil(tonumber(it.spans[1].line_end) / 2)
end,
code = function(it)
return it.code.code
end,
col = function(it)
return it.spans[1].column_start
end,
col_end = function(it)
return it.spans[1].column_end
end,
severity = "level", -- "it.level"
message = "message", -- "it.message"
},
```

Et voilà!

![image](https://github.com/xiaoshihou514/guard.nvim/assets/108414369/f9137b5a-ae69-494f-9f5b-b6044ae63c86)

## Take advantage of autocmd events

Guard exposes a `GuardFmt` user event that you can use. It is called both before formatting starts and after it is completely done. To differentiate between pre-format and post-format calls, a `data` table is passed.
Expand All @@ -8,17 +190,16 @@ Guard exposes a `GuardFmt` user event that you can use. It is called both before
-- for pre-format calls
data = {
status = "pending", -- type: string, called whenever a format is requested
using = {...} -- type: table, whatever formatters you are using for this format action
using = {...} -- type: table, formatters that are going to run
}
-- for post-format calls
data = {
status = "done", -- type: string, only called on success
results = {...} -- type: table, formatted buffer text as a list of lines
}
-- or
data = {
status = "failed" -- type: string, buffer remain unchanged
msg = "..." -- type: string, currently only if buffer became invalid or changed during formatting
msg = "..." -- type: string, reason for failure
}
```

Expand Down Expand Up @@ -50,4 +231,6 @@ vim.api.nvim_create_autocmd("User", {
})
```

[demo](https://github.com/xiaoshihou514/guard.nvim/assets/108414369/339ff4ff-288c-49e4-8ab1-789a6175d201)

You can do the similar for your statusline plugin of choice as long as you "refresh" it on `GuardFmt`.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# guard.nvim

Async formatting and linting utility for neovim.
Async formatting and linting utility for neovim `0.10+`.

## Features

Expand Down Expand Up @@ -88,19 +88,26 @@ Easily setup your custom tool if not in the defaults or you do not want guard-co

```
{
-- specify an executable
cmd -- string: tool command
args -- table: command arguments
fname -- boolean: insert filename to args tail
stdin -- boolean: pass buffer contents into stdin
timeout -- integer

-- or provide your own logic
fn -- function: write your own logic for formatting / linting, more in ADVANCED.md

-- running condition
ignore_patterns -- table: don't run formatter when pattern match against file name
ignore_error -- boolean: when has lsp error ignore format
find -- string: format if the file is found in the lsp root dir

-- misc
env -- table: environment variables passed to cmd (key value pair)
timeout -- integer

-- special
parse -- function: used to parse linter output to neovim diagnostic
fn -- function: if fn is set other fields will not take effect
parse -- function: linter only, parses linter output to neovim diagnostic
}
```

Expand All @@ -115,6 +122,14 @@ ft('asm'):fmt({

Consult the [builtin tools](https://github.com/nvimdev/guard-collection/tree/main/lua/guard-collection) if needed.

### Advanced configuration

[ADVANCED.md](https://github.com/nvimdev/guard.nvim/blob/main/ADVANCED.md) contains tiny tutorials to:

- Write your own formatting logic using the fn field
- Write your own linting logic using the fn field
- leverage guard's autocmds, say showing formatting status?

### Supported Tools

See [here](https://github.com/nvimdev/guard-collection) for an exhaustive list.
2 changes: 1 addition & 1 deletion doc/guard.nvim.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
*guard.nvim.txt* For NVIM v0.8.0 Last change: 2024 June 05
*guard.nvim.txt* For NVIM v0.8.0 Last change: 2024 June 06

==============================================================================
Table of Contents *guard.nvim-table-of-contents*
Expand Down
Loading