├── .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 | 
8 | Code Auto-completion
9 | 
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 |
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 ];
--------------------------------------------------------------------------------