Skip to content

Commit 97ac839

Browse files
RodgeFuchrisradek
andauthored
support importing from openapi in vscode (#5451)
support importing typespec from openapi in vscode by Integrating with convert tool (tsp-openapi3). fixes: #4857 --------- Co-authored-by: Christopher Radek <[email protected]>
1 parent 5ee3f8b commit 97ac839

File tree

7 files changed

+731
-4
lines changed

7 files changed

+731
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- typespec-vscode
5+
---
6+
7+
Support importing TypeSpec from OpenAPI 3.0 doc

packages/typespec-vscode/package.json

+13
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@
112112
}
113113
}
114114
],
115+
"menus": {
116+
"explorer/context": [
117+
{
118+
"command": "typespec.importFromOpenApi3",
119+
"group": "typespec@1"
120+
}
121+
]
122+
},
115123
"grammars": [
116124
{
117125
"language": "typespec",
@@ -155,6 +163,11 @@
155163
"command": "typespec.installGlobalCompilerCli",
156164
"title": "Install TypeSpec Compiler/CLI globally",
157165
"category": "TypeSpec"
166+
},
167+
{
168+
"command": "typespec.importFromOpenApi3",
169+
"title": "Import TypeSpec from OpenApi3",
170+
"category": "TypeSpec"
158171
}
159172
],
160173
"menus": {

packages/typespec-vscode/src/extension.ts

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { isWhitespaceStringOrUndefined } from "./utils.js";
1717
import { createTypeSpecProject } from "./vscode-cmd/create-tsp-project.js";
1818
import { emitCode } from "./vscode-cmd/emit-code/emit-code.js";
19+
import { importFromOpenApi3 } from "./vscode-cmd/import-from-openapi3.js";
1920
import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js";
2021

2122
let client: TspLanguageClient | undefined;
@@ -107,6 +108,12 @@ export async function activate(context: ExtensionContext) {
107108
}),
108109
);
109110

