diff --git a/package.json b/package.json index 2b1bfa5e15..e3f6262eb8 100644 --- a/package.json +++ b/package.json @@ -219,30 +219,30 @@ "type": "webview", "id": "tabnine.chat", "name": "Chat", - "when": "tabnine.authenticated && tabnine.chat.ready" + "when": "tabnine.chat.webview == 'chat'" }, { "id": "tabnine.chat.welcome", "type": "webview", "name": "Welcome to Chat", - "when": "tabnine.authenticated && !tabnine.chat.ready" + "when": "tabnine.chat.webview == 'capability_required'" }, { - "id": "tabnine.authenticate", + "id": "tabnine.chat.authenticate", "type": "webview", "name": "Please log in", - "when": "!tabnine.authenticated && tabnine.process.ready && (tabnine.enterprise || tabnine.capabilities.ready) && tabnine.authentication.ready" + "when": "tabnine.chat.webview == 'authnetication_required'" }, { "id": "tabnine.chat.not_part_of_a_team", "type": "webview", "name": "Please join a team", - "when": "tabnine.chat.not_enabled_reason == 'part_of_a_team_required'" + "when": "tabnine.chat.webview == 'part_of_a_team_required'" }, { "id": "tabnine.loading", "name": "Loading", - "when": "!tabnine.process.ready || !(tabnine.enterprise || tabnine.capabilities.ready) || !tabnine.authentication.ready" + "when": "tabnine.chat.webview == 'loading'" } ] }, diff --git a/src/authentication/TabnineAuthenticationProvider.ts b/src/authentication/TabnineAuthenticationProvider.ts index bcc1454285..c466972530 100644 --- a/src/authentication/TabnineAuthenticationProvider.ts +++ b/src/authentication/TabnineAuthenticationProvider.ts @@ -9,14 +9,14 @@ import { EventEmitter, } from "vscode"; import { once, EventEmitter as Emitter } from "events"; -import { getState, tabNineProcess } from "../binary/requests/requests"; import { State } from "../binary/state"; import { BRAND_NAME } from "../globals/consts"; import { sleep } from "../utils/utils"; import { callForLogin, callForLogout } from "./authentication.api"; import TabnineSession from "./TabnineSession"; +import BINARY_STATE from "../binary/binaryStateSingleton"; +import { getState } from "../binary/requests/requests"; -const SESSION_POLL_INTERVAL = 10000; const LOGIN_HAPPENED_EVENT = "loginHappened"; export default class TabnineAuthenticationProvider @@ -27,7 +27,7 @@ export default class TabnineAuthenticationProvider private initializedDisposable: Disposable | undefined; - private lastState: Promise | undefined; + private lastState: State | undefined | null; private onDidLogin = new Emitter(); @@ -44,12 +44,14 @@ export default class TabnineAuthenticationProvider ); } - async getSessions(): Promise { - const state = await this.lastState; + getSessions(): Promise { + const state = this.lastState; - return state?.is_logged_in - ? [new TabnineSession(state?.user_name, state?.access_token)] - : []; + return Promise.resolve( + state?.is_logged_in + ? [new TabnineSession(state?.user_name, state?.access_token)] + : [] + ); } async createSession(): Promise { @@ -79,36 +81,31 @@ export default class TabnineAuthenticationProvider // This fires when the user initiates a "silent" auth flow via the Accounts menu. return authentication.onDidChangeSessions((e) => { if (e.provider.id === BRAND_NAME) { - void this.checkForUpdates(); + void getState().then((state) => { + void this.checkForUpdates(state); + }); } }); } private pollState(): Disposable { - let interval: NodeJS.Timeout | undefined; - void tabNineProcess.onReady.then(() => { - interval = setInterval(() => { - void this.checkForUpdates(); - }, SESSION_POLL_INTERVAL); - }); - return new Disposable(() => { - if (interval) { - clearInterval(interval); - } + return BINARY_STATE.useState((state) => { + void this.checkForUpdates(state); }); } - private async checkForUpdates(): Promise { + private async checkForUpdates( + state: State | null | undefined + ): Promise { const added: AuthenticationSession[] = []; const removed: AuthenticationSession[] = []; - const state = getState(); const { lastState } = this; this.lastState = state; - const newState = await this.lastState; - const oldState = await lastState; + const newState = this.lastState; + const oldState = lastState; if (newState?.is_logged_in) { this.onDidLogin.emit(LOGIN_HAPPENED_EVENT, newState); diff --git a/src/binary/binaryStateSingleton.ts b/src/binary/binaryStateSingleton.ts new file mode 100644 index 0000000000..c558b0150e --- /dev/null +++ b/src/binary/binaryStateSingleton.ts @@ -0,0 +1,50 @@ +import { Disposable } from "vscode"; +import { Mutex } from "await-semaphore"; +import EventEmitterBasedState from "../utils/EventEmitterBasedState"; +import { State } from "./state"; +import { getState, tabNineProcess } from "./requests/requests"; +import { Logger } from "../utils/logger"; + +const SESSION_POLL_INTERVAL = 10_000; + +export class BinaryState extends EventEmitterBasedState { + private updateStateLock = new Mutex(); + + private intervalDisposabled: Disposable | null = null; + + start(): Disposable { + if (!this.intervalDisposabled) { + let interval: NodeJS.Timeout | undefined; + void tabNineProcess.onReady.then(() => { + interval = setInterval(() => { + void this.checkForUpdates(); + }, SESSION_POLL_INTERVAL); + }); + + this.intervalDisposabled = new Disposable(() => { + if (interval) { + clearInterval(interval); + } + }); + } + + return this.intervalDisposabled; + } + + private async checkForUpdates() { + try { + await this.updateStateLock.use(async () => { + const state = await getState(); + + if (state) { + this.set(state); + } + }); + } catch (error) { + Logger.warn("Failed to refetch state", error); + } + } +} + +const BINARY_STATE = new BinaryState(); +export default BINARY_STATE; diff --git a/src/enterprise/extension.ts b/src/enterprise/extension.ts index 4ff80ff565..79da2ed9a0 100644 --- a/src/enterprise/extension.ts +++ b/src/enterprise/extension.ts @@ -44,6 +44,7 @@ import { WorkspaceUpdater } from "../WorkspaceUpdater"; import SelfHostedChatEnabledState from "./tabnineChatWidget/SelfHostedChatEnabledState"; import { emptyStateAuthenticateView } from "../tabnineChatWidget/webviews/emptyStateAuthenticateView"; import { emptyStateNotPartOfATeamView } from "../tabnineChatWidget/webviews/emptyStateNotPartOfATeamView"; +import BINARY_STATE from "../binary/binaryStateSingleton"; export async function activate( context: vscode.ExtensionContext @@ -156,6 +157,7 @@ function registerAuthenticationProviders( ): void { const provider = new TabnineAuthenticationProvider(); context.subscriptions.push( + BINARY_STATE.start(), vscode.authentication.registerAuthenticationProvider( BRAND_NAME, ENTERPRISE_BRAND_NAME, diff --git a/src/extension.ts b/src/extension.ts index 8e57c7c3f4..164f3a3743 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -57,6 +57,7 @@ import { emptyStateAuthenticateView } from "./tabnineChatWidget/webviews/emptySt import { activeTextEditorState } from "./activeTextEditorState"; import { WorkspaceUpdater } from "./WorkspaceUpdater"; import SaasChatEnabledState from "./tabnineChatWidget/SaasChatEnabledState"; +import BINARY_STATE from "./binary/binaryStateSingleton"; export async function activate( context: vscode.ExtensionContext @@ -113,6 +114,7 @@ async function backgroundInit(context: vscode.ExtensionContext) { isAuthenticationApiSupported() ) { context.subscriptions.push( + BINARY_STATE.start(), vscode.authentication.registerAuthenticationProvider( BRAND_NAME, BRAND_NAME, diff --git a/src/tabnineChatWidget/SaasChatEnabledState.ts b/src/tabnineChatWidget/SaasChatEnabledState.ts index 7e554d6253..8ff1495141 100644 --- a/src/tabnineChatWidget/SaasChatEnabledState.ts +++ b/src/tabnineChatWidget/SaasChatEnabledState.ts @@ -10,6 +10,9 @@ import { onDidRefreshCapabilities, } from "../capabilities/capabilities"; import EventEmitterBasedNonNullState from "../utils/EventEmitterBasedNonNullState"; +import { useDerviedState } from "../utils/deriveState"; +import BINARY_STATE from "../binary/binaryStateSingleton"; +import { State } from "../binary/state"; export default class SaasChatEnabledState extends EventEmitterBasedNonNullState @@ -17,26 +20,34 @@ export default class SaasChatEnabledState constructor(context: ExtensionContext) { super(ChatStates.loading); - this.updateState(); + this.updateState(BINARY_STATE.get()?.is_logged_in || false); context.subscriptions.push( + useDerviedState( + BINARY_STATE, + (state: State) => state.is_logged_in, + (isLoggedIn) => { + this.updateState(isLoggedIn); + } + ), onDidRefreshCapabilities(() => { - this.updateState(); + this.updateState(BINARY_STATE.get()?.is_logged_in || false); }) ); } - updateState() { + private updateState(isLoggedIn: boolean) { if (!isCapabilitiesReady()) { return; } - const isCapabilitesEnabled = getIsCapabilitesEnabled(); - const newEnabled = isCapabilitesEnabled - ? ChatStates.enabled - : ChatStates.disabled("capability_required"); - - this.set(newEnabled); + if (getIsCapabilitesEnabled()) { + this.set(ChatStates.enabled); + } else if (isLoggedIn) { + this.set(ChatStates.disabled("capability_required")); + } else { + this.set(ChatStates.disabled("authnetication_required")); + } } } diff --git a/src/tabnineChatWidget/tabnineChatWidgetWebview.ts b/src/tabnineChatWidget/tabnineChatWidgetWebview.ts index 0d3637cace..55ee88aeb6 100644 --- a/src/tabnineChatWidget/tabnineChatWidgetWebview.ts +++ b/src/tabnineChatWidget/tabnineChatWidgetWebview.ts @@ -22,6 +22,8 @@ export default function registerTabnineChatWidgetWebview( ); } + setTabnineChatWebview("loading"); + context.subscriptions.push( chatEnabledState.useState((state) => { if (state.enabled) { @@ -35,15 +37,20 @@ export default function registerTabnineChatWidgetWebview( function setContextForChatNotEnabled(reason: ChatNotEnabledReason) { setChatReady(false); - setNotEnabledReasonContext(reason); + setTabnineChatWebview(reason); } +let hasRegisteredChatWebview = false; + function registerChatView( serverUrl: string | undefined, context: vscode.ExtensionContext ) { - registerWebview(context, serverUrl); - setNotEnabledReasonContext(null); + if (!hasRegisteredChatWebview) { + registerWebview(context, serverUrl); + } + + setTabnineChatWebview("chat"); setChatReady(true); getState() @@ -107,13 +114,17 @@ function registerWebview(context: ExtensionContext, serverUrl?: string): void { ); registerChatQuickFix(context, chatProvider); registerChatCodeLens(context, chatProvider); + + hasRegisteredChatWebview = true; } -function setNotEnabledReasonContext(reason: ChatNotEnabledReason | null) { +function setTabnineChatWebview( + webviewName: ChatNotEnabledReason | "chat" | "loading" +) { void vscode.commands.executeCommand( "setContext", - "tabnine.chat.not_enabled_reason", - reason + "tabnine.chat.webview", + webviewName ); } diff --git a/src/tabnineChatWidget/webviews/emptyStateAuthenticateView.ts b/src/tabnineChatWidget/webviews/emptyStateAuthenticateView.ts index 2590dd6e39..bda6eae40e 100644 --- a/src/tabnineChatWidget/webviews/emptyStateAuthenticateView.ts +++ b/src/tabnineChatWidget/webviews/emptyStateAuthenticateView.ts @@ -6,7 +6,7 @@ import { getIcon } from "./getIcon"; export function emptyStateAuthenticateView( context: ExtensionContext ): Disposable { - return window.registerWebviewViewProvider("tabnine.authenticate", { + return window.registerWebviewViewProvider("tabnine.chat.authenticate", { resolveWebviewView(webviewView: WebviewView) { context.subscriptions.push( webviewView.onDidChangeVisibility(() => { diff --git a/src/utils/deriveState.ts b/src/utils/deriveState.ts new file mode 100644 index 0000000000..3f5bbe4c8c --- /dev/null +++ b/src/utils/deriveState.ts @@ -0,0 +1,45 @@ +import { Disposable } from "vscode"; +import EventEmitterBasedState from "./EventEmitterBasedState"; + +export type DerivedState = Disposable & EventEmitterBasedState; + +export default function deriveState>( + state: S, + mapping: (value: I) => O +): DerivedState { + class TempDerivedState + extends EventEmitterBasedState + implements Disposable { + useStateDisposabled!: Disposable; + + constructor() { + super(); + + this.useStateDisposabled = state.useState((inputState) => { + this.set(mapping(inputState)); + }); + } + + dispose() { + this.useStateDisposabled.dispose(); + } + } + + return new TempDerivedState(); +} + +export function useDerviedState>( + state: S, + mapping: (value: I) => O, + onChange: (newValue: O) => void +): Disposable { + const derviedState = deriveState(state, mapping); + const disposable = derviedState.useState(onChange); + + return { + dispose() { + derviedState.dispose(); + disposable.dispose(); + }, + }; +}