diff --git a/news/1 Enhancements/17658.md b/news/1 Enhancements/17658.md new file mode 100644 index 000000000000..48023372756a --- /dev/null +++ b/news/1 Enhancements/17658.md @@ -0,0 +1 @@ +Provide IntelliSense status information when using `github.dev` or any other web platform. diff --git a/news/2 Fixes/16980.md b/news/2 Fixes/16980.md new file mode 100644 index 000000000000..2338273d3c2b --- /dev/null +++ b/news/2 Fixes/16980.md @@ -0,0 +1 @@ +Ensure we use fragment when formatting notebook cells. diff --git a/news/2 Fixes/17252.md b/news/2 Fixes/17252.md new file mode 100644 index 000000000000..bbe585b0199e --- /dev/null +++ b/news/2 Fixes/17252.md @@ -0,0 +1 @@ +Hide UI elements that are not applicable when using `github.dev` or any other web platform. diff --git a/news/2 Fixes/17712.md b/news/2 Fixes/17712.md new file mode 100644 index 000000000000..35ec2f5dfee5 --- /dev/null +++ b/news/2 Fixes/17712.md @@ -0,0 +1 @@ +Localize strings on `github.dev` using VSCode FS API. diff --git a/news/2 Fixes/7160.md b/news/2 Fixes/7160.md new file mode 100644 index 000000000000..a7765d71ec16 --- /dev/null +++ b/news/2 Fixes/7160.md @@ -0,0 +1 @@ +Ensure commands run are not logged twice in Python output channel. diff --git a/news/3 Code Health/16732.md b/news/3 Code Health/16732.md new file mode 100644 index 000000000000..e1cd9df75898 --- /dev/null +++ b/news/3 Code Health/16732.md @@ -0,0 +1 @@ +Log commands run by the discovery component in the output channel. diff --git a/package.json b/package.json index 988f41fbb9f5..04930eedb173 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "id": "pythonWelcome", "title": "Get started with Python development", "description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", + "when": "workspacePlatform != webworker", "steps": [ { "id": "python.installPythonWin", @@ -1707,67 +1708,239 @@ ], "menus": { "commandPalette": [ + { + "category": "Python", + "command": "python.analysis.clearCache", + "title": "%python.command.python.analysis.clearCache.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.clearPersistentStorage", + "title": "%python.command.python.clearPersistentStorage.title%", + "when": "!virtualWorkspace" + }, { "category": "Python", "command": "python.clearWorkspaceInterpreter", - "title": "%python.command.python.clearWorkspaceInterpreter.title%" + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.enableLinting", + "title": "%python.command.python.enableLinting.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.enableSourceMapSupport", + "title": "%python.command.python.enableSourceMapSupport.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminal.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.debugInTerminal", + "icon": "$(debug-alt)", + "title": "%python.command.python.debugInTerminal.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.goToPythonObject", + "title": "%python.command.python.goToPythonObject.title%", + "when": "!virtualWorkspace" }, { "category": "Python", - "command": "python.launchTensorBoard" + "command": "python.launchTensorBoard", + "title": "%python.command.python.launchTensorBoard.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.refreshTensorBoard", + "enablement": "python.hasActiveTensorBoardSession", + "icon": "$(refresh)", + "title": "%python.command.python.refreshTensorBoard.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Test", + "command": "python.refreshTests", + "icon": "$(refresh)", + "title": "%python.command.python.refreshTests.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Test", + "command": "python.refreshingTests", + "icon": { + "dark": "resources/dark/discovering-tests.svg", + "light": "resources/light/discovering-tests.svg" + }, + "title": "%python.command.python.refreshingTests.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Test", + "command": "python.stopRefreshingTests", + "icon": "$(stop-circle)", + "title": "%python.command.python.stopRefreshingTests.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.runLinting", + "title": "%python.command.python.runLinting.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.setLinter", + "title": "%python.command.python.setLinter.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python Refactor", + "command": "python.sortImports", + "title": "%python.command.python.sortImports.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startREPL.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%", + "when": "!virtualWorkspace" + }, + { + "category": "Python", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "when": "!virtualWorkspace" }, { "category": "Python", "command": "python.switchOffInsidersChannel", "title": "%python.command.python.switchOffInsidersChannel.title%", - "when": "config.python.insidersChannel != 'default'" + "when": "config.python.insidersChannel != 'default' && !virtualWorkspace" }, { "category": "Python", "command": "python.switchToDailyChannel", "title": "%python.command.python.switchToDailyChannel.title%", - "when": "config.python.insidersChannel != 'daily'" + "when": "config.python.insidersChannel != 'daily' && !virtualWorkspace" }, { "category": "Python", "command": "python.switchToWeeklyChannel", "title": "%python.command.python.switchToWeeklyChannel.title%", - "when": "config.python.insidersChannel != 'weekly'" + "when": "config.python.insidersChannel != 'weekly' && !virtualWorkspace" }, { "category": "Python", "command": "python.viewOutput", - "title": "%python.command.python.viewOutput.title%" + "title": "%python.command.python.viewOutput.title%", + "when": "!virtualWorkspace" } ], "editor/context": [ { "command": "python.execInTerminal", "group": "Python", - "when": "resourceLangId == python" + "when": "resourceLangId == python && !virtualWorkspace" }, { "command": "python.execSelectionInDjangoShell", "group": "Python", - "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject && !virtualWorkspace" }, { "command": "python.execSelectionInTerminal", "group": "Python", - "when": "editorFocus && editorLangId == python" + "when": "editorFocus && editorLangId == python && !virtualWorkspace" }, { "command": "python.sortImports", "group": "Refactor", "title": "Refactor: Sort Imports", - "when": "editorLangId == python && !notebookEditorFocused" + "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace" } ], "editor/title": [ { "command": "python.refreshTensorBoard", "group": "navigation@0", - "when": "python.hasActiveTensorBoardSession" + "when": "python.hasActiveTensorBoardSession && !virtualWorkspace" } ], "editor/title/run": [ @@ -1775,41 +1948,41 @@ "command": "python.execInTerminal-icon", "group": "navigation@0", "title": "%python.command.python.execInTerminal.title%", - "when": "resourceLangId == python && !isInDiffEditor" + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace" }, { "command": "python.debugInTerminal", "group": "navigation@1", "title": "%python.command.python.debugInTerminal.title%", - "when": "resourceLangId == python && !isInDiffEditor" + "when": "resourceLangId == python && !isInDiffEditor && !virtualWorkspace" } ], "explorer/context": [ { "command": "python.execInTerminal", "group": "Python", - "when": "resourceLangId == python" + "when": "resourceLangId == python && !virtualWorkspace" } ], "view/title": [ { "command": "python.refreshTests", - "when": "view == workbench.view.testing && !refreshingTests && inShowRefreshingTestsExperiment", + "when": "view == workbench.view.testing && !refreshingTests && inShowRefreshingTestsExperiment && !virtualWorkspace", "group": "navigation@0" }, { "command": "python.refreshingTests", - "when": "view == workbench.view.testing && refreshingTests && inShowRefreshingTestsExperiment", + "when": "view == workbench.view.testing && refreshingTests && inShowRefreshingTestsExperiment && !virtualWorkspace", "group": "navigation@0" }, { "command": "python.stopRefreshingTests", - "when": "view == workbench.view.testing && refreshingTests && inShowRefreshingTestsExperiment", + "when": "view == workbench.view.testing && refreshingTests && inShowRefreshingTestsExperiment && !virtualWorkspace", "group": "navigation@0" }, { "command": "testing.reRunFailTests", - "when": "view == workbench.view.testing && hasFailedTests && inShowRunFailedTestsExperiment", + "when": "view == workbench.view.testing && hasFailedTests && inShowRunFailedTestsExperiment && !virtualWorkspace", "group": "navigation@1" } ] @@ -1817,7 +1990,8 @@ "viewsWelcome": [ { "view": "testing", - "contents": "Configure a test framework to see your tests here.\n[Configure Python Tests](command:python.configureTests)" + "contents": "Configure a test framework to see your tests here.\n[Configure Python Tests](command:python.configureTests)", + "when": "!virtualWorkspace" } ], "yamlValidation": [ diff --git a/package.nls.json b/package.nls.json index 1a101b51c327..11a0468d4daf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -165,6 +165,9 @@ "Testing.configureTests": "Configure Test Framework", "Testing.testNotConfigured": "No test framework configured.", "Common.openOutputPanel": "Show output", + "LanguageService.statusItem.name":"Python IntelliSense Status", + "LanguageService.statusItem.text": "Partial Mode", + "LanguageService.statusItem.detail": "Limited IntelliSense provided by Pylance", "LanguageService.lsFailedToStart": "We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", "LanguageService.lsFailedToDownload": "We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.", "LanguageService.lsFailedToExtract": "We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 284bf1300ada..88891aebd4a6 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -9,7 +9,9 @@ import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddle import { ILSExtensionApi } from '../activation/node/languageServerFolderService'; import { LanguageServerType } from '../activation/types'; import { AppinsightsKey, PVSC_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { loadLocalizedStringsForBrowser } from '../common/utils/localizeHelpers'; import { EventName } from '../telemetry/constants'; +import { createStatusItem } from './intellisenseStatus'; interface BrowserConfig { distUrl: string; // URL to Pylance's dist folder. @@ -17,7 +19,7 @@ interface BrowserConfig { export async function activate(context: vscode.ExtensionContext): Promise { // Run in a promise and return early so that VS Code can go activate Pylance. - + await loadLocalizedStringsForBrowser(); const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { runPylance(context, pylanceExtension); @@ -114,6 +116,7 @@ async function runPylance( const disposable = languageClient.start(); context.subscriptions.push(disposable); + context.subscriptions.push(createStatusItem()); } catch (e) { console.log(e); } diff --git a/src/client/browser/intellisenseStatus.ts b/src/client/browser/intellisenseStatus.ts new file mode 100644 index 000000000000..8e199aea49ca --- /dev/null +++ b/src/client/browser/intellisenseStatus.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import * as vscode from 'vscode'; +import { Common, LanguageService } from './localize'; + +export function createStatusItem(): vscode.Disposable { + if ('createLanguageStatusItem' in vscode.languages) { + const statusItem = vscode.languages.createLanguageStatusItem('python.projectStatus', { + language: 'python', + }); + statusItem.name = LanguageService.statusItem.name(); + statusItem.severity = vscode.LanguageStatusSeverity.Warning; + statusItem.text = LanguageService.statusItem.text(); + statusItem.detail = LanguageService.statusItem.detail(); + statusItem.command = { + title: Common.learnMore(), + command: 'vscode.open', + arguments: [vscode.Uri.parse('https://aka.ms/AAdzyh4')], + }; + return statusItem; + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + return { dispose: () => {} }; +} diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts new file mode 100644 index 000000000000..ca66b01266ac --- /dev/null +++ b/src/client/browser/localize.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* eslint-disable @typescript-eslint/no-namespace */ + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import { getLocalizedString } from '../common/utils/localizeHelpers'; + +export namespace LanguageService { + export const statusItem = { + name: localize('LanguageService.statusItem.name', 'Python IntelliSense Status'), + text: localize('LanguageService.statusItem.text', 'Partial Mode'), + detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), + }; +} + +export namespace Common { + export const learnMore = localize('Common.learnMore', 'Learn more'); +} + +function localize(key: string, defValue?: string) { + // Return a pointer to function so that we refetch it on each call. + return (): string => getLocalizedString(key, defValue); +} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 517beef02fb4..73c2ac56a96c 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -16,7 +16,6 @@ import { import { LanguageServerType } from '../activation/types'; import './extensions'; import { IInterpreterAutoSelectionProxyService } from '../interpreter/autoSelection/types'; -import { LogLevel } from '../logging/levels'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; @@ -36,12 +35,10 @@ import { IFormattingSettings, IInterpreterPathService, ILintingSettings, - ILoggingSettings, IPythonSettings, ISortImportSettings, ITensorBoardSettings, ITerminalSettings, - LoggingLevelSettingType, Resource, } from './types'; import { debounceSync } from './utils/decorators'; @@ -137,8 +134,6 @@ export class PythonSettings implements IPythonSettings { public languageServerIsDefault = true; - public logging: ILoggingSettings = { level: LogLevel.Error }; - protected readonly changed = new EventEmitter(); private workspaceRoot: Resource; @@ -305,15 +300,6 @@ export class PythonSettings implements IPythonSettings { this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const loggingSettings = systemVariables.resolveAny(pythonSettings.get('logging'))!; - loggingSettings.level = convertSettingTypeToLogLevel(loggingSettings.level); - if (this.logging) { - Object.assign(this.logging, loggingSettings); - } else { - this.logging = loggingSettings; - } - const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; if (this.linting) { Object.assign(this.linting, lintingSettings); @@ -709,23 +695,3 @@ function isValidPythonPath(pythonPath: string): boolean { path.basename(getOSType() === OSType.Windows ? pythonPath.toLowerCase() : pythonPath).startsWith('python') ); } - -function convertSettingTypeToLogLevel(setting: LoggingLevelSettingType | undefined): LogLevel | 'off' { - switch (setting) { - case 'info': { - return LogLevel.Info; - } - case 'warn': { - return LogLevel.Warn; - } - case 'off': { - return 'off'; - } - case 'debug': { - return LogLevel.Debug; - } - default: { - return LogLevel.Error; - } - } -} diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index 2d8becaebba5..eeaded58fce4 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -248,11 +248,13 @@ export async function getTempFileWithDocumentContents(document: TextDocument, fs // because the language server is watching the file system for Python // file add/delete/change and we don't want this temp file to trigger it. - let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath)}.tmp`; + let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath + document.uri.fragment)}.tmp`; try { // When dealing with untitled notebooks, there's no original physical file, hence create a temp file. if (isNotebookCell(document.uri) && !(await fs.fileExists(document.uri.fsPath))) { - fileName = (await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}.tmp`)).filePath; + fileName = ( + await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}-${document.uri.fragment}.tmp`) + ).filePath; } await fs.writeFile(fileName, document.getText()); } catch (ex) { diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts index bb86a183b529..18a1fea363b7 100644 --- a/src/client/common/platform/fs-paths.ts +++ b/src/client/common/platform/fs-paths.ts @@ -134,9 +134,9 @@ export class FileSystemPathUtils implements IFileSystemPathUtils { } public getDisplayName(filename: string, cwd?: string): string { - if (cwd && filename.startsWith(cwd)) { + if (cwd && isParentPath(filename, cwd)) { return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`; - } else if (filename.startsWith(this.home)) { + } else if (isParentPath(filename, this.home)) { return `~${this.paths.sep}${this.raw.relative(this.home, filename)}`; } else { return filename; @@ -154,6 +154,12 @@ export function normCasePath(filePath: string): string { * @param parentPath The potential parent path to check for */ export function isParentPath(filePath: string, parentPath: string): boolean { + if (!parentPath.endsWith(nodepath.sep)) { + parentPath += nodepath.sep; + } + if (!filePath.endsWith(nodepath.sep)) { + filePath += nodepath.sep; + } return normCasePath(filePath).startsWith(normCasePath(parentPath)); } diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 9adc2bc4cac8..8d3e8125fa5b 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -17,22 +17,26 @@ export class ProcessLogger implements IProcessLogger { @inject(IPathUtils) private readonly pathUtils: IPathUtils, ) {} - public logProcess(file: string, args: string[], options?: SpawnOptions) { + public logProcess(fileOrCommand: string, args?: string[], options?: SpawnOptions) { if (!isTestExecution() && isCI && process.env.UITEST_DISABLE_PROCESS_LOGGING) { // Added to disable logging of process execution commands during UI Tests. // Used only during UI Tests (hence this setting need not be exposed as a valid setting). return; } - const argsList = args.reduce((accumulator, current, index) => { - let formattedArg = this.pathUtils.getDisplayName(current).toCommandArgument(); - if (current[0] === "'" || current[0] === '"') { - formattedArg = `${current[0]}${this.pathUtils.getDisplayName(current.substr(1))}`; - } - + // Note: Single quotes maybe converted to double quotes for printing purposes. + let commandList: string[]; + if (!args) { + // It's a quoted command. + commandList = fileOrCommand.split('" "').map((s) => s.trimQuotes()); + } else { + commandList = [fileOrCommand, ...args].map((s) => s.trimQuotes()); + } + const command = commandList.reduce((accumulator, current, index) => { + const formattedArg = this.pathUtils.getDisplayName(current).toCommandArgument(); return index === 0 ? formattedArg : `${accumulator} ${formattedArg}`; }, ''); - const info = [`> ${this.pathUtils.getDisplayName(file)} ${argsList}`]; + const info = [`> ${command}`]; if (options && options.cwd) { info.push(`${Logging.currentWorkingDirectory()} ${this.pathUtils.getDisplayName(options.cwd)}`); } diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index 61e06ed811d1..ea2722338eda 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -58,6 +58,7 @@ export class ProcessService extends EventEmitter implements IProcessService { } public shellExec(command: string, options: ShellOptions = {}): Promise> { + this.emit('exec', command, undefined, options); return shellExec(command, options, this.env, this.processesToKill); } } diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 510da059ef9a..e909a8d6dfe7 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -85,7 +85,6 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { pythonPath = this.configService.getSettings(options.resource).pythonPath; } const processService: IProcessService = await this.processServiceFactory.create(options.resource); - processService.on('exec', this.logger.logProcess.bind(this.logger)); const windowsStoreInterpreterCheck = (await inDiscoveryExperiment(this.experimentService)) ? // Class methods may depend on other properties which belong to the class, so bind the correct context. diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index bb757018bdd8..2f22ea013261 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -40,7 +40,12 @@ export type ExecutionResult = { export const IProcessLogger = Symbol('IProcessLogger'); export interface IProcessLogger { - logProcess(file: string, ars: string[], options?: SpawnOptions): void; + /** + * Pass `args` as `undefined` if first argument is supposed to be a shell command. + * Note it is assumed that command args are always quoted and respect + * `String.prototype.toCommandArgument()` prototype. + */ + logProcess(fileOrCommand: string, args?: string[], options?: SpawnOptions): void; } export interface IProcessService extends IDisposable { diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 0e7523317cb1..1b38318c1189 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -19,7 +19,6 @@ import { WorkspaceEdit, } from 'vscode'; import { LanguageServerType } from '../activation/types'; -import { LogLevel } from '../logging/levels'; import type { ExtensionChannels } from './insidersBuild/types'; import type { InterpreterUri, ModuleInstallFlags } from './installer/types'; import { EnvironmentVariables } from './variables/types'; @@ -189,7 +188,6 @@ export interface IPythonSettings { readonly languageServer: LanguageServerType; readonly languageServerIsDefault: boolean; readonly defaultInterpreterPath: string; - readonly logging: ILoggingSettings; readonly tensorBoard: ITensorBoardSettings | undefined; initialize(): void; } @@ -224,11 +222,6 @@ export interface IMypyCategorySeverity { readonly note: DiagnosticSeverity; } -export type LoggingLevelSettingType = 'off' | 'error' | 'warn' | 'info' | 'debug'; - -export interface ILoggingSettings { - readonly level: LogLevel | 'off'; -} export interface ILintingSettings { readonly enabled: boolean; readonly ignorePatterns: string[]; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 2e0d9ebd9ff0..6e490da58dea 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,9 +3,8 @@ 'use strict'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; import { FileSystem } from '../platform/fileSystem'; +import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingNodeFS } from './localizeHelpers'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -549,103 +548,17 @@ export namespace MPLSDeprecation { export const switchToJedi = localize('MPLSDeprecation.switchToJedi', 'Switch to Jedi (open source)'); } -// 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; -let askedForCollection: Record = {}; -let loadedLocale: string; - -// This is exported only for testing purposes. -export function _resetCollections(): void { - loadedLocale = ''; - loadedCollection = undefined; - askedForCollection = {}; -} - -// This is exported only for testing purposes. -export function _getAskedForCollection(): Record { - return askedForCollection; -} - -// Return the effective set of all localization strings, by key. -// -// This should not be used for direct lookup. -export function getCollectionJSON(): string { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // Combine the default and loaded collections - return JSON.stringify({ ...defaultCollection, ...loadedCollection }); -} - -export function localize(key: string, defValue?: string) { +function localize(key: string, defValue?: string) { // Return a pointer to function so that we refetch it on each call. return (): string => getString(key, defValue); } -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(); - } - - // The default collection (package.nls.json) is the fallback. - // Note that we are guaranteed the following (during shipping) - // 1. defaultCollection was initialized by the load() call above - // 2. defaultCollection has the key (see the "keys exist" test) - let collection = defaultCollection!; - - // Use the current locale if the key is defined there. - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - collection = loadedCollection; - } - let result = collection[key]; - if (!result && defValue) { - // This can happen during development if you haven't fixed up the nls file yet or - // if for some reason somebody broke the functional test. - result = defValue; - } - askedForCollection[key] = result; - - return result; -} - -function load() { - const fs = new FileSystem(); - - // 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.fileExistsSync(nlsFile)) { - const contents = fs.readFileSync(nlsFile); - 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.fileExistsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile); - defaultCollection = JSON.parse(contents); - } else { - defaultCollection = {}; - } + if (shouldLoadUsingNodeFS()) { + loadLocalizedStringsUsingNodeFS(new FileSystem()); } + return getLocalizedString(key, defValue); } // Default to loading the current locale -load(); +loadLocalizedStringsUsingNodeFS(new FileSystem()); diff --git a/src/client/common/utils/localizeHelpers.ts b/src/client/common/utils/localizeHelpers.ts new file mode 100644 index 000000000000..5a4eed6d98e6 --- /dev/null +++ b/src/client/common/utils/localizeHelpers.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. + +import * as vscode from 'vscode'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IFileSystem } from '../platform/types'; + +// 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; +let askedForCollection: Record = {}; +let loadedLocale: string; + +// This is exported only for testing purposes. +export function _resetCollections(): void { + loadedLocale = ''; + loadedCollection = undefined; + askedForCollection = {}; +} + +// This is exported only for testing purposes. +export function _getAskedForCollection(): Record { + return askedForCollection; +} + +export function shouldLoadUsingNodeFS(): boolean { + return !loadedCollection || parseLocale() !== loadedLocale; +} + +declare let navigator: { language: string } | undefined; + +function parseLocale(): string { + try { + if (navigator?.language) { + return navigator.language.toLowerCase(); + } + } catch { + // Fall through + } + // 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'; +} + +export function getLocalizedString(key: string, defValue?: string): string { + // The default collection (package.nls.json) is the fallback. + // Note that we are guaranteed the following (during shipping) + // 1. defaultCollection was initialized by the load() call above + // 2. defaultCollection has the key (see the "keys exist" test) + let collection = defaultCollection; + + // Use the current locale if the key is defined there. + if (loadedCollection && loadedCollection.hasOwnProperty(key)) { + collection = loadedCollection; + } + if (collection === undefined) { + throw new Error(`Localizations haven't been loaded yet for key: ${key}`); + } + let result = collection[key]; + if (!result && defValue) { + // This can happen during development if you haven't fixed up the nls file yet or + // if for some reason somebody broke the functional test. + result = defValue; + } + askedForCollection[key] = result; + + return result; +} + +/** + * Can be used to synchronously load localized strings, useful if we want localized strings at module level itself. + * Cannot be used in VSCode web or any browser. Must be called before any use of the locale. + */ +export function loadLocalizedStringsUsingNodeFS(fs: IFileSystem): void { + // 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.fileExistsSync(nlsFile)) { + const contents = fs.readFileSync(nlsFile); + 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.fileExistsSync(defaultNlsFile)) { + const contents = fs.readFileSync(defaultNlsFile); + defaultCollection = JSON.parse(contents); + } else { + defaultCollection = {}; + } + } +} + +/** + * Only uses the VSCode APIs to query filesystem and not the node fs APIs, as + * they're not available in browser. Must be called before any use of the locale. + */ +export async function loadLocalizedStringsForBrowser(): Promise { + // Figure out our current locale. + loadedLocale = parseLocale(); + + loadedCollection = await parseNLS(loadedLocale); + + // Get the default collection if necessary. Strings may be in the default or the locale json + if (!defaultCollection) { + defaultCollection = await parseNLS(); + } +} + +async function parseNLS(locale?: string) { + try { + const filename = locale ? `package.nls.${locale}.json` : `package.nls.json`; + const nlsFile = vscode.Uri.joinPath(vscode.Uri.file(EXTENSION_ROOT_DIR), filename); + const buffer = await vscode.workspace.fs.readFile(nlsFile); + const contents = new TextDecoder().decode(buffer); + return JSON.parse(contents); + } catch { + // If there isn't one, at least remember that we looked so we don't try to load a second time. + return {}; + } +} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 059d8cb82052..784368480b29 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -46,6 +46,7 @@ import * as pythonEnvironments from './pythonEnvironments'; import { ActivationResult, ExtensionState } from './components'; import { Components } from './extensionInit'; import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer'; +import { getLoggingLevel } from './logging/settings'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -111,12 +112,13 @@ async function activateLegacy(ext: ExtensionState): Promise { const extensions = serviceContainer.get(IExtensions); await setDefaultLanguageServer(extensions, serviceManager); - const configuration = serviceManager.get(IConfigurationService); - // We should start logging using the log level as soon as possible, so set it as soon as we can access the level. - // `IConfigurationService` may depend any of the registered types, so doing it after all registrations are finished. - // XXX Move this *after* abExperiments is activated? - setLoggingLevel(configuration.getSettings().logging.level); + // Note we should not trigger any extension related code which logs, until we have set logging level. So we cannot + // use configurations service to get level setting. Instead, we use Workspace service to query for setting as it + // directly queries VSCode API. + setLoggingLevel(getLoggingLevel()); + // `IConfigurationService` may depend any of the registered types, so doing it after all registrations are finished. + const configuration = serviceManager.get(IConfigurationService); const languageServerType = configuration.getSettings().languageServer; // Language feature registrations. diff --git a/src/client/logging/settings.ts b/src/client/logging/settings.ts new file mode 100644 index 000000000000..ba0860dfbbc9 --- /dev/null +++ b/src/client/logging/settings.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { WorkspaceService } from '../common/application/workspace'; +import { LogLevel } from './levels'; + +type LoggingLevelSettingType = 'off' | 'error' | 'warn' | 'info' | 'debug'; + +/** + * Uses Workspace service to query for `python.logging.level` setting and returns it. + */ +export function getLoggingLevel(): LogLevel | 'off' { + const workspace = new WorkspaceService(); + const value = workspace.getConfiguration('python').get('logging.level'); + return convertSettingTypeToLogLevel(value); +} + +function convertSettingTypeToLogLevel(setting: LoggingLevelSettingType | undefined): LogLevel | 'off' { + switch (setting) { + case 'info': { + return LogLevel.Info; + } + case 'warn': { + return LogLevel.Warn; + } + case 'off': { + return 'off'; + } + case 'debug': { + return LogLevel.Debug; + } + default: { + return LogLevel.Error; + } + } +} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts index 29da69dd1f43..0c9155ce2b3a 100644 --- a/src/client/providers/formatProvider.ts +++ b/src/client/providers/formatProvider.ts @@ -3,6 +3,7 @@ import * as vscode from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { PYTHON_LANGUAGE } from '../common/constants'; import { IConfigurationService } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; @@ -76,7 +77,7 @@ export class PythonFormattingEditProvider // Workaround is to resolve promise to nothing here, then execute format document and force new save. // However, we need to know if this is 'format document' or formatting on save. - if (this.saving) { + if (this.saving || document.languageId !== PYTHON_LANGUAGE) { // We are saving after formatting (see onSaveDocument below) // so we do not want to format again. return []; diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts index b6e86fab3a7f..b924036ba1ae 100644 --- a/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -73,7 +73,7 @@ export async function getInterpreterInfo(python: PythonExecInfo): Promise (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), ''); + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c}"`), ''); // Try shell execing the command, followed by the arguments. This will make node kill the process if it // takes too long. diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 300b218864ae..b3fd3f3827bd 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -9,7 +9,6 @@ import { parseVersion } from '../../base/info/pythonVersion'; import { getRegistryInterpreters } from '../windowsUtils'; import { EnvironmentType, PythonEnvironment } from '../../info'; -import { IDisposable } from '../../../common/types'; import { cache } from '../../../common/utils/decorators'; import { isTestExecution } from '../../../common/constants'; @@ -353,19 +352,8 @@ export class Conda { @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this private async getInfoCached(command: string): Promise { - const disposables = new Set(); - const result = await exec(command, ['info', '--json'], { timeout: 50000 }, disposables); + const result = await exec(command, ['info', '--json'], { timeout: 50000 }); traceVerbose(`conda info --json: ${result.stdout}`); - - // Ensure the process we started is cleaned up. - disposables.forEach((p) => { - try { - p.dispose(); - } catch { - // ignore. - } - }); - return JSON.parse(result.stdout); } diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts index 058f0fb59d15..25a18f3a3d00 100644 --- a/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -4,14 +4,12 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; -import { ExecutionResult, ShellOptions, SpawnOptions } from '../../common/process/types'; +import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; import { IExperimentService, IDisposable, IConfigurationService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { normalizeFilename } from '../../common/utils/filesystem'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; -import { plainExec, shellExec } from '../../common/process/rawProcessApis'; -import { BufferDecoder } from '../../common/process/decoder'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -20,39 +18,14 @@ export function initializeExternalDependencies(serviceContainer: IServiceContain // processes -/** - * Specialized version of the more generic shellExecute function to use only in - * cases where we don't need to pass custom environment variables read from env - * files or execution options. - * - * Also ensures to kill the processes created after execution. - */ export async function shellExecute(command: string, options: ShellOptions = {}): Promise> { - const disposables = new Set(); - return shellExec(command, options, undefined, disposables).finally(() => { - // Ensure the process we started is cleaned up. - disposables.forEach((p) => { - try { - p.dispose(); - } catch { - // ignore. - } - }); - }); + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + return service.shellExec(command, options); } -/** - * Specialized version of the more generic exec function to use only in - * cases where we don't need to pass custom environment variables read from - * env files. - */ -export async function exec( - file: string, - args: string[], - options: SpawnOptions = {}, - disposables?: Set, -): Promise> { - return plainExec(file, args, options, new BufferDecoder(), undefined, disposables); +export async function exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + return service.exec(file, args, options); } // filesystem diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 0db9e654757f..6b20b9036038 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -18,7 +18,6 @@ import { IExperiments, IFormattingSettings, ILintingSettings, - ILoggingSettings, ISortImportSettings, ITerminalSettings, } from '../../../client/common/types'; @@ -97,7 +96,6 @@ suite('Python Settings', async () => { config.setup((c) => c.get('devOptions')).returns(() => sourceSettings.devOptions); // complex settings - config.setup((c) => c.get('logging')).returns(() => sourceSettings.logging); config.setup((c) => c.get('linting')).returns(() => sourceSettings.linting); config.setup((c) => c.get('analysis')).returns(() => sourceSettings.analysis); config.setup((c) => c.get('sortImports')).returns(() => sourceSettings.sortImports); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts index d04024843f66..974a395f6cf3 100644 --- a/src/test/common/process/logger.unit.test.ts +++ b/src/test/common/process/logger.unit.test.ts @@ -60,11 +60,35 @@ suite('ProcessLogger suite', () => { }); test('Logger preserves quotes around arguments if they contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar', '"import test"'], options); + + const expectedResult = `> test --foo --bar \"import test\"\n${Logging.currentWorkingDirectory()} ${path.join( + 'debug', + 'path', + )}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test('Logger converts single quotes around arguments to double quotes if they contain spaces', async () => { const options = { cwd: path.join('debug', 'path') }; const logger = new ProcessLogger(outputChannel.object, pathUtils); logger.logProcess('test', ['--foo', '--bar', "'import test'"], options); - const expectedResult = `> test --foo --bar \'import test\'\n${Logging.currentWorkingDirectory()} ${path.join( + const expectedResult = `> test --foo --bar \"import test\"\n${Logging.currentWorkingDirectory()} ${path.join( + 'debug', + 'path', + )}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test('Logger removes single quotes around arguments if they do not contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar', "'importtest'"], options); + + const expectedResult = `> test --foo --bar importtest\n${Logging.currentWorkingDirectory()} ${path.join( 'debug', 'path', )}\n`; @@ -95,6 +119,17 @@ suite('ProcessLogger suite', () => { expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); }); + test('Logger replaces the path/to/home with ~ if shell command is provided', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess(`"${path.join(untildify('~'), 'test')}" "--foo" "--bar"`, undefined, options); + + const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n${Logging.currentWorkingDirectory()} ${ + options.cwd + }\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + test("Logger doesn't display the working directory line if there is no options parameter", async () => { const logger = new ProcessLogger(outputChannel.object, pathUtils); logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar']); diff --git a/src/test/common/utils/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts index f165025cb695..1e1aa443400c 100644 --- a/src/test/common/utils/localize.functional.test.ts +++ b/src/test/common/utils/localize.functional.test.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import * as localize from '../../../client/common/utils/localize'; +import * as localizeHelpers from '../../../client/common/utils/localizeHelpers'; const defaultNLSFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); @@ -26,7 +27,7 @@ suite('Localization', () => { setLocale('en-us'); // Ensure each test starts fresh. - localize._resetCollections(); + localizeHelpers._resetCollections(); }); teardown(() => { @@ -102,7 +103,7 @@ suite('Localization', () => { useEveryLocalization(localize); // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); + const askedFor = localizeHelpers._getAskedForCollection(); const missing: Record = {}; Object.keys(askedFor).forEach((key: string) => { // Now check that this key exists somewhere in the nls collection @@ -133,7 +134,7 @@ suite('Localization', () => { useEveryLocalization(localize); // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); + const askedFor = localizeHelpers._getAskedForCollection(); const extra: Record = {}; Object.keys(nlsCollection).forEach((key: string) => { // Now check that this key exists somewhere in the nls collection diff --git a/tsconfig.browser.json b/tsconfig.browser.json index d4aae7e98208..cda0a494e0f2 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "include": [ - "./src/client/browser" + "./src/client/browser", + "./types/vscode.proposed.d.ts" ] } diff --git a/types/vscode.proposed.d.ts b/types/vscode.proposed.d.ts index 3550b5c667e9..786b3017b8b2 100644 --- a/types/vscode.proposed.d.ts +++ b/types/vscode.proposed.d.ts @@ -750,6 +750,33 @@ declare module 'vscode' { replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], outputId: string): Thenable; } + //#region https://github.com/microsoft/vscode/issues/129037 + + enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2, + } + + interface LanguageStatusItem { + readonly id: string; + selector: DocumentSelector; + // todo@jrieken replace with boolean ala needsAttention + severity: LanguageStatusSeverity; + name: string | undefined; + text: string; + detail?: string; + command: Command | undefined; + accessibilityInformation?: AccessibilityInformation; + dispose(): void; + } + + namespace languages { + export function createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; + } + + //#endregion + export interface QuickPick extends QuickInput { /** * An optional flag to sort the final results by index of first query match in label. Defaults to true.