Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 33 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ Git source for [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) and [Sagh

## Features

| Git | Trigger |
| ------------------ | ------------------------------------------------------------------------ |
| Commits | : |
| Trailer keys | A word at the start of a line in a commit message |
| Trailer identities | `Name <email>` after an identity key, such as `Co-authored-by: ` |
| Trailer values | An earlier value of the key after any other key, such as `Assisted-by: ` |
| Git | Trigger |
| ------------------- | ------------------------------------------------------------------------ |
| Commits | : |
| Trailer keys | A word at the start of a line in a commit message |
| Trailer identities | `Name <email>` after an identity key, such as `Co-authored-by: ` |
| Trailer values | An earlier value of the key after any other key, such as `Assisted-by: ` |
| Autosquash prefixes | The start of `fixup`, `squash`, or `amend` on line 1 of a commit message |
| Autosquash titles | `fixup!`, `squash!`, or `amend!` on line 1 of a commit message |

| GitHub | Trigger |
| ------------- | ------- |
Expand Down Expand Up @@ -197,6 +199,21 @@ require("cmp_git").setup({
format = format.git.trailers.values,
},
},
autosquash = {
filetypes = { "gitcommit", "NeogitCommitMessage" },
-- the prefix words at the start of line 1, such as `fixup! ` for `fi`
prefixes = {
words = { "fixup", "squash", "amend" },
min_length = 2, -- letters to type before the words show
sort_by = sort.git.autosquash.prefixes,
format = format.git.autosquash.prefixes,
},
-- the titles of earlier commits after the prefix, from the commits of `git.commits`
commits = {
sort_by = sort.git.autosquash.commits,
format = format.git.autosquash.commits,
},
},
},
github = {
hosts = {}, -- list of private instances of github
Expand Down Expand Up @@ -282,6 +299,15 @@ require("cmp_git").setup({
},
},
trigger_actions = {
{
pattern = [[^[[:alnum:]-]\+]],
actions = { "git_autosquash_prefixes" },
},
{
pattern = [[^\%(fixup\|squash\|amend\)!\s*\zs.*]],
keyword_pattern = [[^\%(fixup\|squash\|amend\)\%(\zs!\S*\|!\s*\zs.*\)]],
actions = { "git_autosquash" },
},
{
pattern = [[^[[:alnum:]-]\+]],
actions = { "git_trailer_keys" },
Expand Down Expand Up @@ -321,6 +347,7 @@ require("cmp_git").setup({

- [Trigger actions](docs/configuration.md#trigger-actions)
- [Trailer completion](docs/configuration.md#trailer-completion)
- [Autosquash completion](docs/configuration.md#autosquash-completion)
- [Format and sort items](docs/configuration.md#format-and-sort-items)
- [Filetypes](docs/configuration.md#filetypes)
- [Remotes and SSH aliases](docs/configuration.md#remotes-and-ssh-aliases)
Expand Down
12 changes: 12 additions & 0 deletions benchmarks/cases/keystroke.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ return {
keyword_offset("prose_300", fixtures.prose(300)),
keyword_offset("prose_1000", fixtures.prose(1000)),
keyword_offset("trailer_value", "Co-authored-by: Ada Lov"),
keyword_offset("autosquash", "fixup! feat: add the par"),
{
-- The first word of the subject matches the trailer key pattern, but trailers start below the subject
name = "keystroke/nvim_cmp/complete_declined",
Expand All @@ -70,6 +71,17 @@ return {
state.source:complete(state.params, noop)
end,
},
{
-- Lowercase letters on the subject line that start no autosquash prefix word, so each word is compared
name = "keystroke/nvim_cmp/complete_declined_letters",
setup = function()
fixtures.scratch_buffer(LINES)
return { source = require("cmp_git.source").new({}), params = cmp_params("refactor", 1) }
end,
run = function(state)
state.source:complete(state.params, noop)
end,
},
{
name = "keystroke/nvim_cmp/complete_declined_prose_300",
setup = function()
Expand Down
121 changes: 119 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ cmp-git uses the current trailer value patterns for it.
The default trailer entries come before the `:` entry. As a result, `Co-authored-by:` completes identities, not
commits.

The default autosquash entries come first. As a result, on line 1 after `fixup!`, `squash!`, or `amend!`, the `:`,
`#`, `@`, and `!` triggers complete commit titles. For example, `fixup!` completes commit titles, not GitLab merge
requests, and `fixup! feat:` completes commit titles, not commits.

### Built-in actions

The built-in actions are:

- `git_commits`, `git_trailer_keys`, `git_trailer_identities`, and `git_trailer_values`. These actions need a
`pattern` entry, except `git_commits`.
- `git_commits`, `git_trailer_keys`, `git_trailer_identities`, `git_trailer_values`, `git_autosquash_prefixes`, and
`git_autosquash`. These actions need a `pattern` entry, except `git_commits`.
- `gitlab_issues`, `gitlab_mentions`, and `gitlab_change_requests`.
- `github_issues`, `github_change_requests`, `github_issues_and_change_requests`, and `github_mentions`.
- `gitea_issues`, `gitea_change_requests`, `gitea_issues_and_change_requests`, and `gitea_mentions`.
Expand Down Expand Up @@ -96,6 +100,114 @@ To turn off trailer completion, set `trigger_actions` without the two `pattern`
If you set your own `trigger_actions`, add the two `pattern` entries from the [defaults](../README.md#config) to get
trailer completion.

## Autosquash completion

On the subject line of a commit message, cmp-git completes the subjects of `fixup!`, `squash!`, and `amend!` commits.
`git rebase --autosquash` moves each of these commits after its target commit.

| You type | Completion | From |
| ------------ | -------------------------- | ----------------------------------------- |
| `fi` | `fixup! ` | A word in `git.autosquash.prefixes.words` |
| `fixup!` | `fixup! feat: add parser` | The title of an earlier commit |
| `squash! fe` | `squash! feat: add parser` | The title of an earlier commit |
| `amend!` | `amend! fix: handle input` | The title of an earlier commit |

After you select a prefix word, the completion menu opens again and shows the commit titles. This works with
`nvim-cmp` and with `blink.cmp`.

Autosquash completion works only when all of these conditions are true:

- The filetype of the buffer is in `git.autosquash.filetypes`. This list does not support `"*"`. Autosquash completion
also needs the filetype in `filetypes`.
- The cursor is on line 1.

The prefix words also need these conditions:

- The text before the cursor has only letters, and it is the start of a word in `git.autosquash.prefixes.words`. The
match is case-sensitive, because git reads only lowercase prefixes.
- The text has at least `git.autosquash.prefixes.min_length` letters (default 2). A value below 1 acts as 1. With fewer
letters, cmp-git gives an incomplete list without items, so the completion engine asks again on the next letter.

If the text starts no word, the request goes to the next entries. For example, `fe` gives no prefix word.

The commit titles come from the last `git.commits.limit` commits, the same commits that `:` completes. Thus a commit
title request after a `:` request in the same buffer starts no git process. The list has these properties:

- It has no commit whose title starts with `fixup! `, `squash! `, or `amend! `.
- It has one item for each title. The item is the newest commit with that title.
- It can show commits that you pushed, and commits outside the range of your rebase.

After the prefix and its spaces, `:`, `#`, `@`, and `!` start no other completion. Only the first prefix counts, so in
`squash! fixup! x`, the commit title replaces `fixup! x`.

When the typed text touches the `!`, as in `fixup!` or `fixup!feat`, `trigger_char` of `git.autosquash.commits.format`
is `!`. The default `insertText` then starts with a space, and the default `filterText` starts with `!`. After a
space, `trigger_char` is `nil`.

### How git finds the target commit

`git rebase --autosquash` reads the text after the prefix. It does not read the body of the commit. It finds the
target commit in this order:

1. A commit with exactly this title.
2. A commit that the text names, such as a hash or `HEAD~2`. Git does this step only if the text has no space.
3. A commit whose title starts with the text.

Git skips a repeated prefix, so `fixup! fixup! feat: add parser` targets `feat: add parser`.

If two commits in the range of the rebase have the same title, git targets the oldest one. `git commit --fixup` has the
same behavior. cmp-git inserts the title, so git can target an older commit than the commit that you selected. To insert
the hash in place of the title, change `insertText`. For example:

```lua
require("cmp_git").setup({
git = {
autosquash = {
commits = {
format = {
insertText = function(trigger_char, commit)
return (trigger_char and " " or "") .. commit.sha
end,
},
},
},
},
})
```

### Prefix words and the commit titles

Two settings name the prefixes:

- `git.autosquash.prefixes.words` sets the prefix words.
- The `pattern` of the `git_autosquash` entry sets the prefixes after which the commit titles complete.

If a word is not in the pattern, its item inserts the word, `!`, and a space, but the commit titles do not show.
The commit titles show only when line 1 starts with letters and `!`, so each prefix of the pattern must have this
form. `git rebase --autosquash` reads only `fixup!`, `squash!`, and `amend!`.

### Turn off autosquash completion

- To turn off all autosquash completion, set `git.autosquash.filetypes = {}`.
- To turn off the prefix words, set `git.autosquash.prefixes.words = {}`, or set `trigger_actions` without the entry
with the `git_autosquash_prefixes` action.
- To turn off the commit titles, set `trigger_actions` without the entry with the `git_autosquash` action.

If you set your own `trigger_actions`, add these two entries to get autosquash completion. Put them before the other
entries, so that the `:`, `#`, `@`, and `!` entries do not handle the request first:

```lua
{
pattern = [[^[[:alnum:]-]\+]],
actions = { "git_autosquash_prefixes" },
},
{
pattern = [[^\%(fixup\|squash\|amend\)!\s*\zs.*]],
keyword_pattern = [[^\%(fixup\|squash\|amend\)\%(\zs!\S*\|!\s*\zs.*\)]],
actions = { "git_autosquash" },
},
```

## Format and sort items

Each capability, such as `git.commits` or `github.issues`, has a `format` table and a `sort_by` value.
Expand Down Expand Up @@ -144,6 +256,8 @@ For compatibility, cmp-git also supports `filter_fn` on a provider, as an alias
| Capability | Fields |
| ---------------------------------------- | --------------------------------------------------------------------------------------- |
| `git.commits` | `sha`, `title`, `description`, `author_name`, `author_mail`, `commit_timestamp`, `diff` |
| `git.autosquash.prefixes` | `word`, `index` |
| `git.autosquash.commits` | The same fields as `git.commits` |
| `git.trailers.keys` | `key`, `count`, `builtin_index` |
| `git.trailers.identities` | `name`, `email`, `source`, `is_self`, `timestamp`, `diff` |
| `git.trailers.values` | `key`, `value`, `count`, `timestamp`, `diff` |
Expand All @@ -167,6 +281,7 @@ For compatibility, cmp-git also supports `filter_fn` on a provider, as an alias
- `diff` is the number of seconds since the commit, or since the most recent use of the identity or the value.
- `count` is the number of uses in the scanned commits.
- `builtin_index` is the position of the key in `git.trailers.keys.builtin`. It is `nil` for a key from history.
- `word` is a word of `git.autosquash.prefixes.words`, and `index` is its position in that list.
- `source` is `"author"`, the trailer key, or `"self"`. `is_self` is `true` for your git identity.

### Sort
Expand All @@ -177,6 +292,8 @@ field name, such as `"title"`. For a field name, cmp-git uses the value of that
| Capability | Default order |
| ----------------------------------------- | -------------------------------------------------------------------------- |
| `git.commits` | Newest first |
| `git.autosquash.prefixes` | The order of `words` |
| `git.autosquash.commits` | Newest first |
| `git.trailers.keys` | Most used first, then the order of `keys.builtin`, then alphabetical order |
| `git.trailers.identities` | Most recent first. Your git identity comes first if it is not in history. |
| `git.trailers.values` | Most used first, then most recent |
Expand Down
24 changes: 24 additions & 0 deletions lua/cmp_git/blink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ local function cursor_from_context(ctx)
return { row = position[1], character = position[2] }
end

---@class cmp_git.BlinkCompletionItem: cmp_git.CompletionItem
---@field source_id string The id of the blink.cmp provider of the item.

---blink.cmp calls this to accept an item. `default_implementation` inserts the text.
---@param item cmp_git.BlinkCompletionItem
---@param callback fun()
---@param default_implementation fun()
function Blink:execute(_ctx, item, callback, default_implementation)
default_implementation()
callback()
if not Core.reopens_menu(item) then
return
end

-- After blink.cmp has finished the accept
vim.schedule(function()
-- Only blink.cmp calls this, but the module can be missing for other callers
local ok, blink = pcall(require, "blink.cmp")
if ok then
blink.show({ providers = { item.source_id } })
end
end)
end

function Blink:get_completions(ctx, callback)
local cancelled = false
local responded = false
Expand Down
45 changes: 45 additions & 0 deletions lua/cmp_git/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ local TRAILER_VALUE_KEYWORD_PATTERN = [[^[[:alnum:]-]\+\zs:\S*\|]] .. TRAILER_VA
-- users who set their own `trigger_actions` to copy the default entries, so `normalize` replaces it.
local LEGACY_TRAILER_VALUE_PATTERN = [[\%(^[[:alnum:]-]\+:\s*\)\@<=]] .. TRAILER_VALUE

-- Keep these in sync with the autosquash entry in the README. `git rebase --autosquash` recognizes only these three
-- prefixes. `\zs` sets the edit start after the prefix and its spaces, so that `:`, `#`, `@`, and `!` in the title
-- start no other completion. Only the first prefix counts, so an item replaces `fixup! x` in `squash! fixup! x`.
local AUTOSQUASH_PATTERN = [[^\%(fixup\|squash\|amend\)!\s*\zs.*]]
-- As for `Key:` in `TRAILER_VALUE_KEYWORD_PATTERN`, the first branch starts the keyword at the `!` when the typed text
-- touches it (`fixup!` or `fixup!feat`). The edit then starts after the `!` (`AUTOSQUASH_PATTERN`), and the commit
-- format adds a space before the title and filters on the `!`. The second branch is `AUTOSQUASH_PATTERN`. The branches
-- share one `^`, because Vim tries each `^` branch of the keyword pattern at each column of the line.
local AUTOSQUASH_KEYWORD_PATTERN = [[^\%(fixup\|squash\|amend\)\%(\zs!\S*\|!\s*\zs.*\)]]

local IDENTITY_TRAILER_KEYS = {
"Co-authored-by",
"Signed-off-by",
Expand Down Expand Up @@ -102,6 +112,30 @@ local defaults = {
format = format.git.trailers.values,
},
},
---Completion on the subject line (line 1) of `fixup!`, `squash!`, and `amend!` commits
---@class cmp_git.Config.GitAutosquash
autosquash = {
---@type string[]
filetypes = { "gitcommit", "NeogitCommitMessage" },
---The prefix words at the start of the line, such as `fixup!` for `fi`. The commit list opens only after a
---prefix that the pattern of the `git_autosquash` entry matches.
---@class cmp_git.Config.GitAutosquashPrefixes
prefixes = {
---@type string[]
words = { "fixup", "squash", "amend" },
---Letters to type before the words show. A value below 1 acts as 1.
---@type integer
min_length = 2,
sort_by = sort.git.autosquash.prefixes,
format = format.git.autosquash.prefixes,
},
---The titles of earlier commits after the prefix. The history and its cache are the ones of `commits`.
---@class cmp_git.Config.GitAutosquashCommits
commits = {
sort_by = sort.git.autosquash.commits,
format = format.git.autosquash.commits,
},
},
},
---@class cmp_git.Config.GitHub
---@field filter_fn? fun(trigger_char: string, item: cmp_git.GitHub.Issue | cmp_git.GitHub.Mention | cmp_git.GitHub.PullRequest): string Compatibility alias for all GitHub capability format.filterText functions.
Expand Down Expand Up @@ -215,6 +249,17 @@ local defaults = {
},
---@type (cmp_git.Config.NamedTriggerActions|cmp_git.Config.PatternTriggerActions|cmp_git.Config.TriggerAction)[]
trigger_actions = {
{
-- The pattern of the trailer keys, so that core matches one regex for both entries, and the keyword
-- pattern does not grow. The `available` check of the action tests the line.
pattern = TRAILER_KEY_PATTERN,
actions = { "git_autosquash_prefixes" },
},
{
pattern = AUTOSQUASH_PATTERN,
keyword_pattern = AUTOSQUASH_KEYWORD_PATTERN,
actions = { "git_autosquash" },
},
{
pattern = TRAILER_KEY_PATTERN,
actions = { "git_trailer_keys" },
Expand Down
Loading
Loading