@@ -2,11 +2,12 @@ import { inject, injectable, named } from 'inversify';
22import * as os from 'os' ;
33import * as path from 'path' ;
44import * as vscode from 'vscode' ;
5+ import '../../common/extensions' ;
56import { IFormatterHelper } from '../../formatters/types' ;
67import { IServiceContainer } from '../../ioc/types' ;
78import { ILinterManager } from '../../linters/types' ;
89import { ITestsHelper } from '../../unittests/common/types' ;
9- import { IApplicationShell } from '../application/types' ;
10+ import { IApplicationShell , IWorkspaceService } from '../application/types' ;
1011import { STANDARD_OUTPUT_CHANNEL } from '../constants' ;
1112import { IPlatformService } from '../platform/types' ;
1213import { IProcessServiceFactory , IPythonExecutionFactory } from '../process/types' ;
@@ -28,16 +29,34 @@ enum ProductType {
2829}
2930
3031// tslint:disable-next-line:max-classes-per-file
31- abstract class BaseInstaller {
32+ export abstract class BaseInstaller {
33+ private static readonly PromptPromises = new Map < string , Promise < InstallerResponse > > ( ) ;
3234 protected appShell : IApplicationShell ;
3335 protected configService : IConfigurationService ;
36+ private readonly workspaceService : IWorkspaceService ;
3437
3538 constructor ( protected serviceContainer : IServiceContainer , protected outputChannel : vscode . OutputChannel ) {
3639 this . appShell = serviceContainer . get < IApplicationShell > ( IApplicationShell ) ;
3740 this . configService = serviceContainer . get < IConfigurationService > ( IConfigurationService ) ;
41+ this . workspaceService = serviceContainer . get < IWorkspaceService > ( IWorkspaceService ) ;
3842 }
3943
40- public abstract promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > ;
44+ public promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
45+ // If this method gets called twice, while previous promise has not been resolved, then return that same promise.
46+ // E.g. previous promise is not resolved as a message has been displayed to the user, so no point displaying
47+ // another message.
48+ const workspaceFolder = resource ? this . workspaceService . getWorkspaceFolder ( resource ) : undefined ;
49+ const key = `${ product } ${ workspaceFolder ? workspaceFolder . uri . fsPath : '' } ` ;
50+ if ( BaseInstaller . PromptPromises . has ( key ) ) {
51+ return BaseInstaller . PromptPromises . get ( key ) ! ;
52+ }
53+ const promise = this . promptToInstallImplementation ( product , resource ) ;
54+ BaseInstaller . PromptPromises . set ( key , promise ) ;
55+ promise . then ( ( ) => BaseInstaller . PromptPromises . delete ( key ) ) . ignoreErrors ( ) ;
56+ promise . catch ( ( ) => BaseInstaller . PromptPromises . delete ( key ) ) . ignoreErrors ( ) ;
57+
58+ return promise ;
59+ }
4160
4261 public async install ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
4362 if ( product === Product . unittest ) {
@@ -83,22 +102,17 @@ abstract class BaseInstaller {
83102 . catch ( ( ) => false ) ;
84103 }
85104 }
86-
105+ protected abstract promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > ;
87106 protected getExecutableNameFromSettings ( product : Product , resource ?: vscode . Uri ) : string {
88107 throw new Error ( 'getExecutableNameFromSettings is not supported on this object' ) ;
89108 }
90109}
91110
92- class CTagsInstaller extends BaseInstaller {
111+ export class CTagsInstaller extends BaseInstaller {
93112 constructor ( serviceContainer : IServiceContainer , outputChannel : vscode . OutputChannel ) {
94113 super ( serviceContainer , outputChannel ) ;
95114 }
96115
97- public async promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
98- const item = await this . appShell . showErrorMessage ( 'Install CTags to enable Python workspace symbols?' , 'Yes' , 'No' ) ;
99- return item === 'Yes' ? this . install ( product , resource ) : InstallerResponse . Ignore ;
100- }
101-
102116 public async install ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
103117 if ( this . serviceContainer . get < IPlatformService > ( IPlatformService ) . isWindows ) {
104118 this . outputChannel . appendLine ( 'Install Universal Ctags Win32 to enable support for Workspace Symbols' ) ;
@@ -115,15 +129,19 @@ class CTagsInstaller extends BaseInstaller {
115129 }
116130 return InstallerResponse . Ignore ;
117131 }
132+ protected async promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
133+ const item = await this . appShell . showErrorMessage ( 'Install CTags to enable Python workspace symbols?' , 'Yes' , 'No' ) ;
134+ return item === 'Yes' ? this . install ( product , resource ) : InstallerResponse . Ignore ;
135+ }
118136
119137 protected getExecutableNameFromSettings ( product : Product , resource ?: vscode . Uri ) : string {
120138 const settings = this . configService . getSettings ( resource ) ;
121139 return settings . workspaceSymbols . ctagsPath ;
122140 }
123141}
124142
125- class FormatterInstaller extends BaseInstaller {
126- public async promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
143+ export class FormatterInstaller extends BaseInstaller {
144+ protected async promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
127145 // Hard-coded on purpose because the UI won't necessarily work having
128146 // another formatter.
129147 const formatters = [ Product . autopep8 , Product . black , Product . yapf ] ;
@@ -159,8 +177,8 @@ class FormatterInstaller extends BaseInstaller {
159177}
160178
161179// tslint:disable-next-line:max-classes-per-file
162- class LinterInstaller extends BaseInstaller {
163- public async promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
180+ export class LinterInstaller extends BaseInstaller {
181+ protected async promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
164182 const productName = ProductNames . get ( product ) ! ;
165183 const install = 'Install' ;
166184 const disableAllLinting = 'Disable linting' ;
@@ -188,8 +206,8 @@ class LinterInstaller extends BaseInstaller {
188206}
189207
190208// tslint:disable-next-line:max-classes-per-file
191- class TestFrameworkInstaller extends BaseInstaller {
192- public async promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
209+ export class TestFrameworkInstaller extends BaseInstaller {
210+ protected async promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
193211 const productName = ProductNames . get ( product ) ! ;
194212 const item = await this . appShell . showErrorMessage ( `Test framework ${ productName } is not installed. Install?` , 'Yes' , 'No' ) ;
195213 return item === 'Yes' ? this . install ( product , resource ) : InstallerResponse . Ignore ;
@@ -208,8 +226,8 @@ class TestFrameworkInstaller extends BaseInstaller {
208226}
209227
210228// tslint:disable-next-line:max-classes-per-file
211- class RefactoringLibraryInstaller extends BaseInstaller {
212- public async promptToInstall ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
229+ export class RefactoringLibraryInstaller extends BaseInstaller {
230+ protected async promptToInstallImplementation ( product : Product , resource ?: vscode . Uri ) : Promise < InstallerResponse > {
213231 const productName = ProductNames . get ( product ) ! ;
214232 const item = await this . appShell . showErrorMessage ( `Refactoring library ${ productName } is not installed. Install?` , 'Yes' , 'No' ) ;
215233 return item === 'Yes' ? this . install ( product , resource ) : InstallerResponse . Ignore ;
0 commit comments