Skip to content

Commit 8e24c87

Browse files
authored
Configure autocomplete as a part of env panel (#1554)
## Changes "Configure autocomplete" logic was disconnected from the enc setup, and it was still possible to install pyspark into the global python installation. Or re-install it, even if we already installed it before through env panel. Now "Setup Databricks builtins" appears in the env panel as an optional step if all the previous env checks are green. Also fix a bug where we were settig up builtins file in the bundle root, while the python extension expects a workspace root. <img width="417" alt="Screenshot 2025-02-12 at 11 11 13" src="https://github.com/user-attachments/assets/3377a10c-b884-4a40-b6f8-0cb6fc53f139" /> <img width="516" alt="Screenshot 2025-02-12 at 11 11 24" src="https://github.com/user-attachments/assets/8ea3800c-9a1f-4fc6-91aa-00758f67bd78" /> <!-- Summary of your changes that are easy to understand -->
1 parent 968c084 commit 8e24c87

File tree

7 files changed

+143
-174
lines changed

7 files changed

+143
-174
lines changed

packages/databricks-vscode/src/extension.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,19 @@ export async function activate(
357357
)
358358
);
359359

360-
const clusterModel = new ClusterModel(connectionManager);
360+
const configureAutocomplete = new ConfigureAutocomplete(
361+
context,
362+
stateStorage,
363+
workspaceFolderManager
364+
);
365+
context.subscriptions.push(
366+
configureAutocomplete,
367+
telemetry.registerCommand(
368+
"databricks.autocomplete.configure",
369+
configureAutocomplete.configureCommand,
370+
configureAutocomplete
371+
)
372+
);
361373

