diff --git a/package-lock.json b/package-lock.json index ac8f5e6..7d2f7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1460,8 +1460,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -1481,6 +1480,28 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", + "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", @@ -2009,7 +2030,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -2167,8 +2187,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "detect-newline": { "version": "3.1.0", @@ -2912,6 +2931,11 @@ "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4625,14 +4649,12 @@ "mime-db": { "version": "1.46.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", - "dev": true + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" }, "mime-types": { "version": "2.1.29", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", - "dev": true, "requires": { "mime-db": "1.46.0" } @@ -5264,6 +5286,11 @@ "sisteransi": "^1.0.5" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", diff --git a/package.json b/package.json index 9d62285..db05968 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "yamljs": "0.3.0" }, "dependencies": { + "axios": "1.3.2", "native-url": "0.3.4", "node-fetch": "2.6.1", "node-gzip": "1.1.2", diff --git a/scripts/populate-cache.js b/scripts/populate-cache.js index 07fde70..e9a3ddf 100644 --- a/scripts/populate-cache.js +++ b/scripts/populate-cache.js @@ -31,7 +31,7 @@ const getOpenApiMetadata = async () => { } return null; -} +}; /** * Retrieves the API response @@ -61,7 +61,7 @@ const getData = async (version, path, cache) => { } return cache; -} +}; /** * Start building the cache diff --git a/src/extension.ts b/src/extension.ts index bef278e..8a91b5b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,7 +9,7 @@ export async function activate(context: vscode.ExtensionContext) { pattern: '**' }]; - const disposable = vscode.languages.registerCompletionItemProvider(selector, new AutoCompleteProvider(context), '/', '?', '&', '=', ','); + const disposable = vscode.languages.registerCompletionItemProvider(selector, new AutoCompleteProvider(context), '/', '?', '&', '=', ',', '-'); const clearCache = vscode.commands.registerCommand('msgraph.autocomplete.clearCache', async () => { const cache: CacheProvider = CacheProvider.getInstance(context, "name"); diff --git a/src/providers/AutoCompleteProvider.ts b/src/providers/AutoCompleteProvider.ts index 165c54a..a922e7f 100644 --- a/src/providers/AutoCompleteProvider.ts +++ b/src/providers/AutoCompleteProvider.ts @@ -4,6 +4,7 @@ import { MS_GRAPH, PATH_BETA, PATH_V1 } from '../constants'; import { Suggestion, OpenApiResponse, Value } from '../models'; import { CacheProvider } from './CacheProvider'; import { AutoComplete, GraphTokens, OpenApiParser, sanitizePath, ApiSuggestion } from '../utils'; +import { SnippetProvider } from './SnippetProvider'; export class AutoCompleteProvider implements CompletionItemProvider { private lastApiPath: string = ""; @@ -63,6 +64,16 @@ export class AutoCompleteProvider implements CompletionItemProvider { } } + else if(character === "-"){ + const splicedPath = currentLine.slice(0,-1); + const snippetObject = await SnippetProvider.initialize(splicedPath); + const snippet = snippetObject.getSnippet(); + const snippetCompletionItem = new CompletionItem(snippet); + snippetCompletionItem.documentation = snippet; + snippetCompletionItem.insertText = "\n" + snippet; + return [snippetCompletionItem]; + } + return suggestions.map(s => { const suggestion = new CompletionItem(s.value, s.completion || CompletionItemKind.Value); if (s.description) { diff --git a/src/providers/CacheProvider.ts b/src/providers/CacheProvider.ts index f2f85b0..9650f70 100644 --- a/src/providers/CacheProvider.ts +++ b/src/providers/CacheProvider.ts @@ -52,7 +52,7 @@ export class CacheProvider { this.put(version, path, null); } } - } catch (e) { + } catch (e: any) { console.log(e.message); } return null; diff --git a/src/providers/SnippetProvider.ts b/src/providers/SnippetProvider.ts new file mode 100644 index 0000000..de9b28b --- /dev/null +++ b/src/providers/SnippetProvider.ts @@ -0,0 +1,90 @@ +import { nodeFetch } from "../utils/SnippetNodeFetch"; +import * as vscode from 'vscode'; + +interface ISnippetRequestInformation { + requestUrl: string; + version: string; + snippetLanguage: string; +} + +export interface IGeneratedSnippetURL { + method: string; + url: string; + data: string, + headers: { + [key: string] : string + } +} + +export class SnippetProvider { + private snippet: string = ""; + + private constructor(snippet: string){ + this.snippet = snippet; + } + + static async initialize(currentPath: string){ + const { version, requestUrl } = SnippetProvider.generateParts(currentPath); + const snippetLanguage = SnippetProvider.getCurrentLanguage(); + const generatedParts = { version, requestUrl, snippetLanguage }; + const generatedSnippetUrl = SnippetProvider.generateSnippetRequestUrl(generatedParts); + const snippet = await SnippetProvider.makeSnippetRequest(generatedSnippetUrl); + return new SnippetProvider(snippet); + } + + private static getCurrentLanguage(){ + let activeEditor = vscode.window.activeTextEditor; + let document = activeEditor!.document; + return document.languageId || 'csharp'; + } + + private static generateParts = (currentPath: string) => { + // https://graph.microsoft.com/v1.0/ + const v1Index = currentPath.indexOf('v1.0'); + const betaIndex = currentPath.indexOf('beta'); + const version = v1Index === -1 ? 'beta' : 'v1.0'; + const requestUrl = SnippetProvider.getRequestUrl(version, currentPath, v1Index, betaIndex); + return { version, requestUrl }; + }; + + private static getRequestUrl = (version: string, currentPath: string, vIndex: number, betaIndex: number) => { + let requestUrl = ''; + if(version === 'beta'){ + requestUrl = currentPath.substring(betaIndex+4); + return requestUrl; + }; + requestUrl = currentPath.substring(vIndex+4); + return requestUrl; + }; + + private static generateSnippetRequestUrl = (snippetRequestInformation: ISnippetRequestInformation): IGeneratedSnippetURL => { + const { requestUrl, version, snippetLanguage } = snippetRequestInformation; + + const method = 'post'; + let url = 'https://graphexplorerapi.azurewebsites.net/api/graphexplorersnippets'; + const headers = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/http' + }; + const data = `GET /${version}/${requestUrl} HTTP/1.1\r\nHost: graph.microsoft.com\r\nContent-Type: application/json\r\n\r\n}`; + + if (snippetLanguage !== 'csharp') { + url += `?lang=${snippetLanguage}`; + } + const openApiSnippets: string[] = ['go', 'powershell']; + if (openApiSnippets.includes(snippetLanguage)) { + url += '&generation=openapi'; + } + return { method, url, headers, data }; + }; + + + private static async makeSnippetRequest(generatedSnippetUrl: IGeneratedSnippetURL): Promise{ + const response: string= await nodeFetch(generatedSnippetUrl); + return response; + } + + public getSnippet(): string{ + return this.snippet; + } +} \ No newline at end of file diff --git a/src/utils/ApiSuggestion.ts b/src/utils/ApiSuggestion.ts index 1ec750b..18a4e9a 100644 --- a/src/utils/ApiSuggestion.ts +++ b/src/utils/ApiSuggestion.ts @@ -4,7 +4,7 @@ import pluralize = require('pluralize'); import { FILE_APIS } from '../constants'; export class ApiSuggestion { - private static apis: API[] + private static apis: API[]; /** * Get the API methods for the current path @@ -32,7 +32,7 @@ export class ApiSuggestion { ${api.methods.map((m: any) => `- **${m.name.toUpperCase()}**: ${m.description}`).join(`\n`)}`; } - } catch (e) { + } catch (e: any) { console.log(e.message, `Path: ${path}`, `Link: ${link}`); } diff --git a/src/utils/AutoComplete.ts b/src/utils/AutoComplete.ts index f72e9d8..f5d7a37 100644 --- a/src/utils/AutoComplete.ts +++ b/src/utils/AutoComplete.ts @@ -21,7 +21,7 @@ export class AutoComplete { } return null; - } catch (e) { + } catch (e: any) { console.log(e.message); return null; } diff --git a/src/utils/OpenApiParser.ts b/src/utils/OpenApiParser.ts index 8279194..fa41308 100644 --- a/src/utils/OpenApiParser.ts +++ b/src/utils/OpenApiParser.ts @@ -21,7 +21,7 @@ export class OpenApiParser { }); return { url, parameters }; - } catch (error) { + } catch (error: any) { throw new Error(error); } } diff --git a/src/utils/SnippetNodeFetch.ts b/src/utils/SnippetNodeFetch.ts new file mode 100644 index 0000000..79b77d2 --- /dev/null +++ b/src/utils/SnippetNodeFetch.ts @@ -0,0 +1,24 @@ +import { IGeneratedSnippetURL } from "../providers/SnippetProvider"; + +// @ts-check +const fetch = require('node-fetch'); +export const nodeFetch = async (args: IGeneratedSnippetURL): Promise => { + try { + const { url, method, headers, data } = args; + const response = await fetch.default(url, { + method, + headers, + body: data + } ); + if (response && response.ok) { + console.log('Here is the fetch result ', response); + const data = await response.text(); + return data; + } + console.log('Something happened ', response); + return response.body.statusText || 'An error occurred while fetching snippet'; + } + catch (e: any) { + return 'Error encountered while fetching snippet'; + } +}; \ No newline at end of file