diff --git a/packages/databricks-vscode/.vscodeignore b/packages/databricks-vscode/.vscodeignore index 5b3130c88..82813923e 100644 --- a/packages/databricks-vscode/.vscodeignore +++ b/packages/databricks-vscode/.vscodeignore @@ -18,3 +18,4 @@ logs/ extension/ **/*.vsix .build/ +tmp/** diff --git a/packages/databricks-vscode/package.json b/packages/databricks-vscode/package.json index 8a27ea6f1..ddf3f2844 100644 --- a/packages/databricks-vscode/package.json +++ b/packages/databricks-vscode/package.json @@ -544,7 +544,7 @@ { "submenu": "databricks.run", "group": "navigation@0", - "when": "resourceLangId == python || resourceLangId == scala || resourceLangId == r || resourceLangId == sql || resourceExtname == .ipynb" + "when": "databricks.context.isActiveFileInActiveWorkspace && resourceLangId =~ /^(python|scala|r|sql)$/ || databricks.context.isActiveFileInActiveWorkspace && resourceExtname == .ipynb" } ], "databricks.run": [ @@ -881,7 +881,7 @@ "useYarn": false }, "cli": { - "version": "0.219.0" + "version": "0.221.0" }, "scripts": { "vscode:prepublish": "rm -rf out && yarn run package:compile && yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && yarn run package:copy-webview-toolkit && yarn run generate-telemetry", diff --git a/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts b/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts index 0a5c7ea97..3772b04e2 100644 --- a/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts +++ b/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts @@ -1,4 +1,4 @@ -import {Uri} from "vscode"; +import {Uri, WorkspaceFolder} from "vscode"; import {BundleFileSet, getAbsoluteGlobPath} from "./BundleFileSet"; import {expect} from "chai"; import path from "path"; @@ -6,6 +6,8 @@ import * as tmp from "tmp-promise"; import * as fs from "fs/promises"; import {BundleSchema} from "./types"; import * as yaml from "yaml"; +import {instance, mock, when} from "ts-mockito"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; describe(__filename, async function () { let tmpdir: tmp.DirectoryResult; @@ -18,6 +20,16 @@ describe(__filename, async function () { await tmpdir.cleanup(); }); + function getWorkspaceFolderManagerMock() { + const mockWorkspaceFolderManager = mock(); + const mockWorkspaceFolder = mock(); + when(mockWorkspaceFolder.uri).thenReturn(Uri.file(tmpdir.path)); + when(mockWorkspaceFolderManager.activeWorkspaceFolder).thenReturn( + instance(mockWorkspaceFolder) + ); + return instance(mockWorkspaceFolderManager); + } + it("should return the correct absolute glob path", () => { const tmpdirUri = Uri.file(tmpdir.path); let expectedGlob = path.join(tmpdirUri.fsPath, "test.txt"); @@ -34,7 +46,9 @@ describe(__filename, async function () { it("should find the correct root bundle yaml", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); expect(await bundleFileSet.getRootFile()).to.be.undefined; @@ -47,7 +61,9 @@ describe(__filename, async function () { it("should return undefined if more than one root bundle yaml is found", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); await fs.writeFile(path.join(tmpdirUri.fsPath, "bundle.yaml"), ""); await fs.writeFile(path.join(tmpdirUri.fsPath, "databricks.yaml"), ""); @@ -80,7 +96,9 @@ describe(__filename, async function () { it("should return correct included files", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); expect(await bundleFileSet.getIncludedFilesGlob()).to.equal( `{included.yaml,${path.join("includes", "**", "*.yaml")}}` @@ -102,7 +120,9 @@ describe(__filename, async function () { it("should return all bundle files", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); const actual = (await bundleFileSet.allFiles()).map( (v) => v.fsPath @@ -117,7 +137,9 @@ describe(__filename, async function () { it("isRootBundleFile should return true only for root bundle file", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); const possibleRoots = [ "bundle.yaml", @@ -143,7 +165,9 @@ describe(__filename, async function () { it("isIncludedBundleFile should return true only for included files", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); expect( await bundleFileSet.isIncludedBundleFile( @@ -168,7 +192,9 @@ describe(__filename, async function () { it("isBundleFile should return true only for bundle files", async () => { const tmpdirUri = Uri.file(tmpdir.path); - const bundleFileSet = new BundleFileSet(tmpdirUri); + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock() + ); const possibleBundleFiles = [ "bundle.yaml", diff --git a/packages/databricks-vscode/src/bundle/BundleFileSet.ts b/packages/databricks-vscode/src/bundle/BundleFileSet.ts index bf03f08f0..ab275faeb 100644 --- a/packages/databricks-vscode/src/bundle/BundleFileSet.ts +++ b/packages/databricks-vscode/src/bundle/BundleFileSet.ts @@ -7,6 +7,7 @@ import {BundleSchema} from "./types"; import {readFile, writeFile} from "fs/promises"; import {CachedValue} from "../locking/CachedValue"; import minimatch from "minimatch"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; const rootFilePattern: string = "{bundle,databricks}.{yaml,yml}"; const subProjectFilePattern: string = path.join("**", rootFilePattern); @@ -61,7 +62,17 @@ export class BundleFileSet { return bundle as BundleSchema; }); - constructor(private readonly workspaceRoot: Uri) {} + private get workspaceRoot() { + return this.workspaceFolderManager.activeWorkspaceFolder.uri; + } + + constructor( + private readonly workspaceFolderManager: WorkspaceFolderManager + ) { + workspaceFolderManager.onDidChangeActiveWorkspaceFolder(() => { + this.bundleDataCache.invalidate(); + }); + } async getRootFile() { const rootFile = await glob.glob( diff --git a/packages/databricks-vscode/src/bundle/BundleProjectManager.ts b/packages/databricks-vscode/src/bundle/BundleProjectManager.ts index c4e5ba5ff..bda7d8e9a 100644 --- a/packages/databricks-vscode/src/bundle/BundleProjectManager.ts +++ b/packages/databricks-vscode/src/bundle/BundleProjectManager.ts @@ -18,6 +18,7 @@ import {randomUUID} from "crypto"; import {onError} from "../utils/onErrorDecorator"; import {BundleInitWizard, promptToOpenSubProjects} from "./BundleInitWizard"; import {EventReporter, Events, Telemetry} from "../telemetry"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; export class BundleProjectManager { private logger = logging.NamedLogger.getOrCreate(Loggers.Extension); @@ -36,6 +37,10 @@ export class BundleProjectManager { private subProjects?: {relative: string; absolute: Uri}[]; private legacyProjectConfig?: ProjectConfigFile; + get workspaceUri() { + return this.workspaceFolderManager.activeWorkspaceFolder.uri; + } + constructor( private context: ExtensionContext, private cli: CliWrapper, @@ -43,10 +48,15 @@ export class BundleProjectManager { private connectionManager: ConnectionManager, private configModel: ConfigModel, private bundleFileSet: BundleFileSet, - private workspaceUri: Uri, + private workspaceFolderManager: WorkspaceFolderManager, private telemetry: Telemetry ) { this.disposables.push( + this.workspaceFolderManager.onDidChangeActiveWorkspaceFolder( + async () => { + await this.isBundleProjectCache.refresh(); + } + ), this.bundleFileSet.bundleDataCache.onDidChange(async () => { try { await this.isBundleProjectCache.refresh(); diff --git a/packages/databricks-vscode/src/bundle/BundleWatcher.ts b/packages/databricks-vscode/src/bundle/BundleWatcher.ts index 18acb31a4..fe24b9c7b 100644 --- a/packages/databricks-vscode/src/bundle/BundleWatcher.ts +++ b/packages/databricks-vscode/src/bundle/BundleWatcher.ts @@ -1,7 +1,7 @@ import {Disposable, EventEmitter, Uri, workspace} from "vscode"; import {BundleFileSet, getAbsoluteGlobPath} from "./BundleFileSet"; -import {WithMutex} from "../locking"; import path from "path"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; export class BundleWatcher implements Disposable { private disposables: Disposable[] = []; @@ -18,15 +18,30 @@ export class BundleWatcher implements Disposable { private readonly _onDidDelete = new EventEmitter(); public readonly onDidDelete = this._onDidDelete.event; - private bundleFileSet: WithMutex; + private initCleanup: Disposable; + constructor( + private readonly bundleFileSet: BundleFileSet, + private readonly workspaceFolderManager: WorkspaceFolderManager + ) { + this.initCleanup = this.init(); + this.disposables.push( + this.workspaceFolderManager.onDidChangeActiveWorkspaceFolder(() => { + this.initCleanup.dispose(); + this.initCleanup = this.init(); + this.bundleFileSet.bundleDataCache.invalidate(); + }) + ); + } - constructor(bundleFileSet: BundleFileSet, workspaceUri: Uri) { - this.bundleFileSet = new WithMutex(bundleFileSet); + private init() { const yamlWatcher = workspace.createFileSystemWatcher( - getAbsoluteGlobPath(path.join("**", "*.{yaml,yml}"), workspaceUri) + getAbsoluteGlobPath( + path.join("**", "*.{yaml,yml}"), + this.workspaceFolderManager.activeWorkspaceFolder.uri + ) ); - this.disposables.push( + const disposables: Disposable[] = [ yamlWatcher, yamlWatcher.onDidCreate((e) => { this.yamlFileChangeHandler(e, "CREATE"); @@ -36,22 +51,28 @@ export class BundleWatcher implements Disposable { }), yamlWatcher.onDidDelete((e) => { this.yamlFileChangeHandler(e, "DELETE"); - }) - ); + }), + ]; + + return { + dispose: () => { + disposables.forEach((i) => i.dispose()); + }, + }; } private async yamlFileChangeHandler( e: Uri, type: "CREATE" | "CHANGE" | "DELETE" ) { - if (!(await this.bundleFileSet.value.isBundleFile(e))) { + if (!(await this.bundleFileSet.isBundleFile(e))) { return; } - this.bundleFileSet.value.bundleDataCache.invalidate(); + this.bundleFileSet.bundleDataCache.invalidate(); this._onDidChange.fire(); // to provide additional granularity, we also fire an event when the root bundle file changes - if (this.bundleFileSet.value.isRootBundleFile(e)) { + if (this.bundleFileSet.isRootBundleFile(e)) { this._onDidChangeRootFile.fire(); } switch (type) { @@ -66,5 +87,6 @@ export class BundleWatcher implements Disposable { dispose() { this.disposables.forEach((i) => i.dispose()); + this.initCleanup.dispose(); } } diff --git a/packages/databricks-vscode/src/bundle/models/BundleRemoteStateModel.ts b/packages/databricks-vscode/src/bundle/models/BundleRemoteStateModel.ts index 13a8daf4a..5485b3fe0 100644 --- a/packages/databricks-vscode/src/bundle/models/BundleRemoteStateModel.ts +++ b/packages/databricks-vscode/src/bundle/models/BundleRemoteStateModel.ts @@ -1,4 +1,3 @@ -import {Uri} from "vscode"; import {CliWrapper} from "../../cli/CliWrapper"; import {BaseModelWithStateCache} from "../../configuration/models/BaseModelWithStateCache"; import {Mutex} from "../../locking"; @@ -9,6 +8,7 @@ import lodash from "lodash"; import {WorkspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; import {logging} from "@databricks/databricks-sdk"; import {Loggers} from "../../logger"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; /* eslint-disable @typescript-eslint/naming-convention */ export type BundleResourceModifiedStatus = "created" | "deleted" | "updated"; @@ -45,9 +45,13 @@ export class BundleRemoteStateModel extends BaseModelWithStateCache { - if (this.target === undefined || this.authProvider === undefined) { + if ( + !this.target || + !this.authProvider || + !this.workspaceFolderManager.activeWorkspaceFolder + ) { return {}; } @@ -71,7 +75,7 @@ export class BundleValidateModel extends BaseModelWithStateCache["variables"][string] & { valueInTarget?: string; @@ -23,10 +24,15 @@ export class BundleVariableModel extends BaseModelWithStateCache { + await this.configModel.setTarget(undefined); + }, + {log: true} + ) ) ); this.initialization.resolve(); @@ -218,21 +232,27 @@ export class ConnectionManager implements Disposable { } private async loginWithSavedAuth(source: AutoLoginSource) { - const recordEvent = this.telemetry.start(Events.AUTO_LOGIN); - try { - await this.disconnect(); - const authProvider = await this.resolveAuth(); - if (authProvider) { - await this.connect(authProvider); - } else { - await this.logout(); - } - recordEvent({success: this.state === "CONNECTED", source}); - } catch (e) { - await this.disconnect(); - recordEvent({success: false, source}); - throw e; + if (this.savedAuthMutex.locked) { + return; } + + await this.savedAuthMutex.synchronise(async () => { + const recordEvent = this.telemetry.start(Events.AUTO_LOGIN); + try { + await this.disconnect(); + const authProvider = await this.resolveAuth(); + if (authProvider) { + await this.connect(authProvider); + } else { + await this.logout(); + } + recordEvent({success: this.state === "CONNECTED", source}); + } catch (e) { + await this.disconnect(); + recordEvent({success: false, source}); + throw e; + } + }); } @onError({popup: {prefix: "Failed to login."}}) diff --git a/packages/databricks-vscode/src/configuration/models/OverrideableConfigModel.ts b/packages/databricks-vscode/src/configuration/models/OverrideableConfigModel.ts index 7f45970f1..14a381576 100644 --- a/packages/databricks-vscode/src/configuration/models/OverrideableConfigModel.ts +++ b/packages/databricks-vscode/src/configuration/models/OverrideableConfigModel.ts @@ -4,6 +4,7 @@ import {BaseModelWithStateCache} from "./BaseModelWithStateCache"; import {Uri} from "vscode"; import path from "path"; import {existsSync} from "fs"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; export type OverrideableConfigState = { authProfile?: string; @@ -57,7 +58,13 @@ export class OverrideableConfigModel extends BaseModelWithStateCache { + databricksEnvFileManager.dispose(); + databricksEnvFileManager.init(); + }), showRestartNotebookDialogue(databricksEnvFileManager) ); featureManager.isEnabled("environment.dependencies"); @@ -470,7 +490,7 @@ export async function activate( const configureAutocomplete = new ConfigureAutocomplete( context, stateStorage, - workspaceUri.fsPath, + workspaceFolderManager, pythonExtensionWrapper, environmentDependenciesInstaller ); @@ -488,7 +508,8 @@ export async function activate( bundleProjectManager, configModel, cli, - featureManager + featureManager, + workspaceFolderManager ); const configurationView = window.createTreeView("configurationView", { treeDataProvider: configurationDataProvider, @@ -675,7 +696,7 @@ export async function activate( const bundleVariableModel = new BundleVariableModel( configModel, bundleValidateModel, - workspaceUri + workspaceFolderManager ); cli.bundleVariableModel = bundleVariableModel; const bundleVariableTreeDataProvider = new BundleVariableTreeDataProvider( @@ -815,7 +836,7 @@ export async function activate( ); }); - setDbnbCellLimits(workspaceUri, connectionManager).catch((e) => { + setDbnbCellLimits(workspaceFolderManager, connectionManager).catch((e) => { logging.NamedLogger.getOrCreate(Loggers.Extension).error( "Error while setting jupyter configs for parsing databricks notebooks", e @@ -847,19 +868,25 @@ export async function activate( const recordInitializationEvent = telemetry.start( Events.EXTENSION_INITIALIZATION ); - bundleProjectManager - .configureWorkspace() - .catch((e) => { - recordInitializationEvent({success: false}); - logging.NamedLogger.getOrCreate(Loggers.Extension).error( - "Failed to configure workspace", - e - ); - window.showErrorMessage(e); - }) - .finally(() => { - customWhenContext.setInitialized(); - }); + + const configureWorkspace = () => { + bundleProjectManager + .configureWorkspace() + .catch((e) => { + recordInitializationEvent({success: false}); + logging.NamedLogger.getOrCreate(Loggers.Extension).error( + "Failed to configure workspace", + e + ); + window.showErrorMessage(e); + }) + .finally(() => { + customWhenContext.setInitialized(); + }); + }; + + configureWorkspace(); + workspaceFolderManager.onDidChangeActiveWorkspaceFolder(configureWorkspace); customWhenContext.setActivated(true); telemetry.recordEvent(Events.EXTENSION_ACTIVATION); diff --git a/packages/databricks-vscode/src/file-managers/DatabricksEnvFileManager.ts b/packages/databricks-vscode/src/file-managers/DatabricksEnvFileManager.ts index dd8de2b7a..62f65c58d 100644 --- a/packages/databricks-vscode/src/file-managers/DatabricksEnvFileManager.ts +++ b/packages/databricks-vscode/src/file-managers/DatabricksEnvFileManager.ts @@ -12,16 +12,27 @@ import {EnvVarGenerators, FileUtils} from "../utils"; import {Mutex} from "../locking/Mutex"; import {ConfigModel} from "../configuration/models/ConfigModel"; import path from "path"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; export class DatabricksEnvFileManager implements Disposable { private disposables: Disposable[] = []; private userEnvFileWatcherDisposables: Disposable[] = []; private mutex = new Mutex(); - public readonly databricksEnvPath: Uri; private userEnvPath?: Uri; - private readonly systemVariableResolver = new SystemVariables( - this.workspacePath - ); + + get databricksEnvPath() { + return Uri.joinPath( + this.workspacePath, + ".databricks", + ".databricks.env" + ); + } + private get systemVariableResolver() { + return new SystemVariables(this.workspacePath); + } + private get workspacePath() { + return this.workspaceFolderManager.activeWorkspaceFolder.uri; + } private readonly onDidChangeEnvironmentVariablesEmitter = new EventEmitter(); @@ -29,18 +40,11 @@ export class DatabricksEnvFileManager implements Disposable { this.onDidChangeEnvironmentVariablesEmitter.event; constructor( - private readonly workspacePath: Uri, + private readonly workspaceFolderManager: WorkspaceFolderManager, private readonly featureManager: FeatureManager, private readonly connectionManager: ConnectionManager, private readonly configModel: ConfigModel - ) { - this.systemVariableResolver = new SystemVariables(this.workspacePath); - this.databricksEnvPath = Uri.joinPath( - this.workspacePath, - ".databricks", - ".databricks.env" - ); - } + ) {} private updateUserEnvFileWatcher() { const userEnvPath = workspaceConfigs.msPythonEnvFile @@ -114,10 +118,10 @@ export class DatabricksEnvFileManager implements Disposable { }, this ), - this.connectionManager.onDidChangeCluster(async () => { + this.connectionManager.onDidChangeCluster(() => { this.writeFile(); }, this), - this.connectionManager.onDidChangeState(async () => { + this.connectionManager.onDidChangeState(() => { this.writeFile(); }, this) ); diff --git a/packages/databricks-vscode/src/language/ConfigureAutocomplete.ts b/packages/databricks-vscode/src/language/ConfigureAutocomplete.ts index 8c7d644f0..f7fa504de 100644 --- a/packages/databricks-vscode/src/language/ConfigureAutocomplete.ts +++ b/packages/databricks-vscode/src/language/ConfigureAutocomplete.ts @@ -12,6 +12,7 @@ import {Loggers} from "../logger"; import {StateStorage} from "../vscode-objs/StateStorage"; import {MsPythonExtensionWrapper} from "./MsPythonExtensionWrapper"; import {EnvironmentDependenciesInstaller} from "./EnvironmentDependenciesInstaller"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; async function getImportString(context: ExtensionContext) { try { @@ -43,7 +44,7 @@ export class ConfigureAutocomplete implements Disposable { constructor( private readonly context: ExtensionContext, private readonly stateStorage: StateStorage, - private readonly workspaceFolder: string, + private readonly workspaceFolderManager: WorkspaceFolderManager, private readonly pythonExtension: MsPythonExtensionWrapper, private readonly environmentDependenciesInstaller: EnvironmentDependenciesInstaller ) { @@ -173,6 +174,10 @@ export class ConfigureAutocomplete implements Disposable { this.environmentDependenciesInstaller.show(false); } + private get workspaceFolder() { + return this.workspaceFolderManager.activeWorkspaceFolder.uri.fsPath; + } + private async addBuiltinsFile(dryRun = false): Promise { const stubPath = workspace .getConfiguration("python") diff --git a/packages/databricks-vscode/src/language/MsPythonExtensionWrapper.ts b/packages/databricks-vscode/src/language/MsPythonExtensionWrapper.ts index 9d2b48cbc..9b51ae7f9 100644 --- a/packages/databricks-vscode/src/language/MsPythonExtensionWrapper.ts +++ b/packages/databricks-vscode/src/language/MsPythonExtensionWrapper.ts @@ -13,6 +13,7 @@ import {IExtensionApi as MsPythonExtensionApi} from "./MsPythonExtensionApi"; import {Mutex} from "../locking"; import * as childProcess from "node:child_process"; import {promisify} from "node:util"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; export const execFile = promisify(childProcess.execFile); export class MsPythonExtensionWrapper implements Disposable { @@ -22,7 +23,7 @@ export class MsPythonExtensionWrapper implements Disposable { private _terminal?: Terminal; constructor( pythonExtension: Extension, - private readonly workspaceFolder: Uri, + private readonly workspaceFolderManager: WorkspaceFolderManager, private readonly stateStorage: StateStorage ) { this.api = pythonExtension.exports as MsPythonExtensionApi; @@ -79,7 +80,7 @@ export class MsPythonExtensionWrapper implements Disposable { get pythonEnvironment() { return this.api.environments?.resolveEnvironment( this.api.environments?.getActiveEnvironmentPath( - this.workspaceFolder + this.workspaceFolderManager.activeWorkspaceFolder ) ); } diff --git a/packages/databricks-vscode/src/language/notebooks/DatabricksNbCellLimits.ts b/packages/databricks-vscode/src/language/notebooks/DatabricksNbCellLimits.ts index d56624674..ca200a534 100644 --- a/packages/databricks-vscode/src/language/notebooks/DatabricksNbCellLimits.ts +++ b/packages/databricks-vscode/src/language/notebooks/DatabricksNbCellLimits.ts @@ -1,13 +1,16 @@ import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; import {FileUtils} from "../../utils"; -import {Uri} from "vscode"; import {ConnectionManager} from "../../configuration/ConnectionManager"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; export async function setDbnbCellLimits( - workspacePath: Uri, + workspaceFolderManager: WorkspaceFolderManager, connectionManager: ConnectionManager ) { - await FileUtils.waitForDatabricksProject(workspacePath, connectionManager); + await FileUtils.waitForDatabricksProject( + workspaceFolderManager.activeWorkspaceFolder.uri, + connectionManager + ); if (workspaceConfigs.jupyterCellMarkerRegex === undefined) { workspaceConfigs.jupyterCellMarkerRegex = "^(# Databricks notebook source|# COMMAND ----------)"; diff --git a/packages/databricks-vscode/src/language/notebooks/NotebookInitScriptManager.ts b/packages/databricks-vscode/src/language/notebooks/NotebookInitScriptManager.ts index 6230e18e9..412434059 100644 --- a/packages/databricks-vscode/src/language/notebooks/NotebookInitScriptManager.ts +++ b/packages/databricks-vscode/src/language/notebooks/NotebookInitScriptManager.ts @@ -1,6 +1,5 @@ import { Disposable, - Uri, ExtensionContext, window, OutputChannel, @@ -24,6 +23,7 @@ import {FileUtils} from "../../utils"; import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs"; import {LocalUri} from "../../sync/SyncDestination"; import {DatabricksEnvFileManager} from "../../file-managers/DatabricksEnvFileManager"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; const execFile = promisify(ef); @@ -51,7 +51,7 @@ export class NotebookInitScriptManager implements Disposable { private currentEnvPath?: string | null = null; constructor( - private readonly workspacePath: Uri, + private readonly workspaceFolderManager: WorkspaceFolderManager, private readonly extensionContext: ExtensionContext, private readonly connectionManager: ConnectionManager, private readonly featureManager: FeatureManager, @@ -236,7 +236,8 @@ export class NotebookInitScriptManager implements Disposable { ["-m", "IPython", file], { env, - cwd: this.workspacePath.fsPath, + cwd: this.workspaceFolderManager.activeWorkspaceFolder.uri + .fsPath, } ); const correctlyFormatttedErrors = stderr @@ -278,6 +279,10 @@ export class NotebookInitScriptManager implements Disposable { fromCommand = false, @context ctx?: Context ) { + if (this.connectionManager.state !== "CONNECTED") { + return; + } + // If we are not in a jupyter notebook or a databricks notebook, // then we don't need to verify the init script if ( @@ -290,7 +295,7 @@ export class NotebookInitScriptManager implements Disposable { } await FileUtils.waitForDatabricksProject( - this.workspacePath, + this.workspaceFolderManager.activeWorkspaceFolder.uri, this.connectionManager ); diff --git a/packages/databricks-vscode/src/ui/configuration-view/ConfigurationDataProvider.ts b/packages/databricks-vscode/src/ui/configuration-view/ConfigurationDataProvider.ts index 1863d9091..cda27aa0f 100644 --- a/packages/databricks-vscode/src/ui/configuration-view/ConfigurationDataProvider.ts +++ b/packages/databricks-vscode/src/ui/configuration-view/ConfigurationDataProvider.ts @@ -20,6 +20,8 @@ import {logging} from "@databricks/databricks-sdk"; import {Loggers} from "../../logger"; import {FeatureManager} from "../../feature-manager/FeatureManager"; import {EnvironmentComponent} from "./EnvironmentComponent"; +import {WorkspaceFolderComponent} from "./WorkspaceFolderComponent"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; /** * Data provider for the cluster tree view @@ -40,6 +42,7 @@ export class ConfigurationDataProvider private disposables: Array = []; private components: Array = [ + new WorkspaceFolderComponent(this.workspaceFolderManager), new BundleTargetComponent(this.configModel), new AuthTypeComponent( this.connectionManager, @@ -59,7 +62,8 @@ export class ConfigurationDataProvider private readonly bundleProjectManager: BundleProjectManager, private readonly configModel: ConfigModel, private readonly cli: CliWrapper, - private readonly featureManager: FeatureManager + private readonly featureManager: FeatureManager, + private readonly workspaceFolderManager: WorkspaceFolderManager ) { this.disposables.push( this.bundleProjectManager.onDidChangeStatus(async () => { diff --git a/packages/databricks-vscode/src/ui/configuration-view/WorkspaceFolderComponent.ts b/packages/databricks-vscode/src/ui/configuration-view/WorkspaceFolderComponent.ts new file mode 100644 index 000000000..b05af2a50 --- /dev/null +++ b/packages/databricks-vscode/src/ui/configuration-view/WorkspaceFolderComponent.ts @@ -0,0 +1,50 @@ +import {BaseComponent} from "./BaseComponent"; +import {ConfigurationTreeItem} from "./types"; +import {ThemeIcon} from "vscode"; +import {WorkspaceFolderManager} from "../../vscode-objs/WorkspaceFolderManager"; + +export class WorkspaceFolderComponent extends BaseComponent { + constructor( + private readonly workspaceFolderManager: WorkspaceFolderManager + ) { + super(); + this.disposables.push( + this.workspaceFolderManager.onDidChangeActiveWorkspaceFolder(() => { + this.onDidChangeEmitter.fire(); + }) + ); + } + + private async getRoot(): Promise { + const activeWorkspaceFolder = + this.workspaceFolderManager.activeWorkspaceFolder; + if ( + activeWorkspaceFolder === undefined || + !this.workspaceFolderManager.enableUi + ) { + return []; + } + + return [ + { + label: "Active Workspace Folder", + iconPath: new ThemeIcon("folder"), + description: activeWorkspaceFolder.name, + contextValue: "databricks.configuration.activeWorkspaceFolder", + command: { + title: "Select Workspace Folder", + command: "databricks.selectWorkspaceFolder", + }, + }, + ]; + } + public async getChildren( + parent?: ConfigurationTreeItem + ): Promise { + if (parent === undefined) { + return this.getRoot(); + } + + return []; + } +} diff --git a/packages/databricks-vscode/src/vscode-objs/CustomWhenContext.ts b/packages/databricks-vscode/src/vscode-objs/CustomWhenContext.ts index 1fe892882..aa4e647c7 100644 --- a/packages/databricks-vscode/src/vscode-objs/CustomWhenContext.ts +++ b/packages/databricks-vscode/src/vscode-objs/CustomWhenContext.ts @@ -78,4 +78,12 @@ export class CustomWhenContext { ) ); } + + setIsActiveFileInActiveWorkspace(value: boolean) { + commands.executeCommand( + "setContext", + "databricks.context.isActiveFileInActiveWorkspace", + value + ); + } } diff --git a/packages/databricks-vscode/src/vscode-objs/WorkspaceFolderManager.ts b/packages/databricks-vscode/src/vscode-objs/WorkspaceFolderManager.ts new file mode 100644 index 000000000..72db2e78c --- /dev/null +++ b/packages/databricks-vscode/src/vscode-objs/WorkspaceFolderManager.ts @@ -0,0 +1,140 @@ +import { + Disposable, + EventEmitter, + QuickPickItem, + QuickPickItemKind, + StatusBarAlignment, + WorkspaceFolder, + window, + workspace, +} from "vscode"; +import {CustomWhenContext} from "./CustomWhenContext"; + +export class WorkspaceFolderManager implements Disposable { + private disposables: Disposable[] = []; + private _activeWorkspaceFolder: WorkspaceFolder | undefined = + workspace.workspaceFolders?.[0]; + private readonly didChangeActiveWorkspaceFolder = new EventEmitter< + WorkspaceFolder | undefined + >(); + public readonly onDidChangeActiveWorkspaceFolder = + this.didChangeActiveWorkspaceFolder.event; + + private readonly button = window.createStatusBarItem( + StatusBarAlignment.Left, + 999 + ); + + constructor(public readonly customWhenContext: CustomWhenContext) { + if (this.enableUi) { + this.button.text = + this._activeWorkspaceFolder?.name ?? "No Databricks Project"; + this.button.tooltip = "Selected databricks project"; + this.button.command = "databricks.selectWorkspaceFolder"; + this.button.show(); + } + + this.disposables.push( + this.button, + workspace.onDidChangeWorkspaceFolders((e) => { + if ( + e.removed.find( + (v) => + v.uri.fsPath === + this._activeWorkspaceFolder?.uri.fsPath + ) || + this._activeWorkspaceFolder === undefined + ) { + this.setActiveWorkspaceFolder( + workspace.workspaceFolders?.[0] + ); + return; + } + }), + window.onDidChangeActiveTextEditor((e) => { + const isActiveFileInActiveWorkspace = + this.activeWorkspaceFolder !== undefined && + e !== undefined && + e.document.uri.fsPath.startsWith( + this.activeWorkspaceFolder?.uri.fsPath + ); + customWhenContext.setIsActiveFileInActiveWorkspace( + isActiveFileInActiveWorkspace + ); + }) + ); + } + + get activeWorkspaceFolder() { + if (this._activeWorkspaceFolder === undefined) { + throw new Error("No active workspace folder"); + } + + return this._activeWorkspaceFolder; + } + + setActiveWorkspaceFolder(folder?: WorkspaceFolder) { + if (this._activeWorkspaceFolder?.uri.fsPath === folder?.uri.fsPath) { + return; + } + + this._activeWorkspaceFolder = folder; + this.didChangeActiveWorkspaceFolder.fire(folder); + + if (this.enableUi) { + this.button.text = folder?.name ?? "No Databricks Project"; + this.button.show(); + } + } + + get folders() { + return workspace.workspaceFolders; + } + + async selectDatabricksWorkspaceFolderCommand() { + const items: (QuickPickItem & { + workspaceFolder: WorkspaceFolder; + })[] = + this.folders + ?.filter((i) => i.name !== this.activeWorkspaceFolder.name) + .map((folder) => ({ + label: folder.name, + description: folder.uri.fsPath, + workspaceFolder: folder, + })) ?? []; + + const firstItem = this.activeWorkspaceFolder + ? [ + { + label: "Selected Databricks Workspace Folder", + kind: QuickPickItemKind.Separator, + }, + { + label: this.activeWorkspaceFolder.name, + description: this.activeWorkspaceFolder.uri.fsPath, + workspaceFolder: this.activeWorkspaceFolder, + }, + { + label: "", + kind: QuickPickItemKind.Separator, + }, + ] + : []; + + const choice = await window.showQuickPick([...firstItem, ...items], { + title: "Select Databricks Workspace Folder", + }); + if (!choice) { + return; + } + this.setActiveWorkspaceFolder(choice.workspaceFolder); + } + + get enableUi() { + return this.folders && this.folders?.length > 1; + } + + dispose() { + this.disposables.forEach((i) => i.dispose()); + } +} diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 1a4386cf3..b829664fe 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -5,11 +5,12 @@ import { WorkspaceFsUtils, } from "../sdk-extensions"; import {context, Context} from "@databricks/databricks-sdk/dist/context"; -import {Disposable, Uri, window} from "vscode"; +import {Disposable, window} from "vscode"; import {ConnectionManager} from "../configuration/ConnectionManager"; import {Loggers} from "../logger"; import {createDirWizard} from "./createDirectoryWizard"; import {WorkspaceFsDataProvider} from "./WorkspaceFsDataProvider"; +import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager"; const withLogContext = logging.withLogContext; @@ -17,7 +18,7 @@ export class WorkspaceFsCommands implements Disposable { private disposables: Disposable[] = []; constructor( - private workspaceFolder: Uri, + private workspaceFolderManager: WorkspaceFolderManager, private connectionManager: ConnectionManager, private workspaceFsDataProvider: WorkspaceFsDataProvider ) {} @@ -75,7 +76,7 @@ export class WorkspaceFsCommands implements Disposable { const root = await this.getValidRoot(rootPath, ctx); const inputPath = await createDirWizard( - this.workspaceFolder, + this.workspaceFolderManager.activeWorkspaceFolder.uri, "Directory Name", root );