362374
const environmentDependenciesInstaller =
363375
new EnvironmentDependenciesInstaller(
@@ -371,7 +383,8 @@ export async function activate(
371383
new EnvironmentDependenciesVerifier(
372384
connectionManager,
373385
pythonExtensionWrapper,
374-
environmentDependenciesInstaller
386+
environmentDependenciesInstaller,
387+
configureAutocomplete
375388
)
376389
);
377390
const environmentCommands = new EnvironmentCommands(
@@ -458,22 +471,6 @@ export async function activate(
458471
);
459472
featureManager.isEnabled("environment.dependencies");
460473

461-
const configureAutocomplete = new ConfigureAutocomplete(
462-
context,
463-
stateStorage,
464-
workspaceFolderManager,
465-
pythonExtensionWrapper,
466-
environmentDependenciesInstaller
467-
);
468-
context.subscriptions.push(
469-
configureAutocomplete,
470-
telemetry.registerCommand(
471-
"databricks.autocomplete.configure",
472-
configureAutocomplete.configureCommand,
473-
configureAutocomplete
474-
)
475-
);
476-
477474
const codeSynchroniser = new CodeSynchronizer(
478475
connectionManager,
479476
configModel,
@@ -517,6 +514,8 @@ export async function activate(
517514
configModel
518515
);
519516

517+
const clusterModel = new ClusterModel(connectionManager);
518+
520519
const connectionCommands = new ConnectionCommands(
521520
workspaceFsCommands,
522521
connectionManager,

packages/databricks-vscode/src/feature-manager/FeatureManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface FeatureState {
1818
export interface FeatureStepState {
1919
id: string;
2020
available: boolean;
21+
optional?: boolean;
2122
title?: string;
2223
message?: string;
2324
warning?: string;

packages/databricks-vscode/src/feature-manager/MultiStepAccessVerfier.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export abstract class MultiStepAccessVerifier implements Feature {
3535
this.state.steps.set(stepState.id, stepState);
3636
const oldAvailability = this.state.available;
3737
this.state.available = Array.from(this.state.steps).reduce(
38-
(result, current) => result && current[1].available,
38+
(result, current) =>
39+
result &&
40+
(current[1].available || current[1].optional === true),
3941
true
4042
);
4143
const nonComparableFields: Array<keyof FeatureStepState> = ["action"];
@@ -64,14 +66,16 @@ export abstract class MultiStepAccessVerifier implements Feature {
6466
title: string,
6567
message?: string,
6668
action?: FeatureEnableAction,
67-
isDisabledByFf?: boolean
69+
isDisabledByFf?: boolean,
70+
optional?: boolean
6871
) {
6972
return this.updateStep({
7073
id: id,
7174
available: false,
7275
title,
7376
message,
7477
action,
78+
optional,
7579
isDisabledByFf: isDisabledByFf === true,
7680
});
7781
}
Lines changed: 57 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import {logging} from "@databricks/databricks-sdk";
2-
import {appendFile, mkdir, readdir, readFile} from "fs/promises";
1+
import {appendFile, mkdir, stat, readFile} from "fs/promises";
32
import path from "path";
43
import {
54
ExtensionContext,
65
window,
76
Disposable,
87
workspace,
98
ConfigurationTarget,
9+
EventEmitter,
1010
} from "vscode";
11-
import {Loggers} from "../logger";
1211
import {StateStorage} from "../vscode-objs/StateStorage";
13-
import {MsPythonExtensionWrapper} from "./MsPythonExtensionWrapper";
14-
import {EnvironmentDependenciesInstaller} from "./EnvironmentDependenciesInstaller";
1512
import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager";
1613

1714
async function getImportString(context: ExtensionContext) {
@@ -31,29 +28,21 @@ async function getImportString(context: ExtensionContext) {
3128
}
3229
}
3330

34-
type StepResult = "Skip" | "Cancel" | "Error" | undefined;
35-
36-
interface Step {
37-
fn: (dryRun: boolean) => Promise<StepResult>;
38-
required?: boolean;
39-
}
40-
4131
export class ConfigureAutocomplete implements Disposable {
4232
private disposables: Disposable[] = [];
33+
private _onDidUpdateEmitter = new EventEmitter<void>();
34+
public onDidUpdate = this._onDidUpdateEmitter.event;
4335

4436
constructor(
4537
private readonly context: ExtensionContext,
4638
private readonly stateStorage: StateStorage,
47-
private readonly workspaceFolderManager: WorkspaceFolderManager,
48-
private readonly pythonExtension: MsPythonExtensionWrapper,
49-
private readonly environmentDependenciesInstaller: EnvironmentDependenciesInstaller
39+
private readonly workspaceFolderManager: WorkspaceFolderManager
5040
) {
51-
//Remove any type stubs that users already have. We will now start using the python SDK (installed with databricks-connect)
41+
// Remove any type stubs that users already have. We will now start using the python SDK (installed with databricks-connect)
5242
let extraPaths =
5343
workspace
5444
.getConfiguration("python")
5545
.get<Array<string>>("analysis.extraPaths") ?? [];
56-
5746
extraPaths = extraPaths.filter(
5847
(value) =>
5948
!value.endsWith(path.join("resources", "python", "stubs"))
@@ -65,120 +54,17 @@ export class ConfigureAutocomplete implements Disposable {
6554
extraPaths,
6655
ConfigurationTarget.Global
6756
);
68-
this.configure();
6957
}
7058

7159
dispose() {
7260
this.disposables.forEach((i) => i.dispose());
7361
}
7462

75-
private async tryStep(fn: () => Promise<StepResult>) {
76-
try {
77-
return await fn();
78-
} catch (e) {
79-
logging.NamedLogger.getOrCreate(Loggers.Extension).error(
80-
"Error configuring autocomplete",
81-
e
82-
);
83-
84-
if (e instanceof Error) {
85-
window.showErrorMessage(
86-
`Error configuring autocomplete: ${e.message}`
87-
);
88-
}
89-
return "Error";
90-
}
91-
}
92-
93-
/*
94-
Skip run if all the required steps return "Skip".
95-
*/
96-
private async shouldSkipRun(steps: Step[]) {
97-
for (const {fn, required} of steps) {
98-
const result = await this.tryStep(() => fn(true));
99-
if (result === "Error") {
100-
return true;
101-
}
102-
if (result !== "Skip" && required) {
103-
return false;
104-
}
105-
}
106-
return true;
107-
}
108-
109-
async configureCommand() {
110-
this.stateStorage.set("databricks.autocompletion.skipConfigure", false);
111-
return this.configure(true);
112-
}
113-
114-
private async configure(force = false) {
115-
if (
116-
!force ||
117-
this.stateStorage.get("databricks.autocompletion.skipConfigure")
118-
) {
119-
return;
120-
}
121-
122-
const steps = [
123-
{
124-
fn: async (dryRun = false) => await this.installPyspark(dryRun),
125-
},
126-
{
127-
fn: async (dryRun = false) => this.addBuiltinsFile(dryRun),
128-
required: true,
129-
},
130-
];
131-
132-
// Force is only set when running from command pallet and we do a fresh configure if it is set.
133-
if (!force && (await this.shouldSkipRun(steps))) {
134-
return;
135-
}
136-
137-
const choice = await window.showInformationMessage(
138-
"Do you want to configure autocompletion for Databricks specific globals (dbutils etc)?",
139-
"Configure",
140-
"Cancel",
141-
"Never for this workspace"
142-
);
143-
144-
if (choice === "Cancel" || choice === undefined) {
145-
return;
146-
}
147-
148-
if (choice === "Never for this workspace") {
149-
this.stateStorage.set(
150-
"databricks.autocompletion.skipConfigure",
151-
true
152-
);
153-
return;
154-
}
155-
156-
for (const {fn} of steps) {
157-
const result = await this.tryStep(() => fn(false));
158-
if (result === "Error" || result === "Cancel") {
159-
return;
160-
}
161-
}
162-
}
163-
164-
private async installPyspark(dryRun = false): Promise<StepResult> {
165-
const executable = await this.pythonExtension.getPythonExecutable();
166-
167-
if (executable === undefined) {
168-
return "Skip";
169-
}
170-
171-
if (dryRun) {
172-
return;
173-
}
174-
this.environmentDependenciesInstaller.show(false);
175-
}
176-
17763
private get projectRootPath() {
178-
return this.workspaceFolderManager.activeProjectUri.fsPath;
64+
return this.workspaceFolderManager.activeWorkspaceFolder.uri.fsPath;
17965
}
18066

181-
private async addBuiltinsFile(dryRun = false): Promise<StepResult> {
67+
private get builtinsPath() {
18268
const stubPath = workspace
18369
.getConfiguration("python")
18470
.get<string>("analysis.stubPath");
@@ -187,45 +73,76 @@ export class ConfigureAutocomplete implements Disposable {
18773
? path.join(this.projectRootPath, stubPath)
18874
: this.projectRootPath;
18975

76+
return path.join(builtinsDir, "__builtins__.pyi");
77+
}
78+
79+
public async configureCommand() {
80+
await this.stateStorage.set(
81+
"databricks.autocompletion.skipConfigure",
82+
false
83+
);
84+
return this.setupBuiltins();
85+
}
86+
87+
public async shouldSetupBuiltins(): Promise<boolean> {
88+
if (this.stateStorage.get("databricks.autocompletion.skipConfigure")) {
89+
return false;
90+
}
91+
92+
const builtinsPath = this.builtinsPath;
93+
19094
let builtinsFileExists = false;
19195
try {
192-
builtinsFileExists = (await readdir(builtinsDir)).includes(
193-
"__builtins__.pyi"
194-
);
195-
} catch (e) {}
196-
197-
const builtinsPath = path.join(builtinsDir, "__builtins__.pyi");
96+
const stats = await stat(builtinsPath);
97+
builtinsFileExists = stats.isFile();
98+
} catch (e) {
99+
builtinsFileExists = false;
100+
}
198101

199102
const importString = await getImportString(this.context);
200103
if (importString === undefined) {
201-
return "Error";
104+
return false;
202105
}
203106

204107
if (
205108
builtinsFileExists &&
206109
(await readFile(builtinsPath, "utf-8")).includes(importString)
207110
) {
208-
return "Skip";
111+
return false;
209112
}
210113

211-
if (dryRun) {
114+
return true;
115+
}
116+
117+
private async setupBuiltins(): Promise<void> {
118+
if (!(await this.shouldSetupBuiltins())) {
212119
return;
213120
}
214121

215-
const messageString = `${
216-
builtinsFileExists ? "Update" : "Create"
217-
} ${builtinsPath} ?`;
122+
const importString = await getImportString(this.context);
123+
if (!importString) {
124+
return;
125+
}
126+
127+
const builtinsPath = this.builtinsPath;
128+
const messageString = `Create ${builtinsPath}?`;
218129
const choice = await window.showInformationMessage(
219130
messageString,
220131
"Continue",
221-
"Cancel"
132+
"Cancel",
133+
"Never for this workspace"
222134
);
223135

224-
if (choice === "Cancel" || choice === undefined) {
225-
return "Cancel";
136+
if (choice === "Never for this workspace") {
137+
await this.stateStorage.set(
138+
"databricks.autocompletion.skipConfigure",
139+
true
140+
);
141+
this._onDidUpdateEmitter.fire();
142+
} else if (choice === "Continue") {
143+
await mkdir(path.dirname(builtinsPath), {recursive: true});
144+
await appendFile(builtinsPath, `\n${importString}\n`);
145+
this._onDidUpdateEmitter.fire();
226146
}
227-
228-
await mkdir(path.dirname(builtinsPath), {recursive: true});
229-
await appendFile(builtinsPath, `\n${importString}\n`);
230147
}
231148
}

packages/databricks-vscode/src/language/EnvironmentCommands.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ export class EnvironmentCommands {
2626
"environment.dependencies",
2727
true
2828
);
29-
if (state.available) {
30-
window.showInformationMessage(
31-
"Python environment and Databricks Connect are already set up."
32-
);
33-
return true;
34-
}
3529
for (const [, step] of state.steps) {
3630
if (step.available || (stepId && step.id !== stepId)) {
3731
continue;
@@ -43,6 +37,12 @@ export class EnvironmentCommands {
4337
return false;
4438
}
4539
}
40+
if (state.available) {
41+
window.showInformationMessage(
42+
"Python environment and Databricks Connect are set up."
43+
);
44+
return true;
45+
}
4646
}
4747

4848
async refresh() {

0 commit comments

Comments
 (0)