diff --git a/src/enterprise/consts.ts b/src/enterprise/consts.ts index a0db452be2..2b83e6a245 100644 --- a/src/enterprise/consts.ts +++ b/src/enterprise/consts.ts @@ -30,3 +30,5 @@ export const UPDATE_PREFIX = "/update/vscode"; export const EXTENSION_SUBSTRING = "tabnine-vscode"; export const EXTENSION_ID = `tabnine-vscode-self-hosted-updater`; + +export const TABNINE_ENTERPISE_CONTEXT_KEY = "tabnine.enterprise"; diff --git a/src/enterprise/extension.ts b/src/enterprise/extension.ts index 2d39fc5553..68cd9820f5 100644 --- a/src/enterprise/extension.ts +++ b/src/enterprise/extension.ts @@ -31,13 +31,13 @@ import { TABNINE_HOST_CONFIGURATION, EXTENSION_ID, OPEN_SETTINGS_COMMAND, + TABNINE_ENTERPISE_CONTEXT_KEY, } from "./consts"; import TabnineAuthenticationProvider from "../authentication/TabnineAuthenticationProvider"; import { BRAND_NAME, CONFIG_COMMAND, ENTERPRISE_BRAND_NAME, - IS_SELF_HOSTED_CONTEXT_KEY, } from "../globals/consts"; import { StatusBar } from "./statusBar"; import { isHealthyServer } from "./update/isHealthyServer"; @@ -53,15 +53,15 @@ import { emptyStateAuthenticateView } from "../tabnineChatWidget/webviews/emptyS import { emptyStateNotPartOfATeamView } from "../tabnineChatWidget/webviews/emptyStateNotPartOfATeamView"; import BINARY_STATE from "../binary/binaryStateSingleton"; import { activeTextEditorState } from "../activeTextEditorState"; - -const TABNINE_ENTERPISE_CONTEXT_KEY = "tabnine.enterprise"; +import { ChatAPI } from "../tabnineChatWidget/ChatApi"; +import ChatViewProvider from "../tabnineChatWidget/ChatViewProvider"; export async function activate( context: vscode.ExtensionContext ): Promise { Logger.init(context); setTabnineExtensionContext(context); - context.subscriptions.push(await setEnterpriseContext(context)); + context.subscriptions.push(await setEnterpriseContext()); context.subscriptions.push(new WorkspaceUpdater()); context.subscriptions.push(BINARY_STATE); context.subscriptions.push(activeTextEditorState); @@ -129,7 +129,18 @@ export async function activate( const chatEnabledState = new SelfHostedChatEnabledState(context); context.subscriptions.push(chatEnabledState); - registerTabnineChatWidgetWebview(context, chatEnabledState, server); + registerTabnineChatWidgetWebview( + context, + chatEnabledState, + new ChatViewProvider( + context, + new ChatAPI(context, { + serverUrl: server, + isSelfHosted: true, + isTelemetryEnabled: false, + }) + ) + ); await initBinary([ "--no_bootstrap", @@ -143,18 +154,14 @@ export async function activate( context.subscriptions.push(await registerInlineProvider()); } -async function setEnterpriseContext( - context: vscode.ExtensionContext -): Promise { +async function setEnterpriseContext(): Promise { await vscode.commands.executeCommand( "setContext", TABNINE_ENTERPISE_CONTEXT_KEY, true ); - await context.workspaceState.update(IS_SELF_HOSTED_CONTEXT_KEY, true); return new vscode.Disposable(() => { - void context.workspaceState.update(IS_SELF_HOSTED_CONTEXT_KEY, undefined); void vscode.commands.executeCommand( "setContext", TABNINE_ENTERPISE_CONTEXT_KEY, diff --git a/src/extension.ts b/src/extension.ts index 5fa65cf994..c71d8110a7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -58,6 +58,8 @@ import { WorkspaceUpdater } from "./WorkspaceUpdater"; import SaasChatEnabledState from "./tabnineChatWidget/SaasChatEnabledState"; import BINARY_STATE from "./binary/binaryStateSingleton"; import EvalSaasChatEnabledState from "./tabnineChatWidget/EvalSaasChatEnabledState"; +import { ChatAPI } from "./tabnineChatWidget/ChatApi"; +import ChatViewProvider from "./tabnineChatWidget/ChatViewProvider"; export async function activate( context: vscode.ExtensionContext @@ -152,9 +154,17 @@ async function backgroundInit(context: vscode.ExtensionContext) { registerTabnineChatWidgetWebview( context, chatEnabledState, - context.extensionMode === vscode.ExtensionMode.Test - ? process.env.CHAT_SERVER_URL - : undefined + new ChatViewProvider( + context, + new ChatAPI(context, { + serverUrl: + context.extensionMode === vscode.ExtensionMode.Test + ? process.env.CHAT_SERVER_URL + : undefined, + isSelfHosted: false, + isTelemetryEnabled: isCapabilityEnabled(Capability.ALPHA_CAPABILITY), + }) + ) ); pollNotifications(context); pollStatuses(context); diff --git a/src/globals/consts.ts b/src/globals/consts.ts index eb3db23ec5..ce61126163 100644 --- a/src/globals/consts.ts +++ b/src/globals/consts.ts @@ -178,6 +178,5 @@ export enum SuggestionTrigger { } export const TLS_CONFIG_MIN_SUPPORTED_VERSION = "4.22.0"; -export const IS_SELF_HOSTED_CONTEXT_KEY = "tabnine.isSelfHosted"; export const CONFIG_COMMAND = "TabNine::config"; export const EXTENSION_ID = "TabNine.tabnine-vscode"; diff --git a/src/tabnineChatWidget/ChatApi.ts b/src/tabnineChatWidget/ChatApi.ts index 21ae6e4b8f..988f83b366 100644 --- a/src/tabnineChatWidget/ChatApi.ts +++ b/src/tabnineChatWidget/ChatApi.ts @@ -7,16 +7,11 @@ import { getCapabilities, } from "../binary/requests/requests"; import { sendEvent } from "../binary/requests/sendEvent"; -import { chatEventRegistry } from "./chatEventRegistry"; import { InserCode, insertTextAtCursor } from "./handlers/insertAtCursor"; -import { Capability, isCapabilityEnabled } from "../capabilities/capabilities"; import { resolveSymbols } from "./handlers/resolveSymbols"; import { peekDefinition } from "./handlers/peekDefinition"; import { ServiceLevel } from "../binary/state"; -import { - GET_CHAT_STATE_COMMAND, - IS_SELF_HOSTED_CONTEXT_KEY, -} from "../globals/consts"; +import { GET_CHAT_STATE_COMMAND } from "../globals/consts"; import { BasicContext, getBasicContext, @@ -35,6 +30,7 @@ import { navigateToLocation, } from "./handlers/navigateToLocation"; import { getWorkspaceRootPaths } from "../utils/workspaceFolders"; +import { EventRegistry } from "./EventRegistry"; type GetUserResponse = { token: string; @@ -94,162 +90,176 @@ type WorkspaceFolders = { const CHAT_CONVERSATIONS_KEY = "CHAT_CONVERSATIONS"; const CHAT_SETTINGS_KEY = "CHAT_SETTINGS"; -export function initChatApi( - context: vscode.ExtensionContext, - onInit: () => void, - serverUrl?: string -) { - if (process.env.IS_EVAL_MODE === "true") { - context.subscriptions.push( - vscode.commands.registerCommand( - GET_CHAT_STATE_COMMAND, +type APIConfig = { + serverUrl?: string | undefined; + isSelfHosted: boolean; + isTelemetryEnabled: boolean; +}; + +export class ChatAPI { + private ready = new vscode.EventEmitter(); + + private chatEventRegistry = new EventRegistry(); + + public onReady = new Promise((resolve) => { + this.ready.event(resolve); + }); + + constructor(context: vscode.ExtensionContext, config: APIConfig) { + if (process.env.IS_EVAL_MODE === "true") { + context.subscriptions.push( + vscode.commands.registerCommand( + GET_CHAT_STATE_COMMAND, + () => + context.globalState.get(CHAT_CONVERSATIONS_KEY, { + conversations: {}, + }) as ChatState + ) + ); + } + + this.chatEventRegistry + .registerEvent("init", async () => { + this.ready.fire(); + return Promise.resolve({ + ide: "vscode", + isDarkTheme: [ + ColorThemeKind.HighContrast, + ColorThemeKind.Dark, + ].includes(vscode.window.activeColorTheme.kind), + ...config, + }); + }) + .registerEvent("get_user", async () => { + const state = await getState(); + if (!state) { + throw new Error("state is undefined"); + } + if (!state.access_token) { + throw new Error("state has no access token"); + } + return { + token: state.access_token, + username: state.user_name, + avatarUrl: state.user_avatar_url, + serviceLevel: state.service_level, + }; + }) + .registerEvent( + "get_capabilities", + async () => { + const capabilitiesResponse = await getCapabilities(); + if (!capabilitiesResponse) { + throw new Error("capabilities response is undefined"); + } + return { + enabledFeatures: capabilitiesResponse.enabled_features, + }; + } + ) + .registerEvent( + "send_event", + async (req: SendEventRequest) => { + await sendEvent({ + name: req.eventName, + properties: req.properties, + }); + } + ) + .registerEvent("get_basic_context", getBasicContext) + .registerEvent< + EnrichingContextRequestPayload, + EnrichingContextResponsePayload + >("get_enriching_context", getEnrichingContext) + .registerEvent( + "get_selected_code", + getSelectedCode + ) + .registerEvent("insert_at_cursor", insertTextAtCursor) + .registerEvent< + { symbol: string }, + vscode.SymbolInformation[] | undefined + >("resolve_symbols", resolveSymbols) + .registerEvent<{ symbols: vscode.SymbolInformation[] }, void>( + "peek_definition", + peekDefinition + ) + .registerEvent( + "navigate_to_location", + navigateToLocation + ) + .registerEvent( + "update_chat_conversation", + async (conversation) => { + const chatState = context.globalState.get(CHAT_CONVERSATIONS_KEY, { + conversations: {}, + }) as ChatState; + chatState.conversations[conversation.id] = { + id: conversation.id, + messages: conversation.messages, + }; + await context.globalState.update(CHAT_CONVERSATIONS_KEY, chatState); + } + ) + .registerEvent( + "get_chat_state", () => context.globalState.get(CHAT_CONVERSATIONS_KEY, { conversations: {}, }) as ChatState ) - ); - } - - chatEventRegistry - .registerEvent("init", async () => { - onInit(); - return Promise.resolve({ - ide: "vscode", - isDarkTheme: [ - ColorThemeKind.HighContrast, - ColorThemeKind.Dark, - ].includes(vscode.window.activeColorTheme.kind), - isTelemetryEnabled: isCapabilityEnabled(Capability.ALPHA_CAPABILITY), - serverUrl, - isSelfHosted: context.workspaceState.get( - IS_SELF_HOSTED_CONTEXT_KEY, - false - ), - }); - }) - .registerEvent("get_user", async () => { - const state = await getState(); - if (!state) { - throw new Error("state is undefined"); - } - if (!state.access_token) { - throw new Error("state has no access token"); - } - return { - token: state.access_token, - username: state.user_name, - avatarUrl: state.user_avatar_url, - serviceLevel: state.service_level, - }; - }) - .registerEvent( - "get_capabilities", - async () => { - const capabilitiesResponse = await getCapabilities(); - if (!capabilitiesResponse) { - throw new Error("capabilities response is undefined"); - } - return { - enabledFeatures: capabilitiesResponse.enabled_features, - }; - } - ) - .registerEvent( - "send_event", - async (req: SendEventRequest) => { - await sendEvent({ - name: req.eventName, - properties: req.properties, - }); - } - ) - .registerEvent("get_basic_context", getBasicContext) - .registerEvent< - EnrichingContextRequestPayload, - EnrichingContextResponsePayload - >("get_enriching_context", getEnrichingContext) - .registerEvent( - "get_selected_code", - getSelectedCode - ) - .registerEvent("insert_at_cursor", insertTextAtCursor) - .registerEvent<{ symbol: string }, vscode.SymbolInformation[] | undefined>( - "resolve_symbols", - resolveSymbols - ) - .registerEvent<{ symbols: vscode.SymbolInformation[] }, void>( - "peek_definition", - peekDefinition - ) - .registerEvent( - "navigate_to_location", - navigateToLocation - ) - .registerEvent( - "update_chat_conversation", - async (conversation) => { - const chatState = context.globalState.get(CHAT_CONVERSATIONS_KEY, { + .registerEvent("clear_all_chat_conversations", async () => + context.globalState.update(CHAT_CONVERSATIONS_KEY, { conversations: {}, - }) as ChatState; - chatState.conversations[conversation.id] = { - id: conversation.id, - messages: conversation.messages, - }; - await context.globalState.update(CHAT_CONVERSATIONS_KEY, chatState); - } - ) - .registerEvent( - "get_chat_state", - () => - context.globalState.get(CHAT_CONVERSATIONS_KEY, { - conversations: {}, - }) as ChatState - ) - .registerEvent("clear_all_chat_conversations", async () => - context.globalState.update(CHAT_CONVERSATIONS_KEY, { - conversations: {}, - }) - ) - .registerEvent( - "get_settings", - () => context.globalState.get(CHAT_SETTINGS_KEY, {}) as ChatSettings - ) - .registerEvent( - "update_settings", - async (chatSettings) => { - await context.globalState.update(CHAT_SETTINGS_KEY, chatSettings); - } - ) - .registerEvent( - "get_server_url", - async (request) => { - const serverUri = vscode.Uri.parse( - await getChatCommunicatorAddress(request.kind) - ); - - let externalServerUrl = ( - await vscode.env.asExternalUri(serverUri) - ).toString(); - - if (externalServerUrl.endsWith("/")) { - externalServerUrl = externalServerUrl.slice(0, -1); + }) + ) + .registerEvent( + "get_settings", + () => context.globalState.get(CHAT_SETTINGS_KEY, {}) as ChatSettings + ) + .registerEvent( + "update_settings", + async (chatSettings) => { + await context.globalState.update(CHAT_SETTINGS_KEY, chatSettings); + } + ) + .registerEvent( + "get_server_url", + async (request) => { + const serverUri = vscode.Uri.parse( + await getChatCommunicatorAddress(request.kind) + ); + + let externalServerUrl = ( + await vscode.env.asExternalUri(serverUri) + ).toString(); + + if (externalServerUrl.endsWith("/")) { + externalServerUrl = externalServerUrl.slice(0, -1); + } + + return { + serverUrl: externalServerUrl, + }; } + ) + .registerEvent( + "workspace_folders", + () => { + const rootPaths = getWorkspaceRootPaths(); + if (!rootPaths) return undefined; - return { - serverUrl: externalServerUrl, - }; - } - ) - .registerEvent( - "workspace_folders", - () => { - const rootPaths = getWorkspaceRootPaths(); - if (!rootPaths) return undefined; + return { + rootPaths, + }; + } + ); + } - return { - rootPaths, - }; - } - ); + async handleEvent( + event: string, + requestPayload: Req + ): Promise { + return this.chatEventRegistry.handleEvent(event, requestPayload); + } } diff --git a/src/tabnineChatWidget/ChatViewProvider.ts b/src/tabnineChatWidget/ChatViewProvider.ts index 601fe842d9..39169dd77a 100644 --- a/src/tabnineChatWidget/ChatViewProvider.ts +++ b/src/tabnineChatWidget/ChatViewProvider.ts @@ -3,8 +3,7 @@ import * as path from "path"; import * as fs from "fs"; import axios from "axios"; import { ExtensionContext, WebviewView, WebviewViewProvider } from "vscode"; -import { chatEventRegistry } from "./chatEventRegistry"; -import { initChatApi } from "./ChatApi"; +import { ChatAPI } from "./ChatApi"; import { Logger } from "../utils/logger"; import { fireEvent } from "../binary/requests/requests"; @@ -22,17 +21,8 @@ export default class ChatViewProvider implements WebviewViewProvider { private extensionPath: string; - private isChatInitiated: boolean = false; - - constructor(private context: ExtensionContext, serverUrl?: string) { + constructor(private context: ExtensionContext, private chatApi: ChatAPI) { this.extensionPath = context.extensionPath; - initChatApi( - context, - () => { - this.isChatInitiated = true; - }, - serverUrl - ); } private init() { @@ -49,7 +39,7 @@ export default class ChatViewProvider implements WebviewViewProvider { this.chatWebview.onDidReceiveMessage( async (message: RequestMessage) => { try { - const payload = await chatEventRegistry.handleEvent( + const payload = await this.chatApi.handleEvent( message.command, message.data ); @@ -111,15 +101,8 @@ export default class ChatViewProvider implements WebviewViewProvider { }); } - waitForChatInitiated(): Promise { - return new Promise((resolve) => { - const interval = setInterval(() => { - if (this.isChatInitiated) { - clearInterval(interval); - resolve(); - } - }, 200); - }); + waitForChatInitiated(): Promise { + return this.chatApi.onReady; } resolveWebviewView(webviewView: WebviewView): void | Thenable { diff --git a/src/tabnineChatWidget/chatEventRegistry.ts b/src/tabnineChatWidget/chatEventRegistry.ts deleted file mode 100644 index 6aba9d266e..0000000000 --- a/src/tabnineChatWidget/chatEventRegistry.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EventRegistry } from "./EventRegistry"; - -const chatEventRegistry = new EventRegistry(); - -export { chatEventRegistry }; diff --git a/src/tabnineChatWidget/tabnineChatWidgetWebview.ts b/src/tabnineChatWidget/tabnineChatWidgetWebview.ts index cc5d268c2e..094744507a 100644 --- a/src/tabnineChatWidget/tabnineChatWidgetWebview.ts +++ b/src/tabnineChatWidget/tabnineChatWidgetWebview.ts @@ -12,7 +12,7 @@ const VIEW_ID = "tabnine.chat"; export default function registerTabnineChatWidgetWebview( context: ExtensionContext, chatEnabledState: ChatEnabledState, - serverUrl?: string + chatProvider: ChatViewProvider ): void { if (process.env.IS_EVAL_MODE === "true") { void vscode.commands.executeCommand( @@ -27,7 +27,7 @@ export default function registerTabnineChatWidgetWebview( context.subscriptions.push( chatEnabledState.onChange((state) => { if (state.enabled) { - registerChatView(serverUrl, context); + registerChatView(context, chatProvider); } else if (state.chatNotEnabledReason) { setContextForChatNotEnabled(state.chatNotEnabledReason); } @@ -43,11 +43,11 @@ function setContextForChatNotEnabled(reason: ChatNotEnabledReason) { let hasRegisteredChatWebview = false; function registerChatView( - serverUrl: string | undefined, - context: vscode.ExtensionContext + context: vscode.ExtensionContext, + chatProvider: ChatViewProvider ) { if (!hasRegisteredChatWebview) { - registerWebview(context, serverUrl); + registerWebview(context, chatProvider); } setTabnineChatWebview("chat"); @@ -64,9 +64,10 @@ function registerChatView( .catch((e) => Logger.error(`Failed to get the user state ${e}`)); } -function registerWebview(context: ExtensionContext, serverUrl?: string): void { - const chatProvider = new ChatViewProvider(context, serverUrl); - +function registerWebview( + context: ExtensionContext, + chatProvider: ChatViewProvider +): void { context.subscriptions.push( vscode.window.registerWebviewViewProvider(VIEW_ID, chatProvider, { webviewOptions: {