├── .prettierrc.json ├── small_logo.png ├── assets ├── ext-install.png ├── ext-output.png ├── ext-working.png ├── set-configs.png └── set-api-token.png ├── huggingface-vscode-0.0.1.vsix ├── .prettierignore ├── src ├── binary │ └── requests │ │ ├── openUrl.ts │ │ ├── statusBar.ts │ │ ├── tabSize.ts │ │ └── requests.ts ├── declarations.d.ts ├── events │ └── onPluginInstalledEmitter.ts ├── utils │ ├── time.utils.ts │ ├── retry.ts │ ├── rotate.ts │ ├── semver.utils.ts │ ├── file.utils.ts │ └── utils.ts ├── inlineSuggestions │ ├── clearDecoration.ts │ ├── documentChangesTracker │ │ ├── pythonIndentExtensionFix.ts │ │ ├── index.ts │ │ └── DocumentTextChangeContent.ts │ ├── snippets │ │ ├── autoTriggerHandler.ts │ │ ├── snippetProvider.ts │ │ ├── snippetDecoration.ts │ │ ├── acceptSnippetSuggestion.ts │ │ └── blankSnippet.ts │ ├── vimForVSCodeWorkaround.ts │ ├── tabnineInlineCompletionItem.ts │ ├── positionExtracter.ts │ ├── inlineSuggestionState.ts │ ├── acceptInlineSuggestion.ts │ ├── highlightStackAttributions.ts │ ├── textListener.ts │ ├── setInlineSuggestion.ts │ └── registerHandlers.ts ├── statusBar │ ├── StatusBarPromotionItem.ts │ ├── StatusBarData.ts │ ├── statusBarActionHandler.ts │ └── statusBar.ts ├── capabilities │ ├── getSuggestionMode.ts │ └── capabilities.ts ├── CompletionArguments.ts ├── treeView │ ├── TabnineTreeItem.ts │ ├── TabnineTreeProvider.ts │ └── registerTreeView.ts ├── globals │ ├── tabnineExtensionContext.ts │ ├── versions.ts │ ├── languages.ts │ ├── proposedAPI.ts │ ├── tabnineExtensionProperties.ts │ └── consts.ts ├── handlePluginInstalled.ts ├── vscode.api.ts ├── outputChannels.ts ├── getAutoImportCommand.ts ├── commandsHandler.ts ├── findImports.ts ├── getInlineCompletionItems.ts ├── extension.ts ├── provideInlineCompletionItems.ts ├── handleUninstall.ts ├── selectionHandler.ts ├── autocompleteInstaller.ts ├── runCompletion.ts ├── vscode.proposed.inlineCompletions.d.ts └── provideCompletionItems.ts ├── .vscodeignore ├── .eslintignore ├── .gitignore ├── jest.config.js ├── tsconfig.json ├── LICENSE ├── .vscode └── launch.json ├── .eslintrc.js ├── webpack.config.js ├── README.md └── package.json /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/small_logo.png -------------------------------------------------------------------------------- /assets/ext-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/assets/ext-install.png -------------------------------------------------------------------------------- /assets/ext-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/assets/ext-output.png -------------------------------------------------------------------------------- /assets/ext-working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/assets/ext-working.png -------------------------------------------------------------------------------- /assets/set-configs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/assets/set-configs.png -------------------------------------------------------------------------------- /assets/set-api-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/assets/set-api-token.png -------------------------------------------------------------------------------- /huggingface-vscode-0.0.1.vsix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infomaniak/huggingface-vscode/master/huggingface-vscode-0.0.1.vsix -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | out 5 | assets 6 | binaries 7 | node_modules 8 | .vscode 9 | .github -------------------------------------------------------------------------------- /src/binary/requests/openUrl.ts: -------------------------------------------------------------------------------- 1 | function openUrl(url: string): null { 2 | console.log(url); 3 | return null; 4 | } 5 | 6 | export default openUrl; 7 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !out/ 3 | !out/extension.js 4 | !small_logo.png 5 | !package.json 6 | !logo.svg 7 | !logo-dark.svg 8 | !logo-light.svg 9 | !LICENSE 10 | !README.md 11 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | declare namespace Chai { 3 | export interface LanguageChains { 4 | shallowDeepEqual: (a: unknown) => void; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | out 6 | # don't lint nyc coverage output 7 | coverage 8 | .eslintrc.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/out 3 | dist 4 | 5 | .vscode-test/ 6 | **.DS_Store 7 | binaries/* 8 | 9 | registration_key 10 | auth 11 | sig 12 | 13 | 14 | .idea/ 15 | *.iml 16 | 17 | **/assistant-cache 18 | **/assistant-binaries 19 | -------------------------------------------------------------------------------- /src/events/onPluginInstalledEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "vscode"; 2 | 3 | const onPluginInstalledEmitter = new EventEmitter(); 4 | 5 | // eslint-disable-next-line import/prefer-default-export 6 | export { onPluginInstalledEmitter }; 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.ts?$": "ts-jest", 4 | }, 5 | testRegex: "(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$", 6 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 7 | testEnvironment: "node", 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/time.utils.ts: -------------------------------------------------------------------------------- 1 | export const SECOND_IN_MS = 1000; 2 | export const MINUTE_IN_MS = 60 * SECOND_IN_MS; 3 | const HOUR_IN_MS = 60 * MINUTE_IN_MS; 4 | 5 | export default function isInTheLastHour(date: Date): boolean { 6 | return Date.now() - date.getTime() < HOUR_IN_MS; 7 | } 8 | -------------------------------------------------------------------------------- /src/inlineSuggestions/clearDecoration.ts: -------------------------------------------------------------------------------- 1 | import { clearState } from "./inlineSuggestionState"; 2 | import { clearInlineDecoration } from "./setInlineSuggestion"; 3 | 4 | export default async function clearInlineSuggestionsState(): Promise { 5 | await clearInlineDecoration(); 6 | await clearState(); 7 | } 8 | -------------------------------------------------------------------------------- /src/statusBar/StatusBarPromotionItem.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarItem } from "vscode"; 2 | 3 | export default class StatusBarPromotionItem { 4 | id: string | undefined; 5 | 6 | item: StatusBarItem; 7 | 8 | constructor(item: StatusBarItem, id?: string) { 9 | this.id = id; 10 | this.item = item; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/binary/requests/statusBar.ts: -------------------------------------------------------------------------------- 1 | import { MessageAction } from "../../globals/consts"; 2 | 3 | export type StatusBarStatus = { 4 | id: string; 5 | message: string; 6 | title: string | undefined; 7 | actions: MessageAction[]; 8 | notification_type: unknown; 9 | duration_seconds?: number; 10 | state: unknown; 11 | }; 12 | -------------------------------------------------------------------------------- /src/binary/requests/tabSize.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | 3 | export default function getTabSize(): number { 4 | const tabSize = window.activeTextEditor?.options.tabSize; 5 | if (typeof tabSize !== "number") { 6 | return 4; 7 | } 8 | return tabSize; 9 | } 10 | export function getTabsCount(): number { 11 | return getTabSize() / 4; 12 | } 13 | -------------------------------------------------------------------------------- /src/capabilities/getSuggestionMode.ts: -------------------------------------------------------------------------------- 1 | import { Capability, isCapabilityEnabled } from "./capabilities"; 2 | 3 | export enum SuggestionsMode { 4 | INLINE, 5 | AUTOCOMPLETE, 6 | } 7 | 8 | export default function getSuggestionMode(): SuggestionsMode { 9 | if (isCapabilityEnabled(Capability.INLINE_SUGGESTIONS)) { 10 | return SuggestionsMode.INLINE; 11 | } 12 | return SuggestionsMode.AUTOCOMPLETE; 13 | } 14 | -------------------------------------------------------------------------------- /src/CompletionArguments.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "vscode"; 2 | import { ResultEntry } from "./binary/requests/requests"; 3 | import { SuggestionTrigger } from "./globals/consts"; 4 | 5 | export type CompletionArguments = { 6 | currentCompletion: string; 7 | completions: ResultEntry[]; 8 | position: Position; 9 | limited: boolean; 10 | oldPrefix?: string; 11 | suggestionTrigger?: SuggestionTrigger; 12 | }; 13 | -------------------------------------------------------------------------------- /src/treeView/TabnineTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { Command, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | 3 | export default class TabnineTreeItem extends TreeItem { 4 | constructor( 5 | public readonly label: string, 6 | public readonly command?: Command 7 | ) { 8 | super(label, TreeItemCollapsibleState.None); 9 | 10 | this.tooltip = `${this.label}`; 11 | } 12 | 13 | contextValue = "tabnine"; 14 | } 15 | -------------------------------------------------------------------------------- /src/globals/tabnineExtensionContext.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | let tabnineExtensionContext: vscode.ExtensionContext | null = null; 4 | 5 | export function setTabnineExtensionContext( 6 | context: vscode.ExtensionContext 7 | ): void { 8 | tabnineExtensionContext = context; 9 | } 10 | 11 | export function getTabnineExtensionContext(): vscode.ExtensionContext | null { 12 | return tabnineExtensionContext; 13 | } 14 | -------------------------------------------------------------------------------- /src/handlePluginInstalled.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, Memento } from "vscode"; 2 | import { onPluginInstalledEmitter } from "./events/onPluginInstalledEmitter"; 3 | 4 | type ExtensionContext = { globalState: Memento }; 5 | 6 | export default function handlePluginInstalled( 7 | _context: ExtensionContext 8 | ): Disposable { 9 | return onPluginInstalledEmitter.event(() => { 10 | console.log("todo open webview"); 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | export default async function retry( 2 | func: () => Promise, 3 | isFulfilled: (arg: R) => boolean, 4 | attempts = 1, 5 | attempt = 1 6 | ): Promise { 7 | const result = await func(); 8 | 9 | if (attempt >= attempts) { 10 | return result; 11 | } 12 | 13 | if (!isFulfilled(result)) { 14 | return retry(func, isFulfilled, attempts, attempt + 1); 15 | } 16 | 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /src/vscode.api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DecorationOptions, 3 | Range, 4 | TextEditor, 5 | TextEditorDecorationType, 6 | window, 7 | } from "vscode"; 8 | 9 | // eslint-disable-next-line import/prefer-default-export 10 | export const setDecoration = ( 11 | decorationType: TextEditorDecorationType, 12 | rangesOrOptions: Range[] | DecorationOptions[], 13 | editor?: TextEditor 14 | ): void => 15 | (editor ?? window.activeTextEditor)?.setDecorations( 16 | decorationType, 17 | rangesOrOptions 18 | ); 19 | -------------------------------------------------------------------------------- /src/utils/rotate.ts: -------------------------------------------------------------------------------- 1 | export type Iterator = { 2 | next: () => number; 3 | prev: () => number; 4 | current: () => number; 5 | }; 6 | 7 | export function rotate(value: number): Iterator { 8 | let current = 0; 9 | return { 10 | next() { 11 | current += 1; 12 | if (current > value) { 13 | current = 0; 14 | } 15 | return current; 16 | }, 17 | prev() { 18 | current -= 1; 19 | if (current < 0) { 20 | current = value; 21 | } 22 | return current; 23 | }, 24 | current() { 25 | return current; 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/outputChannels.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { OUTPUT_CHANNEL_NAME } from "./globals/consts"; 3 | 4 | const outputChannel = window.createOutputChannel(OUTPUT_CHANNEL_NAME); 5 | 6 | export function logInput(txt: string, parameters = {}){ 7 | outputChannel.append(`INPUT to API: (with parameters ${JSON.stringify(parameters)}) \n`) 8 | outputChannel.append(txt); 9 | outputChannel.append("\n") 10 | } 11 | 12 | export function logOutput(txt: string){ 13 | outputChannel.append("OUTPUT from API:\n") 14 | outputChannel.append(txt); 15 | outputChannel.append("\n\n"); 16 | } 17 | 18 | export default outputChannel; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "outDir": "out", 6 | "lib": ["es2018"], 7 | "sourceMap": true, 8 | "strict": true, 9 | "alwaysStrict": true, 10 | "strictNullChecks": true, 11 | "strictPropertyInitialization": true, 12 | "strictBindCallApply": true, 13 | "strictFunctionTypes": true, 14 | "noImplicitReturns": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "exclude": ["node_modules", "src/test/fixture"] 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/semver.utils.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | 3 | const FIRST = -1; 4 | const EQUAL = 0; 5 | const SECOND = 1; 6 | 7 | export default function sortBySemver(versions: string[]): string[] { 8 | versions.sort(cmpSemver); 9 | 10 | return versions; 11 | } 12 | 13 | function cmpSemver(a: string, b: string): number { 14 | const aValid = semver.valid(a); 15 | const bValid = semver.valid(b); 16 | 17 | if (aValid && bValid) { 18 | return semver.rcompare(a, b); 19 | } 20 | if (aValid) { 21 | return FIRST; 22 | } 23 | if (bValid) { 24 | return SECOND; 25 | } 26 | if (a < b) { 27 | return FIRST; 28 | } 29 | if (a > b) { 30 | return SECOND; 31 | } 32 | return EQUAL; 33 | } 34 | -------------------------------------------------------------------------------- /src/inlineSuggestions/documentChangesTracker/pythonIndentExtensionFix.ts: -------------------------------------------------------------------------------- 1 | import { commands, extensions } from "vscode"; 2 | import DocumentTextChangeContent from "./DocumentTextChangeContent"; 3 | 4 | const PYTHON_INDENT_EXTENSION_ID = "KevinRose.vsc-python-indent"; 5 | 6 | function isPythonIndentExtensionEnabled() { 7 | return extensions.all.find((x) => x.id.includes(PYTHON_INDENT_EXTENSION_ID)) 8 | ?.isActive; 9 | } 10 | export default function tryApplyPythonIndentExtensionFix( 11 | contentChange: DocumentTextChangeContent 12 | ): void { 13 | if ( 14 | contentChange.isPythonNewLineChange() && 15 | isPythonIndentExtensionEnabled() 16 | ) { 17 | void commands.executeCommand("editor.action.inlineSuggest.trigger"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/inlineSuggestions/snippets/autoTriggerHandler.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from "os"; 2 | import { TextDocumentChangeEvent } from "vscode"; 3 | import getCurrentPosition from "../positionExtracter"; 4 | import { isInSnippetInsertion } from "./blankSnippet"; 5 | import requestSnippet from "./snippetProvider"; 6 | 7 | export default async function snippetAutoTriggerHandler({ 8 | document, 9 | contentChanges, 10 | }: TextDocumentChangeEvent): Promise { 11 | const [change] = contentChanges; 12 | if (!change) { 13 | return; 14 | } 15 | const position = getCurrentPosition(change); 16 | const hasNewlines = change.text.includes(EOL); 17 | const currentLineIsEmpty = document.lineAt(position.line).text.trim() === ""; 18 | 19 | if (!isInSnippetInsertion() && hasNewlines && currentLineIsEmpty) { 20 | await requestSnippet(document, position); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/inlineSuggestions/vimForVSCodeWorkaround.ts: -------------------------------------------------------------------------------- 1 | import { commands, extensions } from "vscode"; 2 | import { ResultEntry } from "../binary/requests/requests"; 3 | 4 | const VIM_FOR_VSCODE_EXTENSION = "vscodevim.vim"; 5 | export function vimActive(): boolean { 6 | return !!extensions.getExtension(VIM_FOR_VSCODE_EXTENSION)?.isActive; 7 | } 8 | 9 | export async function vimReturnToInsertMode({ 10 | new_prefix, 11 | new_suffix, 12 | }: ResultEntry): Promise { 13 | await commands.executeCommand("extension.vim_escape"); 14 | await commands.executeCommand("extension.vim_insert"); 15 | const suggestionString = new_prefix + new_suffix; 16 | vimMoveCursorRight(suggestionString.length); 17 | } 18 | 19 | function vimMoveCursorRight(steps: number): void { 20 | for (let i = 0; i < steps; i += 1) { 21 | void commands.executeCommand("extension.vim_right"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/globals/versions.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | import tabnineExtensionProperties from "./tabnineExtensionProperties"; 3 | 4 | const AUTHENTICATION_API_VERSION = "1.54.0"; 5 | const INLINE_API_PROPOSED_VERSION = "1.58.0"; 6 | const INLINE_API_RELEASE_VERSION = "1.68.0"; 7 | 8 | export default function isAuthenticationApiSupported(): boolean { 9 | return semver.gte( 10 | tabnineExtensionProperties.vscodeVersion, 11 | AUTHENTICATION_API_VERSION 12 | ); 13 | } 14 | 15 | export function isInlineSuggestionProposedApiSupported(): boolean { 16 | return semver.gte( 17 | tabnineExtensionProperties.vscodeVersion, 18 | INLINE_API_PROPOSED_VERSION 19 | ); 20 | } 21 | export function isInlineSuggestionReleasedApiSupported(): boolean { 22 | return semver.gte( 23 | tabnineExtensionProperties.vscodeVersion, 24 | INLINE_API_RELEASE_VERSION 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/inlineSuggestions/snippets/snippetProvider.ts: -------------------------------------------------------------------------------- 1 | import { Position, TextDocument, window } from "vscode"; 2 | import { 3 | getCurrentSuggestion, 4 | setSuggestionsState, 5 | } from "../inlineSuggestionState"; 6 | import runCompletion from "../../runCompletion"; 7 | import setInlineSuggestion from "../setInlineSuggestion"; 8 | 9 | export default async function requestSnippet( 10 | document: TextDocument, 11 | position: Position 12 | ): Promise { 13 | const autocompleteResult = await runCompletion(document, position); 14 | 15 | const currentUri = window.activeTextEditor?.document.uri; 16 | if (currentUri !== document.uri) { 17 | return; 18 | } 19 | 20 | await setSuggestionsState(autocompleteResult); 21 | const currentSuggestion = getCurrentSuggestion(); 22 | if (currentSuggestion) { 23 | await setInlineSuggestion(document, position, currentSuggestion); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/treeView/TabnineTreeProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { Event, ProviderResult, TreeDataProvider, TreeItem } from "vscode"; 3 | import { 4 | BIGCODE_OPEN_WEB_COMMAND, 5 | } from "../globals/consts"; 6 | import TabnineTreeItem from "./TabnineTreeItem"; 7 | 8 | export default class TabnineTreeProvider 9 | implements TreeDataProvider { 10 | onDidChangeTreeData?: 11 | | Event 12 | | undefined; 13 | 14 | getTreeItem(element: TabnineTreeItem): TreeItem { 15 | return element as TreeItem; 16 | } 17 | 18 | getChildren(): ProviderResult { 19 | return [ 20 | new TabnineTreeItem("Read about BigCode project", { 21 | title: "Read about BigCode project", 22 | command: BIGCODE_OPEN_WEB_COMMAND, 23 | arguments: [], 24 | }), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/treeView/registerTreeView.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext, window, env, Uri } from "vscode"; 2 | import { 3 | BIGCODE_PROJECT_URL, 4 | BIGCODE_OPEN_WEB_COMMAND, 5 | } from "../globals/consts"; 6 | import TabnineTreeProvider from "./TabnineTreeProvider"; 7 | 8 | export default function registerTreeView(context: ExtensionContext): void { 9 | try { 10 | context.subscriptions.push( 11 | window.registerTreeDataProvider( 12 | "tabnine-home", 13 | new TabnineTreeProvider() 14 | ), 15 | commands.registerCommand(BIGCODE_OPEN_WEB_COMMAND, () => { 16 | void env.openExternal(Uri.parse(BIGCODE_PROJECT_URL)); 17 | }), 18 | ); 19 | void commands.executeCommand( 20 | "setContext", 21 | "tabnine.tabnine-navigation-ready", 22 | true 23 | ); 24 | } catch (e) { 25 | console.error("Error in registerTreeView", e); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/getAutoImportCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AutocompleteResult, ResultEntry } from "./binary/requests/requests"; 3 | import { SuggestionTrigger } from "./globals/consts"; 4 | import { COMPLETION_IMPORTS } from "./selectionHandler"; 5 | 6 | export default function getAutoImportCommand( 7 | result: ResultEntry, 8 | response: AutocompleteResult | undefined, 9 | position: vscode.Position, 10 | suggestionTrigger?: SuggestionTrigger 11 | ): vscode.Command { 12 | return { 13 | arguments: [ 14 | { 15 | currentCompletion: result.new_prefix, 16 | completions: response?.results, 17 | position, 18 | limited: response?.is_locked, 19 | snippetContext: result.completion_metadata?.snippet_context, 20 | oldPrefix: response?.old_prefix, 21 | suggestionTrigger, 22 | }, 23 | ], 24 | command: COMPLETION_IMPORTS, 25 | title: "accept completion", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/inlineSuggestions/tabnineInlineCompletionItem.ts: -------------------------------------------------------------------------------- 1 | import { Command, InlineCompletionItem, Range } from "vscode"; 2 | import { 3 | CompletionKind, 4 | ResultEntry, 5 | SnippetContext, 6 | } from "../binary/requests/requests"; 7 | 8 | export default class TabnineInlineCompletionItem extends InlineCompletionItem { 9 | isCached?: boolean; 10 | 11 | suggestionEntry: ResultEntry; 12 | 13 | completionKind?: CompletionKind; 14 | 15 | snippetContext?: SnippetContext; 16 | 17 | insertText?: string; 18 | 19 | constructor( 20 | text: string, 21 | suggestionEntry: ResultEntry, 22 | range?: Range, 23 | command?: Command, 24 | completionKind?: CompletionKind, 25 | isCached?: boolean, 26 | snippetContext?: SnippetContext 27 | ) { 28 | super(text, range, command); 29 | this.isCached = isCached; 30 | this.suggestionEntry = suggestionEntry; 31 | this.completionKind = completionKind; 32 | this.snippetContext = snippetContext; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tabnine 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 | -------------------------------------------------------------------------------- /src/commandsHandler.ts: -------------------------------------------------------------------------------- 1 | import { window, commands, ExtensionContext } from "vscode"; 2 | import { TABNINE_HOME_FOCUS_COMMAND } from "./globals/consts"; 3 | import { getTabnineExtensionContext } from "./globals/tabnineExtensionContext"; 4 | 5 | export const SET_API_TOKEN_COMMAND = "HuggingFaceCode::setApiToken"; 6 | export const STATUS_BAR_COMMAND = "TabNine.statusBar"; 7 | 8 | export function registerCommands( 9 | context: ExtensionContext 10 | ): void { 11 | context.subscriptions.push( 12 | commands.registerCommand(SET_API_TOKEN_COMMAND, setApiToken) 13 | ); 14 | context.subscriptions.push( 15 | commands.registerCommand(STATUS_BAR_COMMAND, handleStatusBar()) 16 | ); 17 | } 18 | 19 | function handleStatusBar() { 20 | return (): void => { 21 | void commands.executeCommand(TABNINE_HOME_FOCUS_COMMAND); 22 | }; 23 | } 24 | 25 | async function setApiToken () { 26 | const context = getTabnineExtensionContext(); 27 | const input = await window.showInputBox({ 28 | prompt: 'Please enter your API token (find yours at hf.co/settings/token):', 29 | placeHolder: 'Your token goes here ...' 30 | }); 31 | if (input !== undefined) { 32 | await context?.secrets.store('apiToken', input); 33 | window.showInformationMessage(`Hugging Face Code: API Token was successfully saved`); 34 | } 35 | }; -------------------------------------------------------------------------------- /src/globals/languages.ts: -------------------------------------------------------------------------------- 1 | const languages = { 2 | abap: ".abap", 3 | bat: ".bat", 4 | bibtex: ".bib", 5 | c: ".c", 6 | clojure: ".clj", 7 | coffeescript: ".coffee", 8 | cpp: ".cpp", 9 | csharp: ".cs", 10 | css: ".css", 11 | "cuda-cpp": ".cu", 12 | dart: ".dart", 13 | diff: ".diff", 14 | dockerfile: ".dockerfile", 15 | fsharp: ".fs", 16 | go: ".go", 17 | groovy: ".groovy", 18 | haml: ".haml", 19 | handlebars: ".handlebars", 20 | hlsl: ".hlsl", 21 | html: ".html", 22 | ini: ".ini", 23 | jade: ".jade", 24 | java: ".java", 25 | javascript: ".js", 26 | javascriptreact: ".jsx", 27 | json: ".json", 28 | julia: ".jl", 29 | latex: ".tex", 30 | less: ".less", 31 | lua: ".lua", 32 | makefile: ".make", 33 | markdown: ".md", 34 | "objective-c": ".m", 35 | "objective-cpp": ".mm", 36 | perl: ".pl", 37 | perl6: ".6pl", 38 | php: ".php", 39 | plaintext: ".txt", 40 | powershell: ".ps1", 41 | pug: ".pug", 42 | python: ".py", 43 | r: ".r", 44 | razor: ".cshtml", 45 | ruby: ".rb", 46 | rust: ".rs", 47 | sass: ".sass", 48 | scss: ".scss", 49 | shaderlab: ".shader", 50 | shellscript: ".sh", 51 | slim: ".slim", 52 | sql: ".sql", 53 | stylus: ".styl", 54 | swift: ".swift", 55 | tex: ".tex", 56 | typescript: ".ts", 57 | typescriptreact: ".tsx", 58 | vb: ".vb", 59 | vue: ".vue", 60 | xml: ".xml", 61 | xsl: ".xsl", 62 | yaml: ".yaml", 63 | }; 64 | 65 | export default languages; 66 | -------------------------------------------------------------------------------- /src/statusBar/StatusBarData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { ExtensionContext, StatusBarItem } from "vscode"; 3 | import { Capability, isCapabilityEnabled } from "../capabilities/capabilities"; 4 | import { 5 | FULL_BRAND_REPRESENTATION, 6 | STATUS_BAR_FIRST_TIME_CLICKED, 7 | } from "../globals/consts"; 8 | 9 | export default class StatusBarData { 10 | private _icon?: string; 11 | 12 | private _text?: string; 13 | 14 | constructor( 15 | private _statusBarItem: StatusBarItem, 16 | private _context: ExtensionContext 17 | ) {} 18 | 19 | public set icon(icon: string | undefined | null) { 20 | this._icon = icon || undefined; 21 | this.updateStatusBar(); 22 | } 23 | 24 | public get icon(): string | undefined | null { 25 | return this._icon; 26 | } 27 | 28 | public set text(text: string | undefined | null) { 29 | this._text = text || undefined; 30 | this.updateStatusBar(); 31 | } 32 | 33 | public get text(): string | undefined | null { 34 | return this._text; 35 | } 36 | 37 | private updateStatusBar() { 38 | this._statusBarItem.text = `${this._text??""} ${this._icon??""}`; 39 | this._statusBarItem.tooltip = 40 | isCapabilityEnabled(Capability.SHOW_AGRESSIVE_STATUS_BAR_UNTIL_CLICKED) && 41 | !this._context.globalState.get(STATUS_BAR_FIRST_TIME_CLICKED) 42 | ? "Click 'tabnine' for settings and more information" 43 | : `${FULL_BRAND_REPRESENTATION} (Click to open settings)`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/findImports.ts: -------------------------------------------------------------------------------- 1 | import { CodeAction } from "vscode"; 2 | 3 | const importStatement = /Import ([\S]*) from module [\S]*/; 4 | const existingImportStatement = /Add ([\S]*) to existing import declaration from [\S]*/; 5 | const importDefaultStatement = /Import default ([\S]*) from module [\S]*/; 6 | const existingDefaultImportStatement = /Add default import ([\S]*) to existing import declaration from [\S]*/; 7 | const importStatements = [ 8 | importStatement, 9 | existingImportStatement, 10 | importDefaultStatement, 11 | existingDefaultImportStatement, 12 | ]; 13 | 14 | /* 15 | filter imports with same name from different modules 16 | for example if there are multiple modules with same exported name: 17 | Import {foo} from './a' and Import {foo} from './b' 18 | in this case we will ignore and not auto import it 19 | */ 20 | export default function findImports( 21 | codeActionCommands: CodeAction[] = [] 22 | ): CodeAction[] { 23 | const importCommands = codeActionCommands.filter(({ title }) => 24 | importStatements.some((statement) => statement.test(title)) 25 | ); 26 | 27 | const importNames = importCommands.map(getImportName); 28 | 29 | return importCommands.filter( 30 | (command) => 31 | importNames.filter((name) => name === getImportName(command)).length <= 1 32 | ); 33 | } 34 | 35 | function getImportName({ 36 | title, 37 | }: { 38 | title: string; 39 | }): string | undefined | null { 40 | const statement = importStatements.map((s) => s.exec(title)).find(Boolean); 41 | 42 | return statement && statement[1]; 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/file.utils.ts: -------------------------------------------------------------------------------- 1 | import * as tmp from "tmp"; 2 | import { 3 | watch as fsWatch, 4 | promises as fs, 5 | MakeDirectoryOptions, 6 | PathLike, 7 | FSWatcher, 8 | } from "fs"; 9 | 10 | import { join } from "path"; 11 | 12 | export default function createTempFileWithPostfix( 13 | postfix: string 14 | ): Promise { 15 | return new Promise((resolve, reject) => { 16 | tmp.file({ postfix }, (err, path, fd, cleanupCallback) => { 17 | if (err) { 18 | return reject(err); 19 | } 20 | return resolve({ 21 | name: path, 22 | fd, 23 | removeCallback: cleanupCallback, 24 | }); 25 | }); 26 | }); 27 | } 28 | export async function asyncExists(path: string): Promise { 29 | try { 30 | await fs.access(path); 31 | return true; 32 | } catch { 33 | return false; 34 | } 35 | } 36 | 37 | export async function ensureExists( 38 | path: string, 39 | options: MakeDirectoryOptions = { recursive: true } 40 | ): Promise { 41 | if (!(await asyncExists(path))) await fs.mkdir(path, options); 42 | } 43 | 44 | export function watch( 45 | path: PathLike, 46 | listener: (event: string, filename: string) => void 47 | ): FSWatcher { 48 | return fsWatch(path, (event, filename) => { 49 | if (event === "rename") { 50 | void asyncExists(join(path.toString(), filename)).then((exists) => 51 | listener(exists ? "created" : "rename", filename) 52 | ); 53 | } else { 54 | listener(event, filename); 55 | } 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "code", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceRoot}", 12 | "--logPluginHostCommunication", 13 | "--disable-extensions" 14 | ], 15 | "stopOnEntry": false, 16 | "sourceMaps": true, 17 | "outDir": "${workspaceRoot}/out/src", 18 | "preLaunchTask": "npm: build" 19 | }, 20 | { 21 | "name": "Launch Extension with vscode from source", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${workspaceFolder:vscode}/scripts/code.sh", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceRoot}", 27 | "--logPluginHostCommunication" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outDir": "${workspaceRoot}/out/src", 32 | "preLaunchTask": "npm: build" 33 | },{ 34 | "name": "Extension Tests", 35 | "type": "extensionHost", 36 | "request": "launch", 37 | "runtimeExecutable": "${execPath}", 38 | "args": [ 39 | "--extensionDevelopmentPath=${workspaceFolder}", 40 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 41 | ], 42 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 43 | "preLaunchTask": "npm: test:prepare" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/inlineSuggestions/snippets/snippetDecoration.ts: -------------------------------------------------------------------------------- 1 | import { DecorationOptions, Position, Range } from "vscode"; 2 | import { insertBlankSnippet, removeBlankSnippet } from "./blankSnippet"; 3 | 4 | export async function getSnippetDecorations( 5 | position: Position, 6 | suggestion: string 7 | ): Promise { 8 | const lines = suggestion.split("\n"); 9 | const lastLineLength = lines[lines.length - 1].length; 10 | 11 | await insertBlankSnippet(lines, position); 12 | 13 | const decorations = lines.map((line, index) => 14 | getDecorationFor(line, position, index) 15 | ); 16 | 17 | decorations.push({ 18 | range: new Range( 19 | position, 20 | position.translate(lines.length, lastLineLength) 21 | ), 22 | }); 23 | 24 | return decorations; 25 | } 26 | 27 | function getDecorationFor( 28 | line: string, 29 | startPosition: Position, 30 | index: number 31 | ): DecorationOptions { 32 | const endOfCurrentLine = Math.max(0, -startPosition.character) + line.length; 33 | return { 34 | renderOptions: { 35 | after: { 36 | color: "gray", 37 | contentText: line, 38 | margin: `0 0 0 0`, 39 | textDecoration: "none; white-space: pre;", 40 | }, 41 | }, 42 | // The range of the first line should not change the character position 43 | range: new Range( 44 | startPosition.translate(index, 0), 45 | startPosition.translate(index, index === 0 ? 0 : endOfCurrentLine) 46 | ), 47 | }; 48 | } 49 | 50 | export async function handleClearSnippetDecoration(): Promise { 51 | await removeBlankSnippet(); 52 | } 53 | -------------------------------------------------------------------------------- /src/statusBar/statusBarActionHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { 3 | StatusBarStatus, 4 | } from "../binary/requests/statusBar"; 5 | 6 | import { 7 | OPEN_LP_FROM_STATUS_BAR, 8 | STATUS_BAR_NOTIFICATION_PERIOD, 9 | } from "../globals/consts"; 10 | import { 11 | promotionTextIs, 12 | resetDefaultStatus, 13 | setPromotionStatus, 14 | } from "./statusBar"; 15 | import { sleep } from "../utils/utils"; 16 | 17 | let statusBarCommandDisposable: vscode.Disposable; 18 | 19 | export default function handleStatus( 20 | context: vscode.ExtensionContext, 21 | status: StatusBarStatus 22 | ): void { 23 | 24 | if (!promotionTextIs(status.message)) { 25 | // void setState({ 26 | // [StatePayload.STATUS_SHOWN]: { 27 | // id: status.id, 28 | // text: status.message, 29 | // notification_type: status.notification_type, 30 | // state: status.state, 31 | // }, 32 | // }); 33 | } 34 | 35 | setPromotionStatus( 36 | status.id, 37 | status.message, 38 | status.title, 39 | OPEN_LP_FROM_STATUS_BAR 40 | ); 41 | 42 | let duration = STATUS_BAR_NOTIFICATION_PERIOD; 43 | if (status.duration_seconds) { 44 | duration = status.duration_seconds * 1000; 45 | } 46 | 47 | void asyncRemoveStatusAfterDuration(status.id, duration); 48 | } 49 | 50 | async function asyncRemoveStatusAfterDuration(id: string, duration: number) { 51 | await sleep(duration); 52 | resetDefaultStatus(id); 53 | } 54 | 55 | export function disposeStatusBarCommand(): void { 56 | if (statusBarCommandDisposable) { 57 | statusBarCommandDisposable.dispose(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/inlineSuggestions/positionExtracter.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from "os"; 2 | import { Position, TextDocumentContentChangeEvent } from "vscode"; 3 | 4 | export default function getCurrentPosition( 5 | change: TextDocumentContentChangeEvent 6 | ): Position { 7 | const { 8 | linesDelta, 9 | characterDelta, 10 | lastLineLength, 11 | } = calculateChangeDimensions(change); 12 | 13 | return change.range.start.translate( 14 | linesDelta, 15 | linesDelta === 0 16 | ? characterDelta 17 | : -change.range.start.character + lastLineLength 18 | ); 19 | } 20 | 21 | function calculateChangeDimensions( 22 | change: TextDocumentContentChangeEvent 23 | ): { 24 | linesDelta: number; 25 | characterDelta: number; 26 | lastLineLength: number; 27 | } { 28 | const lines = getLines(change.text); 29 | let linesDelta = lines.length - 1; 30 | // handle auto inserting of newlines, for example when the closing bracket 31 | // of a function is being inserted automatically by vscode. 32 | if (isEmptyLinesWithNewlineAutoInsert(change)) linesDelta -= 1; 33 | const characterDelta = change.text.length; 34 | const lastLineLength = lines[linesDelta].length; 35 | 36 | return { 37 | linesDelta, 38 | characterDelta, 39 | lastLineLength, 40 | }; 41 | } 42 | 43 | export function isOnlyWhitespaces(text: string): boolean { 44 | return text.trim() === ""; 45 | } 46 | 47 | export function isEmptyLinesWithNewlineAutoInsert( 48 | change: TextDocumentContentChangeEvent 49 | ): boolean { 50 | return getLines(change.text).length > 2 && isOnlyWhitespaces(change.text); 51 | } 52 | 53 | function getLines(text: string) { 54 | return text.split(EOL); 55 | } 56 | -------------------------------------------------------------------------------- /src/inlineSuggestions/documentChangesTracker/index.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, TextDocumentChangeEvent, window, workspace } from "vscode"; 2 | import DocumentTextChangeContent from "./DocumentTextChangeContent"; 3 | import tryApplyPythonIndentExtensionFix from "./pythonIndentExtensionFix"; 4 | 5 | let shouldComplete = false; 6 | let change = false; 7 | 8 | function onChange(): void { 9 | change = true; 10 | } 11 | 12 | function onTextSelectionChange(): void { 13 | if (change) { 14 | shouldComplete = true; 15 | change = false; 16 | } else { 17 | shouldComplete = false; 18 | } 19 | } 20 | export function getShouldComplete(): boolean { 21 | return shouldComplete; 22 | } 23 | 24 | export function initTracker(): Disposable[] { 25 | return [ 26 | workspace.onDidChangeTextDocument( 27 | ({ contentChanges, document }: TextDocumentChangeEvent) => { 28 | const currentPosition = window.activeTextEditor?.selection.active; 29 | const relevantChange = contentChanges.find( 30 | ({ range }) => currentPosition && range.contains(currentPosition) 31 | ); 32 | const contentChange = new DocumentTextChangeContent( 33 | document, 34 | relevantChange 35 | ); 36 | const changeHappened = 37 | (contentChange.isValidNonEmptyChange() && 38 | contentChange.isNotIndentationChange() && 39 | contentChange.isSingleCharNonWhitespaceChange()) || 40 | contentChange.isIndentOutChange(); 41 | 42 | if (changeHappened) { 43 | onChange(); 44 | tryApplyPythonIndentExtensionFix(contentChange); 45 | } 46 | } 47 | ), 48 | window.onDidChangeTextEditorSelection(onTextSelectionChange), 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | "airbnb-typescript/base", 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 13 | "plugin:import/errors", 14 | "plugin:import/typescript", 15 | "prettier", 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | project: "./tsconfig.json", 20 | tsconfigRootDir: __dirname, 21 | }, 22 | plugins: ["@typescript-eslint", "import", "no-only-tests"], 23 | ignorePatterns: ["vscode.proposed.inlineCompletions.d.ts"], 24 | rules: { 25 | "no-void": "off", 26 | "no-console": "off", 27 | "import/no-extraneous-dependencies": [ 28 | "error", 29 | { 30 | devDependencies: ["src/test/**/*.ts"], 31 | }, 32 | ], 33 | "@typescript-eslint/restrict-template-expressions": [ 34 | "error", 35 | { 36 | allowAny: true, 37 | allowNumber: true, 38 | allowBoolean: true, 39 | allowNullish: false, 40 | }, 41 | ], 42 | "@typescript-eslint/no-use-before-define": [ 43 | "error", 44 | { functions: false, classes: false }, 45 | ], 46 | "no-use-before-define": ["error", { functions: false, classes: false }], 47 | "import/no-unresolved": "error", 48 | "import/extensions": [ 49 | "error", 50 | "ignorePackages", 51 | { 52 | js: "never", 53 | ts: "never", 54 | }, 55 | ], 56 | }, 57 | settings: { 58 | "import/resolver": { 59 | typescript: { 60 | alwaysTryTypes: true, 61 | }, 62 | }, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/inlineSuggestions/inlineSuggestionState.ts: -------------------------------------------------------------------------------- 1 | import { commands } from "vscode"; 2 | import { AutocompleteResult, ResultEntry } from "../binary/requests/requests"; 3 | import { rotate } from "../utils/rotate"; 4 | 5 | let autocompleteResult: AutocompleteResult | undefined | null; 6 | let iterator = rotate(0); 7 | 8 | export async function setSuggestionsState( 9 | autocompleteResults: AutocompleteResult | undefined | null 10 | ): Promise { 11 | autocompleteResult = autocompleteResults; 12 | 13 | if (autocompleteResult?.results?.length) { 14 | iterator = rotate(autocompleteResult.results.length - 1); 15 | await toggleInlineState(true); 16 | } else { 17 | iterator = rotate(0); 18 | await toggleInlineState(false); 19 | } 20 | } 21 | export async function clearState(): Promise { 22 | autocompleteResult = null; 23 | iterator = rotate(0); 24 | 25 | await toggleInlineState(false); 26 | } 27 | async function toggleInlineState(withinSuggestion: boolean): Promise { 28 | await commands.executeCommand( 29 | "setContext", 30 | "tabnine.in-inline-suggestions", 31 | withinSuggestion 32 | ); 33 | } 34 | 35 | export function getNextSuggestion(): ResultEntry | undefined { 36 | return results()?.[iterator.next()]; 37 | } 38 | 39 | export function getPrevSuggestion(): ResultEntry | undefined { 40 | return results()?.[iterator.prev()]; 41 | } 42 | 43 | export function getCurrentSuggestion(): ResultEntry | undefined { 44 | return results()?.[iterator.current()]; 45 | } 46 | 47 | export function getCurrentPrefix(): string { 48 | return autocompleteResult?.old_prefix || ""; 49 | } 50 | 51 | export function getAllSuggestions(): ResultEntry[] | undefined { 52 | return results(); 53 | } 54 | 55 | function results(): ResultEntry[] | undefined { 56 | return autocompleteResult?.results; 57 | } 58 | -------------------------------------------------------------------------------- /src/capabilities/capabilities.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, EventEmitter } from "vscode"; 2 | 3 | export enum Capability { 4 | ON_BOARDING_CAPABILITY = "vscode.onboarding", 5 | ASSISTANT_CAPABILITY = "vscode.assistant", 6 | ASSISTANT_MODE_A_CAPABILITY_KEY = "vscode.assistant.mode.A", 7 | ASSISTANT_MODE_B_CAPABILITY_KEY = "vscode.assistant.mode.B", 8 | ASSISTANT_BACKGROUND_CAPABILITY = "vscode.assistant.background", 9 | ASSISTANT_PASTE_CAPABILITY = "vscode.assistant.paste", 10 | SUGGESTIONS_SINGLE = "suggestions-single", 11 | SUGGESTIONS_TWO = "suggestions-two", 12 | SUGGESTIONS_ORIGINAL = "suggestions-original", 13 | ALPHA_CAPABILITY = "vscode.validator", 14 | SHOW_AGRESSIVE_STATUS_BAR_UNTIL_CLICKED = "promoteHub1", 15 | INLINE_SUGGESTIONS = "inline_suggestions_mode", 16 | SNIPPET_SUGGESTIONS = "snippet_suggestions", 17 | SNIPPET_SUGGESTIONS_CONFIGURABLE = "snippet_suggestions_configurable", 18 | VSCODE_INLINE_V2 = "vscode_inline_v2", 19 | SNIPPET_AUTO_TRIGGER = "snippet_auto_trigger", 20 | LEFT_TREE_VIEW = "vscode.left_tree_view", 21 | EMPTY_LINE_SUGGESTIONS = "empty_line_suggestions", 22 | AUTHENTICATION = "vscode.authentication", 23 | CODE_REVIEW = "vscode.code-review", 24 | SAVE_SNIPPETS = "save_snippets", 25 | BETA_CAPABILITY = "beta", 26 | FIRST_SUGGESTION_DECORATION = "first_suggestion_hint_enabled", 27 | DEBOUNCE_VALUE_300 = "debounce_value_300", 28 | DEBOUNCE_VALUE_600 = "debounce_value_600", 29 | DEBOUNCE_VALUE_900 = "debounce_value_900", 30 | DEBOUNCE_VALUE_1200 = "debounce_value_1200", 31 | DEBOUNCE_VALUE_1500 = "debounce_value_1500", 32 | } 33 | 34 | export function isCapabilityEnabled(capability: Capability): boolean { 35 | console.log(capability); 36 | return true; 37 | } 38 | 39 | const capabilitiesRefreshed = new EventEmitter(); 40 | 41 | export function onDidRefreshCapabilities(listener: () => void): Disposable { 42 | return capabilitiesRefreshed.event(listener); 43 | } 44 | -------------------------------------------------------------------------------- /src/binary/requests/requests.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export type MarkdownStringSpec = { 4 | kind: string; 5 | value: string; 6 | }; 7 | 8 | export enum CompletionKind { 9 | Classic = "Classic", 10 | Line = "Line", 11 | Snippet = "Snippet", 12 | } 13 | 14 | enum UserIntent { 15 | Comment, 16 | Block, 17 | FunctionDeclaration, 18 | NoScope, 19 | NewLine, 20 | CustomTriggerPoints, 21 | } 22 | 23 | export type SnippetIntentMetadata = { 24 | current_line_indentation?: number; 25 | previous_line_indentation?: number; 26 | triggered_after_character?: string; 27 | }; 28 | 29 | export interface SnippetContext extends Record { 30 | snippet_id?: string; 31 | user_intent: UserIntent; 32 | intent_metadata?: SnippetIntentMetadata; 33 | } 34 | 35 | export type CompletionMetadata = { 36 | kind?: vscode.CompletionItemKind; 37 | detail?: string; 38 | documentation?: string | MarkdownStringSpec; 39 | deprecated?: boolean; 40 | completion_kind?: CompletionKind; 41 | is_cached?: boolean; 42 | snippet_context?: SnippetContext; 43 | }; 44 | 45 | export type ResultEntry = { 46 | new_prefix: string; 47 | old_suffix: string; 48 | new_suffix: string; 49 | completion_metadata?: CompletionMetadata; 50 | }; 51 | 52 | export type AutocompleteResult = { 53 | old_prefix: string; 54 | results: ResultEntry[]; 55 | user_message: string[]; 56 | is_locked: boolean; 57 | }; 58 | 59 | export type AutocompleteParams = { 60 | filename: string; 61 | before: string; 62 | after: string; 63 | region_includes_beginning: boolean; 64 | region_includes_end: boolean; 65 | max_num_results: number; 66 | offset: number; 67 | line: number; 68 | character: number; 69 | indentation_size: number; 70 | }; 71 | 72 | export enum SnippetRequestTrigger { 73 | Auto = "Auto", 74 | User = "User", 75 | } 76 | 77 | export type SnippetAutocompleteParams = AutocompleteParams & { 78 | trigger: SnippetRequestTrigger; 79 | }; 80 | -------------------------------------------------------------------------------- /src/getInlineCompletionItems.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TabnineInlineCompletionItem from "./inlineSuggestions/tabnineInlineCompletionItem"; 3 | import runCompletion from "./runCompletion"; 4 | import getAutoImportCommand from "./getAutoImportCommand"; 5 | import { SuggestionTrigger } from "./globals/consts"; 6 | import { AutocompleteResult, ResultEntry } from "./binary/requests/requests"; 7 | import { isMultiline } from "./utils/utils"; 8 | 9 | const INLINE_REQUEST_TIMEOUT = 3000; 10 | 11 | export default async function getInlineCompletionItems( 12 | document: vscode.TextDocument, 13 | position: vscode.Position 14 | ): Promise> { 15 | const isEmptyLine = document.lineAt(position.line).text.trim().length === 0; 16 | 17 | const response = await runCompletion( 18 | document, 19 | position, 20 | isEmptyLine ? INLINE_REQUEST_TIMEOUT : undefined 21 | ); 22 | 23 | const completions = response?.results.map( 24 | (result) => 25 | new TabnineInlineCompletionItem( 26 | result.new_prefix, 27 | result, 28 | calculateRange(position, response, result), 29 | getAutoImportCommand( 30 | result, 31 | response, 32 | position, 33 | SuggestionTrigger.DocumentChanged 34 | ), 35 | result.completion_metadata?.completion_kind, 36 | result.completion_metadata?.is_cached, 37 | result.completion_metadata?.snippet_context 38 | ) 39 | ); 40 | 41 | return new vscode.InlineCompletionList(completions || []); 42 | } 43 | 44 | function calculateRange( 45 | position: vscode.Position, 46 | response: AutocompleteResult, 47 | result: ResultEntry 48 | ): vscode.Range { 49 | return new vscode.Range( 50 | position.translate(0, -response.old_prefix.length), 51 | isMultiline(result.old_suffix) 52 | ? position 53 | : position.translate(0, result.old_suffix.length) 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { registerCommands } from "./commandsHandler"; 3 | import tabnineExtensionProperties from "./globals/tabnineExtensionProperties"; 4 | import { 5 | COMPLETION_IMPORTS, 6 | handleImports, 7 | HANDLE_IMPORTS, 8 | getSelectionHandler, 9 | } from "./selectionHandler"; 10 | import { registerStatusBar, setDefaultStatus } from "./statusBar/statusBar"; 11 | import { setTabnineExtensionContext } from "./globals/tabnineExtensionContext"; 12 | import registerTreeView from "./treeView/registerTreeView"; 13 | import installAutocomplete from "./autocompleteInstaller"; 14 | import handlePluginInstalled from "./handlePluginInstalled"; 15 | 16 | export async function activate( 17 | context: vscode.ExtensionContext 18 | ): Promise { 19 | void initStartup(context); 20 | handleSelection(context); 21 | 22 | registerStatusBar(context); 23 | 24 | // Do not await on this function as we do not want VSCode to wait for it to finish 25 | // before considering TabNine ready to operate. 26 | void backgroundInit(context); 27 | 28 | if (context.extensionMode !== vscode.ExtensionMode.Test) { 29 | handlePluginInstalled(context); 30 | } 31 | 32 | return Promise.resolve(); 33 | } 34 | 35 | function initStartup(context: vscode.ExtensionContext): void { 36 | setTabnineExtensionContext(context); 37 | } 38 | 39 | async function backgroundInit(context: vscode.ExtensionContext) { 40 | registerTreeView(context); 41 | setDefaultStatus(); 42 | void registerCommands(context); 43 | 44 | await installAutocomplete(context); 45 | } 46 | 47 | export async function deactivate(){ 48 | } 49 | 50 | function handleSelection(context: vscode.ExtensionContext) { 51 | if (tabnineExtensionProperties.isTabNineAutoImportEnabled) { 52 | context.subscriptions.push( 53 | vscode.commands.registerTextEditorCommand( 54 | COMPLETION_IMPORTS, 55 | getSelectionHandler(context) 56 | ), 57 | vscode.commands.registerTextEditorCommand(HANDLE_IMPORTS, handleImports) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/inlineSuggestions/documentChangesTracker/DocumentTextChangeContent.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument, TextDocumentContentChangeEvent } from "vscode"; 2 | import getTabSize from "../../binary/requests/tabSize"; 3 | 4 | const AUTO_CLOSED_BRACKETS_CHANGE = ["()", "{}", "[]", '""', "''", "``"]; 5 | export default class DocumentTextChangeContent { 6 | constructor( 7 | private readonly document: TextDocument, 8 | private readonly contentChange?: TextDocumentContentChangeEvent 9 | ) {} 10 | 11 | isValidNonEmptyChange(): boolean { 12 | return ( 13 | !!this.contentChange && 14 | this.contentChange.rangeLength >= 0 && 15 | this.contentChange.text !== "" 16 | ); 17 | } 18 | 19 | isSingleCharNonWhitespaceChange(): boolean { 20 | return ( 21 | !!this.contentChange && 22 | (this.contentChange?.text.trim().length <= 1 || 23 | AUTO_CLOSED_BRACKETS_CHANGE.includes(this.contentChange.text)) 24 | ); 25 | } 26 | 27 | isNotIndentationChange(): boolean { 28 | const isEndsWithWhitespace = this.contentChange?.text.endsWith( 29 | " ".repeat(getTabSize()) 30 | ); 31 | const isEndsWithTab = this.contentChange?.text.endsWith("\t"); 32 | const isNewLine = this.contentChange?.text.includes("\n"); 33 | return ( 34 | !!this.contentChange && 35 | (isNewLine || (!isEndsWithWhitespace && !isEndsWithTab)) 36 | ); 37 | } 38 | 39 | isPythonNewLineChange(): boolean { 40 | return ( 41 | !!this.contentChange && 42 | this.document.languageId === "python" && 43 | this.contentChange?.text.startsWith("\n") && 44 | this.contentChange?.text.trim() === "" 45 | ); 46 | } 47 | 48 | isIndentOutChange(): boolean { 49 | return ( 50 | !!this.contentChange && 51 | // in case of /t the rangeLength will be 1, in case of spaces the rangeLength will be tabsize 52 | this.contentChange.rangeLength > 0 && 53 | this.contentChange.range.isSingleLine && 54 | this.document.lineAt(this.contentChange.range.end).isEmptyOrWhitespace 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/globals/proposedAPI.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import * as vscode from "vscode"; 3 | import * as path from "path"; 4 | import * as os from "os"; 5 | 6 | const EXTENSION_ID = "TabNine.tabnine-vscode"; 7 | const ARGV_FILE_NAME = "argv.json"; 8 | const PRODUCT_FILE_NAME = "product.json"; 9 | const PRODUCT_FILE_PATH = path.join(vscode.env.appRoot, PRODUCT_FILE_NAME); 10 | const ENABLE_PROPOSED_API = [ 11 | "", 12 | ` "enable-proposed-api": ["${EXTENSION_ID}"]`, 13 | "}", 14 | ]; 15 | 16 | export default async function enableProposed(): Promise { 17 | return handleProposed().catch((error) => { 18 | console.error("failed to enable proposedAPI", error); 19 | return false; 20 | }); 21 | } 22 | 23 | async function getDataFolderName(): Promise { 24 | const data = await fs.readFile(PRODUCT_FILE_PATH); 25 | const file = JSON.parse(data.toString("utf8")) as { 26 | dataFolderName?: string; 27 | }; 28 | return file?.dataFolderName; 29 | } 30 | 31 | function getArgvResource(dataFolderName: string): string { 32 | const vscodePortable = process.env.VSCODE_PORTABLE; 33 | if (vscodePortable) { 34 | return path.join(vscodePortable, ARGV_FILE_NAME); 35 | } 36 | 37 | return path.join(os.homedir(), dataFolderName, ARGV_FILE_NAME); 38 | } 39 | async function handleProposed(): Promise { 40 | const dataFolderName = await getDataFolderName(); 41 | 42 | if (dataFolderName) { 43 | const argvResource = getArgvResource(dataFolderName); 44 | const argvString = (await fs.readFile(argvResource)).toString(); 45 | 46 | if (argvString.includes(`${EXTENSION_ID}`)) { 47 | return true; 48 | } 49 | 50 | const modifiedArgvString = modifyArgvFileContent(argvString); 51 | await fs.writeFile(argvResource, Buffer.from(modifiedArgvString)); 52 | askForReload(); 53 | } 54 | return false; 55 | } 56 | 57 | function askForReload() { 58 | // todo 59 | } 60 | 61 | function modifyArgvFileContent(argvString: string) { 62 | return argvString 63 | .substring(0, argvString.length - 2) 64 | .concat(",\n", ENABLE_PROPOSED_API.join("\n")); 65 | } 66 | -------------------------------------------------------------------------------- /src/provideInlineCompletionItems.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TabnineInlineCompletionItem from "./inlineSuggestions/tabnineInlineCompletionItem"; 3 | import { completionIsAllowed } from "./provideCompletionItems"; 4 | import { getShouldComplete } from "./inlineSuggestions/documentChangesTracker"; 5 | import getInlineCompletionItems from "./getInlineCompletionItems"; 6 | 7 | const END_OF_LINE_VALID_REGEX = new RegExp("^\\s*[)}\\]\"'`]*\\s*[:{;,]?\\s*$"); 8 | const DEBOUNCE_DELAY = 300; 9 | 10 | function debounce( 11 | callback: (...rest: T) => R, 12 | limit: number 13 | ): (...rest: T) => Promise { 14 | let timer: ReturnType; 15 | 16 | return function (...rest): Promise { 17 | return new Promise((resolve) => { 18 | clearTimeout(timer); 19 | timer = setTimeout(() => { 20 | resolve(callback(...rest)); 21 | }, limit); 22 | }); 23 | }; 24 | } 25 | 26 | const debounceCompletions = debounce(getInlineCompletionItems, DEBOUNCE_DELAY); 27 | 28 | export default async function provideInlineCompletionItems( 29 | document: vscode.TextDocument, 30 | position: vscode.Position, 31 | _context: vscode.InlineCompletionContext, 32 | _token: vscode.CancellationToken 33 | ): Promise< 34 | vscode.InlineCompletionList | undefined 35 | > { 36 | try { 37 | if ( 38 | !completionIsAllowed(document, position) || 39 | !isValidMidlinePosition(document, position) || 40 | !getShouldComplete() 41 | ) { 42 | return undefined; 43 | } 44 | 45 | const completions = await debounceCompletions(document, position); 46 | return completions; 47 | } catch (e) { 48 | console.error(`Error setting up request: ${e}`); 49 | 50 | return undefined; 51 | } 52 | } 53 | 54 | function isValidMidlinePosition( 55 | document: vscode.TextDocument, 56 | position: vscode.Position 57 | ): boolean { 58 | const lineSuffix = document.getText( 59 | new vscode.Range(position, document.lineAt(position.line).range.end) 60 | ); 61 | return END_OF_LINE_VALID_REGEX.test(lineSuffix); 62 | } 63 | -------------------------------------------------------------------------------- /src/inlineSuggestions/snippets/acceptSnippetSuggestion.ts: -------------------------------------------------------------------------------- 1 | import { commands, Position, Range, SnippetString, TextEditor } from "vscode"; 2 | import { ResultEntry } from "../../binary/requests/requests"; 3 | import { CompletionArguments } from "../../CompletionArguments"; 4 | import { COMPLETION_IMPORTS } from "../../selectionHandler"; 5 | import { escapeTabStopSign } from "../../utils/utils"; 6 | import clearInlineSuggestionsState from "../clearDecoration"; 7 | 8 | /** 9 | * Inserts the completion text into the editor. 10 | * This function replaces any existing text in the editor with the new text, 11 | * so if for example the text was "a" and the completion text was "abc\nxyz", 12 | * the previous "abc" will be overwritten by the string "abc\nxyz". 13 | */ 14 | export default async function acceptSnippet( 15 | editor: TextEditor, 16 | currentSuggestion: ResultEntry, 17 | currentTextPosition: Position, 18 | allSuggestions: ResultEntry[], 19 | oldPrefix: string 20 | ): Promise { 21 | const position = currentTextPosition.with(undefined, 0); 22 | const indentation = getCurrentIndentation(editor, position); 23 | const insertText = constructInsertSnippet(currentSuggestion, indentation); 24 | 25 | const completion: CompletionArguments = { 26 | currentCompletion: currentSuggestion.new_prefix, 27 | completions: allSuggestions, 28 | position: currentTextPosition, 29 | limited: false, 30 | oldPrefix, 31 | }; 32 | 33 | await clearInlineSuggestionsState(); 34 | const range = new Range(position, currentTextPosition); 35 | await editor.insertSnippet(insertText, range); 36 | 37 | void commands.executeCommand(COMPLETION_IMPORTS, completion); 38 | } 39 | 40 | // take 'abc'.length into consideration 41 | function getCurrentIndentation(editor: TextEditor, position: Position) { 42 | return ( 43 | editor.selection.active.character - 44 | editor.document.lineAt(position).text.trim().length 45 | ); 46 | } 47 | 48 | function constructInsertSnippet({ new_prefix }: ResultEntry, indent: number) { 49 | const insertText = new SnippetString(" ".repeat(indent)); 50 | insertText.appendText(escapeTabStopSign(new_prefix)); 51 | insertText.appendTabstop(0); 52 | return insertText; 53 | } 54 | -------------------------------------------------------------------------------- /src/handleUninstall.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import tabnineExtensionProperties from "./globals/tabnineExtensionProperties"; 4 | 5 | export default function handleUninstall( 6 | onUninstall: () => Promise 7 | ): void { 8 | try { 9 | const extensionsPath = path.dirname( 10 | tabnineExtensionProperties.extensionPath ?? "" 11 | ); 12 | const uninstalledPath = path.join(extensionsPath, ".obsolete"); 13 | const isFileExists = (curr: fs.Stats) => curr.size !== 0; 14 | const isModified = (curr: fs.Stats, prev: fs.Stats) => 15 | new Date(curr.mtimeMs) >= new Date(prev.atimeMs); 16 | const isUpdating = (files: string[]) => 17 | files.filter((f) => 18 | tabnineExtensionProperties.id 19 | ? f 20 | .toLowerCase() 21 | .includes(tabnineExtensionProperties.id.toLowerCase()) 22 | : false 23 | ).length !== 1; 24 | const watchFileHandler = (curr: fs.Stats, prev: fs.Stats) => { 25 | if (isFileExists(curr) && isModified(curr, prev)) { 26 | fs.readFile(uninstalledPath, (err, uninstalled) => { 27 | if (err) { 28 | console.error("failed to read .obsolete file:", err); 29 | throw err; 30 | } 31 | fs.readdir(extensionsPath, (error, files: string[]) => { 32 | if (error) { 33 | console.error( 34 | `failed to read ${extensionsPath} directory:`, 35 | error 36 | ); 37 | 38 | throw error; 39 | } 40 | 41 | if ( 42 | !isUpdating(files) && 43 | uninstalled.includes(tabnineExtensionProperties.name) 44 | ) { 45 | onUninstall() 46 | .then(() => { 47 | fs.unwatchFile(uninstalledPath, watchFileHandler); 48 | }) 49 | .catch((e) => { 50 | console.error("failed to report uninstall:", e); 51 | }); 52 | } 53 | }); 54 | }); 55 | } 56 | }; 57 | fs.watchFile(uninstalledPath, watchFileHandler); 58 | } catch (error) { 59 | console.error("failed to invoke uninstall:", error); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ 2 | // @ts-check 3 | 4 | const path = require("path"); 5 | const webpack = require("webpack"); 6 | const TerserPlugin = require("terser-webpack-plugin"); 7 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 8 | 9 | /* *@type {import('webpack').Configuration} */ 10 | const config = { 11 | target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 12 | 13 | entry: { 14 | extension: "./src/extension.ts", 15 | }, // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, "out"), 19 | filename: `[name].js`, 20 | libraryTarget: "commonjs2", 21 | devtoolModuleFilenameTemplate: "../[resource-path]", 22 | }, 23 | node: { 24 | __dirname: false, // leave the __dirname behavior intact 25 | }, 26 | devtool: "source-map", 27 | externals: { 28 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 29 | }, 30 | resolve: { 31 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 32 | extensions: [".ts", ".js"], 33 | alias: { 34 | semver: path.resolve(__dirname, "node_modules/semver"), 35 | }, 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.ts$/, 41 | exclude: /node_modules/, 42 | use: [ 43 | { 44 | loader: "ts-loader", 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | plugins: [ 51 | new webpack.IgnorePlugin({ 52 | checkResource: (resource) => 53 | [ 54 | "osx-temperature-sensor", 55 | "@opentelemetry/tracing", 56 | "applicationinsights-native-metrics", 57 | ].includes(resource), 58 | }), 59 | ], 60 | optimization: { 61 | minimizer: [new TerserPlugin({ extractComments: false })], 62 | }, 63 | }; 64 | 65 | module.exports = (env) => { 66 | if (env.analyzeBundle) { 67 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 68 | config.plugins.push(new BundleAnalyzerPlugin()); 69 | } 70 | 71 | return [config]; 72 | }; 73 | -------------------------------------------------------------------------------- /src/selectionHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CodeAction, 3 | CodeActionKind, 4 | commands, 5 | Selection, 6 | TextEditor, 7 | TextEditorEdit, 8 | workspace, 9 | ExtensionContext, 10 | } from "vscode"; 11 | import findImports from "./findImports"; 12 | import { 13 | DELAY_FOR_CODE_ACTION_PROVIDER, 14 | } from "./globals/consts"; 15 | import { CompletionArguments } from "./CompletionArguments"; 16 | 17 | export const COMPLETION_IMPORTS = "tabnine-completion-imports"; 18 | export const HANDLE_IMPORTS = "tabnine-handle-imports"; 19 | 20 | export function getSelectionHandler( 21 | context: ExtensionContext 22 | ): ( 23 | editor: TextEditor, 24 | edit: TextEditorEdit, 25 | args: CompletionArguments 26 | ) => void { 27 | console.log(context); 28 | return function selectionHandler( 29 | editor: TextEditor, 30 | _edit: TextEditorEdit, 31 | { 32 | currentCompletion, 33 | completions, 34 | position, 35 | limited, 36 | oldPrefix, 37 | suggestionTrigger, 38 | }: CompletionArguments 39 | ): void { 40 | try { 41 | console.log(completions, 42 | position, 43 | limited, 44 | oldPrefix, 45 | suggestionTrigger) 46 | 47 | void commands.executeCommand(HANDLE_IMPORTS, { 48 | completion: currentCompletion, 49 | }); 50 | } catch (error) { 51 | console.error(error); 52 | } 53 | }; 54 | } 55 | 56 | export function handleImports( 57 | editor: TextEditor, 58 | edit: TextEditorEdit, 59 | { completion }: { completion: string } 60 | ): void { 61 | const lines = completion.split("\n"); 62 | 63 | const { selection } = editor; 64 | const completionSelection = new Selection( 65 | selection.active.translate( 66 | -(lines.length - 1), 67 | lines.length > 1 ? -selection.active.character : -completion.length 68 | ), 69 | selection.active 70 | ); 71 | setTimeout(() => { 72 | void doAutoImport(editor, completionSelection, completion); 73 | }, DELAY_FOR_CODE_ACTION_PROVIDER); 74 | } 75 | 76 | async function doAutoImport( 77 | editor: TextEditor, 78 | completionSelection: Selection, 79 | completion: string 80 | ) { 81 | try { 82 | const codeActionCommands = await commands.executeCommand( 83 | "vscode.executeCodeActionProvider", 84 | editor.document.uri, 85 | completionSelection, 86 | CodeActionKind.QuickFix.value 87 | ); 88 | const importCommand = findImports(codeActionCommands)[0]; 89 | 90 | if (importCommand && importCommand.edit) { 91 | await workspace.applyEdit(importCommand.edit); 92 | await commands.executeCommand(HANDLE_IMPORTS, { completion }); 93 | } 94 | } catch (error) { 95 | console.error(error); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/statusBar/statusBar.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, StatusBarAlignment, window } from "vscode"; 2 | import { STATUS_BAR_COMMAND } from "../commandsHandler"; 3 | import { FULL_BRAND_REPRESENTATION, STATUS_NAME } from "../globals/consts"; 4 | import StatusBarData from "./StatusBarData"; 5 | import StatusBarPromotionItem from "./StatusBarPromotionItem"; 6 | 7 | const SPINNER = "$(sync~spin)"; 8 | 9 | let statusBarData: StatusBarData | undefined; 10 | let promotion: StatusBarPromotionItem | undefined; 11 | 12 | export function registerStatusBar(context: ExtensionContext): void { 13 | if (statusBarData) { 14 | return; 15 | } 16 | 17 | const statusBar = window.createStatusBarItem(StatusBarAlignment.Left, -1); 18 | promotion = new StatusBarPromotionItem( 19 | window.createStatusBarItem(StatusBarAlignment.Left, -1) 20 | ); 21 | statusBarData = new StatusBarData(statusBar, context); 22 | statusBar.command = STATUS_BAR_COMMAND; 23 | statusBar.show(); 24 | try { 25 | (statusBar as { name?: string }).name = STATUS_NAME; 26 | (promotion.item as { name?: string }).name = STATUS_NAME; 27 | } catch (err) { 28 | console.error("failed to rename status bar"); 29 | } 30 | 31 | setLoadingStatus("Starting..."); 32 | context.subscriptions.push(statusBar); 33 | context.subscriptions.push(promotion.item); 34 | } 35 | 36 | export function pollServiceLevel() { 37 | } 38 | 39 | export function promotionTextIs(text: string): boolean { 40 | return promotion?.item?.text === text; 41 | } 42 | 43 | export function onStartServiceLevel() { 44 | } 45 | 46 | export function setDefaultStatus(): void { 47 | if (!statusBarData) { 48 | return; 49 | } 50 | 51 | statusBarData.icon = null; 52 | statusBarData.text = FULL_BRAND_REPRESENTATION; 53 | } 54 | 55 | export function resetDefaultStatus(id?: string): void { 56 | if (!id || (promotion && promotion.id && promotion.id === id)) { 57 | setDefaultStatus(); 58 | clearPromotion(); 59 | } 60 | } 61 | 62 | export function setLoadingStatus(issue?: string | undefined | null): void { 63 | if (!statusBarData) { 64 | return; 65 | } 66 | 67 | statusBarData.text = issue; 68 | statusBarData.icon = SPINNER; 69 | } 70 | 71 | export function setPromotionStatus( 72 | id: string, 73 | message: string, 74 | tooltip: string | undefined, 75 | command: string 76 | ): void { 77 | if (!statusBarData || !promotion) { 78 | return; 79 | } 80 | 81 | promotion.id = id; 82 | promotion.item.text = message; 83 | promotion.item.command = command; 84 | promotion.item.tooltip = `${FULL_BRAND_REPRESENTATION}${ 85 | tooltip ? ` - ${tooltip}` : "" 86 | }`; 87 | promotion.item.color = "white"; 88 | statusBarData.text = " "; 89 | promotion.item.show(); 90 | } 91 | 92 | export function clearPromotion(): void { 93 | if (!promotion) { 94 | return; 95 | } 96 | 97 | promotion.item.text = ""; 98 | promotion.item.tooltip = ""; 99 | promotion.item.hide(); 100 | } 101 | -------------------------------------------------------------------------------- /src/inlineSuggestions/acceptInlineSuggestion.ts: -------------------------------------------------------------------------------- 1 | import { commands, Position, Range, SnippetString, TextEditor } from "vscode"; 2 | import { ResultEntry } from "../binary/requests/requests"; 3 | import { CompletionArguments } from "../CompletionArguments"; 4 | import { COMPLETION_IMPORTS } from "../selectionHandler"; 5 | import { escapeTabStopSign, isMultiline } from "../utils/utils"; 6 | import clearInlineSuggestionsState from "./clearDecoration"; 7 | import { 8 | getCurrentSuggestion, 9 | getCurrentPrefix, 10 | getAllSuggestions, 11 | } from "./inlineSuggestionState"; 12 | import acceptSnippetSuggestion from "./snippets/acceptSnippetSuggestion"; 13 | import { vimActive, vimReturnToInsertMode } from "./vimForVSCodeWorkaround"; 14 | 15 | export default async function acceptInlineSuggestion( 16 | editor: TextEditor 17 | ): Promise { 18 | const currentSuggestion = getCurrentSuggestion(); 19 | const currentTextPosition = editor.selection.active; 20 | const prefix = getCurrentPrefix(); 21 | const allSuggestions = getAllSuggestions(); 22 | 23 | if (currentSuggestion && currentTextPosition && allSuggestions) { 24 | await (isMultiline(currentSuggestion?.new_prefix) 25 | ? acceptSnippetSuggestion( 26 | editor, 27 | currentSuggestion, 28 | currentTextPosition, 29 | allSuggestions, 30 | prefix 31 | ) 32 | : acceptOneLineSuggestion( 33 | currentTextPosition, 34 | prefix, 35 | currentSuggestion, 36 | allSuggestions, 37 | editor, 38 | prefix 39 | )); 40 | 41 | if (vimActive()) { 42 | await vimReturnToInsertMode(currentSuggestion); 43 | } 44 | } 45 | } 46 | 47 | async function acceptOneLineSuggestion( 48 | currentTextPosition: Position, 49 | prefix: string, 50 | currentSuggestion: ResultEntry, 51 | allSuggestions: ResultEntry[], 52 | editor: TextEditor, 53 | oldPrefix: string 54 | ) { 55 | const range = getSuggestionRange( 56 | currentTextPosition, 57 | prefix, 58 | currentSuggestion.old_suffix 59 | ); 60 | const insertText = constructInsertSnippet(currentSuggestion); 61 | 62 | const completion: CompletionArguments = { 63 | currentCompletion: currentSuggestion.new_prefix, 64 | completions: allSuggestions, 65 | position: currentTextPosition, 66 | limited: false, 67 | oldPrefix, 68 | }; 69 | await clearInlineSuggestionsState(); 70 | await editor.insertSnippet(insertText, range); 71 | 72 | void commands.executeCommand(COMPLETION_IMPORTS, completion); 73 | } 74 | 75 | function constructInsertSnippet({ new_prefix, new_suffix }: ResultEntry) { 76 | const insertText = new SnippetString(escapeTabStopSign(new_prefix)); 77 | 78 | if (new_suffix) { 79 | insertText.appendTabstop(0).appendText(escapeTabStopSign(new_suffix)); 80 | } 81 | return insertText; 82 | } 83 | 84 | function getSuggestionRange( 85 | currentTextPosition: Position, 86 | prefix: string, 87 | oldPrefix: string 88 | ) { 89 | return new Range( 90 | currentTextPosition.translate(undefined, -prefix.trim().length), 91 | currentTextPosition.translate(undefined, oldPrefix.trim().length) 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export function withPolling( 4 | callback: (clear: () => void) => void | Promise, 5 | interval: number, 6 | timeout: number, 7 | shouldImmediatelyInvoke = false, 8 | onTimeout = () => {} 9 | ): void { 10 | const pollingInterval = setInterval( 11 | () => void callback(clearPolling), 12 | interval 13 | ); 14 | 15 | const pollingTimeout = setTimeout(() => { 16 | clearInterval(pollingInterval); 17 | onTimeout(); 18 | }, timeout); 19 | 20 | function clearPolling() { 21 | clearInterval(pollingInterval); 22 | clearTimeout(pollingTimeout); 23 | } 24 | 25 | if (shouldImmediatelyInvoke) { 26 | void callback(clearPolling); 27 | } 28 | } 29 | 30 | export async function assertFirstTimeReceived( 31 | key: string, 32 | context: vscode.ExtensionContext 33 | ): Promise { 34 | return new Promise((resolve, reject) => { 35 | if (!context.globalState.get(key)) { 36 | void context.globalState.update(key, true).then(resolve, reject); 37 | } else { 38 | reject(new Error("Already happened")); 39 | } 40 | }); 41 | } 42 | 43 | export function sleep(time: number): Promise { 44 | return new Promise((resolve) => setTimeout(resolve, time)); 45 | } 46 | 47 | // eslint-disable-next-line 48 | export function isFunction(functionToCheck: any): boolean { 49 | // eslint-disable-next-line 50 | return ( 51 | functionToCheck && {}.toString.call(functionToCheck) === "[object Function]" 52 | ); 53 | } 54 | 55 | export async function asyncFind( 56 | arr: T[], 57 | predicate: (element: T) => Promise 58 | ): Promise { 59 | // eslint-disable-next-line no-restricted-syntax 60 | for await (const element of arr) { 61 | if (await predicate(element)) { 62 | return element; 63 | } 64 | } 65 | return null; 66 | } 67 | 68 | export function formatError(error: Error): string { 69 | return `OS: ${process.platform} - ${process.arch}\n Error: ${ 70 | error.name 71 | }\nMessage: ${error.message}\nStack: ${error.stack || ""}`; 72 | } 73 | 74 | export function escapeRegExp(value: string): string { 75 | return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 76 | } 77 | 78 | export function trimEnd(str: string, suffix: string): string { 79 | return str.replace(new RegExp(`${escapeRegExp(suffix)}$`), ""); 80 | } 81 | 82 | export function escapeTabStopSign(value: string): string { 83 | return value.replace(new RegExp("\\$", "g"), "\\$"); 84 | } 85 | 86 | export function isMultiline(text?: string): boolean { 87 | return text?.includes("\n") || false; 88 | } 89 | 90 | export async function timed( 91 | fn: () => Promise 92 | ): Promise<{ time: number; value: T }> { 93 | const time = process.hrtime(); 94 | const value = await fn(); 95 | const after = hrtimeToMs(process.hrtime(time)); 96 | return { time: after, value }; 97 | } 98 | 99 | export function hrtimeToMs(hrtime: [number, number]): number { 100 | const seconds = hrtime[0]; 101 | const nanoseconds = hrtime[1]; 102 | return seconds * 1000 + nanoseconds / 1000000; 103 | } 104 | -------------------------------------------------------------------------------- /src/autocompleteInstaller.ts: -------------------------------------------------------------------------------- 1 | import { languages, Disposable, ExtensionContext } from "vscode"; 2 | import getSuggestionMode, { 3 | SuggestionsMode, 4 | } from "./capabilities/getSuggestionMode"; 5 | import { 6 | Capability, 7 | isCapabilityEnabled, 8 | onDidRefreshCapabilities, 9 | } from "./capabilities/capabilities"; 10 | import registerInlineHandlers from "./inlineSuggestions/registerHandlers"; 11 | 12 | import provideCompletionItems from "./provideCompletionItems"; 13 | import { COMPLETION_TRIGGERS } from "./globals/consts"; 14 | 15 | let subscriptions: Disposable[] = []; 16 | 17 | export default async function installAutocomplete( 18 | context: ExtensionContext 19 | ): Promise { 20 | context.subscriptions.push({ 21 | dispose: () => uninstallAutocomplete(), 22 | }); 23 | 24 | let installOptions = InstallOptions.get(); 25 | 26 | await reinstallAutocomplete(installOptions); 27 | 28 | context.subscriptions.push( 29 | onDidRefreshCapabilities(() => { 30 | const newInstallOptions = InstallOptions.get(); 31 | 32 | if (!newInstallOptions.equals(installOptions)) { 33 | void reinstallAutocomplete(newInstallOptions); 34 | installOptions = newInstallOptions; 35 | } 36 | }) 37 | ); 38 | } 39 | 40 | async function reinstallAutocomplete({ 41 | inlineEnabled, 42 | snippetsEnabled, 43 | autocompleteEnabled, 44 | }: InstallOptions) { 45 | uninstallAutocomplete(); 46 | 47 | subscriptions.push( 48 | ...(await registerInlineHandlers(inlineEnabled, snippetsEnabled)) 49 | ); 50 | 51 | if (autocompleteEnabled) { 52 | subscriptions.push( 53 | languages.registerCompletionItemProvider( 54 | { pattern: "**" }, 55 | { 56 | provideCompletionItems, 57 | }, 58 | ...COMPLETION_TRIGGERS 59 | ) 60 | ); 61 | } 62 | } 63 | 64 | class InstallOptions { 65 | inlineEnabled: boolean; 66 | 67 | snippetsEnabled: boolean; 68 | 69 | autocompleteEnabled: boolean; 70 | 71 | constructor( 72 | inlineEnabled: boolean, 73 | snippetsEnabled: boolean, 74 | autocompleteEnabled: boolean 75 | ) { 76 | this.inlineEnabled = inlineEnabled; 77 | this.snippetsEnabled = snippetsEnabled; 78 | this.autocompleteEnabled = autocompleteEnabled; 79 | } 80 | 81 | public equals(other: InstallOptions): boolean { 82 | return ( 83 | this.autocompleteEnabled === other.autocompleteEnabled && 84 | this.inlineEnabled === other.inlineEnabled && 85 | this.snippetsEnabled === other.snippetsEnabled 86 | ); 87 | } 88 | 89 | public static get() { 90 | return new InstallOptions( 91 | isInlineEnabled(), 92 | isSnippetSuggestionsEnabled(), 93 | isAutoCompleteEnabled() 94 | ); 95 | } 96 | } 97 | 98 | function uninstallAutocomplete() { 99 | subscriptions.forEach((s) => { 100 | s.dispose(); 101 | }); 102 | subscriptions = []; 103 | } 104 | 105 | function isInlineEnabled() { 106 | return getSuggestionMode() === SuggestionsMode.INLINE; 107 | } 108 | 109 | function isSnippetSuggestionsEnabled() { 110 | return isCapabilityEnabled(Capability.SNIPPET_SUGGESTIONS); 111 | } 112 | 113 | function isAutoCompleteEnabled() { 114 | return getSuggestionMode() === SuggestionsMode.AUTOCOMPLETE; 115 | } 116 | -------------------------------------------------------------------------------- /src/inlineSuggestions/highlightStackAttributions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Range, 3 | Selection, 4 | WorkspaceConfiguration, 5 | window, 6 | workspace, 7 | env, 8 | Uri 9 | } from "vscode"; 10 | 11 | import fetch from "node-fetch"; 12 | 13 | export default async function highlightStackAttributions(): Promise { 14 | const document = window.activeTextEditor?.document 15 | if (!document) return; 16 | 17 | type Config = WorkspaceConfiguration & { 18 | attributionWindowSize: number; 19 | }; 20 | const config: Config = workspace.getConfiguration("HuggingFaceCode") as Config; 21 | const { attributionWindowSize } = config; 22 | 23 | // get cursor postion and offset 24 | const cursorPosition = window.activeTextEditor?.selection.active; 25 | if (!cursorPosition) return; 26 | const cursorOffset = document.offsetAt(cursorPosition); 27 | 28 | const start = Math.max(0, cursorOffset - attributionWindowSize); 29 | const end = Math.min(document.getText().length, cursorOffset + attributionWindowSize); 30 | 31 | // Select the start to end span 32 | if (!window.activeTextEditor) return; 33 | window.activeTextEditor.selection = new Selection(document.positionAt(start), document.positionAt(end)); 34 | // new Range(document.positionAt(start), document.positionAt(end)); 35 | 36 | 37 | const text = document.getText(); 38 | const textAroundCursor = text.slice(start, end); 39 | 40 | const url = "https://stack.dataportraits.org/overlap"; 41 | const body = { document: textAroundCursor }; 42 | 43 | // notify user request has started 44 | void window.showInformationMessage("Searching for nearby code in the stack..."); 45 | 46 | const resp = await fetch(url, { 47 | method: "POST", 48 | body: JSON.stringify(body), 49 | headers: { "Content-Type": "application/json" }, 50 | }); 51 | 52 | if(!resp.ok){ 53 | return; 54 | } 55 | 56 | const json = await resp.json() as any as {spans: [number, number][]} 57 | const {spans} = json 58 | 59 | if(spans.length === 0) { 60 | void window.showInformationMessage("No code found in the stack"); 61 | return; 62 | } 63 | 64 | void window.showInformationMessage("Highlighted code was found in the stack.", 65 | "Go to stack search" 66 | ).then(clicked => { 67 | if (clicked) { 68 | // open stack search url in browser 69 | void env.openExternal(Uri.parse("https://huggingface.co/spaces/bigcode/search")); 70 | } 71 | }); 72 | 73 | // combine overlapping spans 74 | const combinedSpans: [number,number][] = spans.reduce((acc, span) => { 75 | const [s, e] = span; 76 | if(acc.length === 0) return [[s, e]]; 77 | const [lastStart, lastEnd] = acc[acc.length - 1]; 78 | if(s <= lastEnd) { 79 | acc[acc.length - 1] = [lastStart, Math.max(lastEnd, e)]; 80 | }else{ 81 | acc.push([s, e]); 82 | } 83 | return acc; 84 | }, [] as [number, number][]); 85 | 86 | const decorations = combinedSpans.map(([startChar, endChar]) => ({range: new Range(document.positionAt(startChar + start), document.positionAt(endChar + start)), hoverMessage: "This code might be in the stack!"})) 87 | 88 | // console.log("Highlighting", decorations.map(d => [d.range.start, d.range.end])); 89 | 90 | const decorationType = window.createTextEditorDecorationType({ 91 | color: 'red', 92 | textDecoration: 'underline', 93 | 94 | }); 95 | 96 | window.activeTextEditor?.setDecorations(decorationType, decorations); 97 | 98 | setTimeout(() => { 99 | window.activeTextEditor?.setDecorations(decorationType, []); 100 | }, 5000); 101 | } 102 | 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project for testing open source code completion models 2 | 3 | It was forked from [tabnine-vscode](https://github.com/codota/tabnine-vscode) & modified for making it compatible with open source code models on [hf.co/models](https://huggingface.co/models) 4 | 5 | ## Installing 6 | 7 | Install just like any other [vscode extension](https://marketplace.visualstudio.com/items?itemName=HuggingFace.huggingface-vscode). 8 | 9 | By default, this extension is using [bigcode/starcoder](https://huggingface.co/bigcode/starcoder) & [Hugging Face Inference API](https://huggingface.co/inference-api) for the inference. However, you can [configure](#configuring) to make inference requests to your custom endpoint that is not Hugging Face Inference API. Thus, if you are using the default Hugging Face Inference AP inference, you'd need to provide [HF API Token](#hf-api-token). 10 | 11 | #### HF API token 12 | 13 | You can supply your HF API token ([hf.co/settings/token](https://hf.co/settings/token)) with this command: 14 | 1. `Cmd/Ctrl+Shift+P` to open VSCode command palette 15 | 2. Type: `Hugging Face Code: Set API token` 16 | 17 | 18 | 19 | ## Testing 20 | 21 | 1. Create a new python file 22 | 2. Try typing `def main():` 23 | 24 | 25 | 26 | #### Checking if the generated code in in [The Stack](https://huggingface.co/datasets/bigcode/the-stack) 27 | 28 | Hit `Ctrl+Esc` to check if the generated code is in in [The Stack](https://huggingface.co/datasets/bigcode/the-stack). 29 | This is a rapid first-pass attribution check using [stack.dataportraits.org](https://stack.dataportraits.org). 30 | We check for sequences of at least 50 characters that match a Bloom filter. 31 | This means false positives are possible and long enough surrounding context is necesssary (see the [paper](https://dataportraits.org/) for details on n-gram striding and sequence length). 32 | [The dedicated Stack search tool](https://hf.co/spaces/bigcode/search) is a full dataset index and can be used for a complete second pass. 33 | 34 | 35 | ## Developing 36 | Make sure you've [installed yarn](https://yarnpkg.com/getting-started/install) on your system. 37 | 1. Clone this repo: `git clone https://github.com/huggingface/huggingface-vscode` 38 | 2. Install deps: `cd huggingface-vscode && yarn install --frozen-lockfile` 39 | 3. In vscode, open `Run and Debug` side bar & click `Launch Extension` 40 | 41 | ## Checking output 42 | 43 | You can see input to & output from the code generation API: 44 | 45 | 1. Open VSCode `OUTPUT` panel 46 | 2. Choose `Hugging Face Code` 47 | 48 | 49 | 50 | ## Configuring 51 | 52 | You can configure: endpoint to where request will be sent and special tokens. 53 | 54 | 55 | 56 | Example: 57 | 58 | Let's say your current code is this: 59 | ```py 60 | import numpy as np 61 | import scipy as sp 62 | {YOUR_CURSOR_POSITION} 63 | def hello_word(): 64 | print("Hello world") 65 | ``` 66 | 67 | Then, the request body will look like: 68 | ```js 69 | const inputs = `{start token}import numpy as np\nimport scipy as sp\n{middle token}def hello_word():\n print("Hello world"){end token}` 70 | const data = {inputs, parameters:{max_new_tokens:256}}; 71 | 72 | const res = await fetch(endpoint, { 73 | body: JSON.stringify(data), 74 | headers, 75 | method: "POST" 76 | }); 77 | ``` 78 | -------------------------------------------------------------------------------- /src/inlineSuggestions/textListener.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Position, 3 | TextDocument, 4 | TextDocumentChangeEvent, 5 | TextDocumentContentChangeEvent, 6 | TextLine, 7 | } from "vscode"; 8 | import { debounce } from "debounce"; 9 | import { 10 | getCurrentSuggestion, 11 | setSuggestionsState, 12 | } from "./inlineSuggestionState"; 13 | import runCompletion from "../runCompletion"; 14 | import setInlineSuggestion from "./setInlineSuggestion"; 15 | import clearInlineSuggestionsState from "./clearDecoration"; 16 | import { isInSnippetInsertion } from "./snippets/blankSnippet"; 17 | import { URI_SCHEME_FILE } from "../globals/consts"; 18 | import { sleep } from "../utils/utils"; 19 | import { Capability, isCapabilityEnabled } from "../capabilities/capabilities"; 20 | import getCurrentPosition, { 21 | isEmptyLinesWithNewlineAutoInsert, 22 | isOnlyWhitespaces, 23 | } from "./positionExtracter"; 24 | import { AutocompleteResult } from "../binary/requests/requests"; 25 | import { completionIsAllowed } from "../provideCompletionItems"; 26 | 27 | const EMPTY_LINE_WARMUP_MILLIS = 250; 28 | const EMPTY_LINE_DEBOUNCE = 550; 29 | 30 | const debouncedEmptyLinesRequest = debounce( 31 | (document: TextDocument, currentPosition: Position) => { 32 | runCompletion(document, currentPosition) 33 | .then((autocompleteResult) => 34 | setCompletion(autocompleteResult, document, currentPosition) 35 | ) 36 | .catch((err) => 37 | console.error("Could not request completion at empty line: ", err) 38 | ); 39 | }, 40 | EMPTY_LINE_DEBOUNCE 41 | ); 42 | 43 | export default async function textListener({ 44 | document, 45 | contentChanges, 46 | }: TextDocumentChangeEvent): Promise { 47 | const [change] = contentChanges; 48 | if (!change) { 49 | return; 50 | } 51 | const currentTextPosition = getCurrentPosition(change); 52 | if (!completionIsAllowed(document, currentTextPosition)) { 53 | return; 54 | } 55 | 56 | const emptyLinesEnabled = isCapabilityEnabled( 57 | Capability.EMPTY_LINE_SUGGESTIONS 58 | ); 59 | const shouldHandleEmptyLine = 60 | emptyLinesEnabled && 61 | isEmptyLine(change, document.lineAt(currentTextPosition.line)) && 62 | !isInSnippetInsertion(); 63 | 64 | debouncedEmptyLinesRequest.clear(); 65 | if (shouldHandleEmptyLine) { 66 | await runCompletion(document, currentTextPosition); 67 | await sleep(EMPTY_LINE_WARMUP_MILLIS); 68 | debouncedEmptyLinesRequest(document, currentTextPosition); 69 | return; 70 | } 71 | 72 | if ( 73 | (shouldHandleEmptyLine || isSingleTypingChange(contentChanges, change)) && 74 | document.uri.scheme === URI_SCHEME_FILE 75 | ) { 76 | const autocompleteResult = await runCompletion( 77 | document, 78 | currentTextPosition 79 | ); 80 | 81 | await setCompletion(autocompleteResult, document, currentTextPosition); 82 | } 83 | } 84 | 85 | async function setCompletion( 86 | autocompleteResult: AutocompleteResult | null | undefined, 87 | document: TextDocument, 88 | currentTextPosition: Position 89 | ): Promise { 90 | await setSuggestionsState(autocompleteResult); 91 | const currentSuggestion = getCurrentSuggestion(); 92 | if (currentSuggestion) { 93 | await setInlineSuggestion(document, currentTextPosition, currentSuggestion); 94 | return; 95 | } 96 | void clearInlineSuggestionsState(); 97 | } 98 | 99 | function isEmptyLine( 100 | change: TextDocumentContentChangeEvent, 101 | currentLine: TextLine 102 | ): boolean { 103 | return ( 104 | isEmptyLinesWithNewlineAutoInsert(change) || 105 | isOnlyWhitespaces(currentLine.text) 106 | ); 107 | } 108 | 109 | export function isSingleTypingChange( 110 | contentChanges: readonly TextDocumentContentChangeEvent[], 111 | change: TextDocumentContentChangeEvent 112 | ): boolean { 113 | const isSingleSelectionChange = contentChanges.length === 1; 114 | const isSingleCharacterChange = change.text.length === 1; 115 | return isSingleSelectionChange && isSingleCharacterChange; 116 | } 117 | -------------------------------------------------------------------------------- /src/inlineSuggestions/snippets/blankSnippet.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from "os"; 2 | import { commands, Position, Range, SnippetString, window } from "vscode"; 3 | 4 | let snippetBlankRange: Range | undefined; 5 | 6 | export function isInSnippetInsertion(): boolean { 7 | return !!snippetBlankRange; 8 | } 9 | 10 | // for tests only 11 | export function getSnippetBlankRange(): Range | undefined { 12 | return snippetBlankRange; 13 | } 14 | 15 | export async function insertBlankSnippet( 16 | lines: string[], 17 | position: Position 18 | ): Promise { 19 | snippetBlankRange = undefined; 20 | const currentLineText = window.activeTextEditor?.document.lineAt(position) 21 | .text; 22 | const isCurrentLineEmpty = currentLineText?.trim().length === 0; 23 | 24 | if (isCurrentLineEmpty) { 25 | await insertBlankSnippetAtEmptyLine(lines, position); 26 | } else { 27 | await insertBlankSnippetAtNonEmptyLine(lines, position); 28 | } 29 | } 30 | 31 | async function insertBlankSnippetAtEmptyLine( 32 | lines: string[], 33 | position: Position 34 | ): Promise { 35 | const snippet = new SnippetString(" ".repeat(position.character)); 36 | snippet.appendTabstop(0); 37 | snippet.appendText(EOL.repeat(lines.length - 1)); 38 | snippetBlankRange = new Range( 39 | position, 40 | position.translate(lines.length - 1, -position.character) 41 | ); 42 | 43 | await window.activeTextEditor?.insertSnippet( 44 | snippet, 45 | position.with(position.line, 0) 46 | ); 47 | } 48 | 49 | async function insertBlankSnippetAtNonEmptyLine( 50 | lines: string[], 51 | position: Position 52 | ): Promise { 53 | const snippet = new SnippetString(); 54 | 55 | snippet.appendTabstop(0); 56 | snippet.appendText(EOL.repeat(lines.length - 1)); 57 | snippetBlankRange = new Range( 58 | position, 59 | position.translate(lines.length - 1, -position.character) 60 | ); 61 | 62 | await window.activeTextEditor?.insertSnippet( 63 | snippet, 64 | position.with(position.line + 1, 0) 65 | ); 66 | 67 | await moveCursorBackTo(position); 68 | } 69 | 70 | async function moveCursorBackTo(position: Position) { 71 | await commands.executeCommand("cursorMove", { to: "up", by: "line" }); 72 | await commands.executeCommand("cursorMove", { 73 | to: "right", 74 | by: "character", 75 | value: position.character, 76 | }); 77 | } 78 | 79 | export async function removeBlankSnippet(): Promise { 80 | if (snippetBlankRange) { 81 | const fixedRange = calculateStartAfterUserInput(snippetBlankRange); 82 | 83 | const rangeToRemove = fixedRange || snippetBlankRange; 84 | // a workaround to the issue where `insertSnippet` inserts extra indentation 85 | // to the last line: https://github.com/microsoft/vscode/issues/20112 86 | const lastLineText = window.activeTextEditor?.document.lineAt( 87 | rangeToRemove.end 88 | ).text; 89 | const lastLineLength = lastLineText?.length; 90 | await window.activeTextEditor?.edit((editBuilder) => { 91 | editBuilder.delete( 92 | new Range( 93 | rangeToRemove.start, 94 | rangeToRemove.end.translate(0, lastLineLength) 95 | ) 96 | ); 97 | }); 98 | 99 | snippetBlankRange = undefined; 100 | } 101 | } 102 | 103 | function calculateStartAfterUserInput(range: Range): Range | undefined { 104 | const currentPosition = window.activeTextEditor?.selection.active; 105 | const textInsideSnippetBlankRange = window.activeTextEditor?.document.getText( 106 | range 107 | ); 108 | // a space is considered a text too, so trimming all whitespaces yields the wrong result here. 109 | const blankRangeContainsText = 110 | textInsideSnippetBlankRange?.replace(new RegExp(EOL, "g"), "") !== ""; 111 | 112 | if (currentPosition && blankRangeContainsText) { 113 | const linesDiff = currentPosition.line - range.start.line; 114 | const charsDiff = currentPosition.character - range.start.character; 115 | return new Range( 116 | range.start.translate(linesDiff, charsDiff), 117 | range.end.translate(linesDiff) 118 | ); 119 | } 120 | 121 | return undefined; 122 | } 123 | -------------------------------------------------------------------------------- /src/inlineSuggestions/setInlineSuggestion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DecorationOptions, 3 | Position, 4 | Range, 5 | TextDocument, 6 | window, 7 | } from "vscode"; 8 | import { CompletionKind, ResultEntry } from "../binary/requests/requests"; 9 | import { 10 | clearState, 11 | getCurrentPrefix, 12 | getCurrentSuggestion, 13 | } from "./inlineSuggestionState"; 14 | import { trimEnd } from "../utils/utils"; 15 | import { 16 | getSnippetDecorations, 17 | handleClearSnippetDecoration, 18 | } from "./snippets/snippetDecoration"; 19 | 20 | const inlineDecorationType = window.createTextEditorDecorationType({}); 21 | let showingDecoration = false; 22 | 23 | export default async function setInlineSuggestion( 24 | document: TextDocument, 25 | position: Position, 26 | newSuggestion: ResultEntry 27 | ): Promise { 28 | await clearInlineDecoration(); 29 | const prefix = getCurrentPrefix(); 30 | if ( 31 | shouldNotHandleThisSuggestion(prefix, newSuggestion, document, position) 32 | ) { 33 | void clearState(); 34 | return; 35 | } 36 | 37 | const suggestedHint = constructInlineHint( 38 | document, 39 | position, 40 | newSuggestion, 41 | prefix 42 | ); 43 | 44 | void showInlineDecoration(position, suggestedHint); 45 | } 46 | 47 | function shouldNotHandleThisSuggestion( 48 | prefix: string, 49 | newSuggestion: ResultEntry, 50 | document: TextDocument, 51 | position: Position 52 | ) { 53 | return ( 54 | !isMatchingPrefix(prefix, newSuggestion) || 55 | isInTheMiddleOfWord(document, position) 56 | ); 57 | } 58 | 59 | function isInTheMiddleOfWord( 60 | document: TextDocument, 61 | position: Position 62 | ): boolean { 63 | const nextCharacter = document.getText( 64 | new Range(position, position.translate(0, 1)) 65 | ); 66 | return !isClosingCharacter(nextCharacter) && !!nextCharacter.trim(); 67 | } 68 | 69 | function isClosingCharacter(nextCharacter: string) { 70 | const closingCharacters = ['"', "'", "`", "]", ")", "}", ">"]; 71 | return closingCharacters.includes(nextCharacter); 72 | } 73 | 74 | function isMatchingPrefix(prefix: string, newSuggestion: ResultEntry): boolean { 75 | return newSuggestion.new_prefix?.includes(prefix); 76 | } 77 | 78 | function constructInlineHint( 79 | document: TextDocument, 80 | position: Position, 81 | newSuggestion: ResultEntry, 82 | prefix: string | undefined 83 | ): string { 84 | const suggestionWithoutPrefix = clearPrefixFromSuggestion( 85 | newSuggestion?.new_prefix || "", 86 | prefix || "" 87 | ); 88 | const existingSuffix = document.getText( 89 | new Range(position, position.translate(0, suggestionWithoutPrefix.length)) 90 | ); 91 | return trimEnd(suggestionWithoutPrefix, existingSuffix); 92 | } 93 | 94 | function clearPrefixFromSuggestion(currentCompletion: string, prefix: string) { 95 | return currentCompletion?.replace(prefix, ""); 96 | } 97 | 98 | async function showInlineDecoration( 99 | position: Position, 100 | suggestion: string 101 | ): Promise { 102 | const currentCompletionKind = getCurrentSuggestion()?.completion_metadata 103 | ?.completion_kind; 104 | const decorations = 105 | currentCompletionKind === CompletionKind.Snippet 106 | ? await getSnippetDecorations(position, suggestion) 107 | : getOneLineDecorations(suggestion, position); 108 | 109 | window.activeTextEditor?.setDecorations(inlineDecorationType, decorations); 110 | showingDecoration = true; 111 | } 112 | 113 | function getOneLineDecorations( 114 | suggestion: string, 115 | position: Position 116 | ): DecorationOptions[] { 117 | const decorations: DecorationOptions[] = []; 118 | decorations.push({ 119 | renderOptions: { 120 | after: { 121 | color: "gray", 122 | contentText: suggestion, 123 | margin: `0 0 0 0`, 124 | textDecoration: "none; white-space: pre;", 125 | }, 126 | }, 127 | range: new Range(position, position), 128 | }); 129 | decorations.push({ 130 | range: new Range( 131 | position, 132 | position.translate(undefined, suggestion.length) 133 | ), 134 | }); 135 | return decorations; 136 | } 137 | 138 | export async function clearInlineDecoration(): Promise { 139 | window.activeTextEditor?.setDecorations(inlineDecorationType, []); 140 | await handleClearSnippetDecoration(); 141 | showingDecoration = false; 142 | } 143 | 144 | export function isShowingDecoration(): boolean { 145 | return showingDecoration; 146 | } 147 | -------------------------------------------------------------------------------- /src/runCompletion.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, TextDocument, WorkspaceConfiguration, workspace, window, env, Uri } from "vscode"; 2 | import {URL} from "url"; 3 | import fetch from "node-fetch"; 4 | import { AutocompleteResult, ResultEntry } from "./binary/requests/requests"; 5 | import { CHAR_LIMIT, FULL_BRAND_REPRESENTATION } from "./globals/consts"; 6 | import languages from "./globals/languages"; 7 | import { setDefaultStatus, setLoadingStatus } from "./statusBar/statusBar"; 8 | import { logInput, logOutput } from "./outputChannels"; 9 | import { getTabnineExtensionContext } from "./globals/tabnineExtensionContext"; 10 | 11 | export type CompletionType = "normal" | "snippet"; 12 | 13 | let didShowTokenWarning = false; 14 | 15 | export default async function runCompletion( 16 | document: TextDocument, 17 | position: Position, 18 | timeout?: number, 19 | currentSuggestionText = "" 20 | ): Promise { 21 | setLoadingStatus(FULL_BRAND_REPRESENTATION); 22 | const offset = document.offsetAt(position); 23 | const beforeStartOffset = Math.max(0, offset - CHAR_LIMIT); 24 | // const afterEndOffset = offset + CHAR_LIMIT; 25 | const beforeStart = document.positionAt(beforeStartOffset); 26 | // const afterEnd = document.positionAt(afterEndOffset); 27 | const prefix = document.getText(new Range(beforeStart, position)) + currentSuggestionText; 28 | // const suffix = document.getText(new Range(position, afterEnd)); 29 | 30 | type Config = WorkspaceConfiguration & { 31 | modelIdOrEndpoint: string; 32 | isFillMode: boolean; 33 | startToken: string; 34 | middleToken: string; 35 | endToken: string; 36 | stopToken: string; 37 | temperature: number; 38 | }; 39 | const config: Config = workspace.getConfiguration("HuggingFaceCode") as Config; 40 | const { modelIdOrEndpoint, stopToken, temperature } = config; 41 | 42 | const context = getTabnineExtensionContext(); 43 | const apiToken = await context?.secrets.get("apiToken"); 44 | 45 | let endpoint = "" 46 | try{ 47 | new URL(modelIdOrEndpoint); 48 | endpoint = modelIdOrEndpoint; 49 | }catch(e){ 50 | endpoint = `https://api-inference.huggingface.co/models/${modelIdOrEndpoint}` 51 | 52 | // if user hasn't supplied API Token yet, ask user to supply one 53 | if(!apiToken && !didShowTokenWarning){ 54 | didShowTokenWarning = true; 55 | void window.showInformationMessage(`In order to use "${modelIdOrEndpoint}" through Hugging Face API Inference, you'd need Hugging Face API Token`, 56 | "Get your token" 57 | ).then(clicked => { 58 | if (clicked) { 59 | void env.openExternal(Uri.parse("https://github.com/huggingface/huggingface-vscode#hf-api-token")); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | const inputs = prefix; 66 | 67 | const data = { 68 | inputs, 69 | parameters: { 70 | max_new_tokens: 60, 71 | temperature, 72 | do_sample: temperature > 0, 73 | top_p: 0.95, 74 | stop: [stopToken] 75 | } 76 | }; 77 | 78 | logInput(inputs, data.parameters); 79 | 80 | const headers = { 81 | "Content-Type": "application/json", 82 | "Authorization": "", 83 | }; 84 | if(apiToken){ 85 | headers.Authorization = `Bearer ${apiToken}`; 86 | } 87 | 88 | const res = await fetch(endpoint, { 89 | method: "POST", 90 | headers, 91 | body: JSON.stringify(data), 92 | }); 93 | 94 | if(!res.ok){ 95 | console.error("Error sending a request", res.status, res.statusText); 96 | setDefaultStatus(); 97 | return null; 98 | } 99 | 100 | const generatedTextRaw = getGeneratedText(await res.json()); 101 | 102 | let generatedText = generatedTextRaw.replace(stopToken, ""); 103 | if(generatedText.slice(0, inputs.length) === inputs){ 104 | generatedText = generatedText.slice(inputs.length); 105 | } 106 | 107 | const resultEntry: ResultEntry = { 108 | new_prefix: generatedText, 109 | old_suffix: "", 110 | new_suffix: "" 111 | } 112 | 113 | const result: AutocompleteResult = { 114 | results: [resultEntry], 115 | old_prefix: "", 116 | user_message: [], 117 | is_locked: false, 118 | } 119 | 120 | setDefaultStatus(); 121 | logOutput(generatedTextRaw); 122 | return result; 123 | } 124 | 125 | function getGeneratedText(json: any): string{ 126 | return json?.generated_text ?? json?.[0].generated_text ?? ""; 127 | } 128 | 129 | export type KnownLanguageType = keyof typeof languages; 130 | 131 | export function getLanguageFileExtension( 132 | languageId: string 133 | ): string | undefined { 134 | return languages[languageId as KnownLanguageType]; 135 | } 136 | 137 | export function getFileNameWithExtension(document: TextDocument): string { 138 | const { languageId, fileName } = document; 139 | if (!document.isUntitled) { 140 | return fileName; 141 | } 142 | const extension = getLanguageFileExtension(languageId); 143 | if (extension) { 144 | return fileName.concat(extension); 145 | } 146 | return fileName; 147 | } 148 | -------------------------------------------------------------------------------- /src/vscode.proposed.inlineCompletions.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module "vscode" { 7 | // https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima 8 | 9 | export namespace languages { 10 | /** 11 | * Registers an inline completion provider. 12 | */ 13 | export function registerInlineCompletionItemProvider( 14 | selector: DocumentSelector, 15 | provider: InlineCompletionItemProvider 16 | ): Disposable; 17 | } 18 | 19 | export interface InlineCompletionItemProvider< 20 | T extends InlineCompletionItem = InlineCompletionItem 21 | > { 22 | /** 23 | * Provides inline completion items for the given position and document. 24 | * If inline completions are enabled, this method will be called whenever the user stopped typing. 25 | * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. 26 | * Use `context.triggerKind` to distinguish between these scenarios. 27 | */ 28 | provideInlineCompletionItems( 29 | document: TextDocument, 30 | position: Position, 31 | context: InlineCompletionContext, 32 | token: CancellationToken 33 | ): ProviderResult | T[]>; 34 | } 35 | 36 | export interface InlineCompletionContext { 37 | /** 38 | * How the completion was triggered. 39 | */ 40 | readonly triggerKind: InlineCompletionTriggerKind; 41 | 42 | /** 43 | * Provides information about the currently selected item in the autocomplete widget if it is visible. 44 | * 45 | * If set, provided inline completions must extend the text of the selected item 46 | * and use the same range, otherwise they are not shown as preview. 47 | * As an example, if the document text is `console.` and the selected item is `.log` replacing the `.` in the document, 48 | * the inline completion must also replace `.` and start with `.log`, for example `.log()`. 49 | * 50 | * Inline completion providers are requested again whenever the selected item changes. 51 | * 52 | * The user must configure `"editor.suggest.preview": true` for this feature. 53 | */ 54 | readonly selectedCompletionInfo: SelectedCompletionInfo | undefined; 55 | } 56 | 57 | export interface SelectedCompletionInfo { 58 | range: Range; 59 | text: string; 60 | completionKind: CompletionItemKind; 61 | isSnippetText: boolean; 62 | } 63 | 64 | /** 65 | * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. 66 | */ 67 | export enum InlineCompletionTriggerKind { 68 | /** 69 | * Completion was triggered automatically while editing. 70 | * It is sufficient to return a single completion item in this case. 71 | */ 72 | Automatic = 0, 73 | 74 | /** 75 | * Completion was triggered explicitly by a user gesture. 76 | * Return multiple completion items to enable cycling through them. 77 | */ 78 | Explicit = 1, 79 | } 80 | 81 | export class InlineCompletionList< 82 | T extends InlineCompletionItem = InlineCompletionItem 83 | > { 84 | items: T[]; 85 | 86 | constructor(items: T[]); 87 | } 88 | 89 | export class InlineCompletionItem { 90 | /** 91 | * The text to replace the range with. 92 | * 93 | * The text the range refers to should be a prefix of this value and must be a subword (`AB` and `BEF` are subwords of `ABCDEF`, but `Ab` is not). 94 | */ 95 | text: string; 96 | 97 | /** 98 | * The range to replace. 99 | * Must begin and end on the same line. 100 | * 101 | * Prefer replacements over insertions to avoid cache invalidation: 102 | * Instead of reporting a completion that inserts an extension at the end of a word, 103 | * the whole word should be replaced with the extended word. 104 | */ 105 | range?: Range; 106 | 107 | /** 108 | * An optional {@link Command} that is executed *after* inserting this completion. 109 | */ 110 | command?: Command; 111 | 112 | constructor(text: string, range?: Range, command?: Command); 113 | } 114 | 115 | /** 116 | * Be aware that this API will not ever be finalized. 117 | */ 118 | export namespace window { 119 | export function getInlineCompletionItemController< 120 | T extends InlineCompletionItem 121 | >(provider: InlineCompletionItemProvider): InlineCompletionController; 122 | } 123 | 124 | /** 125 | * Be aware that this API will not ever be finalized. 126 | */ 127 | export interface InlineCompletionController { 128 | /** 129 | * Is fired when an inline completion item is shown to the user. 130 | */ 131 | // eslint-disable-next-line vscode-dts-event-naming 132 | readonly onDidShowCompletionItem: Event< 133 | InlineCompletionItemDidShowEvent 134 | >; 135 | } 136 | 137 | /** 138 | * Be aware that this API will not ever be finalized. 139 | */ 140 | export interface InlineCompletionItemDidShowEvent< 141 | T extends InlineCompletionItem 142 | > { 143 | completionItem: T; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/globals/tabnineExtensionProperties.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const EXTENSION_SUBSTRING = "huggingface-vscode"; 4 | const TELEMETRY_CONFIG_ID = "telemetry"; 5 | const TELEMETRY_CONFIG_ENABLED_ID = "enableTelemetry"; 6 | 7 | type ColorCustomizations = { 8 | "statusBar.background": string; 9 | }; 10 | 11 | interface TabNineExtensionProperties { 12 | extensionPath: string | undefined; 13 | version: string | undefined; 14 | name: string; 15 | vscodeVersion: string; 16 | isTabNineAutoImportEnabled: number | boolean; 17 | isTypeScriptAutoImports: boolean | undefined; 18 | isJavaScriptAutoImports: boolean | undefined; 19 | id: string | undefined; 20 | logFilePath: string; 21 | logLevel: string | undefined; 22 | isRemote: boolean; 23 | remoteName: string; 24 | extensionKind: number; 25 | themeKind: string; 26 | themeName: string | undefined; 27 | statusBarColorCustomizations: string | undefined; 28 | isInstalled: boolean; 29 | isVscodeTelemetryEnabled: boolean; 30 | isExtensionBetaChannelEnabled: boolean; 31 | isVscodeInsiders: boolean; 32 | codeReviewBaseUrl: string; 33 | isVscodeInlineAPIEnabled: boolean | undefined; 34 | } 35 | 36 | function getContext(): TabNineExtensionProperties { 37 | const extension: 38 | | vscode.Extension 39 | | undefined = vscode.extensions.all.find((x) => 40 | x.id.includes(EXTENSION_SUBSTRING) 41 | ); 42 | const configuration = vscode.workspace.getConfiguration(); 43 | const isJavaScriptAutoImports = configuration.get( 44 | "javascript.suggest.autoImports" 45 | ); 46 | const isTypeScriptAutoImports = configuration.get( 47 | "typescript.suggest.autoImports" 48 | ); 49 | const autoImportConfig = "tabnine.experimentalAutoImports"; 50 | const logFilePath = configuration.get("tabnine.logFilePath"); 51 | const logLevel = configuration.get("tabnine.logLevel"); 52 | let isTabNineAutoImportEnabled = configuration.get( 53 | autoImportConfig 54 | ); 55 | const { remoteName } = vscode.env as { remoteName: string }; 56 | const { extensionKind } = extension as { extensionKind: number }; 57 | const isRemote = !!remoteName && extensionKind === 2; 58 | const isInstalled = isTabNineAutoImportEnabled === null; 59 | 60 | if (isTabNineAutoImportEnabled !== false) { 61 | isTabNineAutoImportEnabled = true; 62 | void configuration.update( 63 | autoImportConfig, 64 | isTabNineAutoImportEnabled, 65 | true 66 | ); 67 | } 68 | const isExtensionBetaChannelEnabled = 69 | configuration.get("tabnine.receiveBetaChannelUpdates") || false; 70 | 71 | const isVscodeInsiders = vscode.env.appName 72 | .toLocaleLowerCase() 73 | .includes("insider"); 74 | 75 | return { 76 | get extensionPath(): string | undefined { 77 | return extension?.extensionPath; 78 | }, 79 | 80 | get version(): string | undefined { 81 | return (extension?.packageJSON as { version: string }).version; 82 | }, 83 | get id() { 84 | return extension?.id; 85 | }, 86 | 87 | get name(): string { 88 | return `${EXTENSION_SUBSTRING}-${this.version ?? "unknown"}`; 89 | }, 90 | get vscodeVersion(): string { 91 | return vscode.version; 92 | }, 93 | get isTabNineAutoImportEnabled(): boolean | number { 94 | return !!isTabNineAutoImportEnabled; 95 | }, 96 | get isJavaScriptAutoImports(): boolean | undefined { 97 | return isJavaScriptAutoImports; 98 | }, 99 | get isTypeScriptAutoImports(): boolean | undefined { 100 | return isTypeScriptAutoImports; 101 | }, 102 | get logFilePath(): string { 103 | return logFilePath ? `${logFilePath}-${process.pid}` : ""; 104 | }, 105 | get logLevel(): string | undefined { 106 | return logLevel; 107 | }, 108 | get isRemote(): boolean { 109 | return isRemote; 110 | }, 111 | get remoteName(): string { 112 | return remoteName; 113 | }, 114 | get extensionKind(): number { 115 | return extensionKind; 116 | }, 117 | get themeKind(): string { 118 | return vscode.ColorThemeKind[vscode.window.activeColorTheme.kind]; 119 | }, 120 | get themeName(): string | undefined { 121 | const workbenchConfig = getWorkbenchSettings(); 122 | return workbenchConfig.get("colorTheme"); 123 | }, 124 | get statusBarColorCustomizations(): string | undefined { 125 | const workbenchConfig = getWorkbenchSettings(); 126 | const colorCustomizations = workbenchConfig.get( 127 | "colorCustomizations" 128 | ); 129 | return colorCustomizations?.["statusBar.background"]; 130 | }, 131 | get isInstalled(): boolean { 132 | return isInstalled; 133 | }, 134 | get isVscodeTelemetryEnabled(): boolean { 135 | // This peace of code is taken from https://github.com/microsoft/vscode-extension-telemetry/blob/260c7c3a5a47322a43e8fcfce66cd96e85b886ae/src/telemetryReporter.ts#L46 136 | const telemetrySectionConfig = vscode.workspace.getConfiguration( 137 | TELEMETRY_CONFIG_ID 138 | ); 139 | const isTelemetryEnabled = telemetrySectionConfig.get( 140 | TELEMETRY_CONFIG_ENABLED_ID, 141 | true 142 | ); 143 | 144 | return isTelemetryEnabled; 145 | }, 146 | get isExtensionBetaChannelEnabled(): boolean { 147 | return isExtensionBetaChannelEnabled; 148 | }, 149 | get isVscodeInsiders(): boolean { 150 | return isVscodeInsiders; 151 | }, 152 | get codeReviewBaseUrl(): string { 153 | return ( 154 | configuration.get("tabnine.codeReviewBaseUrl") ?? 155 | "https://api.tabnine.com/code-review/" 156 | ); 157 | }, 158 | get isVscodeInlineAPIEnabled(): boolean | undefined { 159 | const INLINE_API_KEY = "editor.inlineSuggest.enabled"; 160 | if (configuration.has(INLINE_API_KEY)) { 161 | return configuration.get(INLINE_API_KEY, false); 162 | } 163 | return undefined; 164 | }, 165 | }; 166 | } 167 | 168 | function getWorkbenchSettings() { 169 | return vscode.workspace.getConfiguration("workbench"); 170 | } 171 | 172 | const tabnineExtensionProperties: TabNineExtensionProperties = getContext(); 173 | 174 | export default tabnineExtensionProperties; 175 | -------------------------------------------------------------------------------- /src/globals/consts.ts: -------------------------------------------------------------------------------- 1 | export const TABNINE_URL_QUERY_PARAM = "tabnineUrl"; 2 | export const API_VERSION = "4.4.223"; 3 | export const BINARY_UPDATE_URL = "https://update.tabnine.com/bundles"; 4 | export const BINARY_UPDATE_VERSION_FILE_URL = `${BINARY_UPDATE_URL}/version`; 5 | export const ATTRIBUTION_BRAND = ""; 6 | export const BRAND_NAME = "Hugging Face Code"; 7 | export const LIMITATION_SYMBOL = "🔒"; 8 | export const FULL_BRAND_REPRESENTATION = ATTRIBUTION_BRAND + BRAND_NAME; 9 | export const BUNDLE_DOWNLOAD_FAILURE_MESSAGE = 10 | "Tabnine Extension was unable to download its dependencies. Please check your internet connection. If you use a proxy server, please visit https://code.visualstudio.com/docs/setup/network"; 11 | export const OPEN_ISSUE_BUTTON = "Open issue"; 12 | export const OPEN_NETWORK_SETUP_HELP = "Help"; 13 | export const DOWNLOAD_RETRY = "Retry"; 14 | export const RELOAD_BUTTON = "Reload"; 15 | export const OPEN_ISSUE_LINK = 16 | "https://github.com/codota/tabnine-vscode/issues/new"; 17 | export const STATUS_NAME = "Tabnine"; 18 | 19 | export const INSTRUMENTATION_KEY = ""; 20 | 21 | export const CHAR_LIMIT = 4_000; 22 | export const MAX_NUM_RESULTS = 5; 23 | export const CONSECUTIVE_RESTART_THRESHOLD = 100; 24 | export const REQUEST_FAILURES_THRESHOLD = 20; 25 | export const WAIT_BEFORE_RESTART_MILLIS = 1_000; // 1 second 26 | export const DELAY_FOR_CODE_ACTION_PROVIDER = 800; 27 | // Env variable is to make the tests faster. It is not set in production environment. 28 | export const BINARY_STARTUP_GRACE = +( 29 | process.env.BINARY_NOTIFICATION_POLLING_INTERVAL || 9_000 30 | ); // 9 seconds 31 | 32 | export const BINARY_NOTIFICATION_POLLING_INTERVAL = +( 33 | process.env.BINARY_NOTIFICATION_POLLING_INTERVAL || 10_000 34 | ); // 10 seconds 35 | 36 | export const BINARY_STATUS_BAR_POLLING_INTERVAL = +( 37 | process.env.BINARY_STATUS_BAR_POLLING_INTERVAL || 60 * 60 * 1_000 38 | ); // one hour 39 | 40 | export const BINARY_STATUS_BAR_FIRST_MESSAGE_POLLING_INTERVAL = +( 41 | process.env.BINARY_NOTIFICATION_POLLING_INTERVAL || 10_000 42 | ); // 10 seconds 43 | 44 | export const STATUS_BAR_NOTIFICATION_PERIOD = +( 45 | process.env.STATUS_BAR_NOTIFICATION_PERIOD || 2 * 60 * 1_000 46 | ); // 2 minutes 47 | 48 | export const STATUS_BAR_FIRST_TIME_CLICKED = "status-bar-first-time-clicked"; 49 | 50 | export const OPEN_LP_FROM_STATUS_BAR = "tabnine:open_lp"; 51 | export const INSTALL_COMMAND = "workbench.extensions.installExtension"; 52 | export const LATEST_RELEASE_URL = 53 | "https://api.github.com/repos/codota/tabnine-vscode/releases"; 54 | export const MINIMAL_SUPPORTED_VSCODE_API = "1.35.0"; 55 | export const ALPHA_VERSION_KEY = "tabnine.alpha.version"; 56 | export const BETA_CHANNEL_MESSAGE_SHOWN_KEY = 57 | "tabnine.joinBetaChannelMessageShown"; 58 | 59 | export const DEFAULT_DETAIL = BRAND_NAME; 60 | export const PROGRESS_KEY = "tabnine.hide.progress"; 61 | 62 | export const COMPLETION_TRIGGERS = [ 63 | " ", 64 | ".", 65 | "(", 66 | ")", 67 | "{", 68 | "}", 69 | "[", 70 | "]", 71 | ",", 72 | ":", 73 | "'", 74 | '"', 75 | "=", 76 | "<", 77 | ">", 78 | "/", 79 | "\\", 80 | "+", 81 | "-", 82 | "|", 83 | "&", 84 | "*", 85 | "%", 86 | "=", 87 | "$", 88 | "#", 89 | "@", 90 | "!", 91 | ]; 92 | 93 | export enum StateType { 94 | ERROR = "error", 95 | INFO = "info", 96 | PROGRESS = "progress", 97 | STATUS = "status", 98 | PALLETTE = "pallette", 99 | NOTIFICATION = "notification", 100 | STARTUP = "startup", 101 | TREE_VIEW = "treeView", 102 | AUTH = "auth", 103 | } 104 | 105 | export enum StatePayload { 106 | MESSAGE = "Message", 107 | STATE = "State", 108 | NOTIFICATION_SHOWN = "NotificationShown", 109 | STATUS_SHOWN = "StatusShown", 110 | HOVER_SHOWN = "HoverShown", 111 | HINT_SHOWN = "HintShown", 112 | SNIPPET_SHOWN = "SnippetShown", 113 | } 114 | 115 | export enum MessageActionsEnum { 116 | NONE = "None", 117 | OPEN_HUB = "OpenHub", 118 | OPEN_LP = "OpenLp", 119 | OPEN_BUY = "OpenBuy", 120 | OPEN_SIGNUP = "OpenSignup", 121 | OPEN_NOTIFICATIONS = "OpenNotifications", 122 | OPEN_NOTIFICATIONS_IN_HUB = "OpenNotificationsInHub", 123 | ENABLE_ADVANCED_COMPLETIONS = "EnableAdvancedCompletions", 124 | } 125 | 126 | export interface OpenHubWithAction { 127 | OpenHubWith: { 128 | query_params: [string, string][]; 129 | path: string; 130 | }; 131 | } 132 | 133 | export type MessageAction = MessageActionsEnum | OpenHubWithAction; 134 | export const NOTIFICATIONS_OPEN_QUERY_PARAM = "notifications=open"; 135 | 136 | const SLEEP_TIME_BETWEEN_ATTEMPTS = 1000; // 1 second 137 | const MAX_SLEEP_TIME_BETWEEN_ATTEMPTS = 60 * 60 * 1000; // 1 hour 138 | 139 | export function restartBackoff(attempt: number): number { 140 | return Math.min( 141 | SLEEP_TIME_BETWEEN_ATTEMPTS * 2 ** Math.min(attempt, 10), 142 | MAX_SLEEP_TIME_BETWEEN_ATTEMPTS 143 | ); 144 | } 145 | 146 | export const IS_OSX = process.platform === "darwin"; 147 | 148 | export const SLEEP_TIME_BEFORE_OPEN_HUB = 0; 149 | 150 | export const ACCEPT_INLINE_COMMAND = "tabnine.accept-inline-suggestion"; 151 | export const TAB_OVERRIDE_COMMAND = "tabnine.tab-override"; 152 | export const ESCAPE_INLINE_COMMAND = "tabnine.escape-inline-suggestion"; 153 | export const ATTRIBUTION_COMMAND = "tabnine.attribution"; 154 | export const NEXT_INLINE_COMMAND = "tabnine.next-inline-suggestion"; 155 | export const PREV_INLINE_COMMAND = "tabnine.prev-inline-suggestion"; 156 | export const SNIPPET_COMMAND = "tabnine.snippet-suggestion"; 157 | export const BIGCODE_OPEN_WEB_COMMAND = "tabnine:open-app"; 158 | export const TABNINE_OPEN_GETTING_STARTED_COMMAND = 159 | "tabnine:open-getting-started"; 160 | export const TABNINE_NOTIFICATIONS_FOCUS_COMMAND = 161 | "tabnine-notifications.focus"; 162 | export const TABNINE_HOME_FOCUS_COMMAND = "tabnine-home.focus"; 163 | 164 | export const BIGCODE_PROJECT_URL = "https://www.bigcode-project.org"; 165 | export const TABNINE_SITE_URL = "https://tabnine.com"; 166 | export const TABNINE_GETTING_STARTED_FOR_VSCODE_URL = `${TABNINE_SITE_URL}/getting-started/ide?client=vscode`; 167 | 168 | export const URI_SCHEME_FILE = "file"; 169 | export const BINARY_RESTART_EVENT = "binary-restart-event"; 170 | export const MAX_SMALL_INTEGER_V8 = 2 ** 30; // Max number that can be stored in V8's smis (small integers) 171 | export const LOCAL_ADDRESSES = ["localhost", "127.0.0.1"]; 172 | 173 | export enum SuggestionTrigger { 174 | DocumentChanged = "DocumentChanged", 175 | LookAhead = "LookAhead", 176 | } 177 | 178 | export const OUTPUT_CHANNEL_NAME = "Hugging Face Code"; -------------------------------------------------------------------------------- /src/provideCompletionItems.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { 3 | AutocompleteResult, 4 | MarkdownStringSpec, 5 | ResultEntry, 6 | } from "./binary/requests/requests"; 7 | import { 8 | ATTRIBUTION_BRAND, 9 | BRAND_NAME, 10 | DEFAULT_DETAIL, 11 | LIMITATION_SYMBOL, 12 | } from "./globals/consts"; 13 | import tabnineExtensionProperties from "./globals/tabnineExtensionProperties"; 14 | import runCompletion from "./runCompletion"; 15 | import { COMPLETION_IMPORTS } from "./selectionHandler"; 16 | import { escapeTabStopSign } from "./utils/utils"; 17 | 18 | const INCOMPLETE = true; 19 | 20 | export default async function provideCompletionItems( 21 | document: vscode.TextDocument, 22 | position: vscode.Position 23 | ): Promise { 24 | return new vscode.CompletionList( 25 | await completionsListFor(document, position), 26 | INCOMPLETE 27 | ); 28 | } 29 | 30 | async function completionsListFor( 31 | document: vscode.TextDocument, 32 | position: vscode.Position 33 | ): Promise { 34 | try { 35 | if (!completionIsAllowed(document, position)) { 36 | return []; 37 | } 38 | 39 | const response = await runCompletion(document, position); 40 | 41 | if (!response || response?.results.length === 0) { 42 | return []; 43 | } 44 | 45 | const limit = 46 | showFew(response, document, position) || response.is_locked 47 | ? 1 48 | : response.results.length; 49 | 50 | return response.results.slice(0, limit).map((entry, index) => 51 | makeCompletionItem({ 52 | document, 53 | index, 54 | position, 55 | detailMessage: extractDetailMessage(response), 56 | oldPrefix: response?.old_prefix, 57 | entry, 58 | results: response?.results, 59 | limited: response?.is_locked, 60 | }) 61 | ); 62 | } catch (e) { 63 | console.error(`Error setting up request: ${e}`); 64 | 65 | return []; 66 | } 67 | } 68 | 69 | function extractDetailMessage(response: AutocompleteResult) { 70 | return (response.user_message || []).join("\n") || DEFAULT_DETAIL; 71 | } 72 | 73 | function makeCompletionItem(args: { 74 | document: vscode.TextDocument; 75 | index: number; 76 | position: vscode.Position; 77 | detailMessage: string; 78 | oldPrefix: string; 79 | entry: ResultEntry; 80 | results: ResultEntry[]; 81 | limited: boolean; 82 | }): vscode.CompletionItem { 83 | const item = new vscode.CompletionItem( 84 | ATTRIBUTION_BRAND + args.entry.new_prefix 85 | ); 86 | if (args.limited) { 87 | item.detail = `${LIMITATION_SYMBOL} ${BRAND_NAME}`; 88 | } else { 89 | item.detail = BRAND_NAME; 90 | } 91 | 92 | item.sortText = String.fromCharCode(0) + String.fromCharCode(args.index); 93 | item.insertText = new vscode.SnippetString( 94 | escapeTabStopSign(args.entry.new_prefix) 95 | ); 96 | 97 | item.filterText = args.entry.new_prefix; 98 | item.preselect = args.index === 0; 99 | item.kind = args.entry.completion_metadata?.kind; 100 | item.range = new vscode.Range( 101 | args.position.translate(0, -args.oldPrefix.length), 102 | args.position.translate(0, args.entry.old_suffix.length) 103 | ); 104 | 105 | if (tabnineExtensionProperties.isTabNineAutoImportEnabled) { 106 | item.command = { 107 | arguments: [ 108 | { 109 | currentCompletion: args.entry.new_prefix, 110 | completions: args.results, 111 | position: args.position, 112 | limited: args.limited, 113 | oldPrefix: args.oldPrefix, 114 | }, 115 | ], 116 | command: COMPLETION_IMPORTS, 117 | title: "accept completion", 118 | }; 119 | } 120 | 121 | if (args.entry.new_suffix) { 122 | item.insertText 123 | .appendTabstop(0) 124 | .appendText(escapeTabStopSign(args.entry.new_suffix)); 125 | } 126 | 127 | if (args.entry.completion_metadata?.documentation) { 128 | item.documentation = formatDocumentation( 129 | args.entry.completion_metadata?.documentation 130 | ); 131 | } 132 | 133 | return item; 134 | } 135 | 136 | function formatDocumentation( 137 | documentation: string | MarkdownStringSpec 138 | ): string | vscode.MarkdownString { 139 | if (isMarkdownStringSpec(documentation)) { 140 | if (documentation.kind === "markdown") { 141 | return new vscode.MarkdownString(documentation.value); 142 | } 143 | return documentation.value; 144 | } 145 | return documentation; 146 | } 147 | 148 | function isMarkdownStringSpec( 149 | x: string | MarkdownStringSpec 150 | ): x is MarkdownStringSpec { 151 | return !(typeof x === "string"); 152 | } 153 | 154 | export function completionIsAllowed( 155 | document: vscode.TextDocument, 156 | position: vscode.Position 157 | ): boolean { 158 | const configuration = vscode.workspace.getConfiguration(); 159 | const disableLineRegex = getMisnamedConfigPropertyValue( 160 | "tabnine.disableLineRegex", 161 | "tabnine.disable_line_regex", 162 | configuration 163 | ); 164 | 165 | const line = document.getText( 166 | new vscode.Range( 167 | position.with({ character: 0 }), 168 | position.with({ character: 500 }) 169 | ) 170 | ); 171 | 172 | if (disableLineRegex.some((r) => new RegExp(r).test(line))) { 173 | return false; 174 | } 175 | 176 | const disableFileRegex = getMisnamedConfigPropertyValue( 177 | "tabnine.disableFileRegex", 178 | "tabnine.disable_file_regex", 179 | configuration 180 | ); 181 | 182 | return !disableFileRegex.some((r) => new RegExp(r).test(document.fileName)); 183 | } 184 | 185 | function getMisnamedConfigPropertyValue( 186 | properPropName: string, 187 | propMisname: string, 188 | configuration: vscode.WorkspaceConfiguration 189 | ): string[] { 190 | let disableLineRegex = configuration.get(properPropName); 191 | if (!disableLineRegex || !disableLineRegex.length) { 192 | disableLineRegex = configuration.get(propMisname); 193 | } 194 | 195 | if (disableLineRegex === undefined) { 196 | disableLineRegex = []; 197 | } 198 | 199 | return disableLineRegex; 200 | } 201 | 202 | function showFew( 203 | response: AutocompleteResult, 204 | document: vscode.TextDocument, 205 | position: vscode.Position 206 | ): boolean { 207 | if ( 208 | response.results.some( 209 | (entry) => 210 | entry.completion_metadata?.kind || 211 | entry.completion_metadata?.documentation 212 | ) 213 | ) { 214 | return false; 215 | } 216 | 217 | const leftPoint = position.translate(0, -response.old_prefix.length); 218 | const tail = document.getText( 219 | new vscode.Range(document.lineAt(leftPoint).range.start, leftPoint) 220 | ); 221 | 222 | return tail.endsWith(".") || tail.endsWith("::"); 223 | } 224 | -------------------------------------------------------------------------------- /src/inlineSuggestions/registerHandlers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commands, 3 | Disposable, 4 | languages, 5 | TextEditor, 6 | TextEditorSelectionChangeEvent, 7 | TextEditorSelectionChangeKind, 8 | window, 9 | workspace, 10 | } from "vscode"; 11 | import { Capability, isCapabilityEnabled } from "../capabilities/capabilities"; 12 | import { 13 | ACCEPT_INLINE_COMMAND, 14 | ESCAPE_INLINE_COMMAND, 15 | ATTRIBUTION_COMMAND, 16 | NEXT_INLINE_COMMAND, 17 | PREV_INLINE_COMMAND, 18 | SNIPPET_COMMAND, 19 | } from "../globals/consts"; 20 | import enableProposed from "../globals/proposedAPI"; 21 | import { initTracker } from "./documentChangesTracker"; 22 | import acceptInlineSuggestion from "./acceptInlineSuggestion"; 23 | import clearInlineSuggestionsState from "./clearDecoration"; 24 | import { getNextSuggestion, getPrevSuggestion } from "./inlineSuggestionState"; 25 | import setInlineSuggestion, { 26 | isShowingDecoration, 27 | } from "./setInlineSuggestion"; 28 | import snippetAutoTriggerHandler from "./snippets/autoTriggerHandler"; 29 | import { isInSnippetInsertion } from "./snippets/blankSnippet"; 30 | import requestSnippet from "./snippets/snippetProvider"; 31 | import textListener from "./textListener"; 32 | import { 33 | isInlineSuggestionProposedApiSupported, 34 | isInlineSuggestionReleasedApiSupported, 35 | } from "../globals/versions"; 36 | import highlightStackAttributions from "./highlightStackAttributions"; 37 | 38 | function isSnippetAutoTriggerEnabled() { 39 | return isCapabilityEnabled(Capability.SNIPPET_AUTO_TRIGGER); 40 | } 41 | 42 | async function isDefaultAPIEnabled(): Promise { 43 | return ( 44 | (isCapabilityEnabled(Capability.SNIPPET_SUGGESTIONS_CONFIGURABLE) || 45 | isCapabilityEnabled(Capability.VSCODE_INLINE_V2)) && 46 | isInlineSuggestionProposedApiSupported() && 47 | (await enableProposed()) 48 | ); 49 | } 50 | 51 | export default async function registerInlineHandlers( 52 | inlineEnabled: boolean, 53 | snippetsEnabled: boolean 54 | ): Promise { 55 | const subscriptions: Disposable[] = []; 56 | 57 | if (!inlineEnabled && !snippetsEnabled) return subscriptions; 58 | 59 | if ( 60 | isInlineSuggestionReleasedApiSupported() || 61 | (await isDefaultAPIEnabled()) 62 | ) { 63 | const provideInlineCompletionItems = ( 64 | await import("../provideInlineCompletionItems") 65 | ).default; 66 | const inlineCompletionsProvider = { 67 | provideInlineCompletionItems, 68 | }; 69 | subscriptions.push( 70 | languages.registerInlineCompletionItemProvider( 71 | { pattern: "**" }, 72 | inlineCompletionsProvider 73 | ), 74 | ...initTracker(), 75 | registerAttributionHandler() 76 | ); 77 | return subscriptions; 78 | } 79 | 80 | if (inlineEnabled) { 81 | subscriptions.push(await enableInlineSuggestionsContext()); 82 | subscriptions.push(registerTextChangeHandler()); 83 | } 84 | 85 | if (snippetsEnabled) { 86 | subscriptions.push(await enableSnippetSuggestionsContext()); 87 | 88 | if (isSnippetAutoTriggerEnabled()) { 89 | subscriptions.push(registerSnippetAutoTriggerHandler()); 90 | } 91 | 92 | subscriptions.push(registerSnippetHandler()); 93 | } 94 | 95 | subscriptions.push( 96 | registerAcceptHandler(), 97 | registerEscapeHandler(), 98 | registerNextHandler(), 99 | registerPrevHandler() 100 | ); 101 | 102 | subscriptions.push(registerCursorChangeHandler()); 103 | 104 | return subscriptions; 105 | } 106 | 107 | function registerCursorChangeHandler(): Disposable { 108 | return window.onDidChangeTextEditorSelection( 109 | (e: TextEditorSelectionChangeEvent) => { 110 | const inSnippetInsertion = isInSnippetInsertion(); 111 | const showingDecoration = isShowingDecoration(); 112 | const inTheMiddleOfConstructingSnippet = 113 | inSnippetInsertion && !showingDecoration; 114 | 115 | if ( 116 | !inTheMiddleOfConstructingSnippet && 117 | e.kind !== TextEditorSelectionChangeKind.Command 118 | ) { 119 | void clearInlineSuggestionsState(); 120 | } 121 | } 122 | ); 123 | } 124 | 125 | function registerTextChangeHandler(): Disposable { 126 | return workspace.onDidChangeTextDocument(textListener); 127 | } 128 | 129 | function registerSnippetAutoTriggerHandler(): Disposable { 130 | return workspace.onDidChangeTextDocument(snippetAutoTriggerHandler); 131 | } 132 | 133 | function registerSnippetHandler(): Disposable { 134 | return commands.registerTextEditorCommand( 135 | `${SNIPPET_COMMAND}`, 136 | ({ document, selection }: TextEditor) => 137 | void requestSnippet(document, selection.active) 138 | ); 139 | } 140 | 141 | function registerPrevHandler(): Disposable { 142 | return commands.registerTextEditorCommand( 143 | `${PREV_INLINE_COMMAND}`, 144 | ({ document, selection }: TextEditor) => { 145 | const prevSuggestion = getPrevSuggestion(); 146 | if (prevSuggestion) { 147 | void setInlineSuggestion(document, selection.active, prevSuggestion); 148 | } 149 | } 150 | ); 151 | } 152 | 153 | function registerNextHandler(): Disposable { 154 | return commands.registerTextEditorCommand( 155 | `${NEXT_INLINE_COMMAND}`, 156 | ({ document, selection }: TextEditor) => { 157 | const nextSuggestion = getNextSuggestion(); 158 | if (nextSuggestion) { 159 | void setInlineSuggestion(document, selection.active, nextSuggestion); 160 | } 161 | } 162 | ); 163 | } 164 | 165 | function registerEscapeHandler(): Disposable { 166 | return commands.registerTextEditorCommand(`${ESCAPE_INLINE_COMMAND}`, () => { 167 | void clearInlineSuggestionsState(); 168 | }); 169 | } 170 | 171 | function registerAttributionHandler(): Disposable { 172 | return commands.registerTextEditorCommand(`${ATTRIBUTION_COMMAND}`, () => { 173 | void highlightStackAttributions(); 174 | }); 175 | } 176 | 177 | function registerAcceptHandler(): Disposable { 178 | return commands.registerTextEditorCommand( 179 | `${ACCEPT_INLINE_COMMAND}`, 180 | (editor: TextEditor) => { 181 | void acceptInlineSuggestion(editor); 182 | } 183 | ); 184 | } 185 | 186 | async function enableInlineSuggestionsContext(): Promise { 187 | await commands.executeCommand( 188 | "setContext", 189 | "tabnine.inline-suggestion:enabled", 190 | true 191 | ); 192 | 193 | return { 194 | dispose() { 195 | void commands.executeCommand( 196 | "setContext", 197 | "tabnine.inline-suggestion:enabled", 198 | undefined 199 | ); 200 | }, 201 | }; 202 | } 203 | 204 | async function enableSnippetSuggestionsContext(): Promise { 205 | await commands.executeCommand( 206 | "setContext", 207 | "tabnine.snippet-suggestion:enabled", 208 | true 209 | ); 210 | 211 | return { 212 | dispose() { 213 | void commands.executeCommand( 214 | "setContext", 215 | "tabnine.snippet-suggestion:enabled", 216 | undefined 217 | ); 218 | }, 219 | }; 220 | } 221 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "HuggingFace", 3 | "name": "huggingface-vscode", 4 | "version": "0.0.24", 5 | "displayName": "HF Code Autocomplete", 6 | "description": "AI Autocomplete for OSS code-gen models", 7 | "icon": "small_logo.png", 8 | "author": "Mishig Davaadorj (https://hf.co/)", 9 | "license": "License at https://github.com/huggingface/huggingface-vscode", 10 | "galleryBanner": { 11 | "color": "#100f11", 12 | "theme": "dark" 13 | }, 14 | "badges": [ 15 | { 16 | "url": "https://img.shields.io/github/stars/huggingface/huggingface-vscode?style=social", 17 | "description": "Star huggingface-vscode on Github", 18 | "href": "https://github.com/huggingface/huggingface-vscode" 19 | }, 20 | { 21 | "url": "https://img.shields.io/twitter/follow/huggingface?style=social", 22 | "description": "Follow Huggingface on Twitter", 23 | "href": "https://twitter.com/huggingface" 24 | } 25 | ], 26 | "homepage": "https://hf.co", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/huggingface/huggingface-vscode" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/huggingface/huggingface-vscode/issues", 33 | "email": "mishig@hf.co" 34 | }, 35 | "engines": { 36 | "vscode": "^1.50.0" 37 | }, 38 | "categories": [ 39 | "Programming Languages", 40 | "Snippets", 41 | "Other" 42 | ], 43 | "keywords": [ 44 | "javascript", 45 | "python", 46 | "typescript", 47 | "php", 48 | "autocomplete", 49 | "ruby", 50 | "java", 51 | "go", 52 | "golang", 53 | "bash", 54 | "kotlin", 55 | "html", 56 | "css", 57 | "ocaml", 58 | "perl", 59 | "rust", 60 | "julia", 61 | "lua", 62 | "haskell", 63 | "c", 64 | "cpp", 65 | "c++", 66 | "csharp", 67 | "c#", 68 | "react", 69 | "swift", 70 | "objective-c", 71 | "objectivec", 72 | "ai", 73 | "method completion", 74 | "intellicode", 75 | "intellisense", 76 | "snippets", 77 | "kite", 78 | "nodejs", 79 | "node", 80 | "node.js", 81 | "jupyter" 82 | ], 83 | "activationEvents": [ 84 | "*" 85 | ], 86 | "main": "./out/extension", 87 | "scripts": { 88 | "analyze:bundle": "webpack --env analyzeBundle", 89 | "vscode:prepublish": "yarn --frozen-lockfile && yarn clear-out && webpack --mode production", 90 | "compile": "webpack --mode development --watch", 91 | "build": "webpack --mode development", 92 | "test:copyassets": "ncp ./src/test/fixture ./out/test/fixture/", 93 | "prettier": "prettier --write src/", 94 | "prettier:check": "prettier --check src/", 95 | "lint": "eslint . --max-warnings 0", 96 | "lint:fix": "eslint . --fix", 97 | "test:prepare": "yarn clear-out && tsc && yarn test:copyassets", 98 | "test": "yarn test:prepare && node ./out/test/runTest.js", 99 | "vsce:package": "vsce package", 100 | "vsce:publish": "vsce publish", 101 | "ovsx:publish": "ovsx publish", 102 | "teamcity:test": "tsc && node ./out/test/runTest.js", 103 | "clear-out": "rimraf ./out" 104 | }, 105 | "peerDependencies": { 106 | "vscode": "^1.1.37" 107 | }, 108 | "devDependencies": { 109 | "@types/chai": "^4.2.14", 110 | "@types/debounce": "^1.2.1", 111 | "@types/diff": "^5.0.2", 112 | "@types/glob": "^7.1.3", 113 | "@types/mocha": "^8.2.2", 114 | "@types/mock-fs": "^4.13.0", 115 | "@types/ncp": "^2.0.4", 116 | "@types/node": "^12.0.10", 117 | "@types/node-fetch": "^2.6.3", 118 | "@types/rimraf": "^3.0.0", 119 | "@types/semver": "^7.3.4", 120 | "@types/sinon": "^9.0.11", 121 | "@types/tmp": "^0.2.0", 122 | "@types/vscode": "^1.50.0", 123 | "@types/yauzl": "^2.9.1", 124 | "@typescript-eslint/eslint-plugin": "^5.45.0", 125 | "@typescript-eslint/parser": "^4.18.0", 126 | "assert": "^2.0.0", 127 | "chai": "^4.2.0", 128 | "chai-shallow-deep-equal": "^1.4.6", 129 | "deep-object-diff": "^1.1.0", 130 | "eslint": "^8.28.0", 131 | "eslint-config-airbnb-typescript": "^12.3.1", 132 | "eslint-config-prettier": "^8.2.0", 133 | "eslint-import-resolver-typescript": "^3.5.2", 134 | "eslint-plugin-import": "^2.26.0", 135 | "glob": "^7.1.6", 136 | "husky": "^5.1.2", 137 | "lint-staged": "^13.0.4", 138 | "mocha": "^10.1.0", 139 | "mocha-teamcity-reporter": "^3.0.0", 140 | "mock-fs": "^4.13.0", 141 | "ncp": "^2.0.0", 142 | "ovsx": "^0.5.2", 143 | "prettier": "2.2.1", 144 | "rimraf": "^3.0.2", 145 | "sinon": "^10.0.0", 146 | "terser-webpack-plugin": "^5.3.6", 147 | "ts-loader": "^9.4.1", 148 | "ts-mockito": "^2.6.1", 149 | "typescript": "^4.2.2", 150 | "vsce": "^1.93.0", 151 | "vscode-test": "^1.6.1", 152 | "webpack": "^5.75.0", 153 | "webpack-bundle-analyzer": "^4.7.0", 154 | "webpack-cli": "^5.0.0" 155 | }, 156 | "dependencies": { 157 | "axios": "^0.21.0", 158 | "debounce": "^1.2.1", 159 | "diff": "^5.0.0", 160 | "eslint-plugin-no-only-tests": "^3.1.0", 161 | "extract-zip": "^2.0.1", 162 | "https-proxy-agent": "^5.0.0", 163 | "node-fetch": "^3.3.0", 164 | "semver": "^7.3.2", 165 | "systeminformation": "^5.6.10", 166 | "tmp": "^0.2.1", 167 | "vscode-extension-telemetry": "^0.1.7" 168 | }, 169 | "capabilities": { 170 | "virtualWorkspaces": true, 171 | "untrustedWorkspaces": { 172 | "supported": true 173 | } 174 | }, 175 | "contributes": { 176 | "commands": [ 177 | { 178 | "command": "HuggingFaceCode::setApiToken", 179 | "title": "Hugging Face Code: Set API token" 180 | }, 181 | { 182 | "command": "tabnine.attribution", 183 | "title": "Hugging Face Code: Show Code Attribution" 184 | } 185 | ], 186 | "viewsContainers": { 187 | "activitybar": [ 188 | { 189 | "id": "tabnine-access", 190 | "title": "Hugging Face Code", 191 | "icon": "small_logo.png" 192 | } 193 | ] 194 | }, 195 | "views": { 196 | "tabnine-access": [ 197 | { 198 | "id": "tabnine-home", 199 | "name": "Quick Access", 200 | "when": "tabnine.tabnine-navigation-ready" 201 | } 202 | ] 203 | }, 204 | "configuration": [ 205 | { 206 | "title": "Hugging Face Code", 207 | "properties": { 208 | "HuggingFaceCode.modelIdOrEndpoint": { 209 | "type": "string", 210 | "default": "bigcode/starcoder", 211 | "description": "Supply huggignface model id (ex: `bigcode/starcoder`) or custom endpoint (ex: https://bigcode-large-xl.eu.ngrok.io/generate) to which request will be sent to. When huggingface model id is supplied, hugging face API inference will be used." 212 | }, 213 | "HuggingFaceCode.isFillMode": { 214 | "type": "boolean", 215 | "default": true, 216 | "description": "Whether to send to inference server: codes that are on top of cursor only (isFillMode=false) OR codes that are both above & below the cursor (isFillMode=true)" 217 | }, 218 | "HuggingFaceCode.startToken": { 219 | "type": "string", 220 | "default": "", 221 | "description": "String that is sent to server is in format: {startToken}{code above cursor}{middleToken}{code below cursor if isFillMode=true}{endToken}. Leave startToken, middleToken, or endToken empty if there is no special token for those placements." 222 | }, 223 | "HuggingFaceCode.middleToken": { 224 | "type": "string", 225 | "default": "", 226 | "description": "String that is sent to server is in format: {startToken}{code above cursor}{middleToken}{code below cursor if isFillMode=true}{endToken}. Leave startToken, middleToken, or endToken empty if there is no special token for those placements." 227 | }, 228 | "HuggingFaceCode.endToken": { 229 | "type": "string", 230 | "default": "", 231 | "description": "String that is sent to server is in format: {startToken}{code above cursor}{middleToken}{code below cursor if isFillMode=true}{endToken}. Leave startToken, middleToken, or endToken empty if there is no special token for those placements." 232 | }, 233 | "HuggingFaceCode.temperature": { 234 | "type": "float", 235 | "default": 0.2, 236 | "description": "Sampling temperature" 237 | }, 238 | "HuggingFaceCode.stopToken": { 239 | "type": "string", 240 | "default": "<|endoftext|>", 241 | "description": "(Optional) Stop token" 242 | }, 243 | "HuggingFaceCode.attributionWindowSize": { 244 | "type": "integer", 245 | "default": 250, 246 | "description": "Number of characters to scan for code attribution" 247 | } 248 | } 249 | } 250 | ], 251 | "keybindings": [ 252 | { 253 | "key": "tab", 254 | "command": "tabnine.accept-inline-suggestion", 255 | "when": "tabnine.snippet-suggestion:enabled && tabnine.in-inline-suggestions || tabnine.inline-suggestion:enabled && tabnine.in-inline-suggestions" 256 | }, 257 | { 258 | "key": "tab", 259 | "command": "tabnine.tab-override", 260 | "when": "tabnine.tab-override && suggestWidgetHasFocusedSuggestion && suggestWidgetVisible && textInputFocus" 261 | }, 262 | { 263 | "key": "tab", 264 | "command": "editor.action.inlineSuggest.commit", 265 | "when": "tabnine.tab-override && inlineSuggestionVisible && !editorTabMovesFocus" 266 | }, 267 | { 268 | "key": "ctrl+escape", 269 | "command": "tabnine.attribution" 270 | }, 271 | { 272 | "key": "ctrl+z", 273 | "mac": "cmd+z", 274 | "command": "tabnine.escape-inline-suggestion", 275 | "when": "tabnine.snippet-suggestion:enabled && tabnine.in-inline-suggestions || tabnine.inline-suggestion:enabled && tabnine.in-inline-suggestions" 276 | }, 277 | { 278 | "key": "alt+]", 279 | "command": "tabnine.next-inline-suggestion", 280 | "when": "tabnine.snippet-suggestion:enabled && tabnine.in-inline-suggestions || tabnine.inline-suggestion:enabled && tabnine.in-inline-suggestions" 281 | }, 282 | { 283 | "key": "alt+[", 284 | "command": "tabnine.prev-inline-suggestion", 285 | "when": "tabnine.snippet-suggestion:enabled && tabnine.in-inline-suggestions || tabnine.inline-suggestion:enabled && tabnine.in-inline-suggestions" 286 | }, 287 | { 288 | "key": "alt+.", 289 | "mac": "ctrl+.", 290 | "command": "tabnine.snippet-suggestion", 291 | "when": "tabnine.snippet-suggestion:enabled" 292 | } 293 | ] 294 | }, 295 | "husky": { 296 | "hooks": { 297 | "pre-commit": "lint-staged" 298 | } 299 | }, 300 | "lint-staged": { 301 | "*.{ts,js,css,md}": "prettier --write src/" 302 | }, 303 | "__metadata": { 304 | "id": "75da638c-c45a-44ea-aa3b-8570a3559810", 305 | "publisherDisplayName": "TabNine", 306 | "publisherId": "1924b661-7c19-45d9-9800-edeb32848fd7", 307 | "isPreReleaseVersion": false 308 | } 309 | } 310 | --------------------------------------------------------------------------------