├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── autoload ├── ddc_tabnine.vim └── ddc_tabnine │ └── internal.vim ├── denops ├── @ddc-sources │ └── tabnine.ts └── ddc-tabnine │ ├── deps.ts │ ├── internal_autoload_fn.ts │ ├── main.ts │ ├── tabnine │ ├── client_base.ts │ └── client_v2.ts │ └── the_client.ts ├── doc └── ddc-tabnine.txt └── test.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '0 0 1 * *' 12 | 13 | jobs: 14 | check: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - os: ubuntu-latest 21 | deno: v1.x 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: denoland/setup-deno@v1 25 | with: 26 | deno-version: ${{ matrix.deno }} 27 | - run: deno lint 28 | - run: deno fmt --check 29 | - run: deno test --unstable --no-run 30 | test: 31 | needs: [check] 32 | runs-on: ${{ matrix.os }} 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: 37 | - os: ubuntu-latest 38 | deno: v1.x 39 | - os: ubuntu-latest 40 | deno: v1.13 41 | - os: ubuntu-latest 42 | deno: canary 43 | - os: windows-latest 44 | deno: v1.x 45 | - os: macos-latest 46 | deno: v1.x 47 | steps: 48 | - uses: actions/checkout@v2 49 | - uses: denoland/setup-deno@v1 50 | with: 51 | deno-version: ${{ matrix.deno }} 52 | - run: deno test --unstable -A 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Luma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddc-tabnine 2 | 3 | [![Doc](https://img.shields.io/badge/doc-%3Ah%20ddc--tabnine-orange.svg?style=flat-square)](doc/ddc-tabnine.txt) 4 | 5 | [TabNine](https://www.tabnine.com) Completion for ddc.vim 6 | 7 | This source collects candidates from [TabNine](https://www.tabnine.com). 8 | 9 | ## Required 10 | 11 | - [denops.vim](https://github.com/vim-denops/denops.vim) 12 | - [ddc.vim](https://github.com/Shougo/ddc.vim) 13 | 14 | ## Configuration 15 | 16 | ```vim 17 | call ddc#custom#patch_global('sources', ['tabnine']) 18 | call ddc#custom#patch_global('sourceOptions', { 19 | \ 'tabnine': { 20 | \ 'mark': 'TN', 21 | \ 'maxCandidates': 5, 22 | \ 'isVolatile': v:true, 23 | \ }}) 24 | ``` 25 | 26 | ## Special Commands 27 | 28 | You can trigger the 29 | [special commands](https://www.tabnine.com/faq#special_commands) to configure 30 | your TabNine like `TabNine::config` in any buffer. 31 | 32 | (Optional) To configure your purchased API key, use `TabNine::config` or 33 | `:exe 'e' ddc_tabnine#config_dir() . '/tabnine_config.json'`. 34 | 35 | ## Credits 36 | 37 | - https://www.tabnine.com 38 | - https://github.com/codota/TabNine/blob/master/HowToWriteAClient.md 39 | - https://github.com/Shougo/ddc.vim 40 | - https://github.com/neoclide/coc-tabnine 41 | - https://github.com/tbodt/deoplete-tabnine 42 | -------------------------------------------------------------------------------- /autoload/ddc_tabnine.vim: -------------------------------------------------------------------------------- 1 | function! ddc_tabnine#restart() abort 2 | return denops#request('ddc-tabnine', 'restart', []) 3 | endfunction 4 | 5 | function! ddc_tabnine#reinstall() abort 6 | return denops#request('ddc-tabnine', 'reinstall', []) 7 | endfunction 8 | 9 | function! ddc_tabnine#is_running() abort 10 | return denops#request('ddc-tabnine', 'isRunning', []) 11 | endfunction 12 | 13 | function! ddc_tabnine#which() abort 14 | return denops#request('ddc-tabnine', 'which', []) 15 | endfunction 16 | 17 | function! ddc_tabnine#version() abort 18 | return denops#request('ddc-tabnine', 'version', []) 19 | endfunction 20 | 21 | function! ddc_tabnine#config_dir() abort 22 | return denops#request('ddc-tabnine', 'configDir', []) 23 | endfunction 24 | -------------------------------------------------------------------------------- /autoload/ddc_tabnine/internal.vim: -------------------------------------------------------------------------------- 1 | " @param {number} limit 2 | " @return {[ 3 | " string, 4 | " string, 5 | " string, 6 | " boolean, 7 | " boolean, 8 | " number, 9 | " ]} 10 | function! ddc_tabnine#internal#get_around(limit) abort 11 | let [_, line, col; _] = getpos('.') 12 | let last_line = line('$') 13 | let before_line = max([1, line - a:limit]) 14 | let before_lines = getline(before_line, line) 15 | if len(before_lines) > 0 16 | let before_lines[-1] = before_lines[-1][: col - 2] 17 | endif 18 | let after_line = min([last_line, line + a:limit]) 19 | let after_lines = getline(line, after_line) 20 | if len(after_lines) > 0 21 | let after_lines[0] = after_lines[0][col - 1:] 22 | endif 23 | 24 | let filename = bufname() 25 | let before = join(before_lines, "\n") 26 | let after = join(map(after_lines, 'v:val . "\n"'), "") 27 | let region_includes_beginning = before_line == 1 ? v:true : v:false 28 | let region_includes_end = after_line == last_line ? v:true : v:false 29 | 30 | return [ 31 | \ filename, 32 | \ before, 33 | \ after, 34 | \ region_includes_beginning, 35 | \ region_includes_end, 36 | \ ] 37 | endfunction 38 | 39 | " @param {string} oldSuffix 40 | " @param {string} newPrefixMore 41 | " @param {string} newSuffix 42 | function! ddc_tabnine#internal#on_complete_done(old_suffix, new_prefix_more, new_suffix) abort 43 | if mode() !=# 'i' 44 | return 45 | endif 46 | 47 | let col = col('.') 48 | let text = getline('.') 49 | if text[col - 1 : col - 2 + len(a:old_suffix)] !=# a:old_suffix 50 | return 51 | endif 52 | call setline(line('.'), text[: col - 2] . text[col - 1 + len(a:old_suffix) :]) 53 | 54 | call feedkeys(a:new_prefix_more, 'i') 55 | 56 | let col = col('.') 57 | let text = getline('.') 58 | call setline(line('.'), text[: col - 2] . a:new_suffix . text[col - 1 :]) 59 | endfunction 60 | -------------------------------------------------------------------------------- /denops/@ddc-sources/tabnine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseSource, 3 | GatherArguments, 4 | Item, 5 | OnCompleteDoneArguments, 6 | } from "../ddc-tabnine/deps.ts"; 7 | import type { 8 | TabNineV2AutoCompleteRequest, 9 | TabNineV2AutoCompleteResponse, 10 | } from "../ddc-tabnine/tabnine/client_v2.ts"; 11 | import * as internal from "../ddc-tabnine/internal_autoload_fn.ts"; 12 | 13 | type Params = { 14 | maxSize: number; 15 | maxNumResults: number; 16 | }; 17 | 18 | type UserData = { 19 | /** Delete suffix */ 20 | s: string; 21 | /** Add as prefix */ 22 | P: string; 23 | /** Add as suffix */ 24 | S: string; 25 | }; 26 | 27 | export class Source extends BaseSource { 28 | async gather( 29 | args: GatherArguments, 30 | ): Promise[]> { 31 | const p = args.sourceParams as Params; 32 | 33 | const [ 34 | filename, 35 | before, 36 | after, 37 | regionIncludesBeginning, 38 | regionIncludesEnd, 39 | ] = await internal.getAround(args.denops, p.maxSize); 40 | const req: TabNineV2AutoCompleteRequest = { 41 | maxNumResults: p.maxNumResults, 42 | filename, 43 | before, 44 | after, 45 | regionIncludesBeginning, 46 | regionIncludesEnd, 47 | }; 48 | const resUnknown: unknown = await args.denops.dispatch( 49 | "ddc-tabnine", 50 | "internalRequestAutocomplete", 51 | req, 52 | ); 53 | // deno-lint-ignore no-explicit-any 54 | const res: TabNineV2AutoCompleteResponse = resUnknown as any; 55 | const cs: Item[] = 56 | (res?.results?.filter((e) => e?.new_prefix).map((e) => { 57 | const newLine = e.new_prefix.indexOf("\n"); 58 | return { 59 | word: newLine === -1 ? e.new_prefix : e.new_prefix.slice(0, newLine), 60 | abbr: e.new_prefix + (e.new_suffix ?? ""), 61 | menu: e.detail ?? undefined, 62 | user_data: { 63 | s: e.old_suffix, 64 | P: (newLine === -1 ? "" : e.new_prefix.slice(newLine)), 65 | S: e.new_suffix, 66 | }, 67 | }; 68 | }) ?? []); 69 | return cs; 70 | } 71 | 72 | params(): Params { 73 | return { 74 | maxSize: 200, 75 | maxNumResults: 5, 76 | }; 77 | } 78 | 79 | async onCompleteDone( 80 | args: OnCompleteDoneArguments, 81 | ): Promise { 82 | const { s: oldSuffix, P: newPrefixMore, S: newSuffix } = args 83 | .userData; 84 | await internal.onCompleteDone( 85 | args.denops, 86 | oldSuffix, 87 | newPrefixMore, 88 | newSuffix, 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/deps.ts: -------------------------------------------------------------------------------- 1 | export type { Item } from "https://deno.land/x/ddc_vim@v4.1.0/types.ts"; 2 | export { BaseSource } from "https://deno.land/x/ddc_vim@v4.1.0/types.ts"; 3 | export type { Denops } from "https://deno.land/x/ddc_vim@v4.1.0/deps.ts"; 4 | export { fn, vars } from "https://deno.land/x/ddc_vim@v4.1.0/deps.ts"; 5 | export type { 6 | GatherArguments, 7 | OnCompleteDoneArguments, 8 | } from "https://deno.land/x/ddc_vim@v4.1.0/base/source.ts"; 9 | export * as path from "https://deno.land/std@0.206.0/path/mod.ts"; 10 | export * as io from "https://deno.land/std@0.206.0/io/mod.ts"; 11 | export * as fs from "https://deno.land/std@0.206.0/fs/mod.ts"; 12 | export { TextLineStream } from "https://deno.land/std@0.206.0/streams/mod.ts"; 13 | export { decompress } from "https://deno.land/x/zip@v1.2.1/mod.ts"; 14 | export * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts"; 15 | export { Mutex } from "https://deno.land/x/semaphore@v1.1.0/mod.ts"; 16 | export { assert } from "https://deno.land/std@0.206.0/assert/assert.ts"; 17 | import xdg from "https://deno.land/x/xdg@v9.4.0/src/mod.deno.ts"; 18 | export { xdg }; 19 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/internal_autoload_fn.ts: -------------------------------------------------------------------------------- 1 | import { Denops, fn } from "./deps.ts"; 2 | export type { Denops }; 3 | export { fn }; 4 | 5 | // deno-lint-ignore no-explicit-any 6 | const createCaller = (name: string): any => { 7 | return async (denops: Denops, ...args: unknown[]) => { 8 | return await fn.call(denops, name, args); 9 | }; 10 | }; 11 | 12 | export type GetAround = ( 13 | denops: Denops, 14 | limit: number, 15 | ) => Promise<[ 16 | string, 17 | string, 18 | string, 19 | boolean, 20 | boolean, 21 | number, 22 | ]>; 23 | export const getAround = createCaller( 24 | "ddc_tabnine#internal#get_around", 25 | ) as GetAround; 26 | 27 | export type OnCompleteDone = ( 28 | denops: Denops, 29 | oldSuffix: string, 30 | newPrefixMore: string, 31 | newSuffix: string, 32 | ) => Promise; 33 | export const onCompleteDone = createCaller( 34 | "ddc_tabnine#internal#on_complete_done", 35 | ) as OnCompleteDone; 36 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/main.ts: -------------------------------------------------------------------------------- 1 | import type { Denops } from "./deps.ts"; 2 | import { path, vars, xdg } from "./deps.ts"; 3 | import theClient from "./the_client.ts"; 4 | import type { 5 | TabNineV2AutoCompleteRequest, 6 | TabNineV2AutoCompleteResponse, 7 | } from "./tabnine/client_v2.ts"; 8 | 9 | const dispatcherNames = { 10 | DDC_TABNINE_RESTART: "restart", 11 | DDC_TABNINE_REINSTALL: "reinstall", 12 | DDC_TABNINE_CLEAN: "clean", 13 | DDC_TABNINE_IS_RUNNING: "isRunning", 14 | DDC_TABNINE_WHICH: "which", 15 | DDC_TABNINE_VERSION: "version", 16 | DDC_TABNINE_CONFIG_DIR: "configDir", 17 | DDC_TABNINE_INTERNAL_REQUEST_AUTOCOMPLETE: "internalRequestAutocomplete", 18 | } as const; 19 | 20 | // deno-lint-ignore require-await 21 | export async function main(denops: Denops): Promise { 22 | const defaultStorageDir = path.join( 23 | xdg.cache(), 24 | "ddc-tabnine", 25 | "binaries", 26 | ); 27 | 28 | async function getStorageDir(): Promise { 29 | const userStorageDir = await vars.g.get( 30 | denops, 31 | "ddc_tabnine#storage_dir", 32 | ); 33 | const storageDir = typeof userStorageDir === "string" 34 | ? userStorageDir 35 | : defaultStorageDir; 36 | return storageDir; 37 | } 38 | 39 | async function getDisableAutoInstall(): Promise { 40 | const userDisableAutoInstall = await vars.g.get( 41 | denops, 42 | "ddc_tabnine#disable_auto_install", 43 | ); 44 | return Boolean(userDisableAutoInstall); 45 | } 46 | 47 | denops.dispatcher = { 48 | async [dispatcherNames.DDC_TABNINE_RESTART](): Promise { 49 | const storageDirAsync = getStorageDir(); 50 | const disableAutoInstallAsync = getDisableAutoInstall(); 51 | const client = await theClient.getClient( 52 | await storageDirAsync, 53 | await disableAutoInstallAsync, 54 | ); 55 | await client.restartProc(); 56 | }, 57 | async [dispatcherNames.DDC_TABNINE_REINSTALL](): Promise { 58 | const storageDirAsync = getStorageDir(); 59 | const disableAutoInstallAsync = getDisableAutoInstall(); 60 | await theClient.reinstall(await storageDirAsync); 61 | const client = await theClient.getClient( 62 | await storageDirAsync, 63 | await disableAutoInstallAsync, 64 | ); 65 | await client.restartProc(); 66 | }, 67 | async [dispatcherNames.DDC_TABNINE_IS_RUNNING](): Promise { 68 | const storageDirAsync = getStorageDir(); 69 | const disableAutoInstallAsync = getDisableAutoInstall(); 70 | const client = await theClient.getClient( 71 | await storageDirAsync, 72 | await disableAutoInstallAsync, 73 | ); 74 | return client.isRunning(); 75 | }, 76 | async [dispatcherNames.DDC_TABNINE_WHICH](): Promise { 77 | const storageDirAsync = getStorageDir(); 78 | const disableAutoInstallAsync = getDisableAutoInstall(); 79 | const client = await theClient.getClient( 80 | await storageDirAsync, 81 | await disableAutoInstallAsync, 82 | ); 83 | return client.getRunningBinaryPath(); 84 | }, 85 | async [dispatcherNames.DDC_TABNINE_VERSION](): Promise { 86 | const storageDirAsync = getStorageDir(); 87 | const disableAutoInstallAsync = getDisableAutoInstall(); 88 | const client = await theClient.getClient( 89 | await storageDirAsync, 90 | await disableAutoInstallAsync, 91 | ); 92 | return await client.requestVersion(); 93 | }, 94 | async [dispatcherNames.DDC_TABNINE_CONFIG_DIR](): Promise { 95 | const storageDirAsync = getStorageDir(); 96 | const disableAutoInstallAsync = getDisableAutoInstall(); 97 | const client = await theClient.getClient( 98 | await storageDirAsync, 99 | await disableAutoInstallAsync, 100 | ); 101 | return await client.requestConfigDir(); 102 | }, 103 | async [dispatcherNames.DDC_TABNINE_INTERNAL_REQUEST_AUTOCOMPLETE]( 104 | reqUnknown: unknown, 105 | ): Promise { 106 | // deno-lint-ignore no-explicit-any 107 | const req: TabNineV2AutoCompleteRequest = reqUnknown as any; 108 | const storageDirAsync = getStorageDir(); 109 | const disableAutoInstallAsync = getDisableAutoInstall(); 110 | const client = await theClient.getClient( 111 | await storageDirAsync, 112 | await disableAutoInstallAsync, 113 | ); 114 | return await client.requestAutocomplete(req); 115 | }, 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/tabnine/client_base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | decompress, 4 | fs, 5 | Mutex, 6 | path, 7 | semver, 8 | TextLineStream, 9 | } from "../deps.ts"; 10 | 11 | export class TabNine { 12 | private proc?: Deno.ChildProcess; 13 | private lines?: AsyncIterator; 14 | private runningBinaryPath?: string; 15 | 16 | private binaryPath?: string; 17 | private mutex = new Mutex(); 18 | private numRestarts = 0; 19 | 20 | constructor( 21 | public clientName: string, 22 | public storageDir: string, 23 | ) {} 24 | 25 | async request(request: unknown): Promise { 26 | const release = await this.mutex.acquire(); 27 | try { 28 | this.numRestarts = 0; 29 | return await this.requestUnlocked(request); 30 | } finally { 31 | release(); 32 | } 33 | } 34 | 35 | private async requestUnlocked( 36 | request: unknown, 37 | ): Promise { 38 | const requestStr = JSON.stringify(request) + "\n"; 39 | if (!this.isRunning()) { 40 | await this.restartProcLimited(); 41 | } 42 | if (!this.isRunning()) { 43 | throw new Error("TabNine process is dead."); 44 | } 45 | assert(this.proc?.stdin, "this.proc.stdin"); 46 | await new Blob([requestStr]).stream().pipeTo( 47 | this.proc.stdin, 48 | { preventClose: true }, 49 | ); 50 | 51 | const responseResult = await this.lines?.next(); 52 | if (responseResult && !responseResult.done) { 53 | const response: unknown = JSON.parse(responseResult.value); 54 | return response; 55 | } 56 | return undefined; 57 | } 58 | 59 | isRunning(): boolean { 60 | return Boolean(this.proc); 61 | } 62 | 63 | async restartProc(): Promise { 64 | this.numRestarts = 0; 65 | await this.restartProcLimited(); 66 | } 67 | 68 | private async restartProcLimited(): Promise { 69 | if (this.numRestarts >= 10) { 70 | return; 71 | } 72 | this.numRestarts += 1; 73 | if (this.proc) { 74 | const oldProc = this.proc; 75 | this.proc = undefined; 76 | killProcess(oldProc); 77 | } 78 | const args = [ 79 | `--client=${this.clientName}`, 80 | ]; 81 | 82 | const binaryPath = this.binaryPath || 83 | await TabNine.getBinaryPath(this.storageDir); 84 | 85 | this.runningBinaryPath = binaryPath; 86 | this.proc = new Deno.Command(binaryPath, { 87 | args, 88 | stdin: "piped", 89 | stdout: "piped", 90 | }).spawn(); 91 | void this.proc.status.then(() => { 92 | this.proc = undefined; 93 | this.runningBinaryPath = undefined; 94 | }); 95 | assert(this.proc.stdout, "this.proc.stdout"); 96 | this.lines = this.proc.stdout.pipeThrough(new TextDecoderStream()) 97 | .pipeThrough(new TextLineStream()).values(); 98 | } 99 | 100 | async isInstalled(version: string): Promise { 101 | const archAndPlatform = TabNine.getArchAndPlatform(); 102 | const destDir = path.join( 103 | this.storageDir, 104 | version, 105 | archAndPlatform, 106 | Deno.build.os === "windows" ? "TabNine.exe" : "TabNine", 107 | ); 108 | return await fs.exists(destDir); 109 | } 110 | 111 | static async installTabNine( 112 | storageDir: string, 113 | version: string, 114 | ): Promise { 115 | const archAndPlatform = TabNine.getArchAndPlatform(); 116 | const destDir = path.join( 117 | storageDir, 118 | version, 119 | archAndPlatform, 120 | ); 121 | 122 | const url = 123 | `https://update.tabnine.com/bundles/${version}/${archAndPlatform}/TabNine.zip`; 124 | 125 | const zipPath = path.join(destDir, path.basename(url)); 126 | await fs.ensureDir(destDir); 127 | const res = await fetch(url); 128 | if (res.body) { 129 | const destFile = await Deno.open(zipPath, { 130 | write: true, 131 | create: true, 132 | }); 133 | try { 134 | await res.body.pipeTo(destFile.writable); 135 | } finally { 136 | destFile.close(); 137 | } 138 | try { 139 | if (!(await decompress(zipPath, destDir))) { 140 | throw new Error("failed to decompress a TabNine archive"); 141 | } 142 | } catch (e: unknown) { 143 | throw e; 144 | } finally { 145 | await Deno.remove(zipPath); 146 | } 147 | } 148 | if (Deno.build.os === "windows") { 149 | return; 150 | } 151 | for await (const entry of Deno.readDir(destDir)) { 152 | await Deno.chmod(path.resolve(destDir, entry.name), 0o755); 153 | } 154 | } 155 | 156 | static async cleanAllVersions(storageDir: string): Promise { 157 | const versions = await TabNine.getInstalledVersions(storageDir); 158 | await Promise.all( 159 | versions.map((ver) => TabNine.cleanVersion(storageDir, ver)), 160 | ); 161 | } 162 | 163 | static async cleanVersion( 164 | storageDir: string, 165 | version: string, 166 | ): Promise { 167 | const archAndPlatform = TabNine.getArchAndPlatform(); 168 | const destDir = path.join( 169 | storageDir, 170 | version, 171 | archAndPlatform, 172 | ); 173 | if (await fs.exists(destDir)) { 174 | await Deno.remove(destDir, { recursive: true }); 175 | } 176 | } 177 | 178 | close() { 179 | if (this.proc) { 180 | killProcess(this.proc); 181 | } 182 | } 183 | 184 | static async getLatestVersion(): Promise { 185 | const url = "https://update.tabnine.com/bundles/version"; 186 | const res = await fetch(url); 187 | if (!res.body) { 188 | throw Object.assign(new Error(`Body not found: ${url}`), { res }); 189 | } 190 | if (!res.ok) { 191 | throw Object.assign(new Error(`Response status not ok: ${url}`), { res }); 192 | } 193 | 194 | const version = await res.text(); 195 | return version; 196 | } 197 | 198 | static async getInstalledVersions(storageDir: string): Promise { 199 | const versions: string[] = []; 200 | const archAndPlatform = TabNine.getArchAndPlatform(); 201 | if (!(await fs.exists(storageDir))) return []; 202 | for await (const version of Deno.readDir(storageDir)) { 203 | if ( 204 | semver.valid(version.name) && 205 | await fs.exists( 206 | path.join( 207 | storageDir, 208 | version.name, 209 | archAndPlatform, 210 | Deno.build.os == "windows" ? "TabNine.exe" : "TabNine", 211 | ), 212 | ) 213 | ) { 214 | versions.push(version.name); 215 | } 216 | } 217 | return versions; 218 | } 219 | 220 | getRunningBinaryPath(): string | null { 221 | return this.runningBinaryPath ?? null; 222 | } 223 | 224 | static async getBinaryPath(storageDir: string): Promise { 225 | const archAndPlatform = TabNine.getArchAndPlatform(); 226 | const versions = await TabNine.getInstalledVersions(storageDir); 227 | 228 | if (!versions || versions.length == 0) { 229 | throw new Error(`TabNine not installed in ${storageDir}`); 230 | } 231 | 232 | const sortedVersions = TabNine.sortBySemver(versions); 233 | 234 | const tried: string[] = []; 235 | for (const version of sortedVersions) { 236 | const fullPath = path.join( 237 | storageDir, 238 | version, 239 | archAndPlatform, 240 | Deno.build.os == "windows" ? "TabNine.exe" : "TabNine", 241 | ); 242 | if (await fs.exists(fullPath)) { 243 | return fullPath; 244 | } else { 245 | tried.push(fullPath); 246 | } 247 | } 248 | throw new Error( 249 | `Couldn't find a TabNine binary (tried the following paths: versions=${sortedVersions} ${tried})`, 250 | ); 251 | } 252 | 253 | static getArchAndPlatform(): string { 254 | const arch = Deno.build.arch; 255 | 256 | let suffix: string; 257 | switch (Deno.build.os) { 258 | case "windows": 259 | suffix = "pc-windows-gnu"; 260 | break; 261 | case "darwin": 262 | suffix = "apple-darwin"; 263 | break; 264 | case "linux": 265 | suffix = "unknown-linux-musl"; 266 | break; 267 | default: 268 | throw new Error( 269 | `Sorry, the platform '${Deno.build.os}' is not supported by TabNine.`, 270 | ); 271 | } 272 | 273 | return `${arch}-${suffix}`; 274 | } 275 | 276 | static sortBySemver(versions: string[]): string[] { 277 | return versions.sort(TabNine.cmpSemver); 278 | } 279 | 280 | static cmpSemver(a: string, b: string): number { 281 | const aValid = semver.valid(a); 282 | const bValid = semver.valid(b); 283 | if (aValid && bValid) return semver.rcompare(a, b); 284 | else if (aValid) return -1; 285 | else if (bValid) return 1; 286 | else if (a < b) return -1; 287 | else if (a > b) return 1; 288 | else return 0; 289 | } 290 | } 291 | 292 | // https://github.com/vim-denops/denops.vim/blob/17d20561e5eb45657235e92b94b4a9c690b85900/denops/%40denops/test/tester.ts#L176-L196 293 | // Brought under the MIT License ( https://github.com/vim-denops/denops.vim/blob/17d20561e5eb45657235e92b94b4a9c690b85900/LICENSE ) from https://github.com/vim-denops/denops.vim 294 | async function killProcess(proc: Deno.ChildProcess): Promise { 295 | if (semver.rcompare(Deno.version.deno, "1.14.0") < 0) { 296 | // Prior to v1.14.0, `Deno.Signal.SIGTERM` worked on Windows as well 297 | // deno-lint-ignore no-explicit-any 298 | proc.kill((Deno as any).Signal.SIGTERM); 299 | } else if (Deno.build.os === "windows") { 300 | // Signal API in Deno v1.14.0 on Windows 301 | // does not work so use `taskkill` for now 302 | const p = new Deno.Command("taskkill", { 303 | args: ["/pid", proc.pid.toString(), "/F"], 304 | stdin: "null", 305 | stdout: "null", 306 | stderr: "null", 307 | }).spawn(); 308 | await p.status; 309 | } else { 310 | // deno-lint-ignore no-explicit-any 311 | proc.kill("SIGTERM" as any); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/tabnine/client_v2.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file camelcase 2 | import { TabNine } from "./client_base.ts"; 3 | 4 | export interface TabNineV2AutoCompleteRequest { 5 | filename: string | null; 6 | before: string; 7 | after: string; 8 | regionIncludesBeginning: boolean; 9 | regionIncludesEnd: boolean; 10 | maxNumResults: number | null; 11 | } 12 | 13 | export interface TabNineV2AutoCompleteResponseResult { 14 | /** 15 | * @example ");" 16 | */ 17 | new_prefix: string; 18 | /** 19 | * @example ")" 20 | */ 21 | old_suffix: string; 22 | new_suffix: string; 23 | kind: unknown; 24 | /** 25 | * @example "32%" 26 | */ 27 | detail: string | null; 28 | documentation: unknown; 29 | deprecated: boolean | null; 30 | } 31 | 32 | export interface TabNineV2AutoCompleteResponse { 33 | old_prefix: string; 34 | results: TabNineV2AutoCompleteResponseResult[]; 35 | user_message: unknown[]; 36 | docs: unknown[]; 37 | } 38 | 39 | export class TabNineV2 extends TabNine { 40 | static readonly apiVersion = "2.0.0"; 41 | constructor( 42 | clientName: string, 43 | storagePath: string, 44 | ) { 45 | super(clientName, storagePath); 46 | } 47 | async requestAutocomplete( 48 | request: TabNineV2AutoCompleteRequest, 49 | ): Promise { 50 | // deno-lint-ignore no-explicit-any 51 | const response: any = await super.request({ 52 | version: TabNineV2.apiVersion, 53 | request: { 54 | Autocomplete: { 55 | filename: request.filename, 56 | before: request.before, 57 | after: request.after, 58 | region_includes_beginning: request.regionIncludesBeginning, 59 | region_includes_end: request.regionIncludesEnd, 60 | max_num_results: request.maxNumResults, 61 | }, 62 | }, 63 | }); 64 | return response; 65 | } 66 | async requestConfigDir(): Promise { 67 | // deno-lint-ignore no-explicit-any 68 | const response: any = await super.request({ 69 | version: TabNineV2.apiVersion, 70 | request: { 71 | Autocomplete: { 72 | filename: null, 73 | before: "TabNine::config_dir", 74 | after: "", 75 | region_includes_beginning: true, 76 | region_includes_end: true, 77 | max_num_results: 1, 78 | }, 79 | }, 80 | }); 81 | const configDir = response?.results?.[0]?.new_prefix; 82 | if (typeof configDir === "string") return configDir; 83 | return null; 84 | } 85 | async requestVersion(): Promise { 86 | // deno-lint-ignore no-explicit-any 87 | const response: any = await super.request({ 88 | version: TabNineV2.apiVersion, 89 | request: { 90 | Autocomplete: { 91 | filename: null, 92 | before: "TabNine::version", 93 | after: "", 94 | region_includes_beginning: true, 95 | region_includes_end: true, 96 | max_num_results: 1, 97 | }, 98 | }, 99 | }); 100 | const configDir = response?.results?.[0]?.new_prefix; 101 | if (typeof configDir === "string") return configDir; 102 | return null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /denops/ddc-tabnine/the_client.ts: -------------------------------------------------------------------------------- 1 | import { Mutex } from "../ddc-tabnine/deps.ts"; 2 | import { TabNine } from "./tabnine/client_base.ts"; 3 | import { TabNineV2 } from "./tabnine/client_v2.ts"; 4 | 5 | // This class is for singleton, and not targeting to keep it pure. 6 | class ClientCreator { 7 | private client?: TabNineV2; 8 | private clientCloser?: () => void; 9 | private mutex = new Mutex(); 10 | private clientStorageDir?: string; 11 | 12 | private recreateClient(storageDir: string): TabNineV2 { 13 | this.clientCloser?.(); 14 | this.client = undefined; 15 | this.clientCloser = undefined; 16 | this.clientStorageDir = storageDir; 17 | const newClient = this.client = new TabNineV2( 18 | "ddc.vim", 19 | storageDir, 20 | ); 21 | this.clientCloser = () => newClient.close(); 22 | return this.client; 23 | } 24 | 25 | async reinstall(storageDir: string): Promise { 26 | const release = await this.mutex.acquire(); 27 | try { 28 | await TabNine.cleanAllVersions(storageDir); 29 | await TabNine.installTabNine( 30 | storageDir, 31 | await TabNineV2.getLatestVersion(), 32 | ); 33 | } finally { 34 | release(); 35 | } 36 | } 37 | 38 | async getClient( 39 | storageDir: string, 40 | autoInstall: boolean, 41 | ): Promise { 42 | const release = await this.mutex.acquire(); 43 | try { 44 | return await this.getClientUnlocked(storageDir, autoInstall); 45 | } finally { 46 | release(); 47 | } 48 | } 49 | 50 | async getClientUnlocked( 51 | storageDir: string, 52 | disabledAutoInstall: boolean, 53 | ): Promise { 54 | if (!this.client || storageDir !== this.clientStorageDir) { 55 | if (this.clientStorageDir && storageDir !== this.clientStorageDir) { 56 | console.log("[ddc-tabnine] Storage dir is updated. Restarting..."); 57 | } 58 | const client = this.recreateClient(storageDir); 59 | const version = await TabNineV2.getLatestVersion(); 60 | if (!(await client.isInstalled(version))) { 61 | if (disabledAutoInstall) { 62 | throw new Error( 63 | `binary is not installed at ${client.storageDir} in spite of disabled auto-install`, 64 | ); 65 | } 66 | console.log( 67 | `[ddc-tabnine] Installing TabNine cli version ${version}...`, 68 | ); 69 | try { 70 | await TabNine.installTabNine(client.storageDir, version); 71 | } catch (e: unknown) { 72 | try { 73 | console.error( 74 | `[ddc-tabnine] Failed to install TabNine cli version ${version}.`, 75 | ); 76 | } finally { 77 | await TabNine.cleanVersion(client.storageDir, version); 78 | } 79 | throw e; 80 | } 81 | } 82 | await client.restartProc(); 83 | return client; 84 | } 85 | return this.client; 86 | } 87 | } 88 | 89 | // Singleton client. To make this a real singleton, use the client 90 | // from other denops plugins only via denops.dispatch(). 91 | const theClient = new ClientCreator(); 92 | export default theClient; 93 | -------------------------------------------------------------------------------- /doc/ddc-tabnine.txt: -------------------------------------------------------------------------------- 1 | *ddc-tabnine.txt* TabNine Completion for ddc.vim 2 | 3 | Author : Luma 4 | License: MIT 5 | 6 | 7 | ============================================================================== 8 | CONTENTS *ddc-tabnine-contents* 9 | 10 | INTRODUCTION |ddc-tabnine-introduction| 11 | INSTALL |ddc-tabnine-install| 12 | EXAMPLES |ddc-tabnine-examples| 13 | PARAMS |ddc-tabnine-params| 14 | SETTINGS |ddc-tabnine-settings| 15 | FUNCTIONS |ddc-tabnine-functions| 16 | 17 | ============================================================================== 18 | INTRODUCTION *ddc-tabnine-introduction* 19 | 20 | This source collects candidates from TabNine (https://www.tabnine.com). 21 | TabNine binaries are automatically installed when first time completion 22 | starts. 23 | 24 | ============================================================================== 25 | INSTALL *ddc-tabnine-install* 26 | 27 | Please install both "ddc.vim" and "denops.vim". 28 | 29 | https://github.com/Shougo/ddc.vim 30 | https://github.com/vim-denops/denops.vim 31 | 32 | You can trigger the 33 | special commands (https://www.tabnine.com/faq#special_commands) to configure 34 | your TabNine like `TabNine::config` in any buffer. 35 | 36 | (Optional) To configure your purchased API key, use `TabNine::config` or 37 | `:exe 'e' ddc_tabnine#config_dir() . '/tabnine_config.json'`. 38 | 39 | See also |ddc_tabnine#config_dir()|. 40 | 41 | ============================================================================== 42 | EXAMPLES *ddc-tabnine-examples* 43 | > 44 | call ddc#custom#patch_global('sources', ['tabnine']) 45 | call ddc#custom#patch_global('sourceOptions', { 46 | \ 'tabnine': { 47 | \ 'mark': 'TN', 48 | \ 'maxCandidates': 5, 49 | \ 'isVolatile': v:true, 50 | \ }}) 51 | call ddc#custom#patch_global('sourceParams', { 52 | \ 'tabnine': { 53 | \ 'maxNumResults': 10, 54 | \ 'storageDir': expand('~/.cache/....'), 55 | \ }}) 56 | < 57 | 58 | ============================================================================== 59 | PARAMS *ddc-tabnine-params* 60 | 61 | *ddc-tabnine-param-maxSize* 62 | maxSize (number) 63 | Fixed range of looking for words lines above and below your 64 | cursor position. 65 | 66 | Default: 200 67 | 68 | *ddc-tabnine-param-maxNumResults* 69 | maxNumResults (number) 70 | Max number of results to retrieve. This is natively 71 | implemented by TabNine and run before filtering compared to 72 | |ddc-source-option-maxCandidates|. 73 | 74 | Default: 5 75 | 76 | ============================================================================== 77 | SETTINGS *ddc-tabnine-settings* 78 | 79 | *ddc_tabnine#storage_dir* 80 | ddc_tabnine#storage_dir (string) 81 | Storage path placement to store binary files. 82 | ///TabNine(.exe) will be used for 83 | execution. If not present, binary will be installed 84 | automatically. 85 | 86 | Default: /ddc-tabnine/binaries 87 | 88 | *ddc_tabnine#disable_auto_install* 89 | ddc_tabnine#disable_auto_install (boolean) 90 | Set |v:true| to disable automatic installation. You need to 91 | ensure the binary existence. 92 | 93 | Default: |v:false| 94 | 95 | ============================================================================== 96 | FUNCTIONS *ddc-tabnine-functions* 97 | 98 | *ddc_tabnine#restart()* 99 | ddc_tabnine#restart() 100 | Restart the TabNine process. 101 | 102 | *ddc_tabnine#reinstall()* 103 | ddc_tabnine#reinstall() 104 | Clean all version binaries for current architecture, reinstall 105 | binaries and restart the process. 106 | 107 | *ddc_tabnine#is_running()* 108 | ddc_tabnine#is_running() 109 | Returns boolean value whether Process is running. 110 | 111 | *ddc_tabnine#which()* 112 | ddc_tabnine#which() 113 | Returns string value which binary is used to run. 114 | 115 | *ddc_tabnine#version()* 116 | ddc_tabnine#version() 117 | Returns string value by querying TabNine::version to TabNine 118 | process. 119 | 120 | *ddc_tabnine#config_dir()* 121 | ddc_tabnine#config_dir() 122 | Returns string value by querying TabNine::config_dir to TabNine 123 | process. 124 | Example: > 125 | command! -bar EditTabNineConfig 126 | \ execute 'edit' ddc_tabnine#config_dir() . '/tabnine_config.json' 127 | < 128 | 129 | ============================================================================== 130 | vim:tw=78:ts=8:noet:ft=help:norl 131 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import "./denops/@ddc-sources/tabnine.ts"; 2 | import "./denops/ddc-tabnine/main.ts"; 3 | --------------------------------------------------------------------------------