From 531cb008b7e8cf399a045456efe7e9c1509be865 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 4 Apr 2019 08:46:27 -0700 Subject: [PATCH] Display python debug config ui when editing launch.json (#5016) For #3321 --- gulpfile.js | 1 - news/1 Enhancements/3321.md | 1 + package-lock.json | 5 + package.json | 1 + package.nls.json | 4 +- src/client/activation/types.ts | 8 + src/client/common/application/commands.ts | 3 +- .../common/application/languageService.ts | 16 + src/client/common/application/types.ts | 21 + src/client/common/serviceRegistry.ts | 3 + src/client/common/utils/localize.ts | 574 +++++++++--------- .../debugConfigurationService.ts | 15 +- .../launch.json/completionProvider.ts | 59 ++ .../launch.json/updaterService.ts | 106 ++++ .../debugger/extension/serviceRegistry.ts | 5 + src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 1 + src/test/debugger/attach.ptvsd.test.ts | 5 +- .../debugConfigurationService.unit.test.ts | 15 +- .../completionProvider.unit.test.ts | 127 ++++ .../launch.json/updaterServer.unit.test.ts | 340 +++++++++++ 21 files changed, 1005 insertions(+), 306 deletions(-) create mode 100644 news/1 Enhancements/3321.md create mode 100644 src/client/common/application/languageService.ts create mode 100644 src/client/debugger/extension/configuration/launch.json/completionProvider.ts create mode 100644 src/client/debugger/extension/configuration/launch.json/updaterService.ts create mode 100644 src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts create mode 100644 src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts diff --git a/gulpfile.js b/gulpfile.js index 65f6fff26440..98e27ea4bbee 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -811,7 +811,6 @@ function getFilesToProcess(fileList) { * @param {hygieneOptions} options */ function getFileListToProcess(options) { - return []; const mode = options ? options.mode : 'all'; const gulpSrcOptions = { base: '.' }; diff --git a/news/1 Enhancements/3321.md b/news/1 Enhancements/3321.md new file mode 100644 index 000000000000..fcb5fc0ba5f6 --- /dev/null +++ b/news/1 Enhancements/3321.md @@ -0,0 +1 @@ +Launch the `Python` debug configuration UI when manually adding entries into the `launch.json` file. diff --git a/package-lock.json b/package-lock.json index 8da964149de9..4e41ee93fecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9356,6 +9356,11 @@ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, + "jsonc-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.0.3.tgz", + "integrity": "sha512-WJi9y9ABL01C8CxTKxRRQkkSpY/x2bo4Gy0WuiZGrInxQqgxQpvkBCLNcDYcHOSdhx4ODgbFcgAvfL49C+PHgQ==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", diff --git a/package.json b/package.json index a0702de30a87..75fc77147f58 100644 --- a/package.json +++ b/package.json @@ -2270,6 +2270,7 @@ "hash.js": "^1.1.7", "iconv-lite": "^0.4.21", "inversify": "^4.11.1", + "jsonc-parser": "^2.0.3", "line-by-line": "^0.1.6", "lodash": "^4.17.11", "md5": "^2.2.1", diff --git a/package.nls.json b/package.nls.json index 6e6114b8604a..e4d176a57dd3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -182,6 +182,8 @@ "Common.loadingPythonExtension": "Python extension loading...", "debug.selectConfigurationTitle": "Select a debug configuration", "debug.selectConfigurationPlaceholder": "Debug Configuration", + "debug.launchJsonConfigurationsCompletionLabel": "Python", + "debug.launchJsonConfigurationsCompletionDescription": "Select Python Launch Configuration", "debug.debugFileConfigurationLabel": "Python File", "debug.debugFileConfigurationDescription": "Debug Python file", "debug.debugModuleConfigurationLabel": "Module", @@ -229,5 +231,5 @@ "DataScience.noRowsInDataViewer" : "Fetching data ...", "DataScience.pandasTooOldForViewingFormat" : "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", "DataScience.pandasRequiredForViewing" : "Python package 'pandas' is required for viewing data.", - "DataScience.valuesColumn": "values" + "DataScience.valuesColumn": "values" } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index a5e2e2edfc96..654bcaaeb072 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -17,6 +17,14 @@ export interface IExtensionActivationManager extends IDisposable { } export const IExtensionActivationService = Symbol('IExtensionActivationService'); +/** + * Classes implementing this interface will have their `activate` methods + * invoked during the actiavtion of the extension. + * This is a great hook for extension activation code, i.e. you don't need to modify + * the `extension.ts` file to invoke some code when extension gets activated. + * @export + * @interface IExtensionActivationService + */ export interface IExtensionActivationService { activate(resource: Resource): Promise; } diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 3b4ca39d1eb6..c04bdcc51445 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; import { Commands as DSCommands } from '../../datascience/constants'; import { CommandSource } from '../../unittests/common/constants'; import { TestFunction, TestsToRun } from '../../unittests/common/types'; @@ -63,6 +63,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu ['setContext']: [string, boolean]; ['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }]; ['python._loadLanguageServerExtension']: {}[]; + ['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken]; [Commands.Build_Workspace_Symbols]: [boolean, CancellationToken]; [Commands.Sort_Imports]: [undefined, Uri]; [Commands.Exec_In_Terminal]: [undefined, Uri]; diff --git a/src/client/common/application/languageService.ts b/src/client/common/application/languageService.ts new file mode 100644 index 000000000000..bdd407a04acc --- /dev/null +++ b/src/client/common/application/languageService.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { CompletionItemProvider, DocumentSelector, languages } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ILanguageService } from './types'; + +@injectable() +export class LanguageService implements ILanguageService { + public registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable { + return languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } +} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 0be33a628877..3e5bc65e8696 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -5,6 +5,7 @@ import { Breakpoint, BreakpointsChangeEvent, CancellationToken, + CompletionItemProvider, ConfigurationChangeEvent, DebugConfiguration, DebugConfigurationProvider, @@ -12,6 +13,7 @@ import { DebugSession, DebugSessionCustomEvent, Disposable, + DocumentSelector, Event, FileSystemWatcher, GlobPattern, @@ -904,3 +906,22 @@ export interface ILiveShareTestingApi extends ILiveShareApi { startSession(): Promise; stopSession(): Promise; } + +export const ILanguageService = Symbol('ILanguageService'); +export interface ILanguageService { + /** + * Register a completion provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and groups of equal score are sequentially asked for + * completion items. The process stops when one or many providers of a group return a + * result. A failing provider (rejected promise or exception) will not fail the whole + * operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A completion provider. + * @param triggerCharacters Trigger completion when the user types one of the characters, like `.` or `:`. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 572373871893..2bcc023af573 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -10,6 +10,7 @@ import { CommandManager } from './application/commandManager'; import { DebugService } from './application/debugService'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; +import { LanguageService } from './application/languageService'; import { TerminalManager } from './application/terminalManager'; import { IApplicationEnvironment, @@ -17,6 +18,7 @@ import { ICommandManager, IDebugService, IDocumentManager, + ILanguageService, ILiveShareApi, ITerminalManager, IWorkspaceService @@ -91,6 +93,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITerminalManager, TerminalManager); serviceManager.addSingleton(IDebugService, DebugService); serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); + serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); serviceManager.addSingleton(IHttpClient, HttpClient); serviceManager.addSingleton(IEditorUtils, EditorUtils); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index ca3b28f95123..84b3369be623 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -1,286 +1,288 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; - -// External callers of localize use these tables to retrieve localized values. -export namespace Diagnostics { - export const warnSourceMaps = localize('diagnostics.warnSourceMaps', 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.'); - export const disableSourceMaps = localize('diagnostics.disableSourceMaps', 'Disable Source Map Support'); - export const warnBeforeEnablingSourceMaps = localize('diagnostics.warnBeforeEnablingSourceMaps', 'Enabling source map support in the Python Extension will adversely impact performance of the extension.'); - export const enableSourceMapsAndReloadVSC = localize('diagnostics.enableSourceMapsAndReloadVSC', 'Enable and reload Window.'); - export const lsNotSupported = localize('diagnostics.lsNotSupported', 'Your operating system does not meet the minimum requirements of the Language Server. Reverting to the alternative, Jedi.'); - export const invalidPythonPathInDebuggerSettings = localize('diagnostics.invalidPythonPathInDebuggerSettings', 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.'); - export const invalidPythonPathInDebuggerLaunch = localize('diagnostics.invalidPythonPathInDebuggerLaunch', 'The Python path in your debug configuration is invalid.'); -} - -export namespace Common { - export const canceled = localize('Common.canceled', 'Canceled'); - export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); - export const openOutputPanel = localize('Common.openOutputPanel', 'Show output'); -} - -export namespace LanguageService { - export const bannerMessage = localize('LanguageService.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Language Server is working for you?'); - export const bannerLabelYes = localize('LanguageService.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('LanguageService.bannerLabelNo', 'No, thanks'); - export const lsFailedToStart = localize('LanguageService.lsFailedToStart', 'We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const lsFailedToDownload = localize('LanguageService.lsFailedToDownload', 'We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const lsFailedToExtract = localize('LanguageService.lsFailedToExtract', 'We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const downloadFailedOutputMessage = localize('LanguageService.downloadFailedOutputMessage', 'download failed.'); - export const extractionFailedOutputMessage = localize('LanguageService.extractionFailedOutputMessage', 'extraction failed.'); - export const extractionCompletedOutputMessage = localize('LanguageService.extractionCompletedOutputMessage', 'complete.'); - export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); - export const reloadVSCodeIfSeachPathHasChanged = localize('LanguageService.reloadVSCodeIfSeachPathHasChanged', 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); - -} - -export namespace Interpreters { - export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); - export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); -} - -export namespace Linters { - export const installedButNotEnabled = localize('Linter.InstalledButNotEnabled', 'Linter {0} is installed but not enabled.'); - export const replaceWithSelectedLinter = localize('Linter.replaceWithSelectedLinter', 'Multiple linters are enabled in settings. Replace with \'{0}\'?'); -} - -export namespace InteractiveShiftEnterBanner { - export const bannerMessage = localize('InteractiveShiftEnterBanner.bannerMessage', 'Would you like shift-enter to send code to the new Interactive Window experience?'); - export const bannerLabelYes = localize('InteractiveShiftEnterBanner.bannerLabelYes', 'Yes'); - export const bannerLabelNo = localize('InteractiveShiftEnterBanner.bannerLabelNo', 'No'); -} - -export namespace DataScienceSurveyBanner { - export const bannerMessage = localize('DataScienceSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?'); - export const bannerLabelYes = localize('DataScienceSurveyBanner.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks'); -} - -export namespace DataScience { - export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); - export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer'); - export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '

{0} is not a valid file name

'); - export const sessionDisposed = localize('DataScience.sessionDisposed', 'Cannot execute code, session has been disposed.'); - export const unknownMimeTypeFormat = localize('DataScience.unknownMimeTypeFormat', 'Mime type {0} is not currently supported'); - export const exportDialogTitle = localize('DataScience.exportDialogTitle', 'Export to Jupyter Notebook'); - export const exportDialogFilter = localize('DataScience.exportDialogFilter', 'Jupyter Notebooks'); - export const exportDialogComplete = localize('DataScience.exportDialogComplete', 'Notebook written to {0}'); - export const exportDialogFailed = localize('DataScience.exportDialogFailed', 'Failed to export notebook. {0}'); - export const exportOpenQuestion = localize('DataScience.exportOpenQuestion', 'Open in browser'); - export const runCellLensCommandTitle = localize('python.command.python.datascience.runcell.title', 'Run cell'); - export const importDialogTitle = localize('DataScience.importDialogTitle', 'Import Jupyter Notebook'); - export const importDialogFilter = localize('DataScience.importDialogFilter', 'Jupyter Notebooks'); - export const notebookCheckForImportTitle = localize('DataScience.notebookCheckForImportTitle', 'Do you want to import the Jupyter Notebook into Python code?'); - export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); - export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); - export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); - export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); - export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); - export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); - export const jupyterLaunchTimedOut = localize('DataScience.jupyterLaunchTimedOut', 'The Jupyter notebook server failed to launch in time'); - export const jupyterLaunchNoURL = localize('DataScience.jupyterLaunchNoURL', 'Failed to find the URL of the launched Jupyter notebook server'); - export const pythonInteractiveHelpLink = localize('DataScience.pythonInteractiveHelpLink', 'See [https://aka.ms/pyaiinstall] for help on installing jupyter.'); - export const importingFormat = localize('DataScience.importingFormat', 'Importing {0}'); - export const startingJupyter = localize('DataScience.startingJupyter', 'Starting Jupyter server'); - export const connectingToJupyter = localize('DataScience.connectingToJupyter', 'Connecting to Jupyter server'); - export const exportingFormat = localize('DataScience.exportingFormat', 'Exporting {0}'); - export const runAllCellsLensCommandTitle = localize('python.command.python.datascience.runallcells.title', 'Run all cells'); - export const runAllCellsAboveLensCommandTitle = localize('python.command.python.datascience.runallcellsabove.title', 'Run Above'); - export const runCellAndAllBelowLensCommandTitle = localize('python.command.python.datascience.runcellandallbelow.title', 'Run Below'); - export const importChangeDirectoryComment = localize('DataScience.importChangeDirectoryComment', '#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting'); - export const exportChangeDirectoryComment = localize('DataScience.exportChangeDirectoryComment', '# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting'); - - export const restartKernelMessage = localize('DataScience.restartKernelMessage', 'Do you want to restart the Jupter kernel? All variables will be lost.'); - export const restartKernelMessageYes = localize('DataScience.restartKernelMessageYes', 'Restart'); - export const restartKernelMessageNo = localize('DataScience.restartKernelMessageNo', 'Cancel'); - export const restartingKernelStatus = localize('DataScience.restartingKernelStatus', 'Restarting iPython Kernel'); - export const restartingKernelFailed = localize('DataScience.restartingKernelFailed', 'Kernel restart failed. Jupyter server is hung. Please reload VS code.'); - export const interruptingKernelFailed = localize('DataScience.interruptingKernelFailed', 'Kernel interrupt failed. Jupyter server is hung. Please reload VS code.'); - - export const executingCode = localize('DataScience.executingCode', 'Executing Cell'); - export const collapseAll = localize('DataScience.collapseAll', 'Collapse all cell inputs'); - export const expandAll = localize('DataScience.expandAll', 'Expand all cell inputs'); - export const exportKey = localize('DataScience.export', 'Export as Jupyter Notebook'); - export const restartServer = localize('DataScience.restartServer', 'Restart iPython Kernel'); - export const undo = localize('DataScience.undo', 'Undo'); - export const redo = localize('DataScience.redo', 'Redo'); - export const clearAll = localize('DataScience.clearAll', 'Remove All Cells'); - export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:'); - export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:'); - export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:'); - export const pythonVersionHeaderNoPyKernel = localize('DataScience.pythonVersionHeaderNoPyKernel', 'Python Version may not match, no ipykernel found:'); - - export const jupyterSelectURILaunchLocal = localize('DataScience.jupyterSelectURILaunchLocal', 'Launch local Jupyter server'); - export const jupyterSelectURISpecifyURI = localize('DataScience.jupyterSelectURISpecifyURI', 'Type in the URI for the Jupyter server'); - export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of a Jupyter server'); - export const jupyterSelectURIInvalidURI = localize('DataScience.jupyterSelectURIInvalidURI', 'Invalid URI specified'); - export const jupyterNotebookFailure = localize('DataScience.jupyterNotebookFailure', 'Jupyter notebook failed to launch. \r\n{0}'); - export const jupyterNotebookConnectFailed = localize('DataScience.jupyterNotebookConnectFailed', 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'); - export const jupyterNotebookRemoteConnectFailed = localize('DataScience.jupyterNotebookRemoteConnectFailed', 'Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}'); - export const jupyterServerCrashed = localize('DataScience.jupyterServerCrashed', 'Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}'); - export const notebookVersionFormat = localize('DataScience.notebookVersionFormat', 'Jupyter Notebook Version: {0}'); - //tslint:disable-next-line:no-multiline-string - export const jupyterKernelNotSupportedOnActive = localize('DataScience.jupyterKernelNotSupportedOnActive', `iPython kernel cannot be started from '{0}'. Using closest match {1} instead.`); - export const jupyterKernelSpecNotFound = localize('DataScience.jupyterKernelSpecNotFound', 'Cannot create a iPython kernel spec and none are available for use'); - export const interruptKernel = localize('DataScience.interruptKernel', 'Interrupt iPython Kernel'); - export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting iPython Kernel'); - export const exportCancel = localize('DataScience.exportCancel', 'Cancel'); - export const restartKernelAfterInterruptMessage = localize('DataScience.restartKernelAfterInterruptMessage', 'Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.'); - export const pythonInterruptFailedHeader = localize('DataScience.pythonInterruptFailedHeader', 'Keyboard interrupt crashed the kernel. Kernel restarted.'); - export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: '); - export const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}'); - export const inputWatermark = localize('DataScience.inputWatermark', 'Shift-enter to run'); - export const liveShareConnectFailure = localize('DataScience.liveShareConnectFailure', 'Cannot connect to host jupyter session. URI not found.'); - export const liveShareCannotSpawnNotebooks = localize('DataScience.liveShareCannotSpawnNotebooks', 'Spawning jupyter notebooks is not supported over a live share connection'); - export const liveShareCannotImportNotebooks = localize('DataScience.liveShareCannotImportNotebooks', 'Importing notebooks is not currently supported over a live share connection'); - export const liveShareHostFormat = localize('DataScience.liveShareHostFormat', '{0} Jupyter Server'); - export const liveShareSyncFailure = localize('DataScience.liveShareSyncFailure', 'Synchronization failure during live share startup.'); - export const liveShareServiceFailure = localize('DataScience.liveShareServiceFailure', 'Failure starting \'{0}\' service during live share connection.'); - export const documentMismatch = localize('DataScience.documentMismatch', 'Cannot run cells, duplicate documents for {0} found.'); - export const jupyterGetVariablesBadResults = localize('DataScience.jupyterGetVariablesBadResults', 'Failed to fetch variable info from the Jupyter server.'); - export const dataExplorerInvalidVariableFormat = localize('DataScience.dataExplorerInvalidVariableFormat', '\'{0}\' is not an active variable.'); - export const pythonInteractiveCreateFailed = localize('DataScience.pythonInteractiveCreateFailed', 'Failure to create a \'Python Interactive\' window. Try reinstalling the Python extension.'); - export const jupyterGetVariablesExecutionError = localize('DataScience.jupyterGetVariablesExecutionError', 'Failure during variable extraction: \r\n{0}'); - export const loadingMessage = localize('DataScience.loadingMessage', 'loading ...'); - export const noRowsInDataViewer = localize('DataScience.noRowsInDataViewer', 'Fetching data ...'); - export const pandasTooOldForViewingFormat = localize('DataScience.pandasTooOldForViewingFormat', 'Python package \'pandas\' is version {0}. Version 0.20 or greater is required for viewing data.'); - export const pandasRequiredForViewing = localize('DataScience.pandasRequiredForViewing', 'Python package \'pandas\' is required for viewing data.'); - export const valuesColumn = localize('DataScience.valuesColumn', 'values'); -} - -export namespace DebugConfigurationPrompts { - export const selectConfigurationTitle = localize('debug.selectConfigurationTitle', 'Select a debug configuration'); - export const selectConfigurationPlaceholder = localize('debug.selectConfigurationPlaceholder', 'Debug Configuration'); - export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Python File'); - export const debugFileConfigurationDescription = localize('debug.debugFileConfigurationDescription', 'Debug currently active Python file'); - export const debugModuleConfigurationLabel = localize('debug.debugModuleConfigurationLabel', 'Module'); - export const debugModuleConfigurationDescription = localize('debug.debugModuleConfigurationDescription', 'Debug a python module by invoking it with \'-m\''); - export const remoteAttachConfigurationLabel = localize('debug.remoteAttachConfigurationLabel', 'Remote Attach'); - export const remoteAttachConfigurationDescription = localize('debug.remoteAttachConfigurationDescription', 'Attach to a remote ptsvd debug server'); - export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Django'); - export const debugDjangoConfigurationDescription = localize('debug.debugDjangoConfigurationDescription', 'Launch and debug a Django web application'); - export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Flask'); - export const debugFlaskConfigurationDescription = localize('debug.debugFlaskConfigurationDescription', 'Launch and debug a Flask web application'); - export const debugPyramidConfigurationLabel = localize('debug.debugPyramidConfigurationLabel', 'Pyramid'); - export const debugPyramidConfigurationDescription = localize('debug.debugPyramidConfigurationDescription', 'Web Application'); - export const djangoEnterManagePyPathTitle = localize('debug.djangoEnterManagePyPathTitle', 'Debug Django'); - // tslint:disable-next-line:no-invalid-template-strings - export const djangoEnterManagePyPathPrompt = localize('debug.djangoEnterManagePyPathPrompt', 'Enter path to manage.py (\'${workspaceFolder}\' points to the root of the current workspace folder)'); - export const djangoEnterManagePyPathInvalidFilePathError = localize('debug.djangoEnterManagePyPathInvalidFilePathError', 'Enter a valid python file path'); - export const flaskEnterAppPathOrNamePathTitle = localize('debug.flaskEnterAppPathOrNamePathTitle', 'Debug Flask'); - export const flaskEnterAppPathOrNamePathPrompt = localize('debug.flaskEnterAppPathOrNamePathPrompt', 'Enter path to application, e.g. \'app.py\' or \'app\''); - export const flaskEnterAppPathOrNamePathInvalidNameError = localize('debug.flaskEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'); - - export const moduleEnterModuleTitle = localize('debug.moduleEnterModuleTitle', 'Debug Module'); - export const moduleEnterModulePrompt = localize('debug.moduleEnterModulePrompt', 'Enter Python module/package name'); - export const moduleEnterModuleInvalidNameError = localize('debug.moduleEnterModuleInvalidNameError', 'Enter a valid module name'); - export const pyramidEnterDevelopmentIniPathTitle = localize('debug.pyramidEnterDevelopmentIniPathTitle', 'Debug Pyramid'); - // tslint:disable-next-line:no-invalid-template-strings - export const pyramidEnterDevelopmentIniPathPrompt = localize('debug.pyramidEnterDevelopmentIniPathPrompt', '`Enter path to development.ini (\'${workspaceFolderToken}\' points to the root of the current workspace folder)`'); - export const pyramidEnterDevelopmentIniPathInvalidFilePathError = localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError', 'Enter a valid file path'); - export const attachRemotePortTitle = localize('debug.attachRemotePortTitle', 'Remote Debugging'); - export const attachRemotePortPrompt = localize('debug.attachRemotePortPrompt', 'Enter the port number that the ptvsd server is listening on'); - export const attachRemotePortValidationError = localize('debug.attachRemotePortValidationError', 'Enter a valid port number'); - export const attachRemoteHostTitle = localize('debug.attachRemoteHostTitle', 'Remote Debugging'); - export const attachRemoteHostPrompt = localize('debug.attachRemoteHostPrompt', 'Enter host name'); - export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a host name or IP address'); -} - -export namespace UnitTests { - export const testErrorDiagnosticMessage = localize('UnitTests.testErrorDiagnosticMessage', 'Error'); - export const testFailDiagnosticMessage = localize('UnitTests.testFailDiagnosticMessage', 'Fail'); - export const testSkippedDiagnosticMessage = localize('UnitTests.testSkippedDiagnosticMessage', 'Skipped'); - export const configureTests = localize('UnitTests.configureTests', 'Configure Test Framework'); - export const disableTests = localize('UnitTests.disableTests', 'Disable Tests'); -} - -// Skip using vscode-nls and instead just compute our strings based on key values. Key values -// can be loaded out of the nls..json files -let loadedCollection: Record | undefined; -let defaultCollection: Record | undefined; -const askedForCollection: Record = {}; -let loadedLocale: string; - -export function localize(key: string, defValue: string) { - // Return a pointer to function so that we refetch it on each call. - return () => { - return getString(key, defValue); - }; -} - -export function getCollection() { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // Combine the default and loaded collections - return { ...defaultCollection, ...loadedCollection }; -} - -export function getAskedForCollection() { - return askedForCollection; -} - -function parseLocale(): string { - // Attempt to load from the vscode locale. If not there, use english - const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; - return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; -} - -function getString(key: string, defValue: string) { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // First lookup in the dictionary that matches the current locale - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - askedForCollection[key] = loadedCollection[key]; - return loadedCollection[key]; - } - - // Fallback to the default dictionary - if (defaultCollection && defaultCollection.hasOwnProperty(key)) { - askedForCollection[key] = defaultCollection[key]; - return defaultCollection[key]; - } - - // Not found, return the default - askedForCollection[key] = defValue; - return defValue; -} - -function load() { - // Figure out our current locale. - loadedLocale = parseLocale(); - - // Find the nls file that matches (if there is one) - const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); - if (fs.existsSync(nlsFile)) { - const contents = fs.readFileSync(nlsFile, 'utf8'); - loadedCollection = JSON.parse(contents); - } else { - // If there isn't one, at least remember that we looked so we don't try to load a second time - loadedCollection = {}; - } - - // Get the default collection if necessary. Strings may be in the default or the locale json - if (!defaultCollection) { - const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - if (fs.existsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile, 'utf8'); - defaultCollection = JSON.parse(contents); - } else { - defaultCollection = {}; - } - } -} - -// Default to loading the current locale -load(); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +// External callers of localize use these tables to retrieve localized values. +export namespace Diagnostics { + export const warnSourceMaps = localize('diagnostics.warnSourceMaps', 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.'); + export const disableSourceMaps = localize('diagnostics.disableSourceMaps', 'Disable Source Map Support'); + export const warnBeforeEnablingSourceMaps = localize('diagnostics.warnBeforeEnablingSourceMaps', 'Enabling source map support in the Python Extension will adversely impact performance of the extension.'); + export const enableSourceMapsAndReloadVSC = localize('diagnostics.enableSourceMapsAndReloadVSC', 'Enable and reload Window.'); + export const lsNotSupported = localize('diagnostics.lsNotSupported', 'Your operating system does not meet the minimum requirements of the Language Server. Reverting to the alternative, Jedi.'); + export const invalidPythonPathInDebuggerSettings = localize('diagnostics.invalidPythonPathInDebuggerSettings', 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.'); + export const invalidPythonPathInDebuggerLaunch = localize('diagnostics.invalidPythonPathInDebuggerLaunch', 'The Python path in your debug configuration is invalid.'); +} + +export namespace Common { + export const canceled = localize('Common.canceled', 'Canceled'); + export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); + export const openOutputPanel = localize('Common.openOutputPanel', 'Show output'); +} + +export namespace LanguageService { + export const bannerMessage = localize('LanguageService.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Language Server is working for you?'); + export const bannerLabelYes = localize('LanguageService.bannerLabelYes', 'Yes, take survey now'); + export const bannerLabelNo = localize('LanguageService.bannerLabelNo', 'No, thanks'); + export const lsFailedToStart = localize('LanguageService.lsFailedToStart', 'We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); + export const lsFailedToDownload = localize('LanguageService.lsFailedToDownload', 'We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); + export const lsFailedToExtract = localize('LanguageService.lsFailedToExtract', 'We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); + export const downloadFailedOutputMessage = localize('LanguageService.downloadFailedOutputMessage', 'download failed.'); + export const extractionFailedOutputMessage = localize('LanguageService.extractionFailedOutputMessage', 'extraction failed.'); + export const extractionCompletedOutputMessage = localize('LanguageService.extractionCompletedOutputMessage', 'complete.'); + export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); + export const reloadVSCodeIfSeachPathHasChanged = localize('LanguageService.reloadVSCodeIfSeachPathHasChanged', 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); + +} + +export namespace Interpreters { + export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); + export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); +} + +export namespace Linters { + export const installedButNotEnabled = localize('Linter.InstalledButNotEnabled', 'Linter {0} is installed but not enabled.'); + export const replaceWithSelectedLinter = localize('Linter.replaceWithSelectedLinter', 'Multiple linters are enabled in settings. Replace with \'{0}\'?'); +} + +export namespace InteractiveShiftEnterBanner { + export const bannerMessage = localize('InteractiveShiftEnterBanner.bannerMessage', 'Would you like shift-enter to send code to the new Interactive Window experience?'); + export const bannerLabelYes = localize('InteractiveShiftEnterBanner.bannerLabelYes', 'Yes'); + export const bannerLabelNo = localize('InteractiveShiftEnterBanner.bannerLabelNo', 'No'); +} + +export namespace DataScienceSurveyBanner { + export const bannerMessage = localize('DataScienceSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?'); + export const bannerLabelYes = localize('DataScienceSurveyBanner.bannerLabelYes', 'Yes, take survey now'); + export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks'); +} + +export namespace DataScience { + export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); + export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer'); + export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '

{0} is not a valid file name

'); + export const sessionDisposed = localize('DataScience.sessionDisposed', 'Cannot execute code, session has been disposed.'); + export const unknownMimeTypeFormat = localize('DataScience.unknownMimeTypeFormat', 'Mime type {0} is not currently supported'); + export const exportDialogTitle = localize('DataScience.exportDialogTitle', 'Export to Jupyter Notebook'); + export const exportDialogFilter = localize('DataScience.exportDialogFilter', 'Jupyter Notebooks'); + export const exportDialogComplete = localize('DataScience.exportDialogComplete', 'Notebook written to {0}'); + export const exportDialogFailed = localize('DataScience.exportDialogFailed', 'Failed to export notebook. {0}'); + export const exportOpenQuestion = localize('DataScience.exportOpenQuestion', 'Open in browser'); + export const runCellLensCommandTitle = localize('python.command.python.datascience.runcell.title', 'Run cell'); + export const importDialogTitle = localize('DataScience.importDialogTitle', 'Import Jupyter Notebook'); + export const importDialogFilter = localize('DataScience.importDialogFilter', 'Jupyter Notebooks'); + export const notebookCheckForImportTitle = localize('DataScience.notebookCheckForImportTitle', 'Do you want to import the Jupyter Notebook into Python code?'); + export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); + export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); + export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); + export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); + export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); + export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); + export const jupyterLaunchTimedOut = localize('DataScience.jupyterLaunchTimedOut', 'The Jupyter notebook server failed to launch in time'); + export const jupyterLaunchNoURL = localize('DataScience.jupyterLaunchNoURL', 'Failed to find the URL of the launched Jupyter notebook server'); + export const pythonInteractiveHelpLink = localize('DataScience.pythonInteractiveHelpLink', 'See [https://aka.ms/pyaiinstall] for help on installing jupyter.'); + export const importingFormat = localize('DataScience.importingFormat', 'Importing {0}'); + export const startingJupyter = localize('DataScience.startingJupyter', 'Starting Jupyter server'); + export const connectingToJupyter = localize('DataScience.connectingToJupyter', 'Connecting to Jupyter server'); + export const exportingFormat = localize('DataScience.exportingFormat', 'Exporting {0}'); + export const runAllCellsLensCommandTitle = localize('python.command.python.datascience.runallcells.title', 'Run all cells'); + export const runAllCellsAboveLensCommandTitle = localize('python.command.python.datascience.runallcellsabove.title', 'Run Above'); + export const runCellAndAllBelowLensCommandTitle = localize('python.command.python.datascience.runcellandallbelow.title', 'Run Below'); + export const importChangeDirectoryComment = localize('DataScience.importChangeDirectoryComment', '#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting'); + export const exportChangeDirectoryComment = localize('DataScience.exportChangeDirectoryComment', '# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting'); + + export const restartKernelMessage = localize('DataScience.restartKernelMessage', 'Do you want to restart the Jupter kernel? All variables will be lost.'); + export const restartKernelMessageYes = localize('DataScience.restartKernelMessageYes', 'Restart'); + export const restartKernelMessageNo = localize('DataScience.restartKernelMessageNo', 'Cancel'); + export const restartingKernelStatus = localize('DataScience.restartingKernelStatus', 'Restarting iPython Kernel'); + export const restartingKernelFailed = localize('DataScience.restartingKernelFailed', 'Kernel restart failed. Jupyter server is hung. Please reload VS code.'); + export const interruptingKernelFailed = localize('DataScience.interruptingKernelFailed', 'Kernel interrupt failed. Jupyter server is hung. Please reload VS code.'); + + export const executingCode = localize('DataScience.executingCode', 'Executing Cell'); + export const collapseAll = localize('DataScience.collapseAll', 'Collapse all cell inputs'); + export const expandAll = localize('DataScience.expandAll', 'Expand all cell inputs'); + export const exportKey = localize('DataScience.export', 'Export as Jupyter Notebook'); + export const restartServer = localize('DataScience.restartServer', 'Restart iPython Kernel'); + export const undo = localize('DataScience.undo', 'Undo'); + export const redo = localize('DataScience.redo', 'Redo'); + export const clearAll = localize('DataScience.clearAll', 'Remove All Cells'); + export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:'); + export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:'); + export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:'); + export const pythonVersionHeaderNoPyKernel = localize('DataScience.pythonVersionHeaderNoPyKernel', 'Python Version may not match, no ipykernel found:'); + + export const jupyterSelectURILaunchLocal = localize('DataScience.jupyterSelectURILaunchLocal', 'Launch local Jupyter server'); + export const jupyterSelectURISpecifyURI = localize('DataScience.jupyterSelectURISpecifyURI', 'Type in the URI for the Jupyter server'); + export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of a Jupyter server'); + export const jupyterSelectURIInvalidURI = localize('DataScience.jupyterSelectURIInvalidURI', 'Invalid URI specified'); + export const jupyterNotebookFailure = localize('DataScience.jupyterNotebookFailure', 'Jupyter notebook failed to launch. \r\n{0}'); + export const jupyterNotebookConnectFailed = localize('DataScience.jupyterNotebookConnectFailed', 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'); + export const jupyterNotebookRemoteConnectFailed = localize('DataScience.jupyterNotebookRemoteConnectFailed', 'Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}'); + export const jupyterServerCrashed = localize('DataScience.jupyterServerCrashed', 'Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}'); + export const notebookVersionFormat = localize('DataScience.notebookVersionFormat', 'Jupyter Notebook Version: {0}'); + //tslint:disable-next-line:no-multiline-string + export const jupyterKernelNotSupportedOnActive = localize('DataScience.jupyterKernelNotSupportedOnActive', `iPython kernel cannot be started from '{0}'. Using closest match {1} instead.`); + export const jupyterKernelSpecNotFound = localize('DataScience.jupyterKernelSpecNotFound', 'Cannot create a iPython kernel spec and none are available for use'); + export const interruptKernel = localize('DataScience.interruptKernel', 'Interrupt iPython Kernel'); + export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting iPython Kernel'); + export const exportCancel = localize('DataScience.exportCancel', 'Cancel'); + export const restartKernelAfterInterruptMessage = localize('DataScience.restartKernelAfterInterruptMessage', 'Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.'); + export const pythonInterruptFailedHeader = localize('DataScience.pythonInterruptFailedHeader', 'Keyboard interrupt crashed the kernel. Kernel restarted.'); + export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: '); + export const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}'); + export const inputWatermark = localize('DataScience.inputWatermark', 'Shift-enter to run'); + export const liveShareConnectFailure = localize('DataScience.liveShareConnectFailure', 'Cannot connect to host jupyter session. URI not found.'); + export const liveShareCannotSpawnNotebooks = localize('DataScience.liveShareCannotSpawnNotebooks', 'Spawning jupyter notebooks is not supported over a live share connection'); + export const liveShareCannotImportNotebooks = localize('DataScience.liveShareCannotImportNotebooks', 'Importing notebooks is not currently supported over a live share connection'); + export const liveShareHostFormat = localize('DataScience.liveShareHostFormat', '{0} Jupyter Server'); + export const liveShareSyncFailure = localize('DataScience.liveShareSyncFailure', 'Synchronization failure during live share startup.'); + export const liveShareServiceFailure = localize('DataScience.liveShareServiceFailure', 'Failure starting \'{0}\' service during live share connection.'); + export const documentMismatch = localize('DataScience.documentMismatch', 'Cannot run cells, duplicate documents for {0} found.'); + export const jupyterGetVariablesBadResults = localize('DataScience.jupyterGetVariablesBadResults', 'Failed to fetch variable info from the Jupyter server.'); + export const dataExplorerInvalidVariableFormat = localize('DataScience.dataExplorerInvalidVariableFormat', '\'{0}\' is not an active variable.'); + export const pythonInteractiveCreateFailed = localize('DataScience.pythonInteractiveCreateFailed', 'Failure to create a \'Python Interactive\' window. Try reinstalling the Python extension.'); + export const jupyterGetVariablesExecutionError = localize('DataScience.jupyterGetVariablesExecutionError', 'Failure during variable extraction: \r\n{0}'); + export const loadingMessage = localize('DataScience.loadingMessage', 'loading ...'); + export const noRowsInDataViewer = localize('DataScience.noRowsInDataViewer', 'Fetching data ...'); + export const pandasTooOldForViewingFormat = localize('DataScience.pandasTooOldForViewingFormat', 'Python package \'pandas\' is version {0}. Version 0.20 or greater is required for viewing data.'); + export const pandasRequiredForViewing = localize('DataScience.pandasRequiredForViewing', 'Python package \'pandas\' is required for viewing data.'); + export const valuesColumn = localize('DataScience.valuesColumn', 'values'); +} + +export namespace DebugConfigurationPrompts { + export const selectConfigurationTitle = localize('debug.selectConfigurationTitle', 'Select a debug configuration'); + export const selectConfigurationPlaceholder = localize('debug.selectConfigurationPlaceholder', 'Debug Configuration'); + export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Python File'); + export const debugFileConfigurationDescription = localize('debug.debugFileConfigurationDescription', 'Debug currently active Python file'); + export const debugModuleConfigurationLabel = localize('debug.debugModuleConfigurationLabel', 'Module'); + export const debugModuleConfigurationDescription = localize('debug.debugModuleConfigurationDescription', 'Debug a python module by invoking it with \'-m\''); + export const remoteAttachConfigurationLabel = localize('debug.remoteAttachConfigurationLabel', 'Remote Attach'); + export const remoteAttachConfigurationDescription = localize('debug.remoteAttachConfigurationDescription', 'Attach to a remote ptsvd debug server'); + export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Django'); + export const debugDjangoConfigurationDescription = localize('debug.debugDjangoConfigurationDescription', 'Launch and debug a Django web application'); + export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Flask'); + export const debugFlaskConfigurationDescription = localize('debug.debugFlaskConfigurationDescription', 'Launch and debug a Flask web application'); + export const debugPyramidConfigurationLabel = localize('debug.debugPyramidConfigurationLabel', 'Pyramid'); + export const debugPyramidConfigurationDescription = localize('debug.debugPyramidConfigurationDescription', 'Web Application'); + export const djangoEnterManagePyPathTitle = localize('debug.djangoEnterManagePyPathTitle', 'Debug Django'); + // tslint:disable-next-line:no-invalid-template-strings + export const djangoEnterManagePyPathPrompt = localize('debug.djangoEnterManagePyPathPrompt', 'Enter path to manage.py (\'${workspaceFolder}\' points to the root of the current workspace folder)'); + export const djangoEnterManagePyPathInvalidFilePathError = localize('debug.djangoEnterManagePyPathInvalidFilePathError', 'Enter a valid python file path'); + export const flaskEnterAppPathOrNamePathTitle = localize('debug.flaskEnterAppPathOrNamePathTitle', 'Debug Flask'); + export const flaskEnterAppPathOrNamePathPrompt = localize('debug.flaskEnterAppPathOrNamePathPrompt', 'Enter path to application, e.g. \'app.py\' or \'app\''); + export const flaskEnterAppPathOrNamePathInvalidNameError = localize('debug.flaskEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'); + + export const moduleEnterModuleTitle = localize('debug.moduleEnterModuleTitle', 'Debug Module'); + export const moduleEnterModulePrompt = localize('debug.moduleEnterModulePrompt', 'Enter Python module/package name'); + export const moduleEnterModuleInvalidNameError = localize('debug.moduleEnterModuleInvalidNameError', 'Enter a valid module name'); + export const pyramidEnterDevelopmentIniPathTitle = localize('debug.pyramidEnterDevelopmentIniPathTitle', 'Debug Pyramid'); + // tslint:disable-next-line:no-invalid-template-strings + export const pyramidEnterDevelopmentIniPathPrompt = localize('debug.pyramidEnterDevelopmentIniPathPrompt', '`Enter path to development.ini (\'${workspaceFolderToken}\' points to the root of the current workspace folder)`'); + export const pyramidEnterDevelopmentIniPathInvalidFilePathError = localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError', 'Enter a valid file path'); + export const attachRemotePortTitle = localize('debug.attachRemotePortTitle', 'Remote Debugging'); + export const attachRemotePortPrompt = localize('debug.attachRemotePortPrompt', 'Enter the port number that the ptvsd server is listening on'); + export const attachRemotePortValidationError = localize('debug.attachRemotePortValidationError', 'Enter a valid port number'); + export const attachRemoteHostTitle = localize('debug.attachRemoteHostTitle', 'Remote Debugging'); + export const attachRemoteHostPrompt = localize('debug.attachRemoteHostPrompt', 'Enter host name'); + export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a host name or IP address'); + export const launchJsonConfigurationsCompletionLabel = localize('debug.launchJsonConfigurationsCompletionLabel', 'Python'); + export const launchJsonConfigurationsCompletionDescription = localize('debug.launchJsonConfigurationsCompletionDescription', 'Select a debug configuration'); +} + +export namespace UnitTests { + export const testErrorDiagnosticMessage = localize('UnitTests.testErrorDiagnosticMessage', 'Error'); + export const testFailDiagnosticMessage = localize('UnitTests.testFailDiagnosticMessage', 'Fail'); + export const testSkippedDiagnosticMessage = localize('UnitTests.testSkippedDiagnosticMessage', 'Skipped'); + export const configureTests = localize('UnitTests.configureTests', 'Configure Test Framework'); + export const disableTests = localize('UnitTests.disableTests', 'Disable Tests'); +} + +// Skip using vscode-nls and instead just compute our strings based on key values. Key values +// can be loaded out of the nls..json files +let loadedCollection: Record | undefined; +let defaultCollection: Record | undefined; +const askedForCollection: Record = {}; +let loadedLocale: string; + +export function localize(key: string, defValue: string) { + // Return a pointer to function so that we refetch it on each call. + return () => { + return getString(key, defValue); + }; +} + +export function getCollection() { + // Load the current collection + if (!loadedCollection || parseLocale() !== loadedLocale) { + load(); + } + + // Combine the default and loaded collections + return { ...defaultCollection, ...loadedCollection }; +} + +export function getAskedForCollection() { + return askedForCollection; +} + +function parseLocale(): string { + // Attempt to load from the vscode locale. If not there, use english + const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; + return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; +} + +function getString(key: string, defValue: string) { + // Load the current collection + if (!loadedCollection || parseLocale() !== loadedLocale) { + load(); + } + + // First lookup in the dictionary that matches the current locale + if (loadedCollection && loadedCollection.hasOwnProperty(key)) { + askedForCollection[key] = loadedCollection[key]; + return loadedCollection[key]; + } + + // Fallback to the default dictionary + if (defaultCollection && defaultCollection.hasOwnProperty(key)) { + askedForCollection[key] = defaultCollection[key]; + return defaultCollection[key]; + } + + // Not found, return the default + askedForCollection[key] = defValue; + return defValue; +} + +function load() { + // Figure out our current locale. + loadedLocale = parseLocale(); + + // Find the nls file that matches (if there is one) + const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); + if (fs.existsSync(nlsFile)) { + const contents = fs.readFileSync(nlsFile, 'utf8'); + loadedCollection = JSON.parse(contents); + } else { + // If there isn't one, at least remember that we looked so we don't try to load a second time + loadedCollection = {}; + } + + // Get the default collection if necessary. Strings may be in the default or the locale json + if (!defaultCollection) { + const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); + if (fs.existsSync(defaultNlsFile)) { + const contents = fs.readFileSync(defaultNlsFile, 'utf8'); + defaultCollection = JSON.parse(contents); + } else { + defaultCollection = {}; + } + } +} + +// Default to loading the current locale +load(); diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index 80c83d204df1..07e3c63ee8aa 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { DebugConfigurationPrompts } from '../../../common/utils/localize'; -import { IMultiStepInput, InputStep, IQuickPickParameters } from '../../../common/utils/multiStepInput'; +import { IMultiStepInput, IMultiStepInputFactory, InputStep, IQuickPickParameters } from '../../../common/utils/multiStepInput'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; @@ -21,8 +21,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver, @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, @inject(IDebugConfigurationProviderFactory) private readonly providerFactory: IDebugConfigurationProviderFactory, - // tslint:disable-next-line:no-unused-variable - // @inject(IMultiStepInputFactory) private readonly _multiStepFactory: IMultiStepInputFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, @inject(IFileSystem) private readonly fs: IFileSystem) { } public async provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise { @@ -30,8 +29,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi const state = { config, folder, token }; // Disabled until configuration issues are addressed by VS Code. See #4007 - // const multiStep = this.multiStepFactory.create(); - // await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); if (Object.keys(state.config).length === 0) { return this.getDefaultDebugConfig(); @@ -45,6 +44,12 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi } else if (debugConfiguration.request === 'test') { throw Error('Please use the command \'Python: Debug Unit Tests\''); } else { + if (Object.keys(debugConfiguration).length === 0) { + const configs = await this.provideDebugConfigurations(folder, token); + if (Array.isArray(configs) && configs.length === 1) { + debugConfiguration = configs[0]; + } + } return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); } } diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts new file mode 100644 index 000000000000..c61351f3e182 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { getLocation } from 'jsonc-parser'; +import * as path from 'path'; +import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemProvider, Position, SnippetString, TextDocument } from 'vscode'; +import { IExtensionActivationService } from '../../../../activation/types'; +import { ILanguageService } from '../../../../common/application/types'; +import { IDisposableRegistry, Resource } from '../../../../common/types'; +import { DebugConfigurationPrompts } from '../../../../common/utils/localize'; + +const configurationNodeName = 'configurations'; +enum JsonLanguages { + json = 'json', + jsonWithComments = 'jsonc' +} + +@injectable() +export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionActivationService { + constructor(@inject(ILanguageService) private readonly languageService: ILanguageService, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry) { } + public async activate(_resource: Resource): Promise { + this.disposableRegistry.push(this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this)); + this.disposableRegistry.push(this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this)); + } + public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + if (!this.canProvideCompletions(document, position)) { + return []; + } + + return [ + { + command: { + command: 'python.SelectAndInsertDebugConfiguration', + title: DebugConfigurationPrompts.launchJsonConfigurationsCompletionDescription(), + arguments: [document, position, token] + }, + documentation: DebugConfigurationPrompts.launchJsonConfigurationsCompletionDescription(), + sortText: 'AAAA', + preselect: true, + kind: CompletionItemKind.Enum, + label: DebugConfigurationPrompts.launchJsonConfigurationsCompletionLabel(), + insertText: new SnippetString() + } + ]; + } + public canProvideCompletions(document: TextDocument, position: Position) { + if (path.basename(document.uri.fsPath) !== 'launch.json') { + return false; + } + const location = getLocation(document.getText(), document.offsetAt(position)); + // Cursor must be inside the configurations array and not in any nested items. + // Hence path[0] = array, path[1] = array element index. + return (location.path[0] === configurationNodeName && location.path.length === 2); + } +} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts new file mode 100644 index 000000000000..21260ef9acf3 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/updaterService.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; +import { CancellationToken, DebugConfiguration, Position, TextDocument, WorkspaceEdit } from 'vscode'; +import { IExtensionActivationService } from '../../../../activation/types'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IDisposableRegistry, Resource } from '../../../../common/types'; +import { noop } from '../../../../common/utils/misc'; +import { captureTelemetry } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { IDebugConfigurationService } from '../../types'; + +type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; + +export class LaunchJsonUpdaterServiceHelper { + constructor(private readonly commandManager: ICommandManager, + private readonly workspace: IWorkspaceService, + private readonly documentManager: IDocumentManager, + private readonly configurationProvider: IDebugConfigurationService) { } + @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) + public async selectAndInsertDebugConfig(document: TextDocument, position: Position, token: CancellationToken): Promise { + if (this.documentManager.activeTextEditor && this.documentManager.activeTextEditor.document === document) { + const folder = this.workspace.getWorkspaceFolder(document.uri); + const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); + + if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { + // Always use the first available debug configuration. + await this.insertDebugConfiguration(document, position, configs[0]); + } + } + } + /** + * Inserts the debug configuration into the document. + * Invokes the document formatter to ensure JSON is formatted nicely. + * @param {TextDocument} document + * @param {Position} position + * @param {DebugConfiguration} config + * @returns {Promise} + * @memberof LaunchJsonCompletionItemProvider + */ + public async insertDebugConfiguration(document: TextDocument, position: Position, config: DebugConfiguration): Promise { + const cursorPosition = this.getCursorPositionInConfigurationsArray(document, position); + if (!cursorPosition) { + return; + } + const formattedJson = this.getTextForInsertion(config, cursorPosition); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.insert(document.uri, position, formattedJson); + await this.documentManager.applyEdit(workspaceEdit); + this.commandManager.executeCommand('editor.action.formatDocument').then(noop, noop); + } + /** + * Gets the string representation of the debug config for insertion in the document. + * Adds necessary leading or trailing commas (remember the text is added into an array). + * @param {TextDocument} document + * @param {Position} position + * @param {DebugConfiguration} config + * @returns + * @memberof LaunchJsonCompletionItemProvider + */ + public getTextForInsertion(config: DebugConfiguration, cursorPosition: PositionOfCursor) { + const json = JSON.stringify(config); + if (cursorPosition === 'AfterItem') { + return `,${json}`; + } + if (cursorPosition === 'BeforeItem') { + return `${json},`; + } + return json; + } + public getCursorPositionInConfigurationsArray(document: TextDocument, position: Position): PositionOfCursor | undefined { + if (this.isConfigurationArrayEmpty(document)) { + return 'InsideEmptyArray'; + } + const scanner = createScanner(document.getText(), true); + scanner.setPosition(document.offsetAt(position)); + const nextToken = scanner.scan(); + if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { + return 'AfterItem'; + } + if (nextToken === SyntaxKind.OpenBraceToken) { + return 'BeforeItem'; + } + } + public isConfigurationArrayEmpty(document: TextDocument): boolean { + const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { configurations: [] }; + return (!configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0); + } +} + +@injectable() +export class LaunchJsonUpdaterService implements IExtensionActivationService { + constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService) { } + public async activate(_resource: Resource): Promise { + const handler = new LaunchJsonUpdaterServiceHelper(this.commandManager, this.workspace, this.documentManager, this.configurationProvider); + this.disposableRegistry.push(this.commandManager.registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler)); + } +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index a3fed3aab68e..88c6206056a4 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -3,11 +3,14 @@ 'use strict'; +import { IExtensionActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../types'; import { DebuggerBanner } from './banner'; import { ConfigurationProviderUtils } from './configuration/configurationProviderUtils'; import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; +import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; +import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; import { DjangoLaunchDebugConfigurationProvider } from './configuration/providers/djangoLaunch'; import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch'; import { FlaskLaunchDebugConfigurationProvider } from './configuration/providers/flaskLaunch'; @@ -24,6 +27,8 @@ import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/ import { DebugConfigurationType, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IExtensionActivationService, LaunchJsonCompletionProvider); + serviceManager.addSingleton(IExtensionActivationService, LaunchJsonUpdaterService); serviceManager.addSingleton(IDebugConfigurationService, PythonDebugConfigurationService); serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 872cc51a07fa..8acaa3d84ac9 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -36,6 +36,7 @@ export enum EventName { DEBUGGER = 'DEBUGGER', DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', + DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', UNITTEST_STOP = 'UNITTEST.STOP', UNITTEST_DISABLE = 'UNITTEST.DISABLE', UNITTEST_RUN = 'UNITTEST.RUN', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 98124e0ea89e..7dc09d727ccb 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -260,6 +260,7 @@ export interface IEventNamePropertyMapping { [EventName.DEBUGGER]: DebuggerTelemetry; [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: DebuggerConfigurationPromtpsTelemetry; + [EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON]: never | undefined; [EventName.DEFINITION]: never | undefined; [EventName.DIAGNOSTICS_ACTION]: DiagnosticsAction; [EventName.DIAGNOSTICS_MESSAGE]: DiagnosticsMessages; diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index 900d9fb0d8ff..43977ffcc87f 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -25,6 +25,7 @@ import { IServiceContainer } from '../../client/ioc/types'; import { PYTHON_PATH, sleep } from '../common'; import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; import { continueDebugging, createDebugAdapter } from './utils'; +import { MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; // tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement no-unused-variable no-console const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); @@ -100,7 +101,9 @@ suite('Debugging - Attach Debugger', () => { const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); const providerFactory = TypeMoq.Mock.ofType().object; const fs = mock(FileSystem); - const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, providerFactory, instance(fs)); + const multistepFactory = mock(MultiStepInputFactory); + const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, providerFactory, + instance(multistepFactory), instance(fs)); await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); const attachPromise = debugClient.attachRequest(options); diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 3784a2c2e088..611b240b39cc 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -41,8 +41,8 @@ suite('Debugging - Configuration Service', () => { multiStepFactory = typemoq.Mock.ofType(); providerFactory = mock(DebugConfigurationProviderFactory); fs = mock(FileSystem); - configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object, instance(providerFactory), - instance(fs)); + configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object, + instance(providerFactory), multiStepFactory.object, instance(fs)); }); test('Should use attach resolver when passing attach config', async () => { const config = { @@ -114,12 +114,7 @@ suite('Debugging - Configuration Service', () => { multiStepInput.verifyAll(); expect(Object.keys(state.config)).to.be.lengthOf(0); }); - test('Ensure generated config is returned', async function () { - // Disable this test until this is resolved: - // Issue #4007: Disable debugging configuration provider temporarily - // tslint:disable-next-line:no-invalid-this - return this.skip(); - + test('Ensure generated config is returned', async () => { const expectedConfig = { yes: 'Updated' }; const multiStepInput = { run: (_: any, state: any) => { @@ -153,9 +148,7 @@ suite('Debugging - Configuration Service', () => { when(fs.readFile(jsFile)).thenResolve(JSON.stringify([expectedConfig])); const config = await configService.provideDebugConfigurations!({} as any); - // Disable this check until this is resolved: - // Issue #4007: Disable debugging configuration provider temporarily - // multiStepFactory.verifyAll(); + multiStepFactory.verifyAll(); expect(config).to.deep.equal([expectedConfig]); }); diff --git a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts new file mode 100644 index 000000000000..d699e5110823 --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { deepEqual, instance, mock, verify } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CancellationTokenSource, CompletionItem, CompletionItemKind, Position, SnippetString, TextDocument, Uri } from 'vscode'; +import { LanguageService } from '../../../../../client/common/application/languageService'; +import { ILanguageService } from '../../../../../client/common/application/types'; +import { DebugConfigurationPrompts } from '../../../../../client/common/utils/localize'; +import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; + +// tslint:disable:no-any no-multiline-string max-func-body-length +suite('Debugging - launch.json Completion Provider', () => { + let completionProvider: LaunchJsonCompletionProvider; + let languageService: ILanguageService; + + setup(() => { + languageService = mock(LanguageService); + completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); + }); + test('Activation will register the completion provider', async () => { + await completionProvider.activate(undefined); + verify(languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider)).once(); + verify(languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider)).once(); + }); + test('Cannot provide completions for non launch.json files', () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + document.setup(doc => doc.uri).returns(() => Uri.file(__filename)); + assert.equal(completionProvider.canProvideCompletions(document.object, position), false); + + document.reset(); + document.setup(doc => doc.uri).returns(() => Uri.file('settings.json')); + assert.equal(completionProvider.canProvideCompletions(document.object, position), false); + }); + function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { + const document = typemoq.Mock.ofType(); + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.uri).returns(() => Uri.file('launch.json')); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); + const canProvideCompletions = completionProvider.canProvideCompletions(document.object, position); + assert.equal(canProvideCompletions, expectedValue); + } + test('Cannot provide completions when there is no configurations section in json', () => { + const position = new Position(0, 0); + const config = `{ + "version": "0.1.0" +}`; + testCanProvideCompletions(position, 1, config as any, false); + }); + test('Cannot provide completions when cursor position is not in configurations array', () => { + const position = new Position(0, 0); + const json = `{ + "version": "0.1.0", + "configurations": [] +}`; + testCanProvideCompletions(position, 10, json, false); + }); + test('Cannot provide completions when cursor position is in an empty configurations array', () => { + const position = new Position(0, 0); + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] +}`; + testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); + }); + test('No Completions for non launch.json', async () => { + const document = typemoq.Mock.ofType(); + document.setup(doc => doc.uri).returns(() => Uri.file('settings.json')); + const token = new CancellationTokenSource().token; + const position = new Position(0, 0); + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 0); + }); + test('No Completions for files ending with launch.json', async () => { + const document = typemoq.Mock.ofType(); + document.setup(doc => doc.uri).returns(() => Uri.file('x-launch.json')); + const token = new CancellationTokenSource().token; + const position = new Position(0, 0); + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 0); + }); + test('Get Completions', async () => { + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] +}`; + + const document = typemoq.Mock.ofType(); + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.uri).returns(() => Uri.file('launch.json')); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 1); + + const expectedCompletionItem: CompletionItem = { + command: { + command: 'python.SelectAndInsertDebugConfiguration', + title: DebugConfigurationPrompts.launchJsonConfigurationsCompletionDescription(), + arguments: [document.object, position, token] + }, + documentation: DebugConfigurationPrompts.launchJsonConfigurationsCompletionDescription(), + sortText: 'AAAA', + preselect: true, + kind: CompletionItemKind.Enum, + label: DebugConfigurationPrompts.launchJsonConfigurationsCompletionLabel(), + insertText: new SnippetString() + }; + + assert.deepEqual(completions[0], expectedCompletionItem); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts new file mode 100644 index 000000000000..91067453765f --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CancellationTokenSource, DebugConfiguration, Position, TextDocument, TextEditor, Uri } from 'vscode'; +import { CommandManager } from '../../../../../client/common/application/commandManager'; +import { DocumentManager } from '../../../../../client/common/application/documentManager'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; +import { LaunchJsonUpdaterService, LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; +import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; + +type LaunchJsonSchema = { + version: string; + configurations: DebugConfiguration[]; +}; + +// tslint:disable:no-any no-multiline-string max-func-body-length +suite('Debugging - launch.json Updater Service', () => { + let helper: LaunchJsonUpdaterServiceHelper; + let commandManager: ICommandManager; + let workspace: IWorkspaceService; + let documentManager: IDocumentManager; + let debugConfigService: IDebugConfigurationService; + + setup(() => { + commandManager = mock(CommandManager); + workspace = mock(WorkspaceService); + documentManager = mock(DocumentManager); + debugConfigService = mock(PythonDebugConfigurationService); + helper = new LaunchJsonUpdaterServiceHelper(instance(commandManager), + instance(workspace), instance(documentManager), instance(debugConfigService)); + }); + test('Activation will register the required commands', async () => { + const service = new LaunchJsonUpdaterService(instance(commandManager), [], instance(workspace), instance(documentManager), instance(debugConfigService)); + await service.activate(undefined); + verify(commandManager.registerCommand('python.SelectAndInsertDebugConfiguration', helper.selectAndInsertDebugConfig, helper)); + }); + + test('Configuration Array is detected as being empty', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [] + }; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = helper.isConfigurationArrayEmpty(document.object); + assert.equal(isEmpty, true); + }); + test('Configuration Array is not empty', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python' + } + ] + }; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = helper.isConfigurationArrayEmpty(document.object); + assert.equal(isEmpty, false); + }); + test('Cursor is not positioned in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python' + } + ] + }; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, undefined); + }); + test('Cursor is positioned in the empty configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] + }`; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'InsideEmptyArray'); + }); + test('Cursor is positioned before an item in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + } + ] +}`; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned before an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned after an item in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + }] +}`; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'AfterItem'); + }); + test('Cursor is positioned after an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'AfterItem'); + }); + test('Text to be inserted must be prefixed with a comma', async () => { + const config = {} as any; + const expectedText = `,${JSON.stringify(config)}`; + + const textToInsert = helper.getTextForInsertion(config, 'AfterItem'); + + assert.equal(textToInsert, expectedText); + }); + test('Text to be inserted must be suffixed with a comma', async () => { + const config = {} as any; + const expectedText = `${JSON.stringify(config)},`; + + const textToInsert = helper.getTextForInsertion(config, 'BeforeItem'); + + assert.equal(textToInsert, expectedText); + }); + test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { + const config = {} as any; + const expectedText = JSON.stringify(config); + + const textToInsert = helper.getTextForInsertion(config, 'InsideEmptyArray'); + + assert.equal(textToInsert, expectedText); + }); + test('When inserting the debug config into the json file format the document', async () => { + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + const config = {} as any; + const document = typemoq.Mock.ofType(); + document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + when(documentManager.applyEdit(anything())).thenResolve(); + when(commandManager.executeCommand('editor.action.formatDocument')).thenResolve(); + + await helper.insertDebugConfiguration(document.object, new Position(0, 0), config); + + verify(documentManager.applyEdit(anything())).once(); + verify(commandManager.executeCommand('editor.action.formatDocument')).once(); + }); + test('No changes to configuration if there is not active document', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + when(documentManager.activeTextEditor).thenReturn(); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(anything())).never(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if the active document is not same as the document passed in', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + const textEditor = typemoq.Mock.ofType(); + textEditor.setup(t => t.document).returns(() => 'x' as any).verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(anything())).never(); + textEditor.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if cancellation token has been cancelled', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); + textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([''] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if no configuration items are returned', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); + textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('Changes are made to the configuration', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); + textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(['config'] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, true); + }); +});