diff --git a/clients/tabby-agent/src/Agent.ts b/clients/tabby-agent/src/Agent.ts index 95dc461..65b0710 100644 --- a/clients/tabby-agent/src/Agent.ts +++ b/clients/tabby-agent/src/Agent.ts @@ -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 { /** diff --git a/clients/tabby-agent/src/StdIO.ts b/clients/tabby-agent/src/StdIO.ts index c1e4847..269a888 100644 --- a/clients/tabby-agent/src/StdIO.ts +++ b/clients/tabby-agent/src/StdIO.ts @@ -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 = [ id: number, @@ -41,6 +41,7 @@ type StdIOResponse = AgentFunctionResponse | 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); + }); + }); } } diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index fbcf014..081a784 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -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 { + 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; } diff --git a/clients/tabby-agent/src/logger.ts b/clients/tabby-agent/src/logger.ts index ef988ab..c401bab 100644 --- a/clients/tabby-agent/src/logger.ts +++ b/clients/tabby-agent/src/logger.ts @@ -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 { diff --git a/clients/tabby-agent/src/postprocess/trimSpace.ts b/clients/tabby-agent/src/postprocess/trimSpace.ts index b2c23f6..5e4de29 100644 --- a/clients/tabby-agent/src/postprocess/trimSpace.ts +++ b/clients/tabby-agent/src/postprocess/trimSpace.ts @@ -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; diff --git a/clients/vim/CHANGELOG.md b/clients/vim/CHANGELOG.md new file mode 100644 index 0000000..3cbc0af --- /dev/null +++ b/clients/vim/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +### Initial release diff --git a/clients/vim/README.md b/clients/vim/README.md index 0afa4cb..253c42d 100644 --- a/clients/vim/README.md +++ b/clients/vim/README.md @@ -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 `` to accpet the current suggestion, `` to see the next suggestion, `` to see previous suggestion, or `` 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 `` to accept the completion. If you want to dismiss the completion manually, you can press `` to dismiss, and press `` 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 ``. + +```vim +let g:tabby_trigger_mode = 'manual' +``` + +```lua +--- lua +vim.g.tabby_trigger_mode = 'manual' +``` + ### KeyBindings -The default key bindings for accept/dismiss(`/`) can be customized -with the following global settings. +The default key bindings for accept completion(``), manual trigger/dismiss(``) can be customized with the following global settings. -```vimscript -let g:tabby_accept_binding = '' -let g:tabby_dismiss_binding = '' +```vim +let g:tabby_keybinding_accept = '' +let g:tabby_keybinding_trigger_or_dismiss = '' +``` + +```lua +--- lua +vim.g.tabby_keybinding_accept = '' +vim.g.tabby_keybinding_trigger_or_dismiss = '' ``` diff --git a/clients/vim/autoload/tabby.vim b/clients/vim/autoload/tabby.vim index 8a8ab54..2049816 100644 --- a/clients/vim/autoload/tabby.vim +++ b/clients/vim/autoload/tabby.vim @@ -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('