diff --git a/client/package-lock.json b/client/package-lock.json index 08a57160..1ba97d37 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@google-cloud/bigquery": "^7.3.0", "@types/vscode": "^1.83.1", + "@vscode/python-extension": "^1.0.5", "@vscode/test-electron": "^2.3.6" }, "engines": { @@ -545,6 +546,16 @@ "vscode": "^1.75.0" } }, + "node_modules/@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "dev": true, + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, "node_modules/@vscode/test-electron": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.6.tgz", @@ -1969,6 +1980,12 @@ "applicationinsights": "^2.7.1" } }, + "@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "dev": true + }, "@vscode/test-electron": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.6.tgz", diff --git a/client/package.json b/client/package.json index 59420e6b..2bbda473 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "devDependencies": { "@google-cloud/bigquery": "^7.3.0", "@types/vscode": "^1.83.1", + "@vscode/python-extension": "^1.0.5", "@vscode/test-electron": "^2.3.6" } } diff --git a/client/src/DbtLanguageClientManager.ts b/client/src/DbtLanguageClientManager.ts index c579e786..a8564dd4 100644 --- a/client/src/DbtLanguageClientManager.ts +++ b/client/src/DbtLanguageClientManager.ts @@ -10,6 +10,7 @@ import { WorkspaceHelper } from './WorkspaceHelper'; import { DbtLanguageClient } from './lsp_client/DbtLanguageClient'; import { DbtWizardLanguageClient } from './lsp_client/DbtWizardLanguageClient'; import { NoProjectLanguageClient } from './lsp_client/NoProjectLanguageClient'; +import { PythonExtensionWrapper } from './python/PythonExtensionWrapper'; import { StatusHandler } from './status/StatusHandler'; import path = require('node:path'); @@ -23,6 +24,7 @@ export class DbtLanguageClientManager { constructor( private previewContentProvider: SqlPreviewContentProvider, + private pythonExtension: PythonExtensionWrapper, private outputChannelProvider: OutputChannelProvider, private serverAbsolutePath: string, private manifestParsedEventEmitter: EventEmitter, @@ -124,6 +126,7 @@ export class DbtLanguageClientManager { if (!this.clients.has(projectUri.fsPath)) { const client = new DbtLanguageClient( 6009 + this.clients.size, + this.pythonExtension, this.outputChannelProvider, this.serverAbsolutePath, projectUri, @@ -145,7 +148,13 @@ export class DbtLanguageClientManager { async ensureNoProjectClient(): Promise { if (!this.noProjectClient) { - this.noProjectClient = new NoProjectLanguageClient(6008, this.outputChannelProvider, this.statusHandler, this.serverAbsolutePath); + this.noProjectClient = new NoProjectLanguageClient( + 6008, + this.pythonExtension, + this.outputChannelProvider, + this.statusHandler, + this.serverAbsolutePath, + ); await this.initAndStartClient(this.noProjectClient); } } diff --git a/client/src/ExtensionClient.ts b/client/src/ExtensionClient.ts index 254f129e..beeba18e 100644 --- a/client/src/ExtensionClient.ts +++ b/client/src/ExtensionClient.ts @@ -24,6 +24,7 @@ import { DbtDeps } from './commands/DbtDeps'; import { GoogleAuth } from './commands/GoogleAuth'; import { NotUseConfigForRefsPreview } from './commands/NotUseConfigForRefsPreview'; import { UseConfigForRefsPreview } from './commands/UseConfigForRefsPreview'; +import { PythonExtensionWrapper } from './python/PythonExtensionWrapper'; export interface PackageJson { name: string; @@ -38,6 +39,7 @@ export class ExtensionClient { commandManager = new CommandManager(); packageJson?: PackageJson; activeTextEditorHandler: ActiveTextEditorHandler; + pythonExtension = new PythonExtensionWrapper(); constructor( private context: ExtensionContext, @@ -46,6 +48,7 @@ export class ExtensionClient { ) { this.dbtLanguageClientManager = new DbtLanguageClientManager( this.previewContentProvider, + this.pythonExtension, this.outputChannelProvider, this.context.asAbsolutePath(path.join('server', 'out', 'server.js')), manifestParsedEventEmitter, @@ -123,7 +126,7 @@ export class ExtensionClient { this.commandManager.register(new DbtDeps(this.dbtLanguageClientManager)); this.commandManager.register(new GoogleAuth(this.dbtLanguageClientManager)); this.commandManager.register(new AnalyzeEntireProject(this.dbtLanguageClientManager)); - this.commandManager.register(new CreateDbtProject(this.context.globalState)); + this.commandManager.register(new CreateDbtProject(this.context.globalState, this.pythonExtension)); this.commandManager.register(new InstallDbtCore(this.dbtLanguageClientManager, this.outputChannelProvider)); this.commandManager.register(new InstallDbtAdapters(this.dbtLanguageClientManager, this.outputChannelProvider)); this.commandManager.register(new OpenOrCreatePackagesYml()); diff --git a/client/src/commands/CreateDbtProject/CreateDbtProject.ts b/client/src/commands/CreateDbtProject/CreateDbtProject.ts index acc8825f..d16717bc 100644 --- a/client/src/commands/CreateDbtProject/CreateDbtProject.ts +++ b/client/src/commands/CreateDbtProject/CreateDbtProject.ts @@ -3,7 +3,7 @@ import { EOL } from 'node:os'; import { promisify, TextEncoder } from 'node:util'; import { commands, Memento, OpenDialogOptions, Uri, window, workspace } from 'vscode'; import { log } from '../../Logger'; -import { PythonExtension } from '../../python/PythonExtension'; +import { PythonExtensionWrapper } from '../../python/PythonExtensionWrapper'; import { DBT_PROJECT_YML } from '../../Utils'; import { Command } from '../CommandManager'; import { DbtInitTerminal } from './DbtInitTerminal'; @@ -28,7 +28,10 @@ export class CreateDbtProject implements Command { } `; - constructor(private extensionGlobalState: Memento) {} + constructor( + private extensionGlobalState: Memento, + private pythonExtension: PythonExtensionWrapper, + ) {} async execute(projectFolder?: string, skipOpen?: boolean): Promise { const dbtInitCommandPromise = this.getDbtInitCommand(); @@ -97,7 +100,7 @@ export class CreateDbtProject implements Command { } async getDbtInitCommand(): Promise { - const pythonInfo = await new PythonExtension().getPythonInfo(); + const pythonInfo = await this.pythonExtension.getPythonInfo(); const promisifiedExec = promisify(exec); const pythonOldCommand = `${pythonInfo.path} -c "import ${CreateDbtProject.PYTHON_CODE_OLD}(['--version'])"`; diff --git a/client/src/lsp_client/DbtLanguageClient.ts b/client/src/lsp_client/DbtLanguageClient.ts index 60aab3b8..10208046 100644 --- a/client/src/lsp_client/DbtLanguageClient.ts +++ b/client/src/lsp_client/DbtLanguageClient.ts @@ -22,6 +22,7 @@ import { ProjectProgressHandler } from '../ProjectProgressHandler'; import SqlPreviewContentProvider from '../SqlPreviewContentProvider'; import { TelemetryClient } from '../TelemetryClient'; import { DBT_PROJECT_YML, PACKAGES_YML, SNOWFLAKE_SQL_LANG_ID, SQL_LANG_ID, SUPPORTED_LANG_IDS } from '../Utils'; +import { PythonExtensionWrapper } from '../python/PythonExtensionWrapper'; import { StatusHandler } from '../status/StatusHandler'; import { DbtWizardLanguageClient } from './DbtWizardLanguageClient'; @@ -32,6 +33,7 @@ export class DbtLanguageClient extends DbtWizardLanguageClient { constructor( private port: number, + pythonExtension: PythonExtensionWrapper, outputChannelProvider: OutputChannelProvider, private serverAbsolutePath: string, dbtProjectUri: Uri, @@ -41,7 +43,7 @@ export class DbtLanguageClient extends DbtWizardLanguageClient { statusHandler: StatusHandler, private snowflakeExtensionExists: Lazy, ) { - super(outputChannelProvider, statusHandler, dbtProjectUri); + super(pythonExtension, outputChannelProvider, statusHandler, dbtProjectUri); window.onDidChangeVisibleTextEditors((e: readonly TextEditor[]) => this.onDidChangeVisibleTextEditors(e)); } diff --git a/client/src/lsp_client/DbtWizardLanguageClient.ts b/client/src/lsp_client/DbtWizardLanguageClient.ts index 031b0ecc..45e2607f 100644 --- a/client/src/lsp_client/DbtWizardLanguageClient.ts +++ b/client/src/lsp_client/DbtWizardLanguageClient.ts @@ -1,10 +1,11 @@ +import { ActiveEnvironmentPathChangeEvent } from '@vscode/python-extension'; import { CustomInitParams, LspModeType, PythonInfo, StatusNotification } from 'dbt-language-server-common'; import { Uri, WorkspaceFolder, commands, workspace } from 'vscode'; import { Disposable, State } from 'vscode-languageclient'; import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node'; import { log } from '../Logger'; import { OutputChannelProvider } from '../OutputChannelProvider'; -import { PythonExtension } from '../python/PythonExtension'; +import { PythonExtensionWrapper } from '../python/PythonExtensionWrapper'; import { StatusHandler } from '../status/StatusHandler'; export abstract class DbtWizardLanguageClient implements Disposable { @@ -23,10 +24,10 @@ export abstract class DbtWizardLanguageClient implements Disposable { } protected disposables: Disposable[] = []; - protected pythonExtension = new PythonExtension(); protected client!: LanguageClient; constructor( + private pythonExtension: PythonExtensionWrapper, protected outputChannelProvider: OutputChannelProvider, protected statusHandler: StatusHandler, protected dbtProjectUri: Uri, @@ -87,9 +88,10 @@ export abstract class DbtWizardLanguageClient implements Disposable { async start(): Promise { await this.client.start().catch(e => log(`Error while starting server: ${e instanceof Error ? e.message : String(e)}`)); - (await this.pythonExtension.onDidChangeExecutionDetails())(async (resource: Uri | undefined) => { - log(`onDidChangeExecutionDetails ${resource?.fsPath ?? 'undefined'}`); - if (this.client.state === State.Running && this.dbtProjectUri.fsPath === resource?.fsPath) { + + (await this.pythonExtension.onDidChangeActiveEnvironmentPath())(async (e: ActiveEnvironmentPathChangeEvent) => { + log(`onDidChangeActiveEnvironmentPath ${e.resource?.uri.fsPath ?? 'undefined'}`); + if (this.client.state === State.Running && this.dbtProjectUri.fsPath === e.resource?.uri.fsPath) { const newPythonInfo = await this.pythonExtension.getPythonInfo(this.client.clientOptions.workspaceFolder); if (newPythonInfo.path !== this.pythonInfo?.path || newPythonInfo.version?.join('.') !== this.pythonInfo.version?.join('.')) { log(`Python info changed: ${JSON.stringify(newPythonInfo)}`); diff --git a/client/src/lsp_client/NoProjectLanguageClient.ts b/client/src/lsp_client/NoProjectLanguageClient.ts index b255b75a..8ff1dce7 100644 --- a/client/src/lsp_client/NoProjectLanguageClient.ts +++ b/client/src/lsp_client/NoProjectLanguageClient.ts @@ -2,17 +2,19 @@ import { LspModeType, NO_PROJECT_PATH } from 'dbt-language-server-common'; import { Uri } from 'vscode'; import { LanguageClient } from 'vscode-languageclient/node'; import { OutputChannelProvider } from '../OutputChannelProvider'; +import { PythonExtensionWrapper } from '../python/PythonExtensionWrapper'; import { StatusHandler } from '../status/StatusHandler'; import { DbtWizardLanguageClient } from './DbtWizardLanguageClient'; export class NoProjectLanguageClient extends DbtWizardLanguageClient { constructor( private port: number, + pythonExtension: PythonExtensionWrapper, outputChannelProvider: OutputChannelProvider, statusHandler: StatusHandler, private serverAbsolutePath: string, ) { - super(outputChannelProvider, statusHandler, Uri.file(NO_PROJECT_PATH)); + super(pythonExtension, outputChannelProvider, statusHandler, Uri.file(NO_PROJECT_PATH)); } initializeClient(): LanguageClient { diff --git a/client/src/python/PythonApi.ts b/client/src/python/PythonApi.ts deleted file mode 100644 index b124dc1b..00000000 --- a/client/src/python/PythonApi.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Event, Uri } from 'vscode'; - -// https://github.com/microsoft/vscode-python/blob/3698950c97982f31bb9dbfc19c4cd8308acda284/src/client/api.ts#L22 -export interface IExtensionApi { - settings: { - /** - * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. - */ - readonly onDidChangeExecutionDetails: Event; - - getExecutionDetails(resource?: Resource): { - execCommand: string[] | undefined; - }; - }; -} - -// https://github.com/microsoft/vscode-python/blob/main/src/client/proposedApiTypes.ts -export interface ProposedExtensionAPI { - readonly environments: { - /** - * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : If string, it represents the full path to environment folder or python executable - * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. - */ - resolveEnvironment(environment: Environment | EnvironmentPath | string): Promise; - }; -} - -/** - * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an - * {@link Environment} with complete information. - */ -type ResolvedEnvironment = Environment & { - /** - * Carries complete details about python executable. - */ - readonly executable: { - /** - * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to - * the environment. - */ - readonly uri: Uri | undefined; - /** - * Bitness of the environment. - */ - readonly bitness: Bitness; - /** - * Value of `sys.prefix` in sys module. - */ - readonly sysPrefix: string; - }; - /** - * Carries complete Python version information, carries `undefined` for envs without python. - */ - readonly version: - | (ResolvedVersionInfo & { - /** - * Value of `sys.version` in sys module if known at this moment. - */ - readonly sysVersion: string; - }) - | undefined; -}; - -type Environment = EnvironmentPath & { - /** - * Carries details about python executable. - */ - readonly executable: { - /** - * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to - * the environment. - */ - readonly uri: Uri | undefined; - /** - * Bitness if known at this moment. - */ - readonly bitness: Bitness | undefined; - /** - * Value of `sys.prefix` in sys module if known at this moment. - */ - readonly sysPrefix: string | undefined; - }; - /** - * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. - */ - readonly environment: - | { - /** - * Type of the environment. - */ - readonly type: EnvironmentType; - /** - * Name to the environment if any. - */ - readonly name: string | undefined; - /** - * Uri of the environment folder. - */ - readonly folderUri: Uri; - /** - * Any specific workspace folder this environment is created for. - */ - readonly workspaceFolder: Uri | undefined; - } - | undefined; - /** - * Carries Python version information known at this moment. - */ - readonly version: VersionInfo & { - /** - * Value of `sys.version` in sys module if known at this moment. - */ - readonly sysVersion: string | undefined; - }; - /** - * Tools/plugins which created the environment or where it came from. First value in array corresponds - * to the primary tool which manages the environment, which never changes over time. - * - * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for - * global interpreters. - */ - readonly tools: readonly EnvironmentTools[]; -}; - -type EnvironmentPath = { - /** - * The ID of the environment. - */ - readonly id: string; - /** - * Path to environment folder or path to python executable that uniquely identifies an environment. Environments - * lacking a python executable are identified by environment folder paths, whereas other envs can be identified - * using python executable path. - */ - readonly path: string; -}; - -/** - * Type of the environment. It can be 'VirtualEnvironment' | 'Conda' | 'Unknown' or custom string which was contributed. - */ -type EnvironmentType = string; - -/** - * Tool/plugin where the environment came from. It can be 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' | 'Pyenv' | 'Unknown' or custom string which - * was contributed. - */ -type EnvironmentTools = string; - -/** - * Carries bitness for an environment. - */ -type Bitness = '64-bit' | '32-bit' | 'Unknown'; - -/** - * The possible Python release levels. - */ -type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; - -/** - * Release information for a Python version. - */ -type PythonVersionRelease = { - readonly level: PythonReleaseLevel; - readonly serial: number; -}; - -type VersionInfo = { - readonly major: number | undefined; - readonly minor: number | undefined; - readonly micro: number | undefined; - readonly release: PythonVersionRelease | undefined; -}; - -type ResolvedVersionInfo = { - readonly major: number; - readonly minor: number; - readonly micro: number; - readonly release: PythonVersionRelease; -}; - -type Resource = Uri | undefined; diff --git a/client/src/python/PythonExtension.ts b/client/src/python/PythonExtension.ts deleted file mode 100644 index c90ffb0d..00000000 --- a/client/src/python/PythonExtension.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PythonInfo } from 'dbt-language-server-common'; -import { Event, Extension, extensions, Uri, workspace, WorkspaceFolder } from 'vscode'; -import { log } from '../Logger'; -import { IExtensionApi, ProposedExtensionAPI } from './PythonApi'; - -export class PythonExtension { - extension: Extension; - - constructor() { - const extension = extensions.getExtension('ms-python.python'); - if (!extension) { - throw new Error('Could not find Python extension'); - } - this.extension = extension; - - this.activate().catch(e => log(`Error while activating Python extension: ${e instanceof Error ? e.message : String(e)}`)); - } - - async onDidChangeExecutionDetails(): Promise> { - await this.activate(); - - return (this.extension.exports as IExtensionApi).settings.onDidChangeExecutionDetails; - } - - async getPythonInfo(workspaceFolder?: WorkspaceFolder): Promise { - await this.activate(); - - const api = this.extension.exports as IExtensionApi & ProposedExtensionAPI; - - const details = api.settings.getExecutionDetails(workspaceFolder?.uri); - if (!details.execCommand) { - throw new Error('Python extension not configured. Please select a Python interpreter.'); - } - - const [path] = details.execCommand; - - const environment = await api.environments.resolveEnvironment(path); - const major = String(environment?.version.major ?? 3); - const minor = String(environment?.version.minor ?? 10); - const micro = String(environment?.version.micro ?? 0); - - const pythonSettings = workspace.getConfiguration('python', workspaceFolder); - const dotEnvFile = pythonSettings.get('envFile'); - - return { path: `"${path}"`, version: [major, minor, micro], dotEnvFile }; - } - - async activate(): Promise { - if (!this.extension.isActive) { - await this.extension.activate(); - } - } -} diff --git a/client/src/python/PythonExtensionWrapper.ts b/client/src/python/PythonExtensionWrapper.ts new file mode 100644 index 00000000..4fc60701 --- /dev/null +++ b/client/src/python/PythonExtensionWrapper.ts @@ -0,0 +1,39 @@ +import { ActiveEnvironmentPathChangeEvent, PythonExtension } from '@vscode/python-extension'; +import { PythonInfo } from 'dbt-language-server-common'; +import { Event, WorkspaceFolder, workspace } from 'vscode'; + +export class PythonExtensionWrapper { + async getExtension(): Promise { + const extension = await PythonExtension.api(); + await extension.ready; + return extension; + } + + async onDidChangeActiveEnvironmentPath(): Promise> { + const extension = await this.getExtension(); + return extension.environments.onDidChangeActiveEnvironmentPath; + } + + async getPythonInfo(workspaceFolder?: WorkspaceFolder): Promise { + const extension = await this.getExtension(); + + const envFolderOrPythonPath = extension.environments.getActiveEnvironmentPath(workspaceFolder?.uri).path; + + if (!envFolderOrPythonPath) { + throw new Error('Python extension not configured. Please select a Python interpreter.'); + } + + const environment = await extension.environments.resolveEnvironment(envFolderOrPythonPath); + + const path = environment?.path ?? envFolderOrPythonPath; + + const major = String(environment?.version?.major ?? 3); + const minor = String(environment?.version?.minor ?? 10); + const micro = String(environment?.version?.micro ?? 0); + + const pythonSettings = workspace.getConfiguration('python', workspaceFolder); + const dotEnvFile = pythonSettings.get('envFile'); + + return { path: `"${path}"`, version: [major, minor, micro], dotEnvFile }; + } +} diff --git a/package-lock.json b/package-lock.json index 9fc3a380..797aa77d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dbt-language-server", - "version": "0.32.0", + "version": "0.33.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dbt-language-server", - "version": "0.32.0", + "version": "0.33.0", "hasInstallScript": true, "license": "MIT", "devDependencies": { diff --git a/package.json b/package.json index eaa17471..328f2c1f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Wizard for dbt Core (TM)", "description": "This extension will help you work with dbt", "icon": "images/Icon.png", - "version": "0.32.0", + "version": "0.33.0", "publisher": "Fivetran", "license": "MIT", "preview": true,