Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@
"command": "vscode-objectscript.loadStudioColors",
"when": "isWindows"
},
{
"command": "vscode-objectscript.newFile.class",
"when": "false"
},
{
"command": "vscode-objectscript.newFile.businessOperation",
"when": "false"
Expand Down Expand Up @@ -591,6 +595,11 @@
}
],
"file/newFile": [
{
"command": "vscode-objectscript.newFile.class",
"when": "workspaceFolderCount != 0",
"group": "file"
},
{
"command": "vscode-objectscript.newFile.kpi",
"when": "workspaceFolderCount != 0",
Expand Down Expand Up @@ -1021,6 +1030,11 @@
"command": "vscode-objectscript.loadStudioColors",
"title": "Load Studio Syntax Colors"
},
{
"category": "ObjectScript",
"command": "vscode-objectscript.newFile.class",
"title": "Class"
},
{
"category": "ObjectScript",
"command": "vscode-objectscript.newFile.kpi",
Expand Down
90 changes: 87 additions & 3 deletions src/commands/newFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DocumentContentProvider } from "../providers/DocumentContentProvider";
import { replaceFile, getWsFolder, handleError, displayableUri } from "../utils";
import { getFileName } from "./export";
import { getUrisForDocument } from "../utils/documentIndex";
import { pickClass } from "./project";

interface InputStepItem extends vscode.QuickPickItem {
value?: string;
Expand All @@ -25,7 +26,13 @@ interface QuickPickStepOptions {
items: InputStepItem[];
}

type InputStepOptions = InputBoxStepOptions | QuickPickStepOptions;
interface ClassPickStepOptions {
type: "classPick";
title: string;
api: AtelierAPI | undefined;
}

type InputStepOptions = InputBoxStepOptions | QuickPickStepOptions | ClassPickStepOptions;

/**
* Get input from the user using multiple steps.
Expand Down Expand Up @@ -101,6 +108,58 @@ async function multiStepInput(steps: InputStepOptions[]): Promise<string[] | und
});
inputBox.show();
});
} else if (stepOptions.type == "classPick") {
// Optional step: escape = skip (store ""), back = go back one step, pick = store class name
let picked: string | undefined;
if (stepOptions.api) {
picked = await pickClass(stepOptions.api, stepOptions.title, step > 0);
} else {
// Fallback InputBox when there's no server connection
picked = await new Promise<string | undefined>((resolve) => {
let settled = false;
const settle = (v: string | undefined) => {
if (!settled) {
settled = true;
resolve(v);
}
};
const inputBox = vscode.window.createInputBox();
inputBox.ignoreFocusOut = true;
inputBox.step = step + 1;
inputBox.totalSteps = steps.length;
inputBox.buttons = step > 0 ? [vscode.QuickInputButtons.Back] : [];
inputBox.title = stepOptions.title;
inputBox.placeholder = "Package.Subpackage.Class";
inputBox.onDidTriggerButton(() => {
settle(undefined); // Back was pressed
inputBox.hide();
});
inputBox.onDidAccept(() => {
if (typeof inputBox.validationMessage != "string") {
settle(inputBox.value); // "" = skip, or a valid class name
inputBox.hide();
}
});
inputBox.onDidHide(() => {
settle(""); // Escape = skip this optional step
inputBox.dispose();
});
inputBox.onDidChangeValue((value) => {
inputBox.validationMessage = value ? validateClassName(value) : undefined;
});
inputBox.show();
});
}
if (picked === undefined) {
// Back button was pressed: go back one step
step--;
} else {
// "" = skipped, or a class name was entered/picked
results[step] = picked;
step++;
}
// This is an optional step; never cancel the wizard on escape
escape = false;
} else {
// Show the QuickPick
escape = await new Promise<boolean>((resolve) => {
Expand Down Expand Up @@ -222,6 +281,7 @@ function getAdapterPrompt(adapters: InputStepItem[], type: AdapaterClassType): I

/** The types of classes we can create */
export enum NewFileType {
Class = "Class",
BusinessOperation = "Business Operation",
BusinessService = "Business Service",
BPL = "Business Process",
Expand Down Expand Up @@ -262,7 +322,7 @@ export async function newFile(type: NewFileType): Promise<void> {
api = undefined;
}

if (type != NewFileType.KPI) {
if (type != NewFileType.KPI && type != NewFileType.Class) {
// Check if we're connected to an Interoperability namespace
const ensemble: boolean = api
? await api.getNamespace().then((data) => data.result.content.features[0].enabled)
Expand Down Expand Up @@ -402,7 +462,7 @@ export async function newFile(type: NewFileType): Promise<void> {
inputSteps.push(
{
type: "inputBox",
title: `Enter a name for the new ${type} class`,
title: `Enter a name for the new ${type == NewFileType.Class ? "class" : type + " class"}`,
placeholder: "Package.Subpackage.Class",
validateInput: (value: string) => {
const valid = validateClassName(value);
Expand Down Expand Up @@ -933,6 +993,30 @@ Parameter RESPONSECLASSNAME As CLASSNAME = "${respClass}";`
/// InterSystems IRIS purges message bodies based on the class when the option to purge message bodies is enabled
Parameter ENSPURGE As BOOLEAN = 1;

}
`;
} else if (type == NewFileType.Class) {
// Add the superclass picker as the third step
inputSteps.push({
type: "classPick",
title: "Pick an optional superclass. Press 'Escape' to skip.",
api: api,
});

// Prompt the user
const results = await multiStepInput(inputSteps);
if (!results) {
return;
}
cls = results[0];
const [, desc, superclass] = results;

// Generate the file's content
clsContent = `
${typeof desc == "string" ? "/// " + desc.replace(/\n/g, "\n/// ") : ""}
Class ${cls}${superclass ? ` Extends ${superclass}` : ""}
{

}
`;
}
Expand Down
154 changes: 154 additions & 0 deletions src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,160 @@ async function pickAdditions(
});
}

/**
* Show an expandable QuickPick to let the user pick a single class from the server.
* Packages can be expanded to reveal their classes. Only leaf class items can be accepted.
* - Returns the class name (without the `.cls` extension) when a class is picked.
* - Returns `""` when the user presses Escape (skip this optional step).
* - Returns `undefined` when the user presses the Back button (only when `canGoBack` is `true`).
*/
export async function pickClass(api: AtelierAPI, title: string, canGoBack = false): Promise<string | undefined> {
const query = "SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,?,0,0,?)";
let sys: "0" | "1" = "0";
let gen: "0" | "1" = "0";

return new Promise<string | undefined>((resolve) => {
// Use a settled flag so that onDidHide (which always fires) never double-resolves.
let settled = false;
const settle = (value: string | undefined) => {
if (!settled) {
settled = true;
resolve(value);
}
};

const quickPick = vscode.window.createQuickPick<PickAdditionsItem>();
quickPick.title = title;
quickPick.ignoreFocusOut = true;
quickPick.keepScrollPosition = true;
quickPick.matchOnDescription = true;
quickPick.buttons = [
...(canGoBack ? [vscode.QuickInputButtons.Back] : []),
{
iconPath: new vscode.ThemeIcon("library"),
tooltip: "System",
location: vscode.QuickInputButtonLocation.Input,
toggle: { checked: false },
},
{
iconPath: new vscode.ThemeIcon("server-process"),
tooltip: "Generated",
location: vscode.QuickInputButtonLocation.Input,
toggle: { checked: false },
},
];

const getRootItems = (): Promise<void> => {
return api
.actionQuery(query, ["*.cls", sys, gen])
.then((data) => {
quickPick.items = data.result.content.map((i) => sodItemToPickAdditionsItem(i));
quickPick.busy = false;
})
.catch((error) => {
quickPick.hide();
handleError(error, "Failed to get namespace contents.");
});
};

const expandItem = (itemIdx: number): Promise<void> => {
const item = quickPick.items[itemIdx];
// Switch the expand button to a collapse button
const newItems = [...quickPick.items];
newItems[itemIdx] = {
...item,
buttons: [{ iconPath: new vscode.ThemeIcon("chevron-down"), tooltip: "Collapse" }],
};
quickPick.items = newItems;
return api
.actionQuery(query, [item.fullName + "/*.cls", sys, gen])
.then((data) => {
const insertItems: PickAdditionsItem[] = data.result.content.map((i) =>
sodItemToPickAdditionsItem(i, item.fullName, item.label.search(/\S/))
);
const updatedItems = [...quickPick.items];
updatedItems.splice(itemIdx + 1, 0, ...insertItems);
quickPick.items = updatedItems;
quickPick.busy = false;
})
.catch((error) => {
quickPick.hide();
handleError(error, "Failed to get namespace contents.");
});
};

quickPick.onDidTriggerButton((button) => {
if (button === vscode.QuickInputButtons.Back) {
settle(undefined); // signal "go back" to the caller
quickPick.hide();
} else {
quickPick.busy = true;
if (button.tooltip == "System") {
sys = button.toggle.checked ? "1" : "0";
} else {
gen = button.toggle.checked ? "1" : "0";
}
getRootItems();
}
});

quickPick.onDidTriggerItemButton((event) => {
quickPick.busy = true;
const itemIdx = quickPick.items.findIndex((i) => i.fullName === event.item.fullName);
if (event.button.tooltip.charAt(0) == "E") {
expandItem(itemIdx);
} else {
// Collapse: remove the button and all descendants
const newItems = [...quickPick.items];
newItems[itemIdx] = {
...newItems[itemIdx],
buttons: [{ iconPath: new vscode.ThemeIcon("chevron-right"), tooltip: "Expand" }],
};
quickPick.items = newItems.filter(
(i, idx) => idx <= itemIdx || !i.fullName.startsWith(event.item.fullName + ".")
);
quickPick.busy = false;
}
});

quickPick.onDidChangeValue((filter: string) => {
// Auto-expand a package when the user types its name followed by a dot
if (filter.endsWith(".")) {
const itemIdx = quickPick.items.findIndex(
(i) => i.fullName.toLowerCase() === filter.slice(0, -1).toLowerCase()
);
if (
itemIdx != -1 &&
quickPick.items[itemIdx].buttons?.length &&
quickPick.items[itemIdx].buttons[0].tooltip.charAt(0) == "E"
) {
quickPick.busy = true;
expandItem(itemIdx);
}
}
});

quickPick.onDidAccept(() => {
const selected = quickPick.activeItems[0];
if (selected && !selected.buttons?.length) {
// Leaf class item (no expand button): strip the .cls extension and resolve
const name = selected.fullName.endsWith(".cls") ? selected.fullName.slice(0, -4) : selected.fullName;
settle(name);
quickPick.hide();
}
});

quickPick.onDidHide(() => {
settle(""); // Escape pressed: resolve with "" to signal "skip this optional step"
quickPick.dispose();
});

quickPick.busy = true;
quickPick.show();
getRootItems();
});
}

export async function modifyProject(
nodeOrUri: NodeBase | vscode.Uri | undefined,
type: "add" | "remove"
Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
sendCommandTelemetryEvent("loadStudioColors");
loadStudioColors(languageServerExt);
}),
vscode.commands.registerCommand("vscode-objectscript.newFile.class", () => {
sendCommandTelemetryEvent("newFile.class");
newFile(NewFileType.Class);
}),
vscode.commands.registerCommand("vscode-objectscript.newFile.businessOperation", () => {
sendCommandTelemetryEvent("newFile.businessOperation");
newFile(NewFileType.BusinessOperation);
Expand Down