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
parent
0fcb04e370
commit
c2db171ef9
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
## 1.0.0
|
||||
|
||||
### Initial release
|
||||
|
|
@ -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-\\>'
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in New Issue