feat(vim): update vim plugin. (#604)

* feat(vim): update vim plugin.

* fix(vim): fix bugs.

* docs: update vim doc.

* docs: update vim readme.

* docs(vim): add vim plugin changelog.
r0.4
Zhiming Ma 2023-10-22 14:32:17 +08:00 committed by GitHub
parent 0fcb04e370
commit c2db171ef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1035 additions and 856 deletions

View File

@ -35,15 +35,16 @@ export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeout
/**
* Represents the status of the agent.
* @enum
* @property {string} notInitialized - When the agent is not initialized.
* @property {string} notInitialized - When the agent has not been initialized.
* @property {string} ready - When the agent gets a valid response from the server.
* @property {string} disconnected - When the agent fails to connect to the server.
* @property {string} unauthorized - When the server is set to a Tabby Cloud endpoint that requires auth,
* and no `Authorization` request header is provided in the agent config,
* and the user has not completed the auth flow or the auth token is expired.
* See also `requestAuthUrl` and `waitForAuthToken`.
* @property {string} finalized - When the agent is finalized.
*/
export type AgentStatus = "notInitialized" | "ready" | "disconnected" | "unauthorized";
export type AgentStatus = "notInitialized" | "ready" | "disconnected" | "unauthorized" | "finalized";
export interface AgentFunction {
/**

View File

@ -1,6 +1,6 @@
import { AgentFunction, AgentEvent, Agent, agentEventNames } from "./Agent";
import { rootLogger } from "./logger";
import { splitLines } from "./utils";
import { splitLines, isCanceledError } from "./utils";
type AgentFunctionRequest<T extends keyof AgentFunction> = [
id: number,
@ -41,6 +41,7 @@ type StdIOResponse = AgentFunctionResponse<any> | AgentEventNotification | Cance
* Every request and response should be single line JSON string and end with a newline.
*/
export class StdIO {
private readonly process: NodeJS.Process = process;
private readonly inStream: NodeJS.ReadStream = process.stdin;
private readonly outStream: NodeJS.WriteStream = process.stdout;
private readonly logger = rootLogger.child({ component: "StdIO" });
@ -107,7 +108,11 @@ export class StdIO {
response[1] = await func.apply(this.agent, args);
}
} catch (error) {
this.logger.error({ error, request }, `Failed to handle request`);
if (isCanceledError(error)) {
this.logger.debug({ error, request }, `Request canceled`);
} else {
this.logger.error({ error, request }, `Failed to handle request`);
}
} finally {
if (this.abortControllers[requestId]) {
delete this.abortControllers[requestId];
@ -141,5 +146,14 @@ export class StdIO {
listen() {
this.inStream.on("data", this.handleInput.bind(this));
["SIGTERM", "SIGINT"].forEach((sig) => {
this.process.on(sig, async () => {
if (this.agent && this.agent.getStatus() !== "finalized") {
await this.agent.finalize();
}
this.process.exit(0);
});
});
}
}

View File

@ -254,7 +254,8 @@ export class TabbyAgent extends EventEmitter implements Agent {
}
}
} catch (_) {
// ignore
this.changeStatus("disconnected");
this.serverHealthState = null;
}
}
@ -332,6 +333,10 @@ export class TabbyAgent extends EventEmitter implements Agent {
}
public async finalize(): Promise<boolean> {
if (this.status === "finalized") {
return false;
}
await this.submitStats();
if (this.tryingConnectTimer) {
@ -342,7 +347,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
clearInterval(this.submitStatsTimer);
this.submitStatsTimer = null;
}
this.logger.debug("Finalized");
this.changeStatus("finalized");
return true;
}

View File

@ -16,7 +16,8 @@ const stream =
interval: "1d",
});
export const rootLogger = !!stream ? pino(stream) : pino();
const options = { serializers: { error: pino.stdSerializers.err } };
export const rootLogger = !!stream ? pino(options, stream) : pino(options);
if (isTest && testLogDebug) {
rootLogger.level = "debug";
} else {

View File

@ -13,11 +13,7 @@ export const trimSpace: (context: CompletionContext) => PostprocessFilter = (con
trimmedInput = trimmedInput.trimStart();
}
if (
inputLines.length > 1 ||
isBlank(suffixCurrentLine) ||
(!isBlank(suffixCurrentLine) && suffixCurrentLine.match(/^\s/))
) {
if (isBlank(suffixCurrentLine) || (!isBlank(suffixCurrentLine) && suffixCurrentLine.match(/^\s/))) {
trimmedInput = trimmedInput.trimEnd();
}
return trimmedInput;

3
clients/vim/CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 1.0.0
### Initial release

View File

@ -1,105 +1,167 @@
# Tabby VIM extension
# Tabby Plugin for Vim and NeoVim
Tabby is compatible with both Vim and NeoVim text editor via a plugin.
Tabby is a self-hosted AI coding assistant that can suggest multi-line code or full functions in real-time. For more information, please check out our [website](https://tabbyml.com/) and [github](https://github.com/TabbyML/tabby).
If you encounter any problem or have any suggestion, please [open an issue](https://github.com/TabbyML/tabby/issues/new) or join our [Slack community](https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA) for support.
## Table of Contents
- [Tabby Plugin for Vim and NeoVim](#tabby-plugin-for-vim-and-neovim)
- [Table of Contents](#table-of-contents)
- [Requirements](#requirements)
- [Installation](#installation)
- [🔌 Vim-plug](#-vim-plug)
- [📦 Packer.nvim](#-packernvim)
- [💤 Lazy.nvim](#-lazynvim)
- [Usage](#usage)
- [Configuration](#configuration)
- [Tabby Server](#tabby-server)
- [Node.js Binary Path](#nodejs-binary-path)
- [Completion Trigger Mode](#completion-trigger-mode)
- [KeyBindings](#keybindings)
## Requirements
Before installing the plugin you will need to have installed:
Tabby plugin requires the following dependencies:
1. VIM 9.0+ with `+job` and `+textprop` features enabled, or NeoVIM 0.6.0+.
2. Node.js 16.0+.
- Vim 9.0+ with `+job` and `+textprop` features enabled, or NeoVim 0.6.0+.
- Tabby server. You can install Tabby server locally or have it hosted on a remote server. For Tabby server installation, please refer to this [documentation](https://tabby.tabbyml.com/docs/installation/).
- [Node.js](https://nodejs.org/en/download/) version v18.0+.
- If you need have multiple Node.js versions installed, you can use Node.js version manager such as [nvm](https://github.com/nvm-sh/nvm).
- Vim filetype plugin enabled. You can add following lines in vim config file (`~/.vimrc`). For NeoVim, filetype plugin is enabled by default, you don't need to add these lines.
## Getting started
```vim
filetype plugin on
```
You can either install TabbyML vim extension using [Vim-Plug](https://github.com/junegunn/vim-plug), [Packer](https://github.com/wbthomason/packer.nvim) or [Lazy](https://github.com/folke/lazy.nvim).
## Installation
### 🔌 Vim-Plug
You can install Tabby plugin using your favorite plugin manager. Here are some examples using popular plugin managers, you can choose one to follow.
[Vim-Plug](https://github.com/junegunn/vim-plug) is a minimalist Vim plugin manager that you can use to install TabbyML plugin.
You can install Vim-Plug by following these [intructions](https://github.com/junegunn/vim-plug#installation).
### 🔌 Vim-plug
[Vim-plug](https://github.com/junegunn/vim-plug) is a minimalist Vim plugin manager that you can use to install Tabby plugin. You can install Vim-plug by following these [instructions](https://github.com/junegunn/vim-plug#installation).
Once Vim-plug is installed, you can install Tabby plugin by adding the following line to your vim config file (`~/.vimrc` for Vim and `~/.config/nvim/init.vim` for NeoVim), between the `plug#begin()` and `plug#end()` lines.
You will need to edit your vim config file (`~/.vimrc` for vim and `~/.config/nvim/init.vim` for neovim) and copy paste the following lines in it (between the `plug#begin` and `plug#end` lines)
```vim
" ...your vim configs...
" Section for plugins managed by vim-plug
plug#begin()
```
" Make sure that the filetype plugin has been enabled.
filetype plugin on
" ...other plugins...
" Add this to the vim-plug config
Plug 'TabbyML/tabby', {'rtp': 'clients/vim'}
" Set URL of Tabby server
let g:tabby_server_url = 'http://127.0.0.1:8080'
" Add Tabby plugin
Plug 'TabbyML/vim-tabby'
plug#end()
```
Note that you can change the tabby server url here.
You then need to actually install the plugin, to do so you need to type in your vim command.
Then, run the following command in your vim command line:
```
:PlugInstall
```
You should see the tabbyML plugin beeing installed.
### 📦 Packer.nvim
### 📦 Packer and Lazy
You first need to install either [Packer](https://github.com/wbthomason/packer.nvim) or [Lazy](https://github.com/folke/lazy.nvim).
[Packer.nvim](https://github.com/wbthomason/packer.nvim) is a plugin manager for NeoVim that is written in Lua. You can install Packer.nvim by following these [instructions](https://github.com/wbthomason/packer.nvim#quickstart).
In this case, you first need to clone the repo in your machine
```
git clone https://github.com/TabbyML/tabby.git ~/tabby
```
You will need to edit `~/.config/nvim/init.vim` for and copy paste the following lines in it.
Once Packer is installed, you can install Tabby plugin by adding the following line to your plugin specification, e.g. (in `~/.config/nvim/lua/plugins.lua`).
```
" For lazy
return { name = "tabby", dir = '~/tabby/clients/vim', enabled = true }
```lua
--- Packer plugin specification
return require('packer').startup(function(use)
--- ...other plugins...
" For packer
use {'~/tabby/clients/vim', as = 'tabby', enabled = true}
" Set URL of Tabby server
" With Lua
vim.g.tabby_server_url = 'http://127.0.0.1:8080'
" With VimScript
let g:tabby_server_url = 'http://127.0.0.1:8080'
```
> In the future, the ideal would be to export the Vim extension to a separate Git repository. This would simplify the installation process [#252](https://github.com/TabbyML/tabby/issues/252).
## Checking the installation
Once the plugin is installed you can check if the install was done sucessfully by doing in your vim command
```
:Tabby status
--- Add Tabby plugin
use 'TabbyML/vim-tabby'
end)
```
You should see
Then, run the following command in your NeoVim command line:
```
Tabby is online
:PackerSync
```
If you se `Tabby cannot connect to the server` it means that you need to start the tabby server first. Refer to this [documentation](https://tabby.tabbyml.com/docs/installation/)
### 💤 Lazy.nvim
[Lazy.nvim](https://github.com/folke/lazy.nvim) is an alternative plugin manager for NeoVim. You can install Lazy.nvim by following these [instructions](https://github.com/folke/lazy.nvim#-installation).
Once Lazy is installed, you can install Tabby plugin by adding the following line to your plugin specification in `~/.config/nvim/init.lua`.
```lua
--- ...your NeoVim configs...
--- Lazy plugin specification
require("lazy").setup({
--- ...other plugins...
--- Add Tabby plugin
"TabbyML/vim-tabby",
})
```
## Usage
1. In insert mode, Tabby will show code suggestion when you stop typing. Press `<Tab>` to accpet the current suggestion, `<M-]>` to see the next suggestion, `<M-[>` to see previous suggestion, or `<C-]>` to dismiss.
2. Use command `:Tabby enable` to enable, `:Tabby disable` to disable Tabby, and `:Tabby status` to check status.
3. Use command `:help Tabby` for more information.
After installation, please exit and restart Vim or NeoVim. Then you can check the Tabby plugin status by running `:Tabby` in your vim command line. If you see any message reported by Tabby, it means the plugin is installed successfully. If you see `Not an editor command: Tabby` or any other error message, please check the installation steps.
In insert mode, Tabby plugin will show inline completion automatically when you stop typing. You can simply press `<Tab>` to accept the completion. If you want to dismiss the completion manually, you can press `<C-\>` to dismiss, and press `<C-\>` again to show the completion again.
## Configuration
### Tabby Server
You need to start the Tabby server before using the plugin. For Tabby server installation, please refer to this [documentation](https://tabby.tabbyml.com/docs/installation/).
If your Tabby server endpoint is different from the default `http://localhost:8080`, please set the endpoint in `~/.tabby-client/config.toml`.
```toml
# Server
# You can set the server endpoint here.
[server]
endpoint = "http://localhost:8080" # http or https URL
```
### Node.js Binary Path
Normally, this config is not required as the Tabby plugin will try to find the Node.js binary in your `PATH` environment variable.
But if you have installed Node.js in a non-standard location, or you are using a Node.js version manager such as nvm, you can set the Node.js binary path in your vim config file (`~/.vimrc` for Vim and `~/.config/nvim/init.vim` or `~/.config/nvim/init.lua` for NeoVim).
```vim
let g:tabby_node_binary = '/path/to/node'
```
```lua
--- lua
vim.g.tabby_node_binary = '/path/to/node'
```
### Completion Trigger Mode
Completion trigger mode is set to `auto` by default, Tabby plugin will show inline completion automatically when you stop typing.
If you prefer to trigger code completion manually, add this config in your vim config file. Tabby plugin will not show inline completion automatically, you can trigger the completion manually by pressing `<C-\>`.
```vim
let g:tabby_trigger_mode = 'manual'
```
```lua
--- lua
vim.g.tabby_trigger_mode = 'manual'
```
### KeyBindings
The default key bindings for accept/dismiss(`<Tab>/<C-]>`) can be customized
with the following global settings.
The default key bindings for accept completion(`<Tab>`), manual trigger/dismiss(`<C-\>`) can be customized with the following global settings.
```vimscript
let g:tabby_accept_binding = '<Tab>'
let g:tabby_dismiss_binding = '<C-]>'
```vim
let g:tabby_keybinding_accept = '<Tab>'
let g:tabby_keybinding_trigger_or_dismiss = '<C-\>'
```
```lua
--- lua
vim.g.tabby_keybinding_accept = '<Tab>'
vim.g.tabby_keybinding_trigger_or_dismiss = '<C-\\>'
```

View File

@ -1,565 +1,254 @@
" Main functions
if exists('g:autoloaded_tabby')
finish
endif
let g:autoloaded_tabby = 1
" Table of Contents
" 1. Commands: Implement *:Tabby* commands
" 2. Settings: Handle global *Tabby-options*
" 3. Node Job: Manage node process, implement IO callbacks
" 4. Scheduler: Schedule completion requests
" 5. Completion UI: Show up completion, handle hotkeys
" 6. Utils: Utility functions
let s:status = "initializing"
let s:message = ""
" 1. Commands
" See *:Tabby* in help document for more details.
"
" Notable script-local variables:
" - s:commmands
" A dictionary contains all commands. Use name as key and function as value.
"
let s:commands = {}
function! s:commands.status(...)
call tabby#Status()
endfunction
function! s:commands.enable(...)
call tabby#Enable()
call tabby#Status()
endfunction
function! s:commands.disable(...)
call tabby#Disable()
call tabby#Status()
endfunction
function! s:commands.toggle(...)
call tabby#Toggle()
endfunction
function! s:commands.help(...)
let args = get(a:, 1, [])
if len(args) < 1
execute 'help Tabby'
return
endif
try
execute 'help Tabby-' . join(args, '-')
return
catch
endtry
try
execute 'help tabby_' . join(args, '_')
return
catch
endtry
execute 'help Tabby'
endfunction
function! tabby#CompleteCommands(arglead, cmd, pos)
let words = split(a:cmd[0:a:pos].'#', ' ')
if len(words) > 3
return []
endif
if len(words) == 3
if words[1] == 'help'
let candidates = ['compatibility', 'commands', 'options', 'keybindings']
else
return []
function! tabby#Status()
if s:status == "initializing"
echo 'Tabby is initializing.'
elseif s:status == "initialization_failed"
echo 'Tabby initialization failed.'
echo s:message
elseif s:status == "initialization_done"
let agent_status = tabby#agent#Status()
if agent_status == 'notInitialized'
echo 'Tabby is initializing.'
elseif agent_status == 'exited'
echo 'Tabby agent exited unexpectedly.'
elseif agent_status == 'ready'
echo 'Tabby is online.'
let agent_issues = tabby#agent#Issues()
if len(agent_issues) > 0
if agent_issues[0] == 'slowCompletionResponseTime'
echo 'Completion requests appear to take too much time.'
elseif agent_issues[0] == 'highCompletionTimeoutRate'
echo 'Most completion requests timed out.'
endif
elseif g:tabby_trigger_mode == 'manual'
echo 'You can use ' . g:tabby_keybinding_trigger_or_dismiss .
\ ' in insert mode to trigger completion manually.'
elseif g:tabby_trigger_mode == 'auto'
echo 'Automatic inline completion is enabled.'
endif
elseif agent_status == 'disconnected'
echo 'Tabby cannot connect to server. Please check your settings.'
elseif agent_status == 'unauthorized'
echo 'Authorization required. Use `:Tabby auth` to continue.'
endif
else
let candidates = keys(s:commands)
endif
let end_index = len(a:arglead) - 1
if end_index < 0
return candidates
else
return filter(candidates, { idx, val ->
\ val[0:end_index] == a:arglead
\})
endif
endfunction
function! tabby#Command(args)
let args = split(a:args, ' ')
if len(args) < 1
call tabby#Status()
echo 'Use :help Tabby to see available commands.'
return
endif
if has_key(s:commands, args[0])
call s:commands[args[0]](args[1:])
else
echo 'Unknown command'
endif
endfunction
" 2. Settings
" See *Tabby-options* in help document for more details.
"
" Available global options:
" - g:tabby_enabled
" - g:tabby_suggestion_delay
" - g:tabby_filetype_to_languages
" - g:tabby_server_url
" - g:tabby_max_prefix_lines
" - g:tabby_max_suffix_lines
"
if !exists('g:tabby_enabled')
let g:tabby_enabled = v:true
endif
if !exists('g:tabby_suggestion_delay')
let g:tabby_suggestion_delay = 150
endif
if !exists('g:tabby_max_prefix_lines')
let g:tabby_max_prefix_lines = 20
endif
if !exists('g:tabby_max_suffix_lines')
let g:tabby_max_suffix_lines = 20
endif
if !exists('g:tabby_filetype_to_languages')
" From: vim filetype https://github.com/vim/vim/blob/master/runtime/filetype.vim
" To: vscode language identifier https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
" Not listed filetype will be used as language identifier directly.
let g:tabby_filetype_to_languages = {
\ "bash": "shellscript",
\ "cs": "csharp",
\ "objc": "objective-c",
\ "objcpp": "objective-cpp",
\ }
endif
if !exists('g:tabby_server_url')
let g:tabby_server_url = 'http://localhost:8080'
endif
if !exists('g:tabby_agent_logs')
let g:tabby_agent_logs = 'error'
endif
" 3. Node Job
"
" Notable script-local variables:
" - s:tabby
" Stores the job id of current node process
"
" - s:tabby_status
" Syncs with status of node agent, updated by notification from agent
"
" - s:errmsg
" Stores error message if self check failed before starting node process
"
function! tabby#Enable()
let g:tabby_enabled = v:true
if !tabby#IsRunning()
call tabby#Start()
endif
endfunction
function! tabby#Disable()
let g:tabby_enabled = v:false
if tabby#IsRunning()
call tabby#Stop()
endif
endfunction
function! tabby#Toggle()
if g:tabby_enabled
call tabby#Disable()
else
call tabby#Enable()
endif
endfunction
function! tabby#Start()
if !g:tabby_enabled || tabby#IsRunning()
return
endif
function! tabby#OnVimEnter()
call tabby#globals#Load()
let check_job = tabby#job#Check()
if !check_job.ok
let s:errmsg = check_job.message
let s:status = "initialization_failed"
let s:message = check_job.message
return
endif
let check_virtual_text = tabby#virtual_text#Check()
if !check_virtual_text.ok
let s:status = "initialization_failed"
let s:errmsg = check_virtual_text.message
return
endif
call tabby#virtual_text#Init()
if !executable('node')
let s:errmsg = 'Tabby requires node to be installed.'
let node_binary = expand(g:tabby_node_binary)
if !executable(node_binary)
let s:status = "initialization_failed"
let s:message = 'Node.js binary not found. Please install Node.js version >= 18.0.'
return
endif
let tabby_root = expand('<script>:h:h')
let node_script = tabby_root . '/node_scripts/tabby-agent.js'
if !filereadable(node_script)
let s:errmsg = 'Tabby node script should be download first. Try to run `yarn upgrade-agent`.'
let node_version_command = node_binary . ' --version'
let version_output = system(node_version_command)
let node_version = matchstr(version_output, '\d\+\.\d\+\.\d\+')
let major_version = str2nr(split(node_version, '\.')[0])
if major_version < 18
let s:status = "initialization_failed"
let s:message = 'Node.js version is too old: ' . node_version . '. Please install Node.js version >= 18.0.'
return
endif
let s:tabby_status = 'connecting'
let command = 'node ' . node_script
let s:tabby = tabby#job#Start(command, #{
\ in_mode: 'json',
\ out_mode: 'json',
\ out_cb: function('s:HandleNotification'),
\ err_cb: function('s:HandleError'),
\ exit_cb: function('s:HandleExit'),
\ })
call s:Initialize()
endfunction
function! tabby#Stop()
if tabby#IsRunning()
call tabby#job#Stop(s:tabby)
endif
endfunction
function! tabby#IsRunning()
return exists('s:tabby')
endfunction
function! tabby#Status()
if !g:tabby_enabled
echo 'Tabby is disabled'
return
endif
if tabby#IsRunning()
if s:tabby_status == 'ready'
echo 'Tabby is online'
elseif s:tabby_status == 'connecting'
echo 'Tabby is connecting to server'
elseif s:tabby_status == 'disconnected'
echo 'Tabby cannot connect to server'
endif
elseif exists('s:errmsg')
echo s:errmsg
else
echo 'Tabby is enabled but not running'
endif
endfunction
function! s:Initialize()
if !tabby#IsRunning()
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'initialize',
\ args: [#{
\ config: #{
\ server: #{ endpoint: g:tabby_server_url },
\ logs: #{ level: g:tabby_agent_logs },
\ },
\ client: s:GetVersionString()
\ }],
\ })
endfunction
function! s:GetCompletion(id)
if !tabby#IsRunning()
if !filereadable(g:tabby_node_script)
let s:message = 'Tabby agent script not found. Please reinstall Tabby plugin.'
return
endif
if exists('s:pending_request_id')
call tabby#job#Send(s:tabby, #{
\ func: 'cancelRequest',
\ args: [s:pending_request_id],
\ })
endif
let command = node_binary . ' ' . g:tabby_node_script
call tabby#agent#Open(command)
let s:pending_request_id = tabby#job#Send(s:tabby, #{
\ func: 'getCompletions',
\ args: [s:CreateCompletionRequest()],
\ }, #{
\ callback: function('s:HandleCompletion', [a:id]),
\ })
call tabby#keybindings#Map()
let s:status = "initialization_done"
endfunction
function! s:PostEvent(event_type)
if !tabby#IsRunning()
function! tabby#OnVimLeave()
call tabby#agent#Close()
endfunction
function! tabby#OnTextChanged()
if s:status != "initialization_done"
return
endif
if !exists('s:completion') || !exists('s:choice_index')
if g:tabby_trigger_mode == 'auto'
" FIXME: Do not dismiss when type over the same as the completion text, or backspace in replace range.
call tabby#Dismiss()
call tabby#Trigger(v:false)
endif
endfunction
function! tabby#OnCursorMoved()
if s:current_completion_request == s:GetCompletionContext(v:false)
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'postEvent',
\ args: [#{
\ type: a:event_type,
\ completion_id: s:completion.id,
\ choice_index: s:completion.choices[s:choice_index].index,
\ }],
\ })
endfunction
function! s:HandleNotification(channel, data)
if (type(a:data) == v:t_dict) && has_key(a:data, 'event') && (a:data.event == 'statusChanged')
let s:tabby_status = a:data.status
call tabby#Dismiss()
if s:ongoing_request_id != 0
call tabby#agent#CancelRequest(s:ongoing_request_id)
endif
endfunction
function! s:HandleCompletion(id, channel, data)
if !exists('s:trigger_id') || (a:id != s:trigger_id)
return
endif
if (type(a:data) == v:t_dict) && has_key(a:data, 'choices') &&
\ (type(a:data.choices) == v:t_list) && (len(a:data.choices) > 0)
let s:completion = a:data
let s:choice_index = 0
call tabby#Show()
function! tabby#OnInsertLeave()
call tabby#Dismiss()
if s:ongoing_request_id != 0
call tabby#agent#CancelRequest(s:ongoing_request_id)
endif
endfunction
function! s:HandleError(channel, data)
" For Debug
" echoerr "HandleError: " . string(a:data)
endfunction
function! s:HandleExit(channel, data)
if exists('s:tabby')
unlet s:tabby
endif
if exists('s:tabby_status')
unlet s:tabby_status
endif
endfunction
" 4. Scheduler
"
" Notable script-local variables:
" - s:scheduled:
" Stores the timer id of current scheduled next trigger.
"
" - s:trigger_id:
" Use a timestamp to identify current triggered completion request. This is
" used to filter out outdated completion results.
"
function! tabby#Schedule()
if !tabby#IsRunning()
return
endif
call tabby#Clear()
let s:scheduled = timer_start(g:tabby_suggestion_delay, function('tabby#Trigger'))
endfunction
function! tabby#Trigger(...)
if !tabby#IsRunning()
return
endif
call tabby#Clear()
let id = join(reltime(), '.')
let s:trigger_id = id
call s:GetCompletion(id)
endfunction
" 5. Completion UI
"
" Notable script-local variables:
" - s:completion:
" Stores current completion data, a dictionary that has same struct as server
" returned completion response.
"
" - s:choice_index:
" Stores index of current choice to display. A 0-based index of choice item
" of `s:completion.choices` array, may not equals to the value of `index`
" field `s:completion.choices[s:choice_index].index`.
" A exception is that when `s:choice_index` is equal to the length of
" `s:completion.choices`. In this state, the completion UI should show nothing
" to notice users that they are cycling from last choice forward to first
" choice, or from first choice back to last choice.
" This variable does not change when user dismisses the completion UI.
"
" - s:shown_lines:
" Stores the text that are shown in completion UI. `s:shown_lines` exists or
" not means whether the completion UI is shown or not.
"
" - s:text_to_insert:
" Used as a buffer to store the text that should be inserted when user accepts
" the completion. We hide completion UI first and clear `s:shown_lines` at
" same time, then insert the text, so that we need to store the text in a
" buffer until text is inserted.
function! tabby#Show()
call s:HideCompletion()
if !s:IsCompletionAvailable()
return
endif
if s:choice_index == len(s:completion.choices)
" Show empty to indicate that user is cycling back to first choice.
return
endif
let choice = s:completion.choices[s:choice_index]
if (type(choice.text) != v:t_string) || (len(choice.text) == 0)
return
endif
let lines = split(choice.text, "\n")
" split will not give an empty line if text starts with "\n"
if choice.text[0] == "\n"
call insert(lines, '')
endif
" ignore trailing empty lines
while len(lines) > 0 && (lines[-1] == '')
call remove(lines, -1)
endwhile
call tabby#virtual_text#Show(lines)
let s:shown_lines = lines
call s:PostEvent('view')
endfunction
function! tabby#ConsumeInsertion()
if !exists('s:text_to_insert')
function! tabby#TriggerOrDismiss()
if s:status != "initialization_done"
return ''
endif
if s:current_completion_response != {}
call tabby#Dismiss()
else
let text = s:text_to_insert
unlet s:text_to_insert
return text
call tabby#Trigger(v:true)
endif
endfunction
" This function is designed to replace <Tab> key input, so we need a fallback
" when completion UI is not shown.
function! tabby#Accept(...)
if !exists('s:shown_lines')
if a:0 < 1
return "\<Ignore>"
elseif type(a:1) == v:t_string
return a:1
elseif type(a:1) == v:t_func
return call(a:1, [])
endif
endif
let lines = s:shown_lines
if len(lines) == 1
let s:text_to_insert = lines[0]
let insertion = "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>"
else
let suffix_chars_to_replace = strchars(getline(line('.'))[col('.') - 1:])
let s:text_to_insert = join(lines, "\n")
let insertion = repeat("\<Del>", suffix_chars_to_replace) . "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>\<End>"
endif
call s:HideCompletion()
call s:PostEvent('select')
return insertion
endfunction
" This function is designed to replace <C-]> key input, so we need a fallback
" when completion UI is not shown.
function! tabby#Dismiss(...)
if !exists('s:shown_lines')
if a:0 < 1
return "\<Ignore>"
elseif type(a:1) == v:t_string
return a:1
elseif type(a:1) == v:t_func
return call(a:1, [])
endif
endif
call s:HideCompletion()
return ''
endfunction
function! tabby#Next()
if !s:IsCompletionAvailable()
" Store the context of ongoing completion request
let s:current_completion_request = {}
let s:ongoing_request_id = 0
function! tabby#Trigger(is_manual)
if s:status != "initialization_done"
return
endif
if !exists('s:shown_lines')
if s:choice_index == len(s:completion.choices)
let s:choice_index = 0
endif
else
let s:choice_index += 1
if s:choice_index > len(s:completion.choices)
let s:choice_index = 0
endif
if s:ongoing_request_id != 0
call tabby#agent#CancelRequest(s:ongoing_request_id)
endif
call tabby#Show()
let s:current_completion_request = s:GetCompletionContext(a:is_manual)
let request = s:current_completion_request
let OnResponse = { response -> s:HandleCompletionResponse(request, response) }
let s:ongoing_request_id = tabby#agent#ProvideCompletions(request, OnResponse)
endfunction
function! tabby#Prev()
if !s:IsCompletionAvailable()
" Store the completion response that is shown as inline completion.
let s:current_completion_response = {}
function! s:HandleCompletionResponse(request, response)
if s:current_completion_request != a:request
return
endif
if !exists('s:shown_lines')
if s:choice_index == len(s:completion.choices)
let s:choice_index = len(s:completion.choices) - 1
endif
else
let s:choice_index -= 1
if s:choice_index < 0
let s:choice_index = len(s:completion.choices)
let s:ongoing_request_id = 0
if (type(a:response) != v:t_dict) || !has_key(a:response, 'choices') ||
\ (type(a:response.choices) != v:t_list)
return
endif
call tabby#Dismiss()
if (len(a:response.choices) == 0)
return
endif
" Only support single choice completion for now
let choice = a:response.choices[0]
call tabby#virtual_text#Render(s:current_completion_request, choice)
let s:current_completion_response = a:response
call tabby#agent#PostEvent(#{
\ type: "view",
\ completion_id: a:response.id,
\ choice_index: choice.index,
\ })
endfunction
" Used as a buffer to store the text that should be inserted when user accepts
" the completion.
let s:text_to_insert = ''
function! tabby#ConsumeInsertion()
let text = s:text_to_insert
let s:text_to_insert = ''
return text
endfunction
function! tabby#Accept(...)
if s:current_completion_response == {}
" keybindings fallback
if a:0 < 1
return "\<Ignore>"
elseif type(a:1) == v:t_string
return a:1
elseif type(a:1) == v:t_func
return call(a:1, [])
endif
endif
call tabby#Show()
let response = s:current_completion_response
let choice = response.choices[0]
if (type(choice.text) != v:t_string) || (len(choice.text) == 0)
return
endif
let prefix_replace_chars = s:current_completion_request.position - choice.replaceRange.start
let suffix_replace_chars = choice.replaceRange.end - s:current_completion_request.position
let s:text_to_insert = strcharpart(choice.text, prefix_replace_chars)
let insertion = repeat("\<Del>", suffix_replace_chars) . "\<C-R>\<C-O>=tabby#ConsumeInsertion()\<CR>"
if s:text_to_insert[-1:] == "\n"
" Add a char and remove, workaround for insertion bug if ends with newline
let s:text_to_insert .= "_"
let insertion .= "\<BS>"
endif
call tabby#Dismiss()
call tabby#agent#PostEvent(#{
\ type: "select",
\ completion_id: response.id,
\ choice_index: choice.index,
\ })
return insertion
endfunction
function! tabby#Clear()
call s:HideCompletion()
if exists('s:scheduled')
call timer_stop(s:scheduled)
unlet s:scheduled
endif
if exists('s:trigger_id')
unlet s:trigger_id
endif
if exists('s:completion')
unlet s:completion
endif
if exists('s:choice_index')
unlet s:choice_index
endif
endfunction
function! s:IsCompletionAvailable()
if !exists('s:completion') || !exists('s:choice_index')
return v:false
endif
if (type(s:completion.choices) != v:t_list) || (len(s:completion.choices) == 0)
return v:false
endif
return v:true
endfunction
function! s:HideCompletion()
function! tabby#Dismiss()
let s:current_completion_response = {}
call tabby#virtual_text#Clear()
if exists('s:shown_lines')
unlet s:shown_lines
endif
endfunction
" 6. Utils
function! s:CreateCompletionRequest()
function! s:GetCompletionContext(is_manual)
return #{
\ filepath: expand('%:p'),
\ language: s:GetLanguage(),
\ text: join(getbufline('%', 1, '$'), "\n"),
\ position: s:CountCharsToCursor(),
\ maxPrefixLines: g:tabby_max_prefix_lines,
\ maxSuffixLines: g:tabby_max_suffix_lines,
\ position: s:GetCursorPosition(),
\ manually: a:is_manual,
\ }
endfunction
function! s:CountCharsToCursor()
" Count the number of characters from the beginning of the buffer to the cursor.
function! s:GetCursorPosition()
let lines = getline(1, line('.') - 1)
if col('.') > 1
let lines += [getline(line('.'))[:col('.') - 2]]
let lines += [strpart(getline(line('.')), 0, col('.') - 1)]
else
let lines += ['']
endif
@ -568,14 +257,42 @@ endfunction
function! s:GetLanguage()
let filetype = getbufvar('%', '&filetype')
if has_key(g:tabby_filetype_to_languages, filetype)
return g:tabby_filetype_to_languages[filetype]
if has_key(g:tabby_filetype_dict, filetype)
return g:tabby_filetype_dict[filetype]
else
return filetype
endif
endfunction
function! s:GetVersionString()
let version_string = execute('version')
return split(version_string, "\n")[0]
function! tabby#Auth()
if s:status != "initialization_done"
echo 'Tabby is not ready for auth.'
return
endif
if tabby#agent#Status() != 'unauthorized'
echo 'Already authorized.'
return
endif
call tabby#agent#RequestAuthUrl({ data -> s:HandleAuthUrl(data) })
echo 'Generating authorization URL, please wait...'
endfunction
function! s:HandleAuthUrl(data)
if (type(a:data) != v:t_dict) || !has_key(a:data, 'authUrl') || !has_key(a:data, 'code')
echo 'Failed to create authorization URL.'
return
endif
call tabby#agent#WaitForAuthToken(a:data.code)
let command = ''
if executable('xdg-open')
let command = 'xdg-open ' . a:data.authUrl
elseif executable('open')
let command = 'open ' . a:data.authUrl
elseif executable('wslview')
let command = 'wslview ' . a:data.authUrl
endif
if command != ''
call system(command)
endif
echo a:data.authUrl
endfunction

View File

@ -0,0 +1,157 @@
" Implementation of agent interface
if exists('g:autoloaded_tabby_agent')
finish
endif
let g:autoloaded_tabby_agent = 1
" Stores the job of the current tabby agent node process
let s:tabby = 0
" Stores the status of the tabby agent
let s:tabby_status = 'notInitialized'
" Stores the name of issues if any
let s:tabby_issues = []
function! tabby#agent#Status()
return s:tabby_status
endfunction
function! tabby#agent#Issues()
return s:tabby_issues
endfunction
function! tabby#agent#Open(command)
if type(s:tabby) != v:t_number || s:tabby != 0
return
endif
let s:tabby = tabby#job#Start(a:command, #{
\ out_cb: { _, data -> s:OnNotification(data) },
\ err_cb: { _, data -> s:OnError(data) },
\ exit_cb: { _ -> s:OnExit() },
\ })
call tabby#agent#Initialize()
endfunction
function! s:OnNotification(data)
if (type(a:data) == v:t_dict) && has_key(a:data, 'event')
if a:data.event == 'statusChanged'
let s:tabby_status = a:data.status
elseif a:data.event == 'issuesUpdated'
let s:tabby_issue = a:data.issues
endif
endif
endfunction
function! s:OnError(data)
" For Debug
" echoerr "OnError: " . string(a:data)
endfunction
function! s:OnExit()
let s:tabby = {}
let s:tabby_status = 'exited'
endfunction
function! tabby#agent#Close()
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Stop(s:tabby)
let s:tabby = {}
let s:tabby_status = 'exited'
endfunction
function! tabby#agent#Initialize()
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'initialize',
\ args: [#{
\ clientProperties: s:GetClientProperties(),
\ }],
\ })
endfunction
function! tabby#agent#RequestAuthUrl(OnResponse)
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'requestAuthUrl',
\ args: [],
\ }, #{
\ callback: { _, data -> a:OnResponse(data) },
\ })
endfunction
function! tabby#agent#WaitForAuthToken(code)
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'waitForAuthToken',
\ args: [a:code],
\ })
endfunction
function! tabby#agent#ProvideCompletions(request, OnResponse)
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
let requestId = tabby#job#Send(s:tabby, #{
\ func: 'provideCompletions',
\ args: [a:request, { "signal": v:true }],
\ }, #{
\ callback: { _, data -> a:OnResponse(data) },
\ })
return requestId
endfunction
function! tabby#agent#CancelRequest(requestId)
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'cancelRequest',
\ args: [a:requestId],
\ })
endfunction
function! tabby#agent#PostEvent(event)
if type(s:tabby) == v:t_number && s:tabby == 0
return
endif
call tabby#job#Send(s:tabby, #{
\ func: 'postEvent',
\ args: [a:event],
\ })
endfunction
function! s:GetClientProperties()
let version_output = execute('version')
let client = split(version_output, "\n")[0]
let name = split(client, ' ')[0]
return #{
\ user: #{
\ vim: #{
\ triggerMode: g:tabby_trigger_mode
\ }
\ },
\ session: #{
\ client: client,
\ ide: #{
\ name: name,
\ version: client,
\ },
\ tabby_plugin: #{
\ name: 'TabbyML/vim-tabby',
\ version: g:tabby_version,
\ },
\ }
\ }
endfunction

View File

@ -0,0 +1,77 @@
" Commands for Tabby
if exists('g:autoloaded_tabby_commands')
finish
endif
let g:autoloaded_tabby_commands = 1
" See `*Tabby-commands*` section in `doc/tabby.txt` for more details.
" A dictionary contains all commands. Use name as key and function as value.
let s:commands = {}
function! s:commands.status(...)
call tabby#Status()
endfunction
function! s:commands.auth(...)
call tabby#Auth()
endfunction
function! s:commands.help(...)
let args = get(a:, 1, [])
if len(args) < 1
execute 'help Tabby'
return
endif
try
execute 'help Tabby-' . join(args, '-')
return
catch
endtry
try
execute 'help tabby_' . join(args, '_')
return
catch
endtry
execute 'help Tabby'
endfunction
function! tabby#commands#Main(args)
let args = split(a:args, ' ')
if len(args) < 1
call tabby#Status()
echo 'Use `:help Tabby` to see available commands.'
return
endif
if has_key(s:commands, args[0])
call s:commands[args[0]](args[1:])
else
echo 'Unknown command.'
echo 'Use `:help Tabby` to see available commands.'
endif
endfunction
function! tabby#commands#Complete(arglead, cmd, pos)
let words = split(a:cmd[0:a:pos].'#', ' ')
if len(words) > 3
return []
endif
if len(words) == 3
if words[1] == 'help'
let candidates = ['compatibility', 'commands', 'options', 'keybindings']
else
return []
endif
else
let candidates = keys(s:commands)
endif
let end_index = len(a:arglead) - 1
if end_index < 0
return candidates
else
return filter(candidates, { idx, val -> val[0:end_index] == a:arglead })
endif
endfunction

View File

@ -0,0 +1,54 @@
" Global variables of Tabby plugin. Include options and internal variables.
if exists('g:autoloaded_tabby_globals')
finish
endif
function! tabby#globals#Load()
let g:autoloaded_tabby_globals = 1
" See *Tabby-options* section in `doc/tabby.txt` for more details about options.
" The trigger mode of compleiton, default is "auto".
" - auto: Tabby automatically show inline completion when you stop typing.
" - manual: You need to press <C-\> to show inline completion.
let g:tabby_trigger_mode = get(g:, 'tabby_trigger_mode', 'auto')
" Tabby requires Node.js version 18.0 or higher to run the tabby agent.
" Specify the binary of Node.js, default is "node", which means search in $PATH.
let g:tabby_node_binary = get(g:, 'tabby_node_binary', 'node')
" The script of tabby agent.
let g:tabby_node_script = expand('<script>:h:h:h') . '/node_scripts/tabby-agent.js'
" Tabby use `getbufvar('%', '&filetype')` to get filetype of current buffer, and
" then use `g:tabby_filetype_dict` to map it to language identifier.
" From: vim filetype https://github.com/vim/vim/blob/master/runtime/filetype.vim
" To: vscode language identifier https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
" Not listed filetype will be used as language identifier directly.
let s:default_filetype_dict = #{
\ bash: "shellscript",
\ sh: "shellscript",
\ cs: "csharp",
\ objc: "objective-c",
\ objcpp: "objective-cpp",
\ make: "makefile",
\ cuda: "cuda-cpp",
\ text: "plaintext",
\ }
let g:tabby_filetype_dict = get(g:, 'tabby_filetype_dict', {})
let g:tabby_filetype_dict = extend(s:default_filetype_dict, g:tabby_filetype_dict)
" Keybinding of accept completion, default is "<Tab>".
let g:tabby_keybinding_accept = get(g:, 'tabby_keybinding_accept', '<Tab>')
" Keybinding of trigger or dismiss completion, default is "<C-\>".
let g:tabby_keybinding_trigger_or_dismiss = get(g:, 'tabby_keybinding_trigger_or_dismiss', '<C-\>')
" Version of Tabby plugin. Not configurable.
let g:tabby_version = "1.0.0-dev"
endfunction

View File

@ -1,3 +1,5 @@
" Handles IO with child processes jobs
if exists('g:autoloaded_tabby_job')
finish
endif
@ -10,7 +12,7 @@ function! tabby#job#Check()
return #{
\ ok: s:vim || s:nvim,
\ message: 'Tabby requires Vim 9.0+ with +job feature support, or NeoVim 0.6.0+.',
\}
\ }
endfunction
let s:nvim_job_map = {}
@ -41,11 +43,11 @@ function! tabby#job#Start(command, ...)
\ on_stdout: function('s:NvimHandleStdout'),
\ on_stderr: function('s:NvimHandleStderr'),
\ on_exit: function('s:NvimHandleExit'),
\})
\ })
let s:nvim_job_map[id] = #{
\ out_buffer: '',
\ requests: {},
\}
\ }
if has_key(options, 'out_cb')
let s:nvim_job_map[id].out_cb = options.out_cb
endif
@ -65,7 +67,6 @@ function! tabby#job#Stop(job)
endif
if s:nvim
let ret = jobstop(a:job)
call jobwait([a:job], 100)
if has_key(s:nvim_job_map, a:job)
unlet s:nvim_job_map[a:job]
endif

View File

@ -0,0 +1,31 @@
" Keybindings for Tabby
if exists('g:autoloaded_tabby_keybindings')
finish
endif
let g:autoloaded_tabby_keybindings = 1
function! tabby#keybindings#Map()
" map `tabby#Accept`
if toupper(g:tabby_keybinding_accept) == '<TAB>'
" to solve <Tab> binding conflicts, we store the original <Tab> mapping and fallback to it when tabby completions is not shown
if !empty(mapcheck('<Tab>', 'i'))
" fallback to the original <Tab> mapping
let tab_maparg = maparg('<Tab>', 'i', 0, 1)
" warp as function if rhs is expr
let fallback_rhs = tab_maparg.expr ? '{ -> ' . tab_maparg.rhs . ' }' : tab_maparg.rhs
" inject <SID>
let fallback_rhs = substitute(fallback_rhs, '<SID>', "\<SNR>" . get(tab_maparg, 'sid') . '_', 'g')
exec 'imap <script><silent><nowait><expr> <Tab> tabby#Accept(' . fallback_rhs . ')'
else
" fallback to input \t
imap <script><silent><nowait><expr> <Tab> tabby#Accept("\t")
endif
else
" map directly without fallback if the user has set keybinding to other than <Tab>
exec 'imap <script><silent><nowait><expr> ' . g:tabby_keybinding_accept . ' tabby#Accept()'
endif
" map `tabby#TriggerOrDismiss`, default to <C-\>
exec 'imap <script><silent><nowait><expr> ' . g:tabby_keybinding_trigger_or_dismiss . ' tabby#TriggerOrDismiss()'
endfunction

View File

@ -1,3 +1,5 @@
" Handles virtual text (aka ghost text) for rendering inline completion
if exists('g:autoloaded_tabby_virtual_text')
finish
endif
@ -10,65 +12,186 @@ function! tabby#virtual_text#Check()
return #{
\ ok: s:vim || s:nvim,
\ message: 'Tabby requires Vim 9.0.0534+ with +textprop feature support, or NeoVim 0.6.0+.',
\}
\ }
endfunction
function! tabby#virtual_text#Init()
hi def TabbyCompletion guifg=#808080 ctermfg=8
hi def TabbyCompletion guifg=#808080 ctermfg=245
hi def TabbyCompletionReplaceRange guifg=#303030 ctermfg=236 guibg=#808080 ctermbg=245
if s:vim
let s:prop_type = 'TabbyCompletion'
if prop_type_get(s:prop_type) != v:null
call prop_type_delete(s:prop_type)
let s:prop_type_completion = 'TabbyCompletion'
if prop_type_get(s:prop_type_completion) != {}
call prop_type_delete(s:prop_type_completion)
endif
call prop_type_add(s:prop_type, {'highlight': 'TabbyCompletion'})
call prop_type_add(s:prop_type_completion, #{
\ highlight: 'TabbyCompletion',
\ priority: 99,
\ combine: 0,
\ override: 1,
\ })
let s:prop_type_replace = 'TabbyCompletionReplaceRange'
if prop_type_get(s:prop_type_replace) != {}
call prop_type_delete(s:prop_type_replace)
endif
call prop_type_add(s:prop_type_replace, #{
\ highlight: 'TabbyCompletionReplaceRange',
\ priority: 99,
\ combine: 0,
\ override: 1,
\ })
endif
if s:nvim
let s:nvim_namespace = nvim_create_namespace('TabbyCompletion')
let s:nvim_highlight = 'TabbyCompletion'
let s:nvim_extmark_id = 1
let s:nvim_highlight_completion = 'TabbyCompletion'
let s:nvim_highlight_replace = 'TabbyCompletionReplaceRange'
endif
endfunction
function! tabby#virtual_text#Show(lines)
if len(a:lines) == 0
function! tabby#virtual_text#Render(request, choice)
if (type(a:choice.text) != v:t_string) || (len(a:choice.text) == 0)
return
endif
let prefix_replace_chars = a:request.position - a:choice.replaceRange.start
let suffix_replace_chars = a:choice.replaceRange.end - a:request.position
let text = strcharpart(a:choice.text, prefix_replace_chars)
if len(text) == 0
return
endif
let current_line_suffix = strpart(getline('.'), col('.') - 1)
if strchars(current_line_suffix) < suffix_replace_chars
return
endif
let text_lines = split(text, "\n")
" split will not give an empty line if text starts or ends with "\n"
if text[:0] == "\n"
call insert(text_lines, '')
endif
if text[-1:] == "\n"
call add(text_lines, '')
endif
" FIXME: no replace range processing for nvim for now, we need
" feat `virt_text_pos: "inline"` after nvim 0.10.0
if s:nvim
let text_lines[-1] .= strcharpart(current_line_suffix, suffix_replace_chars)
call s:AddInlay(text_lines[0], col('.'))
if len(text_lines) > 1
call s:AddLinesBelow(text_lines[1:])
endif
return
endif
" Replace range processing for vim
if suffix_replace_chars == 0
call s:AddInlay(text_lines[0], col('.'))
if len(text_lines) > 1
if strchars(current_line_suffix) > 0
call s:MarkReplaceRange(range(col('.'), col('.') + len(current_line_suffix)))
let text_lines[-1] .= current_line_suffix
endif
call s:AddLinesBelow(text_lines[1:])
endif
elseif suffix_replace_chars == 1
let replace_char = strcharpart(current_line_suffix, 0, 1)
let inlay = ''
if stridx(text_lines[0], replace_char) != 0
let inlay = split(text_lines[0], replace_char)[0]
endif
call s:AddInlay(inlay, col('.'))
if inlay != text_lines[0]
let inlay_suffix = strpart(text_lines[0], len(inlay) + len(replace_char))
call s:AddInlay(inlay_suffix, col('.') + len(replace_char))
endif
if len(text_lines) > 1
if strchars(current_line_suffix) > 1
let range_start = col('.')
if inlay != text_lines[0]
let range_start += len(replace_char)
endif
call s:MarkReplaceRange(range(range_start, col('.') + len(current_line_suffix)))
let text_lines[-1] .= strcharpart(current_line_suffix, 1)
endif
call s:AddLinesBelow(text_lines[1:])
endif
else
let replace_char = strcharpart(current_line_suffix, 0, suffix_replace_chars)
call s:AddInlay(text_lines[0], col('.'))
call s:MarkReplaceRange(range(col('.'), col('.') + len(replace_char)))
if len(text_lines) > 1
if strchars(current_line_suffix) > suffix_replace_chars
call s:MarkReplaceRange(range(col('.') + len(replace_char), col('.') + len(current_line_suffix)))
let text_lines[-1] .= strcharpart(current_line_suffix, suffix_replace_chars)
endif
call s:AddLinesBelow(text_lines[1:])
endif
endif
endfunction
function! s:AddInlay(inlay, column)
if s:vim
" virtual text requires 9.0.0534
" append line with a space to avoid empty line, which will result in unexpected behavior
call prop_add(line('.'), col('.'), #{
\ type: s:prop_type,
\ text: a:lines[0] . ' ',
\ })
for line in a:lines[1:]
if len(a:inlay) > 0
call prop_add(line('.'), a:column, #{
\ type: s:prop_type_completion,
\ text: a:inlay,
\ })
endif
endif
if s:nvim
if len(a:inlay) > 0
" FIXME: using virt_text_pos: "inline" after nvim 0.10.0
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, #{
\ virt_text_win_col: virtcol('.') - 1,
\ virt_text: [[a:inlay, s:nvim_highlight_completion]],
\ })
endif
endif
endfunction
function! s:AddLinesBelow(lines_below)
if s:vim
for line_blow in a:lines_below
let text = line_blow
if len(text) == 0
let text = ' '
endif
call prop_add(line('.'), 0, #{
\ type: s:prop_type,
\ text: line . ' ',
\ type: s:prop_type_completion,
\ text: text,
\ text_align: 'below',
\ })
endfor
endif
if s:nvim
let opt = #{
\ id: s:nvim_extmark_id,
\ virt_text_win_col: virtcol('.') - 1,
\ virt_text: [[a:lines[0], s:nvim_highlight]],
\}
if len(a:lines) > 1
let opt.virt_lines = map(a:lines[1:], { i, l -> [[l, s:nvim_highlight]] })
endif
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, opt)
call nvim_buf_set_extmark(0, s:nvim_namespace, line('.') - 1, col('.') - 1, #{
\ virt_lines: map(a:lines_below, { i, l -> [[l, s:nvim_highlight_completion]] })
\ })
endif
endfunction
function! s:MarkReplaceRange(replace_range)
if s:vim
call prop_add(line('.'), a:replace_range[0], #{
\ type: s:prop_type_replace,
\ length: len(a:replace_range),
\ })
endif
if s:nvim
call nvim_buf_add_highlight(0, s:nvim_namespace, s:nvim_highlight_replace, line('.') - 1,
\ a:replace_range[0] - 1, a:replace_range[-1])
endif
endfunction
function! tabby#virtual_text#Clear()
if s:vim
call prop_remove(#{
\ type: s:prop_type,
\ all: v:true,
\ type: s:prop_type_completion,
\ all: 1,
\ })
call prop_remove(#{
\ type: s:prop_type_replace,
\ all: 1,
\ })
endif
if s:nvim
call nvim_buf_del_extmark(0, s:nvim_namespace, s:nvim_extmark_id)
call nvim_buf_clear_namespace(0, s:nvim_namespace, 0, -1)
endif
endfunction

View File

@ -3,12 +3,14 @@ tabby.txt Tabby
Tabby is a self-hosted AI coding assistant that can suggest multi-line code or
full functions in real-time. For more information, please check out our
{Website}{1} and {Github}{2}. If you encounter any problem or have any
suggestion, please open an {issue}{3}.
suggestion, please {open_an_issue}{3} or join our {Slack_community}{4} for
support.
{1} https://www.tabbyml.com/
{2} https://github.com/TabbyML/tabby
{3} https://github.com/TabbyML/tabby/issues/new
{4} https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA
*Tabby-compatibility* *Tabby-neovim*
*Tabby-compatibility* *Tabby-NeoVim*
Compatibility~
This plugin is compatible with VIM 9.0+ with `+job` and `+textprop` features
enabled, or NeoVIM 0.6.0+.
@ -17,95 +19,65 @@ enabled, or NeoVIM 0.6.0+.
Commands~
*:Tabby*
:Tabby Same as |:Tabby-status|.
*:Tabby-enable*
:Tabby enable Start Tabby if not currently running in current VIM
process.
*:Tabby-disable*
:Tabby disable Stop Tabby in current VIM process. To disable Tabby
globally, set |g:tabby_enable| to v:false.
*:Tabby-toggle*
:Tabby toggle Toggle enable or disable Tabby, same as use command
|:Tabby-enable| or |:Tabby-disable|.
*:Tabby-status*
:Tabby status Check whether Tabby is enabled or not, and the
reachabilty to the Tabby server. Also report errors
if any compatibility problems exist.
:Tabby status Check the status of Tabby. Report error message if any
issue exists.
*:Tabby-auth*
:Tabby auth Check the status of Tabby. Report error message if any
issue exists.
*:Tabby-help*
:Tabby help [subject] Search for help information in this document using
VIM command `:help`.
*Tabby-options*
Options~
*g:tabby_enable*
g:tabby_enable Controls Tabby whether auto-starts along with VIM or
not. Modifying this value do not start or stop Tabby
at the same time. You can use |:Tabby-enable| or
|:Tabby-disable| to start or stop Tabby manually in
current VIM process.
*g:tabby_node_binary* *Tabby-node*
g:tabby_node_binary Tabby plugin will try to find the Node.js binary in
your `PATH` environment variable. If you have installed
Node.js in a non-standard location, or you are using a
version manager such as nvm, you can set the Node.js
binary path here.
>
let g:tabby_enable = v:true
let g:tabby_node_binary = '/path/to/node'
<
*g:tabby_server_url*
g:tabby_server_url Specify the Tabby server URL. You always need this
setting in your vimrc file, unless you are using the
default value: "http://localhost:8080".
*g:tabby_trigger_mode* *Tabby-trigger*
g:tabby_trigger_mode Completion trigger mode.
- 'auto': Trigger completion automatically when you
stop typing.
- 'manual': Only trigger completion when you press
`<C-\>`.
Default value is 'auto'.
>
let g:tabby_server_url = 'http://localhost:8080'
let g:tabby_trigger_mode = 'auto'
let g:tabby_trigger_mode = 'manual'
<
*g:tabby_suggestion_delay*
g:tabby_suggestion_delay
Controls the delay after which the suggestion request
is sent to server. If you want suggestion to show up
more quickly or slowly, try to tune this value.
Default value is 150 milliseconds.
>
let g:tabby_suggestion_delay = 150
<
*g:tabby_filetype_to_languages*
g:tabby_filetype_to_languages
This option is a dictionary that map from the VIM
`:filetype` to {VSCode-Language-Identifier}{1}. Not
*g:tabby_filetype_dict* *Tabby-filetype*
g:tabby_filetype_dict This option is a dictionary that map from the Vim
`:filetype` to {VSCode_Language_Identifier}{1}. Not
listed filetype will be used as language identifier
directly.
A correct language identifier is required for the
Tabby server to generate suggestion. If your filetype
need converting to language identifier but not listed,
add it in this dictionary.
You can also map a filetype to "unknow" to prevent
Tabby giving suggestion for specified filetype.
The following mappings are provided by default, you can
add your own mappings to override them.
{1} https://code.visualstudio.com/docs/languages/identifiers
>
let g:tabby_filetype_to_languages = {
\ "bash": "shellscript",
\ "cs": "csharp",
\ "objc": "objective-c",
\ "objcpp": "objective-cpp",
let g:tabby_filetype_dict = #{
\ bash: "shellscript",
\ sh: "shellscript",
\ cs: "csharp",
\ objc: "objective-c",
\ objcpp: "objective-cpp",
\ make: "makefile",
\ cuda: "cuda-cpp",
\ text: "plaintext",
\ }
<
*g:tabby_agent_logs*
g:tabby_agent_logs
Controls the log level of tabby-agent, could be set to
'debug', 'error' or 'silent', default to 'error'.
You can find log files in "$HOME/.tabby/agent-logs/".
Logs could be prettily visualized by 'pino-pretty'.
>
let g:tabby_agent_logs = 'error'
<
*Tabby-keybindings* *Tabby-maps*
*Tabby-keybindings* *Tabby-map*
Keybindings~
<Tab> Accept the current suggestion, fallback to normal
`<TAB>` if no suggestion is shown.
<Tab> Accept the current completion, fallback to normal
`<Tab>` if no completion is shown.
<C-]> Dismiss the current suggestion. Fallback to normal
`<C-]>` if no suggestion is shown.
<M-]> Show the next suggestion. There is a empty suggestion
after the last, before return to first one.
<M-[> Show the previous suggestion. There is a empty
suggestion before the first, before return to last
one.
<C-\> Trigger completion if not shown. Dismiss the current
compleiton if shown.
vim:tw=78:ts=8:noet:ft=help:norl:

File diff suppressed because one or more lines are too long

View File

@ -3,71 +3,14 @@ if exists('g:loaded_tabby')
endif
let g:loaded_tabby = 1
call tabby#Start()
command! -nargs=* -complete=customlist,tabby#CompleteCommands Tabby call tabby#Command(<q-args>)
if !exists('g:tabby_accept_binding')
let g:tabby_accept_binding = '<Tab>'
endif
if !exists('g:tabby_dismiss_binding')
let g:tabby_dismiss_binding = '<C-]>'
endif
function s:MapKeyBindings()
" map `tabby#Accept`
if g:tabby_accept_binding == '<Tab>'
" to solve <Tab> binding conflicts, we store the original <Tab> mapping and fallback to it when tabby completions is not shown
if exists('g:tabby_binding_tab_fallback')
" map directly if the user has set a custom fallback method
imap <script><silent><nowait><expr> <Tab> tabby#Accept(g:tabby_binding_tab_fallback)
else
if !empty(mapcheck('<Tab>', 'i'))
" fallback to the original <Tab> mapping
let tab_maparg = maparg('<Tab>', 'i', v:false, v:true)
" warp as function if rhs is expr
let fallback_rhs = tab_maparg.expr ? '{ -> ' . tab_maparg.rhs . ' }' : tab_maparg.rhs
" inject <SID>
let fallback_rhs = substitute(fallback_rhs, '<SID>', "\<SNR>" . get(tab_maparg, 'sid') . '_', 'g')
exec 'imap <script><silent><nowait><expr> <Tab> tabby#Accept(' . fallback_rhs . ')'
else
" fallback to input \t
imap <script><silent><nowait><expr> <Tab> tabby#Accept("\t")
endif
endif
else
" map directly without fallback if the user has set a custom binding
exec 'imap <script><silent><nowait><expr> ' . g:tabby_accept_binding . ' tabby#Accept()'
endif
" map `tabby#Dismiss`
if g:tabby_accept_binding == '<C-]>'
imap <script><silent><nowait><expr> <C-]> tabby#Dismiss("\<C-]>")
else
" map directly without fallback if the user has set a custom binding
exec 'imap <script><silent><nowait><expr> ' . g:tabby_dismiss_binding . ' tabby#Dismiss()'
endif
" map `tabby#Next` and `tabby#Prev`
imap <Plug>(tabby-next) <Cmd>call tabby#Next()<CR>
imap <Plug>(tabby-prev) <Cmd>call tabby#Prev()<CR>
if empty(mapcheck('<M-]>', 'i'))
imap <M-]> <Plug>(tabby-next)
endif
if empty(mapcheck('<M-[>', 'i'))
imap <M-[> <Plug>(tabby-prev)
endif
endfunction
command! -nargs=* -complete=customlist,tabby#commands#Complete Tabby call tabby#commands#Main(<q-args>)
silent! execute 'helptags' fnameescape(expand('<sfile>:h:h') . '/doc')
augroup tabby
autocmd!
autocmd TextChangedI,CompleteChanged * call tabby#Schedule()
autocmd CursorMovedI * call tabby#Clear()
autocmd BufLeave * call tabby#Clear()
autocmd InsertLeave * call tabby#Clear()
" map bindings as late as possible, to avoid <Tab> binding override by other scripts
autocmd VimEnter * call s:MapKeyBindings()
autocmd VimEnter * call tabby#OnVimEnter()
autocmd VimLeave * call tabby#OnVimLeave()
autocmd TextChangedI,CompleteChanged * call tabby#OnTextChanged()
autocmd CursorMovedI * call tabby#OnCursorMoved()
autocmd InsertLeave,BufLeave * call tabby#OnInsertLeave()
augroup END
silent! execute 'helptags' fnameescape(expand('<sfile>:h:h') . '/doc')