├── .DS_Store ├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── autocompletion.gif ├── babel.config.js ├── images ├── autocomplete.gif └── visualizer.gif ├── jest.config.js ├── logo.png ├── media ├── .DS_Store ├── blkicon.png ├── icon.png └── icon_old.png ├── package-lock.json ├── package.json ├── scripts └── preview.js ├── src ├── constants.ts ├── extension.ts ├── lib │ ├── config.ts │ ├── models.ts │ └── suggestions.ts ├── parser.ts └── test │ ├── parser.test.js │ ├── runTest.ts │ ├── suite │ ├── extension.test.ts │ └── index.ts │ └── testingAsset │ └── starWar.ts ├── stylesheet └── preview.css ├── tsconfig.json ├── visualization.gif ├── vsc-extension-quickstart.md └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .env 7 | .DS_Store -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/**/*.js", 30 | "${workspaceFolder}/dist/**/*.js" 31 | ], 32 | "preLaunchTask": "tasks: watch-tests" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "surfql" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SurfQL 2 | 3 | SurfQL is a VS Code developer tool that displays user defined GraphQL schema in a visualizer and allows for autocompletion for your query using Intellisense. 4 | 5 | ## Functionality 6 | Schema Visualization 7 | ![Sample](visualization.gif) 8 | Code Auto-completion 9 | ![Sample](autocompletion.gif) 10 | 11 | ## Instructions 12 | 13 | 1. Go to VS Code marketplace and download SurfQL. 14 | 2. Once installed, the first time you open your project, follow the pop-up message to create a surfql.config.json file. 15 | 3. Click the Q logo that appears on your side bar, and click on the View Schemas Button to initiate the visualizer. It will open a webView panel with your visualization of the schema hierarchy. If you don't have a config file, you can also mannually select your schema file following the pop-up file selector manually. 16 | (Currently SurfQL supports parsing schema files written with Apollo Server library. Support for more libraries is under development.) 17 | 4. Beyond visualization: Back to your project, once you begin building up your query in your project file, SurfQL will begin parsing your query and suggesting the available fields to complete your query. 18 | 5. To experiment with SurfQL, we recommend using our playground environment. [Check it out here](https://github.com/surfql/apollo-playgrounds)! 19 | 6. Happy surfing! If you encounter any issues or have suggestions for improving SurfQL, please submit an issue on our GitHub repository. 20 | 21 | #### Test out the extension with our pre-built playgrounds 22 | https://github.com/surfql/apollo-playgrounds 23 | 24 | 25 | ## Built With 26 | 27 | 41 | Referencing 42 | 50 | 51 | ## Supported File Types 52 | ### GraphQL Schema Definition Files 53 | - Supported file types: `.graphql`, `.graphqls`, `.ts`, `.js` 54 | - To request support for additional file types, please create an [issue](https://github.com/oslabs-beta/SurfQL/issues) 55 | 56 | ### Autocomplete Suggestions 57 | - Supported file types: `.js`, `.jsx`, `.ts`, `.tsx` 58 | - To request support for additional file types, please create an [issue](https://github.com/oslabs-beta/SurfQL/issues) 59 | 60 | ## Extension Settings 61 | 62 | Make sure to include a configuration file named `surfql.config.json` 63 | 64 | #### Example 65 | 66 | ```json 67 | { 68 | "schema": "./", 69 | "serverLibrary" : "Apollo Server" 70 | } 71 | ``` 72 | 73 | 74 | 75 | ## Roadmap 76 | 77 | - [ ] Create schema file parsing support for different libaries, eg. GraphQL.js, graphql-yoga, etc. 78 | - [ ] Create a Postman type API that sits in VSCode 79 | - [ ] Create input fields for requests on the webview panel 80 | - [ ] Connect to the GraphQL API to return the data 81 | - [ ] Display the data in the panel 82 | 83 | 84 | 85 | 86 | ## Steps to Contribute 87 | 88 | Contributions really make the open source community an amazing place to learn, inspire, and create. Any contributions made to surfQL are **appreciated**. 89 | 90 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 91 | Don't forget to give the project a star! Thanks again! 92 | 93 | 1. Fork & Clone SurfQL 94 | 2. Create your Feature Branch (`git checkout -b /`) 95 | 3. Make your Changes (See **Making Changes** below) 96 | 4. Commit your Changes (`git commit -m ''`) 97 | 5. Push to the Branch (`git push origin /`) 98 | 6. Open a Pull Request 99 | 100 | 101 | 102 | ### Making Changes 103 | 104 | 1. Make your changes! 105 | 2. Re-compile and re-build your extension using the command line: `npm run compile` & `npm run build` 106 | 3. Press F5 (or use the VS Code Debug console) A new VS Code window should open in development mode. This is your debugging environment! 107 | 4. Repeat step 3 and refresh your debugging environment to test further changes. 108 | 109 | 110 | 111 | ## The SURFQL Team 112 | 113 | - Ethan McRae [LinkedIn](https://www.linkedin.com/in/ethanmcrae/) | [Github](https://github.com/ethanmcrae) 114 | - Tristan Onfroy [LinkedIn](https://www.linkedin.com/in/tristan-onfroy/) | [Github](https://github.com/TristanO45) 115 | - Yanqi Joy Zhang [LinkedIn](https://www.linkedin.com/in/yanqi-joy-zhang-72a41b50/) | [Github](https://github.com/jzhang2018p) 116 | - Steve Benner [LinkedIn](https://www.linkedin.com/in/stephenbenner/) | [Github](https://github.com/CodeBrewLatte) 117 | - Dwayne Neckles [LinkedIn](https://www.linkedin.com/in/dneckles/) | [Github](https://github.com/dnecklesportfolio) 118 | 119 | 120 | 121 | ## Contact Us 122 | 123 | Email: [surfqlapp@gmail.com](surfqlapp@gmail.com) 124 | Website: [http://www.surfql.com/](http://www.surfql.com/) 125 | -------------------------------------------------------------------------------- /autocompletion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/autocompletion.gif -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /images/autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/images/autocomplete.gif -------------------------------------------------------------------------------- /images/visualizer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/images/visualizer.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/logo.png -------------------------------------------------------------------------------- /media/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/media/.DS_Store -------------------------------------------------------------------------------- /media/blkicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/media/blkicon.png -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/media/icon.png -------------------------------------------------------------------------------- /media/icon_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/media/icon_old.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surfql", 3 | "displayName": "SurfQL", 4 | "description": "Extends the VS Code IDE to auto-fill GraphQL queries.", 5 | "version": "0.8.1", 6 | "publisher": "surfql", 7 | "engines": { 8 | "vscode": "^1.70.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "icon": "media/icon.png", 14 | "activationEvents": [ 15 | "workspaceContains:**/surfql.config.json", 16 | "onCommand:surfql.previewSchema" 17 | ], 18 | "main": "./dist/extension.js", 19 | "contributes": { 20 | "commands": [ 21 | { 22 | "command": "surfql.query", 23 | "title": "surfql" 24 | }, 25 | { 26 | "command": "surfql.previewSchema", 27 | "title": "Preview Schema" 28 | }, 29 | { 30 | "command": "surfql.generateConfigFile", 31 | "title": "Generate SurfQL Config Template" 32 | } 33 | ], 34 | "configuration": { 35 | "title": "SurfQL", 36 | "properties": { 37 | "surfql.displayConfigPopup": { 38 | "type": "boolean", 39 | "default": true, 40 | "description": "Displays a popup to automatically generate a config file." 41 | }, 42 | "surfql.displayInvalidConfigPathPopup": { 43 | "type": "boolean", 44 | "default": true, 45 | "description": "Displays a popup when the schema path is invalid in the config file." 46 | } 47 | } 48 | }, 49 | "viewsContainers": { 50 | "activitybar": [ 51 | { 52 | "id": "surfql", 53 | "title": "SurfQL", 54 | "icon": "media/icon.png" 55 | } 56 | ] 57 | }, 58 | "views": { 59 | "surfql": [ 60 | { 61 | "id": "surfql", 62 | "name": "SurfQL", 63 | "icon": "media/icon.png", 64 | "contextualTitle": "Package Explorer" 65 | } 66 | ] 67 | }, 68 | "viewsWelcome": [ 69 | { 70 | "view": "surfql", 71 | "contents": "[View Schemas](command:surfql.previewSchema)\n[Generate Config File](command:surfql.generateConfigFile)" 72 | } 73 | ] 74 | }, 75 | "scripts": { 76 | "vscode:prepublish": "npm run package", 77 | "compile": "webpack", 78 | "watch": "webpack --watch", 79 | "package": "webpack --mode production --devtool hidden-source-map", 80 | "compile-tests": "tsc -p . --outDir out", 81 | "watch-tests": "tsc -p . -w --outDir out", 82 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 83 | "lint": "eslint src --ext ts", 84 | "test": "node ./out/test/runTest.js" 85 | }, 86 | "devDependencies": { 87 | "@types/glob": "^7.2.0", 88 | "@types/mocha": "^9.1.1", 89 | "@types/node": "16.x", 90 | "@types/vscode": "^1.70.0", 91 | "@typescript-eslint/eslint-plugin": "^5.31.0", 92 | "@typescript-eslint/parser": "^5.31.0", 93 | "@vscode/test-electron": "^2.1.5", 94 | "eslint": "^8.20.0", 95 | "glob": "^8.0.3", 96 | "mocha": "^10.0.0", 97 | "ts-loader": "^9.3.1", 98 | "typescript": "^4.7.4", 99 | "webpack": "^5.74.0", 100 | "webpack-cli": "^4.10.0" 101 | }, 102 | "dependencies": { 103 | "apollo-server": "^3.10.1", 104 | "axios": "^0.27.2", 105 | "graphql": "^16.5.0", 106 | "vsce": "^2.13.0" 107 | }, 108 | "repository": { 109 | "type": "git", 110 | "url": "https://github.com/oslabs-beta/SurfQL.git" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /scripts/preview.js: -------------------------------------------------------------------------------- 1 | // Global Memory 2 | let followCode = false; 3 | 4 | //document on load 5 | document.addEventListener("DOMContentLoaded", () => { 6 | //get board element 7 | const board = document.querySelector("#board"); 8 | const vscode = acquireVsCodeApi(); 9 | function getSchematext() { 10 | vscode.postMessage({ 11 | command: "get schema text", 12 | }); 13 | } 14 | getSchematext(); 15 | 16 | // Refresh button functionality 17 | const refreshBtn = document.querySelector("#refresh"); 18 | refreshBtn.addEventListener("click", (e) => { 19 | board.innerHTML = ""; 20 | getSchematext(); 21 | }); 22 | 23 | // Live update button functionality 24 | const liveUpdateBtn = document.querySelector("#follow-code"); 25 | liveUpdateBtn.addEventListener('click', (e) => { 26 | // Invert functionality and appearance 27 | followCode = !followCode; 28 | liveUpdateBtn.classList.toggle('btn-selected'); 29 | liveUpdateBtn.innerText = 30 | followCode 31 | ? '' // Will switch between: ⏺(default) and ⏹(hover) via CSS 32 | : 'Track'; 33 | }); 34 | }); 35 | 36 | //add eventListener to the window 37 | window.addEventListener("message", (event) => { 38 | const message = event.data; 39 | //call parser 40 | if (message.command === "sendSchemaInfo") { 41 | const [schemaArr, queryMutation, enumArr, inputArr, scalarArr, unionArr] = 42 | JSON.parse(message.text); 43 | draw(queryMutation, schemaArr, enumArr, inputArr, scalarArr, unionArr); 44 | return; 45 | } else if (message.command === "followCode" && followCode) { 46 | const [historyArray, typedFields] = JSON.parse(message.text); 47 | openTo(historyArray, typedFields); 48 | } 49 | }); 50 | 51 | // //display function 52 | function draw(qmArr, schemaArr, enumArr, inputArr, scalarArr, unionArr) { 53 | //create enumLeaf array for check type logic 54 | const enumLeaf = []; 55 | enumArr.forEach((e) => { 56 | enumLeaf.push(e.name); 57 | }); 58 | const scalarTypes = ["Int", "Float", "String", "Boolean", "ID"].concat( 59 | scalarArr 60 | ); 61 | 62 | //first div called Entry to demo query and mutation info 63 | const entry = document.createElement("div"); 64 | entry.setAttribute("class", "container"); 65 | entry.setAttribute("style", "padding: 10px"); 66 | board.appendChild(entry); 67 | const category = document.createElement("h5"); 68 | category.innerHTML = "Entry Points"; 69 | entry.appendChild(category); 70 | 71 | //for every root in array we create a list item 72 | qmArr.forEach((root) => { 73 | const rootDisplay = document.createElement("li"); 74 | rootDisplay.setAttribute("class", "queryType-alt"); 75 | rootDisplay.innerHTML = `${root.name}`; 76 | //create fieldDisplay 77 | const fieldDisplay = document.createElement("ul"); 78 | fieldDisplay.setAttribute("class", "fieldGroup"); 79 | for (const field in root.fields) { 80 | //create a li for each key-value pair in the field. 81 | const childLi = document.createElement("li"); 82 | childLi.setAttribute("class", "fieldType-alt"); 83 | const btn = document.createElement("a"); 84 | btnBasic(btn); 85 | btn.setAttribute( 86 | "data-bs-title", 87 | `return ${root.fields[field].returnType} type` 88 | ); 89 | const tooltip = new bootstrap.Tooltip(btn); 90 | btn.textContent = `${field}: ${root.fields[field].returnType}`; 91 | btn.addEventListener("click", function (e) { 92 | e.stopPropagation(); 93 | const parent = e.target.parentNode; 94 | //grab typeinfo from parent node. 95 | const [field, fieldtype] = parent.textContent 96 | .replace(" ", "") 97 | .split(":"); 98 | schemaArr.forEach((e) => { 99 | if (fieldtype === e.name) { 100 | drawNext(schemaArr, btn, e, enumLeaf, scalarTypes, unionArr); 101 | } 102 | }); 103 | }); 104 | childLi.appendChild(btn); 105 | //append to list fieldDisplay 106 | fieldDisplay.appendChild(childLi); 107 | //hide children initially 108 | fieldDisplay.hidden = true; 109 | } 110 | 111 | //append field display to root 112 | rootDisplay.appendChild(fieldDisplay); 113 | rootDisplay.addEventListener("click", function (e) { 114 | const children = this.querySelector("ul"); 115 | children.hidden = !children.hidden; 116 | }); 117 | //append rootDisplay to entry 118 | entry.appendChild(rootDisplay); 119 | }); 120 | 121 | //Second div to save input type 122 | const inputBox = document.createElement("div"); 123 | inputBox.setAttribute("class", "container"); 124 | inputBox.setAttribute("style", "padding: 10px"); 125 | board.appendChild(inputBox); 126 | const category2 = document.createElement("h5"); 127 | category2.innerHTML = "Input Types"; 128 | inputBox.appendChild(category2); 129 | 130 | inputArr.forEach((root) => { 131 | const rootDisplay = document.createElement("li"); 132 | rootDisplay.setAttribute("class", "queryType-alt"); 133 | rootDisplay.innerHTML = `${root.name}`; 134 | //create fieldDisplay 135 | const fieldDisplay = document.createElement("ul"); 136 | fieldDisplay.setAttribute("class", "fieldGroup"); 137 | for (const field in root.fields) { 138 | //create a li for each key-value pair in the field. 139 | const childLi = document.createElement("li"); 140 | childLi.setAttribute("class", "fieldType-alt"); 141 | //check for type 142 | if ( 143 | scalarTypes.includes(root.fields[field]) || 144 | enumLeaf.includes(root.fields[field]) 145 | ) { 146 | childLi.textContent = `${field}: ${root.fields[field]}`; 147 | } else { 148 | const btn = document.createElement("a"); 149 | btn.setAttribute("class", "notleaf"); 150 | btn.textContent = `${field}: ${root.fields[field]}`; 151 | btn.addEventListener("click", function (e) { 152 | e.stopPropagation(); 153 | const parent = e.target.parentNode; 154 | //grab typeinfo from parent node. 155 | const [field, fieldtype] = parent.textContent 156 | .replace(" ", "") 157 | .split(":"); 158 | schemaArr.forEach((e) => { 159 | if (fieldtype === e.name) { 160 | drawNext(schemaArr, btn, e, enumLeaf, scalarTypes, unionArr); 161 | } 162 | }); 163 | }); 164 | childLi.appendChild(btn); 165 | } 166 | //append to list fieldDisplay 167 | fieldDisplay.appendChild(childLi); 168 | //hide children initially 169 | fieldDisplay.hidden = true; 170 | } 171 | 172 | //append field display to root 173 | rootDisplay.appendChild(fieldDisplay); 174 | rootDisplay.addEventListener("click", function (e) { 175 | const children = this.querySelector("ul"); 176 | children.hidden = !children.hidden; 177 | }); 178 | //append rootDisplay to entry 179 | inputBox.appendChild(rootDisplay); 180 | }); 181 | 182 | //Third div to save Enum type 183 | const enumBox = document.createElement("div"); 184 | enumBox.setAttribute("class", "container"); 185 | enumBox.setAttribute("style", "padding: 10px"); 186 | board.appendChild(enumBox); 187 | const category3 = document.createElement("h5"); 188 | category3.innerHTML = "Enumeration Types"; 189 | enumBox.appendChild(category3); 190 | enumArr.forEach((el) => { 191 | const enumD = document.createElement("li"); 192 | enumBox.appendChild(enumD); 193 | const enumDisplay = document.createElement("a"); 194 | enumD.appendChild(enumDisplay); 195 | enumDisplay.setAttribute("data-bs-toggle", "collapse"); 196 | enumDisplay.setAttribute("href", `#E${el.name}`); 197 | enumDisplay.setAttribute("style", "color:rgb(170,170,170"); 198 | enumDisplay.setAttribute("class", "notleaf"); 199 | enumDisplay.innerHTML = el.name; 200 | const enumChoices = document.createElement("div"); 201 | enumChoices.setAttribute("id", `E${el.name}`); 202 | enumChoices.setAttribute("class", "collapse"); 203 | enumChoices.innerHTML = `${el.value.join(",")}`; 204 | enumD.appendChild(enumChoices); 205 | }); 206 | return; 207 | } 208 | 209 | //function draw the next level fields 210 | function drawNext(array, node, rootObj, enumLeaf, scalarTypes, unionArr) { 211 | const unionObj = {}; 212 | unionArr.forEach((el) => { 213 | unionObj[el.name] = el.options; 214 | }); 215 | //create field display 216 | const fieldDisplay = document.createElement("ul"); 217 | fieldDisplay.setAttribute("class", "fieldGroup"); 218 | for (const field in rootObj.fields) { 219 | const childLi = document.createElement("li"); 220 | childLi.setAttribute("class", "fieldType-alt"); 221 | //check the type to see if it is leaf 222 | const returnType = rootObj.fields[field].returnType; 223 | if (scalarTypes.includes(returnType)) { 224 | childLi.textContent = `${field}: ${returnType}`; 225 | } else if (enumLeaf.includes(returnType)) { 226 | childLi.textContent = `${field}: ${returnType}`; 227 | childLi.setAttribute("style", "color:rgb(170, 170, 170"); 228 | } else if (Object.keys(unionObj).includes(returnType)) { 229 | const btn = document.createElement("a"); 230 | btnBasic(btn); 231 | btn.setAttribute( 232 | "data-bs-title", 233 | `return one of the ${JSON.stringify(unionObj[returnType])} object type` 234 | ); 235 | const tooltip = new bootstrap.Tooltip(btn); 236 | btn.textContent = `${field}: ${returnType}`; 237 | //append to list item 238 | childLi.appendChild(btn); 239 | btn.addEventListener("click", function (e) { 240 | e.stopPropagation(); 241 | }); 242 | } else { 243 | //create buttons within li 244 | const btn = document.createElement("a"); 245 | btnBasic(btn); 246 | btn.setAttribute("data-bs-title", `return ${returnType} object type`); 247 | const tooltip = new bootstrap.Tooltip(btn); 248 | btn.textContent = `${field}: ${returnType}`; 249 | //append to list item 250 | childLi.appendChild(btn); 251 | btn.addEventListener("click", function (e) { 252 | e.stopPropagation(); 253 | const parent = e.target.parentNode; 254 | const [field, fieldtype] = parent.textContent 255 | .replace(" ", "") 256 | .split(":"); 257 | array.forEach((e) => { 258 | if (fieldtype === e.name) { 259 | drawNext(array, btn, e, enumLeaf, scalarTypes, unionArr); 260 | } 261 | }); 262 | }); 263 | } 264 | 265 | fieldDisplay.appendChild(childLi); 266 | } 267 | //node is the button but we want to the parent of the button 268 | node.addEventListener("click", function (e) { 269 | //locate children ul 270 | const children = this.parentNode.querySelector("ul"); 271 | children.hidden = !children.hidden; 272 | }); 273 | node.parentNode.appendChild(fieldDisplay); 274 | return; 275 | } 276 | 277 | function btnBasic(btn) { 278 | btn.setAttribute("class", "notleaf"); 279 | btn.setAttribute("data-bs-toggle", "tooltip"); 280 | btn.setAttribute("data-bs-placement", "right"); 281 | btn.setAttribute("data-bs-trigger", "hover"); 282 | } 283 | 284 | /** 285 | * Opens the schema to view the type in the given path 286 | * @param {string[]} schemaPath 287 | * @param {string[]} typedFields 288 | */ 289 | function openTo(schemaPath, typedFields) { 290 | // Navigate inside the correct entry point (query/mutation) 291 | let currentElement = null; // The current element that is aligned with the schema path 292 | let schemaPathIndex = 0; // How deeply nested are we within schemaPath 293 | const operation = schemaPath.shift(); 294 | const entryPoints = board.children[0].querySelectorAll('li'); 295 | for (const entryPoint of entryPoints) { 296 | // Check `li` elements to find a match 297 | if (entryPoint.children[0].innerText === operation) { 298 | // Only click if the children are hidden 299 | if (entryPoint.children[1].hidden) { 300 | entryPoint.children[0].click(); 301 | } 302 | currentElement = entryPoint.querySelector('ul'); 303 | break; 304 | } 305 | } 306 | 307 | // No matching entry point operation was found: Stop here 308 | if (!currentElement) { 309 | throw new Error('Could not find entry point'); 310 | } 311 | 312 | /* HTML structure (if properly rendered via clicks): 313 |
    314 |
  • (repeated for each field) 315 | Contents are either: 316 | - Nothing (just innerText) if it's a scalar node 317 | - fieldName: Type 318 |
      319 | (repeat) 320 |
    321 |
  • 322 |
323 | */ 324 | // Navigate to the correct leaf node 325 | for (let i = 0; i < currentElement.children.length && schemaPath[schemaPathIndex]; i++) { 326 | const element = currentElement.children[i]; // `li` element 327 | 328 | // Handle leaf nodes (scalar types) 329 | if (element.children.length === 0) { 330 | const fieldName = element.innerText.slice(0, element.innerText.indexOf(':')); 331 | // Compare the field name to the schema path 332 | if (fieldName === schemaPath[schemaPathIndex]) { 333 | schemaPathIndex++; // Not needed but here for clarity 334 | break; // Completed the traversal 335 | } else { 336 | continue; // Not a match, continue to next element 337 | } 338 | } 339 | 340 | // Handle field types (nested) 341 | const textContext = element.children[0].innerText; 342 | const fieldName = textContext.slice(0, textContext.indexOf(':')); 343 | // Compare the field name to the schema path 344 | if (fieldName === schemaPath[schemaPathIndex]) { 345 | // Only click if the children are not already rendered 346 | if (!element.children[1] || element.children[1].hidden) { 347 | element.children[0].click(); // Render the children (build the tree) 348 | } 349 | currentElement = element.children[1]; // Reassign to the `ul` element 350 | schemaPathIndex++; // Look for the next field in the schema path 351 | i = -1; // Reset index for next search 352 | } 353 | } 354 | 355 | // Style completed fields differently 356 | for (let i = 0; i < currentElement.children.length; i++) { 357 | const element = currentElement.children[i]; // `li` element 358 | const textContext = element.children[0] 359 | ? element.children[0].innerText 360 | : element.innerText; 361 | const fieldName = textContext.slice(0, textContext.indexOf(':')); 362 | if (typedFields.includes(fieldName)) { 363 | element.classList.add('typedField'); 364 | } else { 365 | element.classList.remove('typedField'); 366 | } 367 | 368 | // Close all open fields when at deepest level 369 | if (element.children[1]) { 370 | element.children[1].hidden = true; 371 | } 372 | } 373 | 374 | // Scroll to the element and have it at the top of the webview 375 | currentElement.parentNode.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" }); 376 | 377 | } 378 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const indentation = ' '; // TODO: Update based off the config (spacing/tab amount) 2 | export const primatives = [ 3 | 'String', 4 | 'Int', 5 | 'ID' 6 | ]; 7 | 8 | export const supportedSchemaParserFileTypes = ["graphql", "graphqls", "ts", "js"]; 9 | 10 | //! When adding new language support: 11 | // - Update `./lib/suggestions` -> `parseDocumentQuery()` 12 | // to look for the associated language id's multi-line 13 | // string character(s) and comment characters. 14 | export const supportedSuggestionFileTypeIds = ['javascript', 'typescript', 'javascriptreact', 'typescriptreact']; 15 | 16 | /* 🌊 Terms 🧠 */ 17 | // Query Operations - "query" or "mutation" keywords at the start of a query. 18 | // Query Field - The term for the property/key on the query. Ex: "name" or "id" 19 | // Query Scalar - The leaf (or end node) of a query. This is also a field. 20 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | /*--------------------------------------------------------- 3 | * Copyright (C) Microsoft Corporation. All rights reserved. 4 | *--------------------------------------------------------*/ 5 | 6 | import { type } from 'os'; 7 | import * as vscode from 'vscode'; 8 | import * as path from 'path'; 9 | import * as fs from 'fs'; 10 | import parser from "./parser"; 11 | import { 12 | offerSuggestions, parseDocumentQuery, fixBadHistoryFormatting, 13 | historyToObject, isolateCursor, getSuggestions, 14 | detectDelete, isolatedArraysFromObject 15 | } from "./lib/suggestions"; 16 | import { configToSchema, generateConfigFile } from './lib/config'; 17 | import { Schema, QueryEntry } from './lib/models'; 18 | import { supportedSuggestionFileTypeIds, supportedSchemaParserFileTypes } from './constants'; 19 | 20 | let schema: Schema; 21 | let queryEntry: QueryEntry; 22 | let schemaPaths: string[] = []; 23 | let enumArr: Array = []; 24 | let enumObj: any = {}; 25 | const webViewPanels: vscode.WebviewPanel[] = []; 26 | 27 | let disposable: vscode.Disposable; 28 | const showSchemaLoaded = statusMessageLimiter("Schema loaded"); 29 | 30 | // This function will only be executed when the extension is activated. 31 | export async function activate(context: vscode.ExtensionContext) { 32 | // At startup 33 | console.log('SurfQL is now active 🌊'); 34 | 35 | // Parse schema files that are referenced in the config file. 36 | const configResult = await configToSchema(); 37 | if (configResult) { // If it didn't error out in the process then assign the global values 38 | [ queryEntry, schema, schemaPaths, enumArr ] = configResult; 39 | enumObj = enumToObj(enumArr); 40 | 41 | // Display that the schema has been loaded. 42 | showSchemaLoaded("Schema loaded"); 43 | } 44 | 45 | // Automatically generate a config file template. 46 | const configCommand = vscode.commands.registerCommand( 47 | 'surfql.generateConfigFile', 48 | generateConfigFile 49 | ); 50 | 51 | // Creates a popup with a schema tree visualizer. 52 | const previewSchemaCommand = vscode.commands.registerCommand( 53 | "surfql.previewSchema", 54 | async () => { 55 | // If no schema path was found from a config file: Open a file selector 56 | if (schemaPaths.length === 0) { 57 | // Prompt user to select a schema file. 58 | const options: vscode.OpenDialogOptions = { 59 | canSelectMany: false, 60 | openLabel: "Open", 61 | filters: { 62 | "graphqlsFiles": supportedSchemaParserFileTypes, 63 | }, 64 | }; 65 | 66 | // Update the schema path. 67 | await vscode.window.showOpenDialog(options).then((fileUri) => { 68 | if (fileUri && fileUri[0]) { 69 | schemaPaths = [fileUri[0].fsPath]; 70 | } 71 | }); 72 | } 73 | for (const schemaPath of schemaPaths) { 74 | //create a new panel in webView 75 | const panel = vscode.window.createWebviewPanel( 76 | "Preview Schema", // viewType, internal use 77 | "Schema Preview", // Preview title in the tag 78 | vscode.ViewColumn.Beside, // where the new panel shows 79 | { 80 | enableScripts: true, 81 | } //option to add scripts 82 | ); 83 | 84 | // Get path to the preview.js script on disk 85 | const onDiskPath = vscode.Uri.file( 86 | path.join(context.extensionPath, "scripts", "preview.js") 87 | ); 88 | 89 | //toDo add stylesheet. 90 | const styleSheetPath = vscode.Uri.file( 91 | path.join(context.extensionPath, "stylesheet", "preview.css") 92 | ); 93 | 94 | const logoPath = vscode.Uri.file( 95 | path.join(context.extensionPath, "media", "icon.png") 96 | ); 97 | 98 | //add the previewjs to panel as a accessible Uri 99 | const scriptSrc = panel.webview.asWebviewUri(onDiskPath); 100 | const styleSrc = panel.webview.asWebviewUri(styleSheetPath); 101 | const logoScr = panel.webview.asWebviewUri(logoPath); 102 | 103 | //Add html content// 104 | panel.webview.html = getWebViewContent( 105 | scriptSrc.toString(), 106 | styleSrc.toString(), 107 | logoScr.toString() 108 | ); 109 | 110 | // Add event listener to the webview panel 111 | panel.webview.onDidReceiveMessage((message) => { 112 | // Load the schema structure into the visualizer 113 | if (message.command === "get schema text") { 114 | let schemaText = fs.readFileSync(schemaPath, "utf8"); 115 | const [objectArr, queryMutation, enumArr, inputArr, scalarArr, unionArr] = parser(schemaText); 116 | schema = arrToObj(objectArr); 117 | queryEntry = arrToObj(queryMutation); 118 | panel.webview.postMessage({ 119 | command: "sendSchemaInfo", 120 | text: JSON.stringify([objectArr, queryMutation, enumArr, inputArr, scalarArr, unionArr]), 121 | }); 122 | 123 | // Display that the schema has been loaded. 124 | showSchemaLoaded("Schema loaded"); 125 | } 126 | return; 127 | }); 128 | 129 | // Push the panel to the array of panels 130 | webViewPanels.push(panel); 131 | } 132 | } 133 | ); 134 | 135 | // Register command functionality to the user's VS Code application. 136 | context.subscriptions.push(previewSchemaCommand, configCommand); 137 | 138 | const hoverProvider: vscode.Disposable = vscode.languages.registerHoverProvider( 139 | supportedSuggestionFileTypeIds, 140 | { 141 | provideHover(document, position, token) { 142 | const range = document.getWordRangeAtPosition(position); 143 | const word = document.getText(range); 144 | if (enumObj[word]) { 145 | return new vscode.Hover({ 146 | language: "graphQL", 147 | value: `Enum Type, Choose from ${JSON.stringify(enumObj[word])}` 148 | }); 149 | } 150 | } 151 | } 152 | ); 153 | context.subscriptions.push(hoverProvider); 154 | 155 | /** 156 | * Event listener logic to respond to document changes 157 | */ 158 | vscode.workspace.onDidChangeTextDocument((e) => { 159 | // Exit early when no schema has been loaded. 160 | if (!schema) { 161 | console.log('Ignoring text events: No schema loaded'); 162 | return; 163 | } 164 | const activeEditor = vscode.window.activeTextEditor; 165 | // Exit early when no editor is active. 166 | if (!activeEditor) { 167 | console.log('Ignoring text events: No text editor open'); 168 | return; 169 | } 170 | 171 | // Dispose of the old suggestion. 172 | if (disposable) disposable.dispose(); 173 | 174 | const cursorPosition = activeEditor.selection.active; 175 | const cursorY: number = cursorPosition.line; 176 | let cursorX: number = cursorPosition.character; 177 | const currLine: string = e.document.lineAt(cursorY).text; 178 | 179 | // Fixes the cursor position with backspaces 180 | if (detectDelete(e)) cursorX -= 2; 181 | 182 | // Parse the document's current query into an array. 183 | const messyHistoryArray: string[] = parseDocumentQuery(cursorY, cursorX, e.document); 184 | // console.log('Original history array:', messyHistoryArray); 185 | // Stimulate spacing around brackets/parentheses for easier parsing. 186 | const formattedHistoryArray: string[] = fixBadHistoryFormatting(messyHistoryArray); 187 | // console.log('Formatted history array:', formattedHistoryArray); 188 | // Parse history array into an object. 189 | const historyObject = historyToObject(formattedHistoryArray); 190 | // console.log('COMPLETE SCHEMA:', historyObject); 191 | // Clean up the history object. 192 | historyObject.typedSchema = isolateCursor(historyObject.typedSchema); 193 | // console.log('ISOLATED SCHEMA:', historyObject); 194 | // Create suggestions based off of the history and schema. 195 | const suggestions = getSuggestions(historyObject, schema, queryEntry); 196 | // console.log('SUGGESTIONS:', suggestions); 197 | 198 | // Create the CompletionItems. 199 | disposable = vscode.languages.registerCompletionItemProvider( 200 | supportedSuggestionFileTypeIds, 201 | { 202 | provideCompletionItems() { 203 | return offerSuggestions(suggestions, currLine) as vscode.CompletionItem[]; 204 | } 205 | }, 206 | '\n' 207 | ); 208 | // Subscribe them to be popped up as suggestions. 209 | context.subscriptions.push(disposable); 210 | 211 | // Update the visualizer to follow the current schema. 212 | const historyData = isolatedArraysFromObject(historyObject) as [string[], string[]]; 213 | for (const panel of webViewPanels) { 214 | panel.webview.postMessage({ 215 | command: 'followCode', 216 | text: JSON.stringify(historyData) 217 | }); 218 | } 219 | 220 | // TODO: 221 | // - Clean up this file (move functions to separate files)! 222 | // - Establish a linter (air bnb?) 223 | // - Add cursor detection within args to auto suggest args instead of fields 224 | // - Create TypeScript types for all these functions 225 | 226 | }); 227 | 228 | /** 229 | * Event listener logic to reprocess the schema parser upon config file updates 230 | */ 231 | const configUpdateListener = vscode.workspace.onDidSaveTextDocument((document) => { 232 | vscode.workspace.findFiles('**/surfql.config.json', '**/node_modules/**', 1).then(async ([ uri ]: vscode.Uri[]) => { 233 | // Exit early when no config file was found. 234 | if (!uri) return; 235 | // Because the config file was updated - the schema should be reprocessed 236 | // and the global state should be updated. 237 | if (document.fileName === uri.fsPath) { 238 | // Parse schema files that are referenced in the config file. 239 | const configResult = await configToSchema(); 240 | if (configResult) { // If it didn't error out in the process then assign the global values 241 | [ queryEntry, schema, schemaPaths, enumArr ] = configResult; 242 | enumObj = enumToObj(enumArr); 243 | 244 | // Display that the schema has been loaded. 245 | showSchemaLoaded("Schema loaded"); 246 | } 247 | } 248 | }); 249 | }); 250 | context.subscriptions.push(configUpdateListener); 251 | }; 252 | 253 | 254 | //Initial preview html content 255 | const getWebViewContent = (scriptSrc: String, styleSrc: String, logoSrc: String) => { 256 | return ` 257 | 258 | 259 | 260 | 261 | PreviewSchema 262 | 263 | 264 | 265 | 266 | 267 | 270 | 271 | 272 | 278 |
279 | # 280 |

Schema Hierarchy

281 | 282 | 283 |
284 |
285 | 286 | `; 287 | }; 288 | 289 | // this method is called when your extension is deactivated 290 | export function deactivate() {} 291 | 292 | //modify the returned schemaObj 293 | function enumToObj(arr: Array | null) { 294 | //loop through obj, for all valueObj, check if valueObj.key exist in obj. 295 | //if so, valueObj.key = obj.key, then call modifyObj on valueObj 296 | const enumObj = {}; 297 | arr.forEach(e => { 298 | enumObj[e.name] = e.value; 299 | }); 300 | return enumObj; 301 | }; 302 | 303 | export function arrToObj(arr: Array) { 304 | const result: any = {}; 305 | arr.forEach(el => { 306 | result[el.name] = el.fields; 307 | }); 308 | return result; 309 | } 310 | 311 | /** 312 | * A higher order function that prevents the same status bar item from being shown multiple times 313 | * @param message The message to be displayed in the status bar at the bottom of the VSCode window 314 | * @param duration The duration in milliseconds for which the status bar item should be shown 315 | * @returns A function that will show the status bar item if it's not already shown 316 | */ 317 | function statusMessageLimiter(message: string, duration: number = 5000): Function { 318 | // Create a new status bar item 319 | const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); 320 | 321 | // Set the text of the status bar item 322 | statusBarItem.text = message; 323 | 324 | let isStatusBarItemShown = false; 325 | 326 | // Only show the status bar item if it's not already shown 327 | return (): void => { 328 | if (!isStatusBarItemShown) { 329 | // Show the status bar item 330 | statusBarItem.show(); 331 | isStatusBarItemShown = true; 332 | 333 | // Hide the status bar item after the specified timeout 334 | setTimeout(() => { 335 | statusBarItem.hide(); 336 | isStatusBarItemShown = false; 337 | }, duration); 338 | } 339 | }; 340 | } 341 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | import { workspace, Uri, WorkspaceConfiguration, window, Disposable } from 'vscode'; 3 | import parser from '../parser'; 4 | import { arrToObj } from '../extension'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | /** 9 | * Searches the root directory of the user's workspace for a schema config file. 10 | * The config file is used to locate the correct schema files to parse. 11 | */ 12 | export async function configToSchema(): Promise<[any, any, string[], Array] | void> { 13 | // TODO: Checkout this documentation I found: 14 | // https://code.visualstudio.com/api/references/vscode-api#WorkspaceConfiguration 15 | // It looks like there is a cleaner, built-in way to do this. 16 | 17 | // Attempt to find the SurfQL config file within the user's workspace. 18 | const filepath: string | undefined = await workspace.findFiles('**/surfql.config.json', '**/node_modules/**', 1).then(([ uri ]: Uri[]) => { 19 | // When no file was found: 20 | if (!uri) { 21 | displayConfigPrompt(); // Prompt the user 22 | return; // Return undefined 23 | } 24 | // When a config file was found return the file path. 25 | return uri.path; 26 | }); 27 | 28 | // Exit early when there is was no SurfQL config file found. 29 | if (!filepath) { 30 | console.log('No config file found at extension startup'); 31 | return [undefined, undefined, [], []]; // Return nothing 32 | } 33 | 34 | // Parse the config file to determine where the schema file(s) are. 35 | const configText = fs.readFileSync(filepath, "utf8"); 36 | const config = JSON.parse(configText); 37 | const schemaPath = path.join(filepath, '../', config.schema); 38 | 39 | try { 40 | // Read the schema file and parse it into a usable object. 41 | const schemaText = fs.readFileSync(schemaPath, "utf8"); 42 | const [objectArr, queryMutation, enumArr, inputArr, scalarArr] = parser(schemaText); 43 | const queryEntry = arrToObj(queryMutation); 44 | const schemaObject = arrToObj(objectArr); 45 | return [queryEntry, schemaObject, [schemaPath], enumArr]; 46 | } catch { 47 | // Inform the user that the schema path in the config file is invalid. 48 | displayInvalidConfigPathPrompt(); 49 | // Nothing is returned. 50 | } 51 | } 52 | 53 | function displayConfigPrompt(): void { 54 | // TODO: Add a "Learn more" button that will send to a link with documentation 55 | // instructions for creating a surfql config file (with an example). 56 | 57 | // Do nothing when the user specified that they no longer want to see this popup. 58 | const surfqlConfig: WorkspaceConfiguration = workspace.getConfiguration(); 59 | if (surfqlConfig.get('surfql.displayConfigPopup') === false) return; 60 | 61 | // Prompt the user to inform them that they can generate a config file, since 62 | // no config file was found. 63 | window.showInformationMessage("No SurfQL config found. Would you like to generate one for this workspace?", 'Generate', 'Okay', 'Don\'t show again') 64 | .then((userChoice) => { 65 | // Do nothing when the prompt popup was closed. 66 | if (userChoice === undefined) return; 67 | 68 | // When the user interacted with the popup: Respond accordingly. 69 | if (userChoice === 'Generate') { 70 | generateConfigFile(); 71 | } else if (userChoice === 'Don\'t show again') { 72 | // The user doesn't want to be notified anymore. Adjust the extension 73 | // settings to disable this popup. 74 | // - The 'true' value updates this config setting globally so that the 75 | // user won't see this popup in any workspace. 76 | surfqlConfig.update('surfql.displayConfigPopup', false, true); 77 | } 78 | }); 79 | } 80 | 81 | function displayInvalidConfigPathPrompt(): void { 82 | // Do nothing when the user specified that they no longer want to see this popup. 83 | const surfqlConfig: WorkspaceConfiguration = workspace.getConfiguration(); 84 | if (surfqlConfig.get('surfql.displayInvalidConfigPathPopup') === false) return; 85 | 86 | // Inform the user that the schema path was invalid. 87 | window.showInformationMessage('Invalid schema path in the surfql.config.json', 'View file', 'Okay', 'Don\'t show again') 88 | .then((userChoice) => { 89 | // Do nothing when the prompt popup was closed. 90 | if (userChoice === undefined) return; 91 | 92 | // When the user interacted with the popup: Respond accordingly. 93 | if (userChoice === 'View file') { 94 | // Open the file so the user can manually update the schema path. 95 | workspace.openTextDocument(path.join(workspace.workspaceFolders[0].uri.fsPath, 'surfql.config.json')) 96 | .then((doc) => window.showTextDocument(doc)); 97 | } else if (userChoice === 'Don\'t show again') { 98 | // The user doesn't want to be notified anymore. Adjust the extension 99 | // settings to disable this popup. 100 | // - The 'true' value updates this config setting globally so that the 101 | // user won't see this popup in any workspace. 102 | surfqlConfig.update('surfql.displayInvalidConfigPathPopup', false, true); 103 | } 104 | }); 105 | } 106 | 107 | /** 108 | * Create a config file for the user automatically in the root directory 109 | */ 110 | export async function generateConfigFile(): Promise { 111 | // If the config file is already there then just open it instead of overwriting 112 | // its contents. Otherwise, generate a template config file. 113 | workspace.findFiles('**/surfql.config.json', '**/node_modules/**', 1).then(([ uri ]: Uri[]) => { 114 | if (uri) { 115 | // A SurfQL config file has been found. Let's open it for the user. 116 | workspace.openTextDocument(uri.fsPath) 117 | .then((doc) => { 118 | window.showTextDocument(doc); 119 | window.showInformationMessage('Opened the previously created SurfQL config. No changes were made.'); 120 | }); 121 | } 122 | else { 123 | // Generate a new config file since one hasn't been created in this directory. 124 | const defaultConfig = { 125 | schema: "./path-to-your-schema-file", 126 | serverLibrary: "Apollo Server" // Currently we only support parsing Apollo Server Libray. 127 | }; 128 | workspace.fs.writeFile( 129 | Uri.file(path.join(workspace.workspaceFolders[0].uri.fsPath, 'surfql.config.json')), 130 | Buffer.from(JSON.stringify(defaultConfig, null, 2)) 131 | ).then(() => { 132 | // After the file is created, open it so the user can manually update 133 | // the schema path to an actual schema file. 134 | workspace.openTextDocument(path.join(workspace.workspaceFolders[0].uri.fsPath, 'surfql.config.json')) 135 | .then((doc) => { 136 | window.showTextDocument(doc); 137 | window.showInformationMessage('The file was created in the root directory. Please update the default schema path within the surfql.config.json file.'); 138 | }); 139 | }); 140 | } 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /src/lib/models.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | [key: string]: SchemaType; 3 | } 4 | 5 | export interface SchemaType { 6 | [key: string]: Field; 7 | } 8 | 9 | interface Field { 10 | arguments: any; // TODO: Understand parameters to replace the 'any' 11 | returnType: string; 12 | } 13 | 14 | export interface QueryEntry { 15 | [key: string]: SchemaType | any; // TODO: Understand mutation to replace the 'any' 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/suggestions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable curly */ 2 | import { CompletionItem, CompletionItemKind, SnippetString, TextDocument, TextDocumentChangeEvent, MarkdownString } from 'vscode'; 3 | import { indentation } from '../constants'; 4 | import { Schema, QueryEntry, SchemaType } from './models'; 5 | 6 | /** 7 | * Navigates current branch and offers suggestions to VSCode Extension 8 | * @param branch Passes in current branch 9 | * @returns VSCode suggestions 10 | */ 11 | export function offerSuggestions(branch: SchemaType, currentLine: string): CompletionItem[] { 12 | 13 | let suggestions: CompletionItem[] = []; 14 | for (const key in branch) { 15 | let tempCompItem = new CompletionItem(`${key}: Option returned type here`, CompletionItemKind.Keyword); // What is displayed 16 | if (branch[key].arguments) { 17 | const insertText = buildArgSnippet(key, branch[key].arguments); 18 | tempCompItem.insertText = completionText(currentLine, insertText); 19 | } else { 20 | tempCompItem.insertText = completionText(currentLine, key); // What is added 21 | } 22 | // tempCompItem.command = { command: 'surfql.levelChecker', title: 'Re-trigger completions...', arguments: [e] }; 23 | //TRY to do popup 24 | tempCompItem.label = `${key}: ${branch[key].returnType}, args: ${branch[key].arguments ? branch[key].arguments.length : 'None'}`; 25 | tempCompItem.detail = `return type ${branch[key].returnType}`; 26 | suggestions.push(tempCompItem); 27 | } 28 | return suggestions; 29 | } 30 | 31 | const completionText = (currentLine: string, text: string): SnippetString => { 32 | const openBraceIndex = currentLine.lastIndexOf('{'); 33 | const closeBraceIndex = currentLine.lastIndexOf('}'); 34 | const newIndent = openBraceIndex !== -1 35 | && (openBraceIndex < closeBraceIndex); 36 | 37 | return (newIndent) 38 | ? new SnippetString('\n' + indentation + text + '${0}' + '\n') 39 | : new SnippetString(text + '${0}'); 40 | }; 41 | 42 | const buildArgSnippet = (key: string, argArr: Array) => { 43 | let text = `${key}(`; 44 | let selectionIndex = 1; // The index used to tab between autofilled sections to manually change 45 | argArr.forEach((e,i) => { 46 | if (e.defaultValue) { 47 | text += `${e.argName}: \${${selectionIndex++}:${e.defaultValue}}`; 48 | } else { 49 | text += `${e.argName}: \${${selectionIndex++}:${e.inputType}}`; 50 | } 51 | if (i < argArr.length -1) { 52 | text += ', '; 53 | }; 54 | }); 55 | text += ')'; 56 | return text; 57 | }; 58 | 59 | /** 60 | * Converts a history array to a history object. 61 | * @param historyArray Any array of strings representing a valid query 62 | * @returns A nested object that resembles the document's query 63 | */ 64 | export function historyToObject(historyArray: string[]) { 65 | const historyObj: any = { typedSchema: {} }; 66 | let newHistory = [...historyArray]; 67 | 68 | // Determine the operator. 69 | if (newHistory[0].toLowerCase() === 'query' || newHistory[0] === '{') { 70 | historyObj.operator = 'query'; 71 | } else if (newHistory[0].toLowerCase() === 'mutation') { 72 | historyObj.operator = 'mutation'; 73 | } else { 74 | console.log('Throwing error: Invalid query format'); 75 | throw new Error('Invalid query format'); 76 | } 77 | 78 | // Determine if there are outter arguments (always the case for valid mutations). 79 | if ((historyObj.operator === 'mutation') || (newHistory[0].toLowerCase() === 'query' && newHistory[2] === '(')) { 80 | const {inners, outters} = collapse(newHistory, '(', ')'); 81 | newHistory = outters; 82 | historyObj.typedSchema._args = parseArgs(inners); 83 | } 84 | 85 | // Recursively nest into the typed schema to build out the historyObj. 86 | traverseHistory(collapse(newHistory, '{', '}').inners, historyObj.typedSchema, historyObj); 87 | 88 | // Return the history object that was constructed from the history array. 89 | return historyObj; 90 | } 91 | 92 | /** 93 | * Takes in an array, removing everything between the first set of opening 94 | * and closing characters. The enclosed area (inners) and surrounding area 95 | * (outters) are returned as well as the modification count (skipped). 96 | * @param arr Any array of strings (history array) 97 | * @param openingChar Any character: '{', '(', etc... 98 | * @param closingChar Any character: '}', ')', etc... 99 | * @returns {inners, outters, skipped} 100 | */ 101 | function collapse(arr: string[], openingChar: string, closingChar: string) { 102 | const outters: string[] = []; // The contents outside the opening/closing chars 103 | const inners: string[] = []; // The contents within the opening/closing chars 104 | let state: number = 0; // 0 when outside (outters); >= 1 when inside (inners) 105 | let skipped: number = 0; // Tracks how many words were added to inners 106 | let initialized: boolean = false; // Helps determine when the encapsulation is finished 107 | let finished: boolean = false; // When finished the rest of the words are added to outters 108 | for (const word of arr) { 109 | if (finished) { 110 | // Checking to see if the encapsulation (collapse) has finished. 111 | outters.push(word); // When collapse is finished add the rest to outters 112 | } else if (word === openingChar) { 113 | // Checking to see if the current word matches the opening char. 114 | initialized = true; // Initialize search (may repeat which is okay) 115 | if (state) inners.push(word); // If this is nested within the encapsulation then include it in the inners 116 | state++; // Increment the state: We are nested 1 level deeper now 117 | skipped++; // Increment the skipped count 118 | } else if (word === closingChar) { 119 | // Checking to see if the current word matches the closing char. 120 | state--; // Decrement the state: We are 1 level less nested now 121 | if (state) inners.push(word); // If this is nested within the encapsulation then include it in the inners 122 | skipped++; // Increment the skipped count 123 | if (initialized && state <= 0) finished = true; // Check for completion 124 | } else { 125 | // Otherwise add the current word to its respective array. 126 | if (state) { 127 | inners.push(word); 128 | skipped++; 129 | } else { 130 | outters.push(word); 131 | } 132 | } 133 | } 134 | return { outters, inners, skipped }; 135 | } 136 | 137 | /** 138 | * Parses an array of strings into an object with argument data. 139 | * @param inners Inners captured from the collapse() method integrate well 140 | * @returns An object resembling key/value pairs of Apollo GraphQL arguments 141 | */ 142 | function parseArgs(inners: string[]) { 143 | // TODO: Convert any type from a string to its intended type for type testing 144 | // - Example: "3" -> 3 (number) 145 | // - Example: "[1," "2," "3]" -> [1, 2, 3] (array/nested) 146 | // - Example: "{" "int:" "3" "}" -> {int: 3} (object/nested) 147 | // - etc... 148 | const args: any = {}; 149 | // Iterate through the argument inners. 150 | for (let i = 0; i < inners.length; i++) { 151 | // Declare the current and next values for convenience. 152 | const current: string = inners[i]; 153 | const next: string = inners[i + 1]; 154 | // The current key/value involves an object. 155 | if (next === '{') { 156 | // Leverage 'skipped' from collapse() to ignore the object details. 157 | const { skipped } = collapse(inners.slice(i), '{', '}'); 158 | i += skipped; 159 | args[current.slice(0, -1)] = {}; // Assign an empty object as a indicator 160 | } 161 | // The current key/value does not involve an object. 162 | else { 163 | // Slice off the ':' and add the key/value pair to obj 164 | args[current.slice(0, -1)] = next.replace(/,$/, ''); // Also ignore trailing commas 165 | i++; // Increment i an extra time here to move to the next 2 key/values. 166 | } 167 | } 168 | return args; 169 | } 170 | 171 | /** 172 | * Traverses a history array to recursively build out the object array. 173 | * @param historyRef The current section of history that needs to be parsed 174 | * @param obj The portion of the history object that is being built 175 | * @param entireHistoryObj The entire history object used to reference root-level properties 176 | */ 177 | function traverseHistory(historyRef: string[], obj: any, entireHistoryObj: any): void { 178 | // Do not mutate the original history to keep this function pure. 179 | let history: string[] = [...historyRef]; 180 | 181 | // Check to see what follows the field to see what type it is (nested?) 182 | for (let i = 0; i < history.length; i++) { 183 | let current = history[i]; 184 | let next = history[i + 1]; 185 | let newObj: string | any = {}; 186 | 187 | // The cursor is at this level. 188 | if (current === '🐭') { 189 | // TODO: Invoke a helper function here that looks to the left and right to see if the cursor 🐭 is within parens (params). I think we would need to also keep track of the word before the opening paren as well as the rest of the contents (besides the 🐭). What to do with this data? I'm not sure yet. I guess just set `obj._paramSuggestion = true` and then when the schema is aligned with the history object later it can work that out to generate accurate param suggestions from the schema? 190 | entireHistoryObj.cursor = obj; // TODO: Remove this? Quick access to the cursor object has never been leveraged. 191 | obj._cursor = true; // ⭐️ Signify that the cursor was found at this level 192 | continue; // Increment i and iterate the loop 193 | } 194 | 195 | obj[current] = newObj; // Default to expect a nested field 196 | 197 | // Check for arguments: 198 | if (next === '(') { 199 | const { inners, skipped } = collapse(history.slice(i), '(', ')'); 200 | const args = parseArgs(inners); // Parse the arguments 201 | newObj._args = args; // Assign the arguments to the new object 202 | i += skipped; // Skip the rest of the argument inners 203 | current = history[i]; // Reassign 'current' for the following if block 204 | next = history[i + 1]; // Reassign 'next' for the following if block 205 | } 206 | // Check for a bracket signifying a nested field: 207 | if (next === '{') { 208 | const { inners, skipped } = collapse(history.slice(i), '{', '}'); 209 | // Skip what was found within the nested field and continue to process 210 | // other fields at this level. 211 | i += skipped; 212 | // Recurse to process the nested field that was skipped. 213 | traverseHistory(inners, newObj, entireHistoryObj); 214 | } 215 | // If the field was not nested: Add the scalar's property. 216 | else { 217 | obj[current] = 'Scalar'; 218 | } 219 | } 220 | } 221 | 222 | /** 223 | * Removed all fields that do not lead up to / surround the cursor. 224 | * @param history The history object 225 | * @returns A smaller history object 226 | */ 227 | export function isolateCursor(history) { 228 | // Break case: the cursor is found 229 | if (history._cursor) { 230 | // Flattens other side paths 231 | return Object.entries(history).reduce((obj, [key, value]) => { 232 | if (key === '_args') obj[key] = value; 233 | else if (typeof value === 'object') obj[key] = 'Field'; 234 | else if (key === '_cursor') obj[key] = true; 235 | else obj[key] = 'Scalar'; 236 | return obj; 237 | }, {}); 238 | } 239 | 240 | // Recurse case: Nest until the cursor is found 241 | for (const field in history) { 242 | if (typeof history[field] === 'object') { 243 | const traverse = isolateCursor(history[field]); 244 | if (traverse) return { [field]: traverse }; 245 | } 246 | } 247 | } 248 | 249 | /** 250 | * Compares the history with the schema to make field suggestions. 251 | * @param history The history object 252 | * @param schema The schema object 253 | * @param queryEntry The query entry object 254 | * @returns An object containing suggestion data 255 | */ 256 | export function getSuggestions(history: any, schema: any, queryEntry: any) { 257 | // Get the right casing for the operator 258 | for (const entry in queryEntry) { 259 | if (entry.toLowerCase() === history.operator) { 260 | history.operator = entry; // Reassign the operator with the correct case 261 | break; // Exit: The correct operator was found and updated 262 | } 263 | } 264 | 265 | // Exit early when there is no entry point (operator) 266 | const entryPoint = queryEntry[history.operator]; 267 | if (!entryPoint) { 268 | console.log('Invalid query entry. Check the schema for entry points.'); 269 | return {}; 270 | } 271 | 272 | // If the cursor is at the outter-most level then return those outter-most 273 | // fields as suggestions. 274 | const typedHistory = history.typedSchema; 275 | if (typedHistory._cursor) { 276 | return filterOutUsedFields(typedHistory, entryPoint); // suggestions 277 | } 278 | 279 | // Exit early when there is no more history. 280 | const nestedHistory = Object.keys(typedHistory)[0]; 281 | if (!nestedHistory) return {}; 282 | 283 | // Traverse the rest of the way. 284 | const returnType = entryPoint[nestedHistory].returnType; 285 | return traverseSchema(typedHistory[nestedHistory], schema, returnType); 286 | } 287 | 288 | /** 289 | * Traverses through the history and schema to find the fields surrounding 290 | * the cursor. Suggestions will be created based off of the remaining unused 291 | * fields. 292 | * @param history The history object 293 | * @param schema The schema object 294 | * @param returnType The current field within the schema 295 | * @returns 296 | */ 297 | function traverseSchema(history: any, schema: any, returnType: string) { 298 | // Exit early: End of history/schema. 299 | if (!history || !returnType) return {}; 300 | 301 | // If the cursor depth was found: 302 | if (history._cursor) { 303 | // Convert the unused fields to suggestion objects. 304 | return filterOutUsedFields(history, schema[returnType]); 305 | } 306 | 307 | // Break early when there is no more history to traverse through. 308 | const nestedHistory: string = Object.keys(history)[0]; 309 | if (!nestedHistory) return {}; 310 | 311 | // Otherwise traverse to find the fields at a deeper level. 312 | const nestedReturnType: string = schema[returnType][nestedHistory].returnType; 313 | return traverseSchema(history[nestedHistory], schema, nestedReturnType); 314 | } 315 | 316 | /** 317 | * Compares the history with the schema to only return unused fields for 318 | * suggestions. 319 | * @param history The history object 320 | * @param schema The schema object 321 | * @returns Suggestion objects 322 | */ 323 | function filterOutUsedFields(history: any, schema: any) { 324 | const suggestion: any = {}; 325 | const historyFields: string[] = Object.keys(history); 326 | // Look through all the possible fields at this level. 327 | for (const [key, value] of Object.entries(schema)) { 328 | const valueWithType: any = value; // A typescript lint fix 329 | // If the schema field hasn't been typed yet: 330 | if (!historyFields.includes(key)) { 331 | // Add it as a suggestion. 332 | suggestion[key] = { 333 | arguments: valueWithType.arguments, 334 | returnType: valueWithType.returnType 335 | }; 336 | } 337 | } 338 | return suggestion; // Return all the suggestion objects 339 | } 340 | 341 | /** 342 | * Parses the document returning an array of words/symbols. 343 | * However it will exit early if it cannot find the start/end of a query near the cursor. 344 | * @param cursorY The line number the cursor is currently located. 345 | * @param cursorX The column number the cursor is currently located. 346 | * @param document The document nested inside a vscode event. 347 | * @return Words/symbols from the start of the query to the cursor. 348 | */ 349 | export function parseDocumentQuery(cursorY: number, cursorX: number, document: TextDocument): string[] { 350 | // TODO: Update from backtick to a dynamic query start/end character depending on the language. (So far only JS is supported.) 351 | // Find the start of the query. 352 | let messyHistory: string[] = findBackTick([], -1, 1000, document, cursorY, cursorX).reverse(); 353 | // Indicate the cursor (mouse) location. 354 | messyHistory.push('🐭'); 355 | // Find the end of the query. 356 | messyHistory = findBackTick(messyHistory, 1, 1000, document, cursorY, cursorX); 357 | // Merge the words between the mouse and move it after. 358 | mouseInjectionFix(messyHistory); 359 | // Filter out the empty strings from the query array. 360 | messyHistory = messyHistory.filter((str) => str); 361 | // Return 362 | return messyHistory; 363 | } 364 | 365 | /** 366 | * Merges the words between the mouse and relocates the mouse position to the next index. The remaining index is removed. 367 | * @param messyHistory The current history that will be appended to. 368 | */ 369 | function mouseInjectionFix(messyHistory: string[]) { 370 | for (let i = 0; i < messyHistory.length; i++) { 371 | if (messyHistory[i] === '🐭') { 372 | // Only merge if there is a word before and after the mouse. 373 | if ((messyHistory[i - 1] !== '{' && 374 | messyHistory[i + 1] !== '}') && 375 | messyHistory[i + 1] !== ' ') { 376 | // Merge 377 | messyHistory[i - 1] = messyHistory[i - 1] + messyHistory[i + 1]; 378 | // Remove the next index location 379 | messyHistory.splice(i + 1, 1); 380 | } 381 | } 382 | } 383 | } 384 | 385 | /** 386 | * Appends all characters between the cursor and a backtick. 387 | * @param history The current history that will be appended to. 388 | * @param direction 1 or -1 depending on the direction (positive moves down the page). 389 | * @param limit Limits amount of lines to process / characters on one line to process. 390 | * @param document The file we will be reading from to find the query. 391 | * @param cursorY The line number the cursor is currently located. 392 | * @param cursorX The column number the cursor is currently located. 393 | * @returns An array of words/characters. 394 | */ 395 | function findBackTick(history: string[], direction: 1 | -1, limit: number, document: TextDocument, lineNumber: number, cursorLocation: number): string[] { 396 | const newHistory = []; 397 | let line: string = document.lineAt(lineNumber).text; 398 | const reverse: boolean = direction === -1; 399 | // The slice will depend on the 'direction' parameter. 400 | // - Ignore everything before/after the cursor 401 | line = reverse 402 | ? line.slice(0, cursorLocation + 1) 403 | : line.slice(cursorLocation + 1); 404 | 405 | // Helper function to update the line number and line. 406 | const updateLine = () => { 407 | // Increment in the correct direction 408 | lineNumber += direction; 409 | if (lineNumber >= 0) { 410 | line = document.lineAt(lineNumber).text; 411 | // If we hit the end of our file exit early 412 | if (lineNumber === document.lineCount) { 413 | console.log('Hit EOF without finding the backtick. Direction:', direction); 414 | return []; 415 | } 416 | } 417 | }; 418 | 419 | const commentRegex: RegExp = /^\s*(\/\/.*)\s*$/; // Example: query { # This query will return all the users 420 | // Helper function to remove commented code. 421 | const removeComments = () => { 422 | if (commentRegex.test(line)) { 423 | // Ignore the comment portion of the line. 424 | line = line.slice(0, line.indexOf('#') - 1); 425 | } 426 | }; 427 | 428 | // Create an array of words / characters found in queries. 429 | // Iterate through the lines of the file (starting from the cursor moving up the file) 430 | while (lineNumber >= 0 && newHistory.length <= limit) { 431 | // When the start of the query was found: This is the last loop 432 | if (line.includes('`')) { 433 | lineNumber = -2; // Set line number to -2 to end the loop (-1 doesn't work and we still want to continue the rest of this logic) 434 | // Slice at the backtick 435 | const backTickIndex = line.indexOf('`'); 436 | // The slice will depend on the 'direction' parameter. 437 | // - Ignore everything before/after the back tick 438 | line = reverse 439 | ? line.slice(backTickIndex + 1) 440 | : line.slice(0, backTickIndex); 441 | } 442 | 443 | // Detect if the file is compressed into a one-line file. 444 | // Exit early if the line is 1000+ characters (the limit). 445 | if (line.length > limit) { 446 | console.log('Line has over', limit, 'characters. Limit reached for parsing.'); 447 | return []; 448 | } 449 | 450 | // Remove commented code. 451 | removeComments(); 452 | 453 | // Parse the line: 454 | // Divide the line (string) into an array of words. 455 | const arrayOfWords = line.split(/\s+/g); 456 | // Depending on the direction, reverse the array. 457 | if (reverse) arrayOfWords.reverse(); 458 | // Append the array of words to the new history. 459 | newHistory.push(...arrayOfWords); 460 | // Continue to the next line 461 | updateLine(); 462 | } 463 | 464 | // The appending location will depend on the 'direction' parameter. 465 | return reverse 466 | ? [...newHistory, ...history] 467 | : [...history, ...newHistory]; 468 | } 469 | 470 | /** 471 | * Fixes cases where the words within the array are attached to the brackets/parentheses. 472 | * @param messyHistory 473 | * @return An array of words with the brackets and parentheses detached. 474 | */ 475 | export function fixBadHistoryFormatting(messyHistory: string[]): string[] { 476 | return messyHistory.reduce((relevant: string[], word: string) => { 477 | let reformedWord = ''; // Will hold the words as they are re-formed 478 | for (const char of word) { 479 | if (/{|}|\(|\)/.test(char)) { // Test if char is '{', '}', '(', or ')' 480 | if (reformedWord) { 481 | relevant.push(reformedWord); // If a word is already formed then push that as its own word 482 | reformedWord = ''; // Reset the word 483 | } 484 | relevant.push(char); // Add the '{', '}', '(', or ')' 485 | } else { 486 | reformedWord += char; // Keep building upon the current word 487 | } 488 | } 489 | if (reformedWord) { 490 | relevant.push(reformedWord); // Before moving on, check to see if there is a word that needs to get added 491 | } 492 | return relevant; // Return the total words so far 493 | }, [] as string[]); 494 | } 495 | 496 | /** 497 | * Uses the document change event to detect new text 498 | * @param e The event from the document change event listener 499 | * @return The last text updates to the document 500 | */ 501 | function textUpdates(e: TextDocumentChangeEvent): string { 502 | const lastChange = e.contentChanges[e.contentChanges.length - 1]; 503 | return lastChange.text; 504 | } 505 | 506 | /** 507 | * Determines if the last document change was a deletion 508 | * @param e The event from the document change event listener 509 | * @returns Whether the last document update was a deletion 510 | */ 511 | export function detectDelete(e: TextDocumentChangeEvent): boolean { 512 | // When the text update is an empty string that signifies that the last operation performed on the document was a deletion. 513 | return textUpdates(e) === ''; 514 | } 515 | 516 | /** 517 | * Flattens the history object into an array of strings. 518 | * @param historyObject An object representing a schema being typed in the user's document. 519 | * @returns [historyArray, typedFields] 520 | */ 521 | export function isolatedArraysFromObject(historyObject: any): [string[], string[]] { 522 | const historyArray: string[] = []; 523 | // This is the entry point 524 | historyArray.push(historyObject.operator); 525 | // Recurse through the rest 526 | const traverse = (obj: any) => { 527 | // When you hit the end return the fields that have already been typed on the same nested level. 528 | if (obj._cursor) { 529 | return Object 530 | .keys(obj) 531 | .filter(([firstChar]) => firstChar !== '_') as string[]; 532 | } 533 | const nextTraversal: string = Object.keys(obj)[0]; 534 | historyArray.push(nextTraversal); 535 | return traverse(obj[nextTraversal]); 536 | }; 537 | const typedFields: string[] = traverse(historyObject.typedSchema); 538 | // Complete 539 | return [historyArray, typedFields]; 540 | } 541 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | 2 | class Root { //Class for regular, query, mutation, interface type 3 | name: string; 4 | fields: any; 5 | interface: string | null; 6 | constructor(val: string, interfaceVal: string | null) { 7 | this.name = val; 8 | this.fields = {}; 9 | this.interface = interfaceVal; 10 | } 11 | }; 12 | 13 | class Enum { //class for enumeration type 14 | name: string; 15 | value: string[]; 16 | constructor(val: string) { 17 | this.name = val; 18 | this.value = []; 19 | } 20 | }; 21 | 22 | class FieldInfo { //class for the field of the Root class 23 | returnType: string; 24 | arguments: any; 25 | constructor(type: string, argArr: null | Array) { 26 | this.returnType = type; 27 | this.arguments = argArr; 28 | }; 29 | } 30 | 31 | class SingleArg { //class for the argument of rht field class 32 | argName: string; 33 | inputType: string; 34 | defaultValue: string; 35 | constructor(name: string, type: string, defaultt: string | null) { 36 | this.argName = name; 37 | this.inputType = type; 38 | this.defaultValue= defaultt; 39 | } 40 | }; 41 | 42 | class Input { //class for input type 43 | name: string; 44 | fields: any; 45 | constructor(val: string) { 46 | this.name = val; 47 | this.fields = {}; 48 | } 49 | }; 50 | 51 | class Union { 52 | name: String; 53 | options: Array; 54 | constructor(val: String, optionArray: Array) { 55 | this.name = val; 56 | this.options = optionArray; 57 | } 58 | } 59 | 60 | //build root Object, nameBuilder works for type and interface 61 | function nameBuilder(string: string): [string, string | null] { 62 | const cleanstr = string.trim(); 63 | if (cleanstr.includes(" ")) { 64 | const [variable, mid, interfaceVal] = cleanstr.split(" "); 65 | return [variable, interfaceVal]; 66 | } else { 67 | let variable = cleanstr; 68 | return [variable, null]; 69 | } 70 | } 71 | 72 | //FieldBuilder for root Object, use the function to build field and return array of [variable, current ending+1] 73 | function fieldBuilder(string: string): Array { 74 | //determine whether it has a argument/it is mutation type 75 | if (string.indexOf("(") > -1) { 76 | // it may be a resolver function that contains '(' and ')' 77 | let resArr = string.split("("); 78 | const fieldName = `${resArr[0].trim()}`; 79 | //split again by closing ) and save the second part 80 | const lastIndex = string.lastIndexOf(":"); 81 | const typeInfo = `${string.slice(lastIndex + 1)}`; 82 | //grab the argument text and parse it 83 | const totalArgtext = resArr[1].split(')')[0].trim(); 84 | const argArr = buildArgArr(totalArgtext); 85 | return [fieldName, typeInfo, argArr]; 86 | } else { 87 | // it's a regular type field 88 | const arr = string.split(":"); 89 | if (arr.length === 2) { 90 | const fieldName = arr[0].trim(); 91 | const typeInfo = arr[1].trim(); 92 | return [fieldName, typeInfo, null]; 93 | } else { 94 | return [undefined, undefined, undefined]; 95 | } 96 | } 97 | } 98 | 99 | //helper function to build argsArr from argText 100 | function buildArgArr(totalArg: string): Array { 101 | const result = []; 102 | let argName = ""; 103 | let returnType = ""; 104 | let defaultt = ""; 105 | let parsingType = false; 106 | let parsingDefault = false; 107 | for (let i = 0; i < totalArg.length; i++) { 108 | if (totalArg[i] === ":") { 109 | parsingType = true; 110 | } else if (totalArg[i] === "=") { 111 | parsingDefault = true; 112 | parsingType = false; 113 | } else if (totalArg[i] === ",") { 114 | const newArg = new SingleArg(argName, returnType, defaultt.length === 0 ? null: defaultt); 115 | result.push(newArg); 116 | parsingDefault = false; 117 | parsingType = false; 118 | argName = ""; 119 | returnType = ""; 120 | defaultt = ""; 121 | } else { 122 | if (totalArg[i] !== " ") { 123 | if (parsingType) { 124 | returnType += totalArg[i]; 125 | } else if (parsingDefault) { 126 | defaultt += totalArg[i]; 127 | } else { 128 | argName += totalArg[i]; 129 | } 130 | } 131 | } 132 | } 133 | const newArg = new SingleArg(argName, returnType, defaultt.length === 0 ? null: defaultt); 134 | result.push(newArg); 135 | parsingDefault = false; 136 | parsingType = false; 137 | return result; 138 | } 139 | 140 | //fieldbuilder for Input type fields. 141 | function inputFieldBuilder(string: string) { 142 | const arr = string.split(":"); 143 | if (arr.length === 2) { 144 | const variable = arr[0].trim(); 145 | const typeInfo = arr[1].trim(); 146 | return [variable, typeInfo]; 147 | } 148 | }; 149 | 150 | //helper function to parsing returned Type for the field, cleaning up the bracket. 151 | function parsingTypeInfo(string: string) { 152 | //remove [ ] or ! if any 153 | const cleanStr = string.trim(); 154 | let parsedType = ""; 155 | let i = 0; 156 | if (cleanStr[0] === "[") { 157 | //means it is a defined type 158 | i = 1; 159 | } 160 | while ( 161 | i < cleanStr.length && 162 | cleanStr[i] !== "]" && 163 | cleanStr[i] !== "!" && 164 | cleanStr[i] !== " " 165 | ) { 166 | parsedType += cleanStr[i++]; 167 | } 168 | return parsedType; 169 | }; 170 | 171 | export default function parser(text: string) { 172 | //declare schema types 173 | const schema = []; 174 | //declare root array to story the root queries 175 | const root: Array = []; 176 | //declare query type and mutation type 177 | const queryMutation: Array = []; 178 | //declare a enum array 179 | const enumArr: Array = []; 180 | //declare a input array 181 | const inputArr: Array = []; 182 | //declare a scale array 183 | const scalarArr: Array = []; 184 | //declare a union array 185 | const unionArr: Array = []; 186 | 187 | //build up the constants 188 | const typeIndex = 4; 189 | const inputIndex = 5; 190 | const interfaceIndex = 9; 191 | const enumIndex = 4; 192 | const scalarIndex = 6; 193 | const unionIndex = 5; 194 | 195 | //declare status for parsing type, interface input 196 | let parsing = false; 197 | //declare status for checking parsing Enum 198 | let parsingEnum = false; 199 | //declare status for checking parsing Input 200 | let parsingInput = false; 201 | //declare status for checking parsing Scalar 202 | let parsingScalar = false; 203 | //declare status for checking parsing Union 204 | let parsingUnion = false; 205 | 206 | 207 | let currentArr = 'root'; 208 | //when parsing initialized, build the right Object and push to the right array 209 | function typeSlicer(strEnd: number, cleanline: string) { 210 | // const [variable, interfaceVal] = nameBuilder(cleanline.slice(strEnd)); 211 | if (parsing) { 212 | const [variable, interfaceVal] = nameBuilder(cleanline.slice(strEnd)); 213 | const newRoot: Root = new Root(variable, interfaceVal); 214 | if (variable.toLowerCase() === 'query') { 215 | queryMutation.push(newRoot); 216 | currentArr = 'queryMutation'; 217 | } else if (variable.toLowerCase() === 'mutation') { 218 | queryMutation.push(newRoot); 219 | currentArr = 'queryMutation'; 220 | } else { 221 | root.push(newRoot); 222 | currentArr = 'root'; 223 | } 224 | } else if (parsingEnum) { 225 | const [variable, interfaceVal] = nameBuilder(cleanline.slice(strEnd)); 226 | const newEnum: Enum = new Enum(variable); 227 | enumArr.push(newEnum); 228 | currentArr = 'enum'; 229 | } else if (parsingInput) { 230 | const [variable, interfaceVal] = nameBuilder(cleanline.slice(strEnd)); 231 | const newInput: Input = new Input(variable); 232 | inputArr.push(newInput); 233 | currentArr = 'input'; 234 | } else if (parsingScalar) { 235 | const [variable, interfaceVal] = nameBuilder(cleanline.slice(strEnd)); 236 | scalarArr.push(variable); 237 | parsingScalar = false; 238 | } else if (parsingUnion) { 239 | console.log('union line', cleanline.slice(strEnd)); 240 | const [unionName, optionArray] = unionCreator(cleanline.slice(strEnd)); 241 | const newUnion = new Union(unionName.trim(), optionArray); 242 | unionArr.push(newUnion); 243 | parsingUnion = false; 244 | } 245 | } 246 | 247 | function unionCreator(str: String): [String, Array] { 248 | const [unionName, options] = str.replace(' ', '').split('='); 249 | const optionArray = options.split('|').map(el => el.trim()); 250 | return [unionName, optionArray]; 251 | } 252 | 253 | //start parsing--->// 254 | const arr = text.split(/\r?\n/); 255 | //read through line by line, conditional check 256 | let curRoot: string = ""; 257 | arr.forEach((line) => { 258 | const cleanline1 = line.trim(); 259 | const cleanline = cleanline1.split('//')[0]; 260 | if(cleanline[0] === "#" || cleanline[0] === "/"){ 261 | //do nothing 262 | }; 263 | //check what type it is parsing now 264 | if (parsingEnum) { 265 | if (cleanline[0] === "}") { 266 | parsingEnum = false; 267 | } else if (cleanline.trim().length === 0) { 268 | //do nothing 269 | } else { 270 | if (currentArr === 'enum') { 271 | enumArr[enumArr.length - 1].value.push(cleanline); 272 | } 273 | } 274 | }; 275 | if (parsingInput) { 276 | if (cleanline[0] === "}") { 277 | parsingInput = false; 278 | } else if (cleanline.trim().length === 0) { 279 | //do nothing 280 | } else { 281 | const [variable, typeInfo] = inputFieldBuilder(cleanline); 282 | if (variable && typeInfo) { 283 | inputArr[inputArr.length - 1].fields[variable] = parsingTypeInfo(typeInfo); 284 | } 285 | } 286 | }; 287 | if (parsing) { //parsing query, mutation, interface, or regular type 288 | if (cleanline[0] === "}") { 289 | parsing = false; 290 | } else if (cleanline.trim().length === 0) { 291 | //do nothing 292 | } else { 293 | const [fieldName, typeInfo, argArr] = fieldBuilder(cleanline); 294 | if (fieldName && typeInfo) { 295 | const parsedType = parsingTypeInfo(typeInfo); 296 | const newField = new FieldInfo(parsedType, argArr); 297 | if (currentArr === 'queryMutation') { 298 | queryMutation[queryMutation.length - 1].fields[fieldName] = newField; 299 | } else { 300 | root[root.length - 1].fields[fieldName] = newField; 301 | } 302 | } 303 | } 304 | } else { //looking for the special initiator keywords 305 | if (cleanline.slice(0, typeIndex) === "type") { 306 | parsing = true; 307 | typeSlicer(typeIndex, cleanline); 308 | } else if (cleanline.slice(0, inputIndex) === "input") { 309 | parsingInput = true; 310 | typeSlicer(inputIndex, cleanline); 311 | } else if (cleanline.slice(0, interfaceIndex) === "interface") { 312 | parsing = true; 313 | typeSlicer(interfaceIndex, cleanline); 314 | } else if (cleanline.slice(0, enumIndex) === "enum") { 315 | parsingEnum = true; 316 | typeSlicer(enumIndex, cleanline); 317 | } else if (cleanline.slice(0, scalarIndex) === "scalar") { 318 | parsingScalar = true; 319 | typeSlicer(scalarIndex, cleanline); 320 | } else if (cleanline.slice(0, unionIndex) === "union") { 321 | console.log(cleanline); 322 | parsingUnion = true; 323 | typeSlicer(unionIndex, cleanline); 324 | } 325 | }; 326 | }); 327 | 328 | return [root, queryMutation, enumArr, inputArr, scalarArr, unionArr]; 329 | }; 330 | -------------------------------------------------------------------------------- /src/test/parser.test.js: -------------------------------------------------------------------------------- 1 | const parser = require('../parser'); 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | 6 | describe('parser module', () => { 7 | let parserData; 8 | beforeAll(() => { 9 | parserData = parser(fs.readFileSync(path.resolve(__dirname, './testingAsset/starWar.ts'))); 10 | }); 11 | 12 | test('parsing data successfully', () => { 13 | expect(parserData.length.toBe(6)); 14 | expect(typeof parserData).toEqual('array'); 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/testingAsset/starWar.ts: -------------------------------------------------------------------------------- 1 | const { gql } = require("apollo-server"); 2 | 3 | // // typeDefs is a required argument and should be a GraphQL schema language string or array of GraphQL schema language strings or a function that takes no arguments and returns an array of GraphQL schema language strings. The order of the strings in the array is not important, but it must include a schema definition. 4 | 5 | const typeDefs = gql` 6 | type Query { 7 | heros: [Character]! 8 | hero(id: ID): Character! 9 | planets(id: ID): Planet 10 | starships: [Starship!]! 11 | human: [Human!]! 12 | droid: [Droid!]! 13 | } 14 | 15 | type Character { 16 | id: Int! 17 | name: String! 18 | birthYear: String! 19 | eyeColor: String 20 | films: [Film]! 21 | gender: String 22 | hairColor: String! 23 | height: Int 24 | homeworld: Planet 25 | skinColor: String 26 | species: Specie 27 | starships: [Starship]! 28 | vehicles: [Vehicle]! 29 | } 30 | 31 | union Transportationtool = Starship | Vehicle 32 | 33 | type Film { 34 | id: Int! 35 | releaseDate: Date! 36 | esipodeId 37 | title: String! 38 | characters: [Character!]! 39 | director: String! 40 | planets: [Planet!]! 41 | producer: String 42 | species: [Specie]! 43 | starships: [Starship]! 44 | vehicles: [Vehicle]! 45 | } 46 | 47 | type Planet { 48 | id: Int! 49 | name: String! 50 | climate: [Climate] 51 | diameter(unit: LengthUnit = KILOMETER): Int! 52 | films: [Film]! 53 | gravity: String 54 | population: Int! 55 | residents: [Character!]! 56 | rotationPeriod: Int! 57 | species: [Specie!]! 58 | surfaceWater: Int 59 | terrain: [Terrain] 60 | } 61 | 62 | enum Terrain { 63 | grasslands 64 | mountains 65 | gas giant 66 | rocky island 67 | oceans 68 | fields 69 | rainforests 70 | plains 71 | forests 72 | rock arches 73 | verdant 74 | jungles 75 | deserts 76 | hills 77 | urban 78 | cityscape 79 | swamp 80 | savannas 81 | } 82 | 83 | type Specie { 84 | id: Int! 85 | name: String 86 | averageHeight: Int 87 | averageLifespan: Int 88 | classification: String 89 | designation: String 90 | language: String 91 | people: [Character!]! 92 | skinColor: [] 93 | } 94 | 95 | scalar Date 96 | 97 | enum Color { 98 | yellow 99 | hazel 100 | blue 101 | green 102 | orange 103 | brown 104 | grey 105 | amber 106 | red 107 | white 108 | brown 109 | black 110 | magenta 111 | peach 112 | tan 113 | pink 114 | } 115 | 116 | enum Climate { 117 | temperate 118 | moist 119 | murky 120 | polluted 121 | hot 122 | humid 123 | arid 124 | frozen 125 | tropical 126 | windy 127 | } 128 | 129 | type Vehicle { 130 | id: Int! 131 | name: String! 132 | model: String 133 | films: [Film!]! 134 | pilots: [Character]! 135 | } 136 | 137 | type Starship { 138 | id: Int! 139 | name: String! 140 | model: String 141 | films: [Film!]! 142 | pilots: [Character]! 143 | length(unit: LengthUnit = METER): Int! 144 | } 145 | 146 | enum LengthUnit { 147 | METER 148 | KILOMETER 149 | } 150 | 151 | input CharacterInput { 152 | id: Int! 153 | name: String! 154 | birthYear: String! 155 | eyeColor: String 156 | films: [Film]! 157 | gender: String 158 | hairColor: String! 159 | height: Int 160 | homeworld: Planet 161 | mass: Int 162 | skinColor: String 163 | species: Specie 164 | starships: Starship 165 | vehicles: Vehicle 166 | } 167 | 168 | type Human implements Character { 169 | id: Int! 170 | name: String! 171 | birthYear: String! 172 | eyeColor: String 173 | appearsIn: [Film]! 174 | gender: String 175 | hairColor: String! 176 | height: Int 177 | homeworld: Planet 178 | skinColor: String 179 | species: Specie 180 | starships: [Starship]! 181 | vehicles: [Vehicle]! 182 | totalCredits: Int 183 | transportation: [Transportationtool!]! 184 | } 185 | 186 | type Droid implements Character { 187 | id: Int! 188 | name: String! 189 | birthYear: String! 190 | eyeColor: String 191 | appearsIn: [Film]! 192 | gender: String 193 | hairColor: String! 194 | height: Int 195 | homeworld: Planet 196 | skinColor: String 197 | species: Specie 198 | starships: [Starship]! 199 | vehicles: [Vehicle]! 200 | primaryFunction: String 201 | transportation: [Transportationtool!]! 202 | } 203 | 204 | 205 | type Mutation { 206 | addCharacter(input: CharacterInput): Character! 207 | addStarship(name: String, model: String): Starship 208 | } 209 | `; 210 | 211 | module.exports = { typeDefs }; 212 | -------------------------------------------------------------------------------- /stylesheet/preview.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | .queryType-alt { 4 | border-radius: 10; 5 | } 6 | 7 | .queryType-alt span:hover { 8 | cursor: pointer; 9 | font-weight: bold; 10 | } 11 | 12 | li.fieldType-alt { 13 | position:relative; 14 | color: #f2f2f2; 15 | } 16 | li.fieldType-alt::before { 17 | position: relative; 18 | top: -4px; 19 | width: 15px; 20 | border-bottom: 1px solid rgb(170, 170, 170); 21 | content: ''; 22 | display: inline-block; 23 | } 24 | 25 | li.fieldType-alt::after { 26 | position: absolute; 27 | left: 0; 28 | top: 0; 29 | height: 18px; 30 | border-left: 1px solid rgb(170, 170, 170); 31 | content: ''; 32 | display: inline-block; 33 | } 34 | 35 | ul { 36 | padding-bottom: 5px; 37 | } 38 | 39 | li { 40 | padding-bottom: 5px; 41 | } 42 | li::marker { 43 | content: "\2022\0020"; /* character: • */ 44 | color:rgb(145, 145, 145); 45 | } 46 | 47 | .notleaf:hover { 48 | font-weight: bold; 49 | cursor: context-menu; 50 | } 51 | 52 | a { 53 | color: #5fefd0; 54 | } 55 | 56 | .btn-selected { 57 | border: 1px solid #6c757d; 58 | animation: color-change 1s infinite !important; 59 | font-family: "Segoe UI Symbol", "Arial Unicode MS", sans-serif; 60 | } 61 | 62 | .btn-selected::before { 63 | content: "\23FA"; 64 | } 65 | 66 | .btn-selected:hover::before { 67 | content: "\23F9"; 68 | } 69 | 70 | /* Live recording icon effect */ 71 | @keyframes color-change { 72 | 0% { 73 | color: #5fefd0; 74 | } 75 | 50% { 76 | color: #282828; 77 | } 78 | 100% { 79 | color: #5fefd0; 80 | } 81 | } 82 | /* TODO: Replace the above code with this .svg technique */ 83 | /* .btn-selected::before { 84 | content: url('path/to/file.svg'); 85 | display: inline-block; 86 | width: 16px; adjust to match the size of your SVG 87 | height: 16px; adjust to match the size of your SVG 88 | margin-right: 5px; optional, adjust as needed 89 | fill: currentColor; sets the fill color of the SVG to match the text color 90 | animation-name: color-change; 91 | animation-duration: 2s; 92 | animation-iteration-count: infinite; 93 | animation-direction: alternate; 94 | } */ 95 | 96 | @keyframes color-change { 97 | 0% { 98 | color: #5fefd0; 99 | } 100 | 50% { 101 | color: #282828; 102 | } 103 | 100% { 104 | color: #5fefd0; 105 | } 106 | } 107 | 108 | .typedField { 109 | color: rgb(144, 151, 142) !important; 110 | } 111 | .typedField > a { 112 | opacity: .5; 113 | } 114 | .typedField::marker { 115 | content: "\2022\0020"; /* character: • */ 116 | color: green; 117 | } 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": false /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /visualization.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SurfQL/727dc9070d92b0a51ea817fae432aa36273b098f/visualization.gif -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 37 | * Press `F5` to run the tests in a new window with your extension loaded. 38 | * See the output of the test result in the debug console. 39 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 40 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 41 | * You can create folders inside the `test` folder to structure your tests any way you want. 42 | 43 | ## Go further 44 | 45 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 46 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 47 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; --------------------------------------------------------------------------------