Skip to content

Commit

Permalink
Change chat to use state to show views
Browse files Browse the repository at this point in the history
  • Loading branch information
ofekby committed Nov 26, 2023
1 parent 74b79ac commit 8b5a67f
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 45 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
]
},
Expand Down
43 changes: 20 additions & 23 deletions src/authentication/TabnineAuthenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +27,7 @@ export default class TabnineAuthenticationProvider

private initializedDisposable: Disposable | undefined;

private lastState: Promise<State | undefined | null> | undefined;
private lastState: State | undefined | null;

private onDidLogin = new Emitter();

Expand All @@ -44,12 +44,14 @@ export default class TabnineAuthenticationProvider
);
}

async getSessions(): Promise<readonly AuthenticationSession[]> {
const state = await this.lastState;
getSessions(): Promise<readonly AuthenticationSession[]> {
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<AuthenticationSession> {
Expand Down Expand Up @@ -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<void> {
private async checkForUpdates(
state: State | null | undefined
): Promise<void> {
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);
Expand Down
50 changes: 50 additions & 0 deletions src/binary/binaryStateSingleton.ts
Original file line number Diff line number Diff line change
@@ -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<State> {
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;
2 changes: 2 additions & 0 deletions src/enterprise/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -113,6 +114,7 @@ async function backgroundInit(context: vscode.ExtensionContext) {
isAuthenticationApiSupported()
) {
context.subscriptions.push(
BINARY_STATE.start(),
vscode.authentication.registerAuthenticationProvider(
BRAND_NAME,
BRAND_NAME,
Expand Down
29 changes: 20 additions & 9 deletions src/tabnineChatWidget/SaasChatEnabledState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,44 @@ 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<ChatEnabledStateData>
implements ChatEnabledState {
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"));
}
}
}

Expand Down
23 changes: 17 additions & 6 deletions src/tabnineChatWidget/tabnineChatWidgetWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default function registerTabnineChatWidgetWebview(
);
}

setTabnineChatWebview("loading");

context.subscriptions.push(
chatEnabledState.useState((state) => {
if (state.enabled) {
Expand All @@ -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()
Expand Down Expand Up @@ -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
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
45 changes: 45 additions & 0 deletions src/utils/deriveState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Disposable } from "vscode";
import EventEmitterBasedState from "./EventEmitterBasedState";

export type DerivedState<T> = Disposable & EventEmitterBasedState<T>;

export default function deriveState<I, O, S extends EventEmitterBasedState<I>>(
state: S,
mapping: (value: I) => O
): DerivedState<O> {
class TempDerivedState
extends EventEmitterBasedState<O>
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<I, O, S extends EventEmitterBasedState<I>>(
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();
},
};
}

0 comments on commit 8b5a67f

Please sign in to comment.