111+
context.subscriptions.push(
112+
commands.registerCommand(CommandName.ImportFromOpenApi3, async (uri: vscode.Uri) => {
113+
await importFromOpenApi3(uri);
114+
}),
115+
);
116+
110117
context.subscriptions.push(
111118
vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
112119
if (e.affectsConfiguration(SettingName.TspServerPath)) {

packages/typespec-vscode/src/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const enum CommandName {
1010
CreateProject = "typespec.createProject",
1111
OpenUrl = "typespec.openUrl",
1212
GenerateCode = "typespec.generateCode",
13+
ImportFromOpenApi3 = "typespec.importFromOpenApi3",
1314
}
1415

1516
export interface InstallGlobalCliCommandArgs {
@@ -52,4 +53,4 @@ interface UnsuccessResult {
5253
details?: any;
5354
}
5455

55-
export type Result<T> = SuccessResult<T> | UnsuccessResult;
56+
export type Result<T = void> = SuccessResult<T> | UnsuccessResult;
+285
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { readdir } from "fs/promises";
2+
import vscode, {
3+
CancellationToken,
4+
OpenDialogOptions,
5+
Progress,
6+
QuickPick,
7+
QuickPickItem,
8+
QuickPickItemButtonEvent,
9+
QuickPickOptions,
10+
} from "vscode";
11+
import logger from "./log/logger.js";
12+
import { Result, ResultCode } from "./types.js";
13+
import { createPromiseWithCancelAndTimeout } from "./utils.js";
14+
15+
interface QuickPickOptionsWithExternalLink extends QuickPickItem {
16+
externalLink?: string;
17+
}
18+
19+
export interface ConfirmOptions<
20+
T extends QuickPickOptionsWithExternalLink,
21+
P extends QuickPickOptionsWithExternalLink,
22+
> {
23+
title?: string;
24+
placeholder?: string;
25+
yesQuickPickItem?: T;
26+
noQuickPickItem?: P;
27+
}
28+
29+
export async function confirm<
30+
T extends QuickPickOptionsWithExternalLink,
31+
P extends QuickPickOptionsWithExternalLink,
32+
>(confirmOptions: ConfirmOptions<T, P>): Promise<boolean | undefined> {
33+
const setButtonForExternalLink = (item: QuickPickOptionsWithExternalLink) => {
34+
if (item.externalLink) {
35+
item.buttons = [
36+
{
37+
iconPath: new vscode.ThemeIcon("link-external"),
38+
tooltip: `Open: ${item.externalLink}`,
39+
},
40+
];
41+
}
42+
};
43+
const yes: QuickPickOptionsWithExternalLink = confirmOptions.yesQuickPickItem ?? { label: "Yes" };
44+
setButtonForExternalLink(yes);
45+
const no: QuickPickOptionsWithExternalLink = confirmOptions.noQuickPickItem ?? { label: "No" };
46+
setButtonForExternalLink(no);
47+
const options: QuickPickOptions = {
48+
title: confirmOptions.title,
49+
placeHolder: confirmOptions.placeholder,
50+
canPickMany: false,
51+
ignoreFocusOut: true,
52+
};
53+
const items = [yes, no];
54+
const selected = await showQuickPickWithButtons(items, options, (_quickpick, event) => {
55+
if (event.item === yes && yes.externalLink) {
56+
vscode.env.openExternal(vscode.Uri.parse(yes.externalLink));
57+
} else if (event.item === no && no.externalLink) {
58+
vscode.env.openExternal(vscode.Uri.parse(no.externalLink));
59+
} else {
60+
logger.warning(`Unexpected QuickPickItemButtonEvent for item ${event.item.label}`);
61+
}
62+
});
63+
return selected === undefined || selected.length === 0 ? undefined : selected[0] === yes;
64+
}
65+
66+
export async function checkAndConfirmEmptyFolder(
67+
targetFolder: string,
68+
placeholder: string = "Selected folder is not empty. Do you want to continue?",
69+
title?: string,
70+
): Promise<boolean | undefined> {
71+
const files = await readdir(targetFolder);
72+
if (files.length === 0) {
73+
return true;
74+
}
75+
logger.info("Selected folder is not empty.");
76+
const confirmed = await confirm({
77+
title: title,
78+
placeholder: placeholder,
79+
yesQuickPickItem: {
80+
label: "Yes",
81+
detail: `Selected folder: ${targetFolder}`,
82+
},
83+
noQuickPickItem: {
84+
label: "No",
85+
},
86+
});
87+
if (confirmed === undefined) {
88+
logger.info("User cancelled the confirm QuickPick for non-empty folder.");
89+
return undefined;
90+
} else if (confirmed) {
91+
logger.info("User confirmed to continue with non empty folder.");
92+
return true;
93+
} else {
94+
logger.info("User confirmed not to continue with non empty folder.");
95+
return false;
96+
}
97+
}
98+
99+
export async function selectFolder(
100+
dlgTitle: string,
101+
btnLabel: string,
102+
): Promise<string | undefined> {
103+
logger.info(`Selecting folder for '${dlgTitle}'...`);
104+
const folderOptions: OpenDialogOptions = {
105+
canSelectMany: false,
106+
openLabel: btnLabel,
107+
canSelectFolders: true,
108+
canSelectFiles: false,
109+
title: dlgTitle,
110+
};
111+
112+
const folderUri = await vscode.window.showOpenDialog(folderOptions);
113+
if (!folderUri || folderUri.length === 0) {
114+
logger.info(`No folder selected for '${dlgTitle}'.`);
115+
return undefined;
116+
}
117+
const selectedFolder = folderUri[0].fsPath;
118+
logger.info(`Selected folder for '${dlgTitle}': ${selectedFolder}`);
119+
return selectedFolder;
120+
}
121+
122+
/**
123+
*
124+
* @param dlgTitle
125+
* @param btnLabel
126+
* @param filters refer to {@link OpenDialogOptions.filters} .
127+
A set of file filters that are used by the dialog. Each entry is a human-readable label
128+
like "TypeScript", and an array of extensions, for example:
129+
```ts
130+
{
131+
'Images': ['png', 'jpg'],
132+
'TypeScript': ['ts', 'tsx']
133+
}
134+
```
135+
* @returns
136+
*/
137+
export async function selectFile(
138+
dlgTitle: string,
139+
btnLabel: string,
140+
filters: { [name: string]: string[] },
141+
): Promise<string | undefined> {
142+
logger.info(`Selecting file for '${dlgTitle}' ...`);
143+
const fileOptions: OpenDialogOptions = {
144+
canSelectMany: false,
145+
openLabel: btnLabel,
146+
canSelectFolders: false,
147+
canSelectFiles: true,
148+
filters: filters,
149+
title: dlgTitle,
150+
};
151+
152+
const fileUri = await vscode.window.showOpenDialog(fileOptions);
153+
if (!fileUri || fileUri.length === 0) {
154+
logger.info(`No file selected for '${dlgTitle}'.`);
155+
return undefined;
156+
}
157+
const selectedFile = fileUri[0].fsPath;
158+
logger.info(`Selected file for '${dlgTitle}': ${selectedFile}`);
159+
return selectedFile;
160+
}
161+
162+
interface ProgressOptions {
163+
title: string;
164+
withCancelAndTimeout: boolean;
165+
/** Only take effect when {@link ProgressOptions.withCancelAndTimeout} is true */
166+
timeoutInMs: number;
167+
}
168+
169+
export interface ExecuteWithUiOptions<
170+
T extends QuickPickOptionsWithExternalLink,
171+
P extends QuickPickOptionsWithExternalLink,
172+
> {
173+
/**
174+
* The name of the execution. Only used for logging now
175+
*/
176+
name: string;
177+
/**
178+
* Confirm options. No confirm step when undefined
179+
*/
180+
confirm?: ConfirmOptions<T, P>;
181+
/**
182+
* Progress options. No progress when undefined
183+
*/
184+
progress?: ProgressOptions;
185+
}
186+
187+
export async function tryExecuteWithUi<
188+
T,
189+
P extends QuickPickOptionsWithExternalLink,
190+
Q extends QuickPickOptionsWithExternalLink,
191+
>(
192+
options: ExecuteWithUiOptions<P, Q>,
193+
func: (
194+
progress:
195+
| Progress<{
196+
message?: string;
197+
increment?: number;
198+
}>
199+
| undefined,
200+
token: CancellationToken | undefined,
201+
) => Promise<T>,
202+
): Promise<Result<T>> {
203+
if (options.confirm) {
204+
const confirmed = await confirm(options.confirm);
205+
if (confirmed !== true) {
206+
logger.info(`User cancelled or declined the confirmation for '${options.name}'.`);
207+
return { code: ResultCode.Cancelled };
208+
}
209+
}
210+
211+
if (options.progress) {
212+
const po = options.progress;
213+
return await vscode.window.withProgress(
214+
{
215+
location: vscode.ProgressLocation.Notification,
216+
cancellable: true,
217+
title: options.progress.title,
218+
},
219+
async (progress, token) => {
220+
try {
221+
const r =
222+
po.withCancelAndTimeout === true
223+
? await createPromiseWithCancelAndTimeout(
224+
func(progress, token),
225+
token,
226+
po.timeoutInMs,
227+
)
228+
: await func(progress, token);
229+
return { code: ResultCode.Success, value: r };
230+
} catch (e: any) {
231+
if (e === ResultCode.Cancelled) {
232+
logger.info(`User cancelled the progress: "${options.name}"`);
233+
return { code: ResultCode.Cancelled };
234+
} else if (e === ResultCode.Timeout) {
235+
logger.error(`Progress "${options.name}" timeout after ${po.timeoutInMs}ms`, [e]);
236+
return { code: ResultCode.Timeout };
237+
} else {
238+
logger.error(`Unexpected error in the progress of "${options.name}"`, [e]);
239+
return { code: ResultCode.Fail, details: e };
240+
}
241+
}
242+
},
243+
);
244+
} else {
245+
try {
246+
const r = await func(undefined, undefined);
247+
return { code: ResultCode.Success, value: r };
248+
} catch (e) {
249+
logger.error(`Unexpected error for ${options.name}`, [e]);
250+
return { code: ResultCode.Fail, details: e };
251+
}
252+
}
253+
}
254+
255+
export async function showQuickPickWithButtons<T extends QuickPickItem>(
256+
items: T[],
257+
options: QuickPickOptions,
258+
onItemButtonTriggered: (quickpick: QuickPick<T>, item: QuickPickItemButtonEvent<T>) => void,
259+
) {
260+
const quickPickup = vscode.window.createQuickPick<T>();
261+
quickPickup.items = items;
262+
if (options.title) quickPickup.title = options.title;
263+
if (options.placeHolder) quickPickup.placeholder = options.placeHolder;
264+
if (options.canPickMany) quickPickup.canSelectMany = options.canPickMany;
265+
if (options.ignoreFocusOut) quickPickup.ignoreFocusOut = options.ignoreFocusOut;
266+
if (options.matchOnDescription) quickPickup.matchOnDescription = options.matchOnDescription;
267+
if (options.matchOnDetail) quickPickup.matchOnDetail = options.matchOnDetail;
268+
quickPickup.onDidTriggerItemButton((event) => {
269+
onItemButtonTriggered(quickPickup, event);
270+
});
271+
const selectionPromise = new Promise<T[] | undefined>((resolve) => {
272+
quickPickup.onDidAccept(() => {
273+
const selectedItem = [...quickPickup.selectedItems];
274+
resolve(selectedItem);
275+
quickPickup.hide();
276+
});
277+
quickPickup.onDidHide(() => {
278+
resolve(undefined);
279+
quickPickup.dispose();
280+
});
281+
});
282+
quickPickup.show();
283+
284+
return selectionPromise;
285+
}

0 commit comments

Comments
 (0)