From c0655e19b60ef34c2f5664c0c9660267980ec794 Mon Sep 17 00:00:00 2001 From: Yair Cohen Date: Thu, 26 Oct 2023 20:18:24 +0300 Subject: [PATCH] DEV2-1707: add a snooze button for tabnine completions (#1341) * DEV2-1707: add a pause button * handle self-hosted * add setting * fix lint * improve naming * track snooze toggled * CompletionState refactor (#1344) * remove unneccessary check --------- Co-authored-by: Dima Abramovich <60742964+dimacodota@users.noreply.github.com> --- package.json | 7 +++ src/autocompleteInstaller.ts | 13 +++++ src/commandsHandler.ts | 28 ++++++----- src/enterprise/statusBar/StatusBar.ts | 9 ++++ src/enterprise/statusBar/StatusItem.ts | 11 ++++- src/enterprise/statusBar/statusAction.ts | 13 +++-- src/provideInlineCompletionItems.ts | 6 +-- src/state/completionsState.ts | 34 +++++++++++++ src/statusBar/StatusBarData.ts | 9 +++- src/statusBar/statusBar.ts | 7 ++- src/statusBar/statusBarNotificationOptions.ts | 49 +++++++++++++++++++ 11 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 src/state/completionsState.ts create mode 100644 src/statusBar/statusBarNotificationOptions.ts diff --git a/package.json b/package.json index a7188593aa..6888afc097 100644 --- a/package.json +++ b/package.json @@ -409,6 +409,13 @@ "default": 0, "description": "debounce milliseconds before rendering tabnine suggestion" }, + "tabnine.snoozeDuration": { + "type": "number", + "default": 1, + "description": "Hours to disable inline completions when clicking the snooze button", + "minimum": 1, + "maximum": 24 + }, "tabnine.useProxySupport": { "type": "boolean", "default": true, diff --git a/src/autocompleteInstaller.ts b/src/autocompleteInstaller.ts index 582b7f3578..f415c59c45 100644 --- a/src/autocompleteInstaller.ts +++ b/src/autocompleteInstaller.ts @@ -16,6 +16,7 @@ import { } from "./globals/versions"; import enableProposed from "./globals/proposedAPI"; import { registerInlineProvider } from "./inlineSuggestions/registerInlineProvider"; +import { completionsState } from "./state/completionsState"; let subscriptions: Disposable[] = []; @@ -40,6 +41,14 @@ export default async function installAutocomplete( } }) ); + + completionsState.on("changed", (enabled) => { + if (enabled) { + void reinstallAutocomplete(InstallOptions.get()); + } else { + uninstallAutocomplete(); + } + }); } async function reinstallAutocomplete({ @@ -49,6 +58,10 @@ async function reinstallAutocomplete({ }: InstallOptions) { uninstallAutocomplete(); + if (!completionsState.value) { + return; + } + if ( (inlineEnabled || snippetsEnabled) && (isInlineSuggestionReleasedApiSupported() || (await isDefaultAPIEnabled())) diff --git a/src/commandsHandler.ts b/src/commandsHandler.ts index 78c65e1e38..31de2f262e 100644 --- a/src/commandsHandler.ts +++ b/src/commandsHandler.ts @@ -1,7 +1,8 @@ import { commands, ExtensionContext } from "vscode"; -import { StateType, STATUS_BAR_FIRST_TIME_CLICKED } from "./globals/consts"; import { Capability, isCapabilityEnabled } from "./capabilities/capabilities"; +import { StateType, STATUS_BAR_FIRST_TIME_CLICKED } from "./globals/consts"; import openHub, { openHubExternal } from "./hub/openHub"; +import { showStatusBarNotificationOptions } from "./statusBar/statusBarNotificationOptions"; const CONFIG_COMMAND = "TabNine::config"; const CONFIG_EXTERNAL_COMMAND = "TabNine::configExternal"; @@ -23,15 +24,20 @@ export function registerCommands(context: ExtensionContext): void { } function handleStatusBar(context: ExtensionContext) { - const openHubWithStatus = openHub(StateType.STATUS); - - return async (args: string[] | null = null): Promise => { - await openHubWithStatus(args); - - if ( - isCapabilityEnabled(Capability.SHOW_AGRESSIVE_STATUS_BAR_UNTIL_CLICKED) - ) { - await context.globalState.update(STATUS_BAR_FIRST_TIME_CLICKED, true); - } + return (args: string[] | null = null) => { + showStatusBarNotificationOptions( + "Open Hub", + () => void openHubHandler(context, args) + ); }; } + +async function openHubHandler( + context: ExtensionContext, + args: string[] | null = null +) { + await openHub(StateType.STATUS)(args); + if (isCapabilityEnabled(Capability.SHOW_AGRESSIVE_STATUS_BAR_UNTIL_CLICKED)) { + await context.globalState.update(STATUS_BAR_FIRST_TIME_CLICKED, true); + } +} diff --git a/src/enterprise/statusBar/StatusBar.ts b/src/enterprise/statusBar/StatusBar.ts index bd02658b13..483bcd6337 100644 --- a/src/enterprise/statusBar/StatusBar.ts +++ b/src/enterprise/statusBar/StatusBar.ts @@ -11,6 +11,7 @@ import { } from "../../globals/consts"; import getUserInfo, { UserInfo } from "../requests/UserInfo"; import { Logger } from "../../utils/logger"; +import { completionsState } from "../../state/completionsState"; export class StatusBar implements Disposable { private item: StatusItem; @@ -37,6 +38,14 @@ export class StatusBar implements Disposable { // eslint-disable-next-line @typescript-eslint/unbound-method this.setServerRequired().catch(Logger.error); + + completionsState.on("changed", (enabled) => { + if (enabled) { + this.item.setDefault(); + } else { + this.item.setCompletionsDisabled(); + } + }); } private async setServerRequired() { diff --git a/src/enterprise/statusBar/StatusItem.ts b/src/enterprise/statusBar/StatusItem.ts index d290962d71..e70b0d6a6f 100644 --- a/src/enterprise/statusBar/StatusItem.ts +++ b/src/enterprise/statusBar/StatusItem.ts @@ -18,7 +18,7 @@ export class StatusItem implements Disposable { private comand: Disposable; constructor() { - this.item = window.createStatusBarItem(StatusBarAlignment.Left, -1); + this.item = window.createStatusBarItem(StatusBarAlignment.Right, -1); this.comand = commands.registerCommand(commandId, action); this.item.show(); } @@ -30,7 +30,7 @@ export class StatusItem implements Disposable { public setDefault() { this.item.backgroundColor = undefined; - this.item.tooltip = `${FULL_BRAND_REPRESENTATION} (Click to open settings)`; + this.item.tooltip = `${FULL_BRAND_REPRESENTATION} (Show options)`; this.item.text = STATUS_NAME; } @@ -54,6 +54,13 @@ export class StatusItem implements Disposable { this.item.text = STATUS_NAME; } + public setCompletionsDisabled() { + this.item.backgroundColor = new ThemeColor( + "statusBarItem.warningBackground" + ); + this.item.text = STATUS_NAME; + } + public setCommand(state: StatusState) { this.item.command = { title: "Status action", diff --git a/src/enterprise/statusBar/statusAction.ts b/src/enterprise/statusBar/statusAction.ts index 3b6c880b5e..12f6e2daa3 100644 --- a/src/enterprise/statusBar/statusAction.ts +++ b/src/enterprise/statusBar/statusAction.ts @@ -1,11 +1,12 @@ import { Uri, commands, env, window } from "vscode"; import { callForLogin } from "../../authentication/authentication.api"; +import { showStatusBarNotificationOptions } from "../../statusBar/statusBarNotificationOptions"; +import { Logger } from "../../utils/logger"; import { EXTENSION_ID, OPEN_SETTINGS_COMMAND, TABNINE_HOST_CONFIGURATION, } from "../consts"; -import { Logger } from "../../utils/logger"; export enum StatusState { SetServer, @@ -91,10 +92,12 @@ export function action(state: StatusState): void { break; default: - void commands.executeCommand( - OPEN_SETTINGS_COMMAND, - `@ext:tabnine.${EXTENSION_ID}` - ); + showStatusBarNotificationOptions("Open Settings", () => { + void commands.executeCommand( + OPEN_SETTINGS_COMMAND, + `@ext:tabnine.${EXTENSION_ID}` + ); + }); break; } } diff --git a/src/provideInlineCompletionItems.ts b/src/provideInlineCompletionItems.ts index 43c58c7fef..dded045348 100644 --- a/src/provideInlineCompletionItems.ts +++ b/src/provideInlineCompletionItems.ts @@ -8,7 +8,6 @@ import { } from "./lookAheadSuggestion"; import debounceCompletions from "./debounceCompletions"; import reportSuggestionShown from "./reportSuggestionShown"; -import { shouldBlockCompletions } from "./registration/forceRegistration"; import { Logger } from "./utils/logger"; const END_OF_LINE_VALID_REGEX = new RegExp("^\\s*[)}\\]\"'`]*\\s*[:{;,]?\\s*$"); @@ -24,10 +23,9 @@ export default async function provideInlineCompletionItems( try { clearCurrentLookAheadSuggestion(); if ( - !completionIsAllowed(document, position) || - !isValidMidlinePosition(document, position) || !getShouldComplete() || - shouldBlockCompletions() + !completionIsAllowed(document, position) || + !isValidMidlinePosition(document, position) ) { return undefined; } diff --git a/src/state/completionsState.ts b/src/state/completionsState.ts new file mode 100644 index 0000000000..dff72e0e05 --- /dev/null +++ b/src/state/completionsState.ts @@ -0,0 +1,34 @@ +import { EventEmitter } from "events"; +import { workspace } from "vscode"; + +class CompletionState extends EventEmitter { + private state: boolean = true; + + private enableTimeout: NodeJS.Timeout | null = null; + + get value(): boolean { + return this.state; + } + + set value(enabled: boolean) { + this.state = enabled; + this.emit("changed", enabled); + + if (this.enableTimeout) { + clearTimeout(this.enableTimeout); + this.enableTimeout = null; + } + + if (!enabled) { + const snoozeDuration = workspace + .getConfiguration("tabnine") + .get("snoozeDuration", 1); + + this.enableTimeout = setTimeout(() => { + this.state = true; + }, snoozeDuration * 60 * 1000); + } + } +} + +export const completionsState = new CompletionState(); diff --git a/src/statusBar/StatusBarData.ts b/src/statusBar/StatusBarData.ts index 5f715b6602..b248939801 100644 --- a/src/statusBar/StatusBarData.ts +++ b/src/statusBar/StatusBarData.ts @@ -19,6 +19,7 @@ import { } from "../globals/consts"; import { getPersistedAlphaVersion } from "../preRelease/versions"; import { shouldStatusBarBeProminent } from "../registration/forceRegistration"; +import { completionsState } from "../state/completionsState"; export default class StatusBarData implements Disposable { private _serviceLevel?: ServiceLevel; @@ -77,7 +78,7 @@ export default class StatusBarData implements Disposable { return this._text; } - private updateStatusBar() { + public updateStatusBar() { const issueText = this._text ? `: ${this._text}` : ""; const serviceLevel = this.getDisplayServiceLevel(); const limited = this._limited ? ` ${LIMITATION_SYMBOL}` : ""; @@ -87,6 +88,10 @@ export default class StatusBarData implements Disposable { this._statusBarItem.backgroundColor = new ThemeColor( "statusBarItem.warningBackground" ); + } else if (!completionsState.value) { + this._statusBarItem.backgroundColor = new ThemeColor( + "statusBarItem.warningBackground" + ); } else { this._statusBarItem.backgroundColor = undefined; } @@ -102,7 +107,7 @@ export default class StatusBarData implements Disposable { Capability.SHOW_AGRESSIVE_STATUS_BAR_UNTIL_CLICKED ) && !this._context.globalState.get(STATUS_BAR_FIRST_TIME_CLICKED) ? "Click 'tabnine' for settings and more information" - : `${FULL_BRAND_REPRESENTATION} (Click to open settings)${ + : `${FULL_BRAND_REPRESENTATION} (Show options)${ getPersistedAlphaVersion(this._context) ?? "" }`; } diff --git a/src/statusBar/statusBar.ts b/src/statusBar/statusBar.ts index 466f87dfd6..d86b651586 100644 --- a/src/statusBar/statusBar.ts +++ b/src/statusBar/statusBar.ts @@ -11,6 +11,7 @@ import StatusBarData from "./StatusBarData"; import StatusBarPromotionItem from "./StatusBarPromotionItem"; import { ServiceLevel } from "../binary/state"; import { Logger } from "../utils/logger"; +import { completionsState } from "../state/completionsState"; const SPINNER = "$(sync~spin)"; @@ -22,9 +23,9 @@ export function registerStatusBar(context: ExtensionContext): Disposable { return statusBarData; } - const statusBar = window.createStatusBarItem(StatusBarAlignment.Left, -1); + const statusBar = window.createStatusBarItem(StatusBarAlignment.Right, -1); promotion = new StatusBarPromotionItem( - window.createStatusBarItem(StatusBarAlignment.Left, -1) + window.createStatusBarItem(StatusBarAlignment.Right, -1) ); statusBarData = new StatusBarData(statusBar, context); statusBar.command = STATUS_BAR_COMMAND; @@ -37,6 +38,8 @@ export function registerStatusBar(context: ExtensionContext): Disposable { Logger.error("failed to rename status bar"); } + completionsState.on("changed", () => statusBarData?.updateStatusBar()); + setLoadingStatus("Starting..."); return Disposable.from(statusBarData, promotion); } diff --git a/src/statusBar/statusBarNotificationOptions.ts b/src/statusBar/statusBarNotificationOptions.ts new file mode 100644 index 0000000000..8e3789ae62 --- /dev/null +++ b/src/statusBar/statusBarNotificationOptions.ts @@ -0,0 +1,49 @@ +import { window, workspace } from "vscode"; +import { completionsState } from "../state/completionsState"; +import { sendEvent } from "../binary/requests/sendEvent"; + +const RESUME_TABNINE = "Resume Tabnine"; + +export function showStatusBarNotificationOptions( + settingsButton: string, + onSettingsClicked: () => void +) { + const snoozeDuration = workspace + .getConfiguration("tabnine") + .get("snoozeDuration", 1); + + const snoozeTabnine = `Snooze Tabnine (${snoozeDuration}h)`; + + const currentAction = completionsState.value ? snoozeTabnine : RESUME_TABNINE; + + void window + .showInformationMessage("Tabnine options", settingsButton, currentAction) + .then((selection) => { + switch (selection) { + case settingsButton: + onSettingsClicked(); + break; + case snoozeTabnine: + trackSnoozeToggled(false, snoozeDuration); + completionsState.value = false; + break; + case RESUME_TABNINE: + trackSnoozeToggled(true, snoozeDuration); + completionsState.value = true; + break; + default: + console.warn("Unexpected selection"); + break; + } + }); +} + +function trackSnoozeToggled(showCompletions: boolean, duration: number) { + void sendEvent({ + name: "snooze-toggled", + properties: { + show_completions: showCompletions.toString(), + duration: duration.toString(), + }, + }); +}