diff --git a/.gitignore b/.gitignore
index fb7adeee3..b9841a6e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,4 +106,6 @@ tests/*/screenshots
tests/*/videos
# Local Netlify folder
-.netlify
\ No newline at end of file
+.netlify
+# Andrew's crazy Volar shim generator
+shims-volar.d.ts
diff --git a/package.json b/package.json
index 3811a757d..4e8fadb52 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,11 @@
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "preinstall": "npx only-allow pnpm",
"prepare": "husky install",
- "pre-commit": "lint-staged"
- },
- "lint-staged": {
- "*.{ts,js,vue}": "eslint",
- "*.{css,scss,vue}": "stylelint"
+ "dev": "pnpm -r dev",
+ "lintfix": "pnpm -r lintfix",
+ "pre-commit": "pnpm -r lint"
},
"workspaces": [
"./packages/*"
@@ -19,5 +18,9 @@
"dependencies": {
"husky": "^7.0.2",
"lint-staged": "^11.1.2"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^13.1.0",
+ "@commitlint/config-conventional": "^13.1.0"
}
}
diff --git a/.eslintrc.js b/packages/hoppscotch-app/.eslintrc.js
similarity index 100%
rename from .eslintrc.js
rename to packages/hoppscotch-app/.eslintrc.js
diff --git a/packages/hoppscotch-app/.gitignore b/packages/hoppscotch-app/.gitignore
new file mode 100644
index 000000000..b9841a6e3
--- /dev/null
+++ b/packages/hoppscotch-app/.gitignore
@@ -0,0 +1,111 @@
+# Created by .ignore support plugin (hsz.mobi)
+
+# Firebase
+.firebase
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# Nuxt generate
+dist
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless
+
+# IDE / Editor
+.idea
+
+# Service worker
+sw.*
+
+# Mac OSX
+.DS_Store
+
+# Vim swap files
+*.swp
+
+# Build data
+.hoppscotch
+
+# File explorer
+.directory
+
+# Tests screenshots
+tests/*/screenshots
+
+# Tests videos
+tests/*/videos
+
+# Local Netlify folder
+.netlify
+# Andrew's crazy Volar shim generator
+shims-volar.d.ts
diff --git a/packages/hoppscotch-app/assets/icons/corner-down-left.svg b/packages/hoppscotch-app/assets/icons/corner-down-left.svg
new file mode 100644
index 000000000..7a8b7473a
--- /dev/null
+++ b/packages/hoppscotch-app/assets/icons/corner-down-left.svg
@@ -0,0 +1 @@
+
diff --git a/packages/hoppscotch-app/assets/scss/styles.scss b/packages/hoppscotch-app/assets/scss/styles.scss
index f54c9535e..3b179a194 100644
--- a/packages/hoppscotch-app/assets/scss/styles.scss
+++ b/packages/hoppscotch-app/assets/scss/styles.scss
@@ -17,7 +17,7 @@
::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content;
@apply rounded-full;
- @apply border-solid border-4 border-transparent;
+ @apply border-solid border-transparent border-4;
@apply hover:(bg-dividerDark bg-clip-content);
}
@@ -36,8 +36,9 @@
}
input::placeholder,
-textarea::placeholder {
- @apply text-secondaryDark;
+textarea::placeholder,
+.CodeMirror-empty {
+ @apply text-secondary;
@apply opacity-25;
}
@@ -116,8 +117,8 @@ a {
&.link {
@apply items-center;
- @apply px-1 py-0.5;
- @apply -mx-1 -my-0.5;
+ @apply py-0.5 px-1;
+ @apply -my-0.5 -mx-1;
@apply text-accent;
@apply rounded;
@apply hover:text-accentDark;
@@ -198,7 +199,7 @@ hr {
.textarea {
@apply flex;
@apply w-full;
- @apply px-4 py-2;
+ @apply py-2 px-4;
@apply bg-transparent;
@apply rounded;
@apply text-secondaryDark;
@@ -293,7 +294,7 @@ input[type="checkbox"] {
@apply cursor-pointer;
&::before {
- @apply border-2 border-divider;
+ @apply border-divider border-2;
@apply rounded;
@apply inline-flex;
@apply items-center;
@@ -347,6 +348,7 @@ input[type="checkbox"] {
@apply justify-start;
@apply shadow;
@apply font-medium;
+ @apply transition;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
@@ -358,7 +360,6 @@ input[type="checkbox"] {
@apply ml-auto;
@apply last:ml-4;
@apply sm:ml-8;
- @apply transition;
@apply rounded;
@apply text-current;
@apply normal-case;
@@ -461,6 +462,32 @@ input[type="checkbox"] {
@apply w-full;
}
+.CodeMirror {
+ @apply !h-auto;
+
+ font-size: var(--body-font-size);
+
+ &:not(.CodeMirror-focused) .CodeMirror-activeline-background {
+ background: transparent !important;
+ }
+
+ .CodeMirror-dialog-top {
+ @apply bg-primaryLight;
+ @apply border-dividerLight;
+ @apply px-4;
+ @apply py-2;
+ @apply z-5;
+ }
+
+ .CodeMirror-scroll {
+ @apply min-h-64;
+ }
+
+ * {
+ font-family: "Roboto Mono", monospace;
+ }
+}
+
@media (max-width: 767px) {
main {
margin-bottom: env(safe-area-inset-bottom);
diff --git a/packages/hoppscotch-app/components/collections/SaveRequest.vue b/packages/hoppscotch-app/components/collections/SaveRequest.vue
index f9f39c09c..9b304e73d 100644
--- a/packages/hoppscotch-app/components/collections/SaveRequest.vue
+++ b/packages/hoppscotch-app/components/collections/SaveRequest.vue
@@ -1,5 +1,9 @@
-
+
@@ -41,11 +45,11 @@
@@ -53,191 +57,294 @@
-
diff --git a/packages/hoppscotch-app/components/firebase/Login.vue b/packages/hoppscotch-app/components/firebase/Login.vue
index 78fefbf73..ca6ddfaf7 100644
--- a/packages/hoppscotch-app/components/firebase/Login.vue
+++ b/packages/hoppscotch-app/components/firebase/Login.vue
@@ -26,7 +26,7 @@
/>
-
+
-
diff --git a/packages/hoppscotch-app/components/graphql/RequestOptions.vue b/packages/hoppscotch-app/components/graphql/RequestOptions.vue
index 7113b4e4a..cd17475be 100644
--- a/packages/hoppscotch-app/components/graphql/RequestOptions.vue
+++ b/packages/hoppscotch-app/components/graphql/RequestOptions.vue
@@ -42,9 +42,7 @@
/>
@@ -57,20 +55,7 @@
/>
-
+
@@ -108,19 +93,7 @@
/>
-
+
@@ -173,27 +146,7 @@
/>
-
-
-
+
-
diff --git a/packages/hoppscotch-app/components/graphql/Response.vue b/packages/hoppscotch-app/components/graphql/Response.vue
index feca15fcd..f0027d92a 100644
--- a/packages/hoppscotch-app/components/graphql/Response.vue
+++ b/packages/hoppscotch-app/components/graphql/Response.vue
@@ -18,6 +18,13 @@
{{ $t("response.title") }}
+
-
+
-
+
- {{ $t("shortcut.request.send_request") }}
-
-
- {{ $t("shortcut.general.show_all") }}
-
-
+
- {{ getSpecialKey() }}
- G
-
-
- {{ getSpecialKey() }}
- K
-
-
+
-
diff --git a/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue
index a2ee88044..230406d0b 100644
--- a/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue
+++ b/packages/hoppscotch-app/components/lenses/renderers/ImageLensRenderer.vue
@@ -27,12 +27,10 @@
/>
-
-
![]()
-
+
diff --git a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue
index fc6bc2f12..79652d54f 100644
--- a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue
+++ b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue
@@ -13,10 +13,18 @@
justify-between
"
>
-
+
+
-
-
+
+
diff --git a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue
index 563f9de1f..9203bddc2 100644
--- a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue
+++ b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue
@@ -17,6 +17,14 @@
{{ $t("response.body") }}
+
-
-
-
+
-
diff --git a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue
index eb5340f41..25d173c91 100644
--- a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue
+++ b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue
@@ -17,6 +17,14 @@
{{ $t("response.body") }}
+
-
-
-
+
-
diff --git a/packages/hoppscotch-app/components/realtime/Log.vue b/packages/hoppscotch-app/components/realtime/Log.vue
index a09fdf7ca..ef1bf74d0 100644
--- a/packages/hoppscotch-app/components/realtime/Log.vue
+++ b/packages/hoppscotch-app/components/realtime/Log.vue
@@ -17,14 +17,13 @@
{{ title }}
-
+
{{ entry.ts }}{{ getSourcePrefix(entry.source)
- }}{{ entry.payload }}{{ entry.ts }}{{ source(entry.source) }}{{ entry.payload }}
{{ $t("response.waiting_for_connection") }}
@@ -32,27 +31,14 @@
-
diff --git a/packages/hoppscotch-app/components/realtime/Mqtt.vue b/packages/hoppscotch-app/components/realtime/Mqtt.vue
index 9f1bb3b7b..5cc5f0b7f 100644
--- a/packages/hoppscotch-app/components/realtime/Mqtt.vue
+++ b/packages/hoppscotch-app/components/realtime/Mqtt.vue
@@ -145,7 +145,7 @@ import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import Paho from "paho-mqtt"
-import debounce from "~/helpers/utils/debounce"
+import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { useSetting } from "~/newstore/settings"
import useWindowSize from "~/helpers/utils/useWindowSize"
diff --git a/packages/hoppscotch-app/components/realtime/Socketio.vue b/packages/hoppscotch-app/components/realtime/Socketio.vue
index e9e461aee..7d933e3bf 100644
--- a/packages/hoppscotch-app/components/realtime/Socketio.vue
+++ b/packages/hoppscotch-app/components/realtime/Socketio.vue
@@ -165,7 +165,7 @@ import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import { io as Client } from "socket.io-client"
import wildcard from "socketio-wildcard"
-import debounce from "~/helpers/utils/debounce"
+import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { useSetting } from "~/newstore/settings"
import useWindowSize from "~/helpers/utils/useWindowSize"
diff --git a/packages/hoppscotch-app/components/realtime/Sse.vue b/packages/hoppscotch-app/components/realtime/Sse.vue
index 3d230dc38..ca78d4b1a 100644
--- a/packages/hoppscotch-app/components/realtime/Sse.vue
+++ b/packages/hoppscotch-app/components/realtime/Sse.vue
@@ -89,8 +89,8 @@
import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
+import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
-import debounce from "~/helpers/utils/debounce"
export default defineComponent({
components: { Splitpanes, Pane },
diff --git a/packages/hoppscotch-app/components/realtime/Websocket.vue b/packages/hoppscotch-app/components/realtime/Websocket.vue
index 75220c973..2bacf97e5 100644
--- a/packages/hoppscotch-app/components/realtime/Websocket.vue
+++ b/packages/hoppscotch-app/components/realtime/Websocket.vue
@@ -205,8 +205,8 @@
import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
+import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
-import debounce from "~/helpers/utils/debounce"
import useWindowSize from "~/helpers/utils/useWindowSize"
import { useSetting } from "~/newstore/settings"
diff --git a/packages/hoppscotch-app/components/smart/AceEditor.vue b/packages/hoppscotch-app/components/smart/AceEditor.vue
deleted file mode 100644
index 299c0f7f6..000000000
--- a/packages/hoppscotch-app/components/smart/AceEditor.vue
+++ /dev/null
@@ -1,282 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/hoppscotch-app/components/smart/AutoComplete.vue b/packages/hoppscotch-app/components/smart/AutoComplete.vue
index 402128a2f..c495247ce 100644
--- a/packages/hoppscotch-app/components/smart/AutoComplete.vue
+++ b/packages/hoppscotch-app/components/smart/AutoComplete.vue
@@ -150,6 +150,16 @@ export default defineComponent({
handleKeystroke(event) {
switch (event.code) {
+ case "Enter":
+ event.preventDefault()
+ if (this.currentSuggestionIndex > -1)
+ this.forceSuggestion(
+ this.suggestions.find(
+ (_item, index) => index === this.currentSuggestionIndex
+ )
+ )
+ break
+
case "ArrowUp":
event.preventDefault()
this.currentSuggestionIndex =
diff --git a/packages/hoppscotch-app/components/smart/EnvInput.vue b/packages/hoppscotch-app/components/smart/EnvInput.vue
index 75c8aa94e..0040ca44e 100644
--- a/packages/hoppscotch-app/components/smart/EnvInput.vue
+++ b/packages/hoppscotch-app/components/smart/EnvInput.vue
@@ -483,7 +483,7 @@ export default defineComponent({
line-height: 1.9;
&::before {
- @apply text-secondaryDark;
+ @apply text-secondary;
@apply opacity-25;
@apply pointer-events-none;
@@ -501,7 +501,6 @@ export default defineComponent({
@apply overflow-y-hidden;
@apply resize-none;
@apply focus:outline-none;
- @apply transition;
}
.env-input::-webkit-scrollbar {
diff --git a/packages/hoppscotch-app/components/smart/JsEditor.vue b/packages/hoppscotch-app/components/smart/JsEditor.vue
deleted file mode 100644
index f78102525..000000000
--- a/packages/hoppscotch-app/components/smart/JsEditor.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/hoppscotch-app/helpers/GQLConnection.ts b/packages/hoppscotch-app/helpers/GQLConnection.ts
index 4b2d4614f..2982819fa 100644
--- a/packages/hoppscotch-app/helpers/GQLConnection.ts
+++ b/packages/hoppscotch-app/helpers/GQLConnection.ts
@@ -195,7 +195,7 @@ export class GQLConnection {
method: "post",
url,
headers: {
- ...headers,
+ ...finalHeaders,
"content-type": "application/json",
},
data: JSON.stringify({
diff --git a/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js b/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js
index b0e38e429..61616cfa8 100644
--- a/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js
+++ b/packages/hoppscotch-app/helpers/__tests__/editorutils.spec.js
@@ -15,16 +15,16 @@ describe("getEditorLangForMimeType", () => {
expect(getEditorLangForMimeType("text/html")).toMatch("html")
})
- test("returns 'plain_text' for plain text mime", () => {
- expect(getEditorLangForMimeType("text/plain")).toMatch("plain_text")
+ test("returns 'text/x-yaml' for plain text mime", () => {
+ expect(getEditorLangForMimeType("text/plain")).toMatch("text/x-yaml")
})
- test("returns 'plain_text' for unimplemented mimes", () => {
- expect(getEditorLangForMimeType("image/gif")).toMatch("plain_text")
+ test("returns 'text/x-yaml' for unimplemented mimes", () => {
+ expect(getEditorLangForMimeType("image/gif")).toMatch("text/x-yaml")
})
- test("returns 'plain_text' for null/undefined mimes", () => {
- expect(getEditorLangForMimeType(null)).toMatch("plain_text")
- expect(getEditorLangForMimeType(undefined)).toMatch("plain_text")
+ test("returns 'text/x-yaml' for null/undefined mimes", () => {
+ expect(getEditorLangForMimeType(null)).toMatch("text/x-yaml")
+ expect(getEditorLangForMimeType(undefined)).toMatch("text/x-yaml")
})
})
diff --git a/packages/hoppscotch-app/helpers/codegen/codegen.ts b/packages/hoppscotch-app/helpers/codegen/codegen.ts
index 45baf2d6f..88c057e70 100644
--- a/packages/hoppscotch-app/helpers/codegen/codegen.ts
+++ b/packages/hoppscotch-app/helpers/codegen/codegen.ts
@@ -150,8 +150,6 @@ function getCodegenGeneralRESTInfo(
.map((x) => ({ ...x, active: true }))
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
- console.log(finalHeaders)
-
return {
name: request.name,
uri: request.effectiveFinalURL,
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js b/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js
index 1f0cb8d92..1d537760b 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/c-libcurl.js
@@ -24,7 +24,7 @@ export const CLibcurlCodegen = {
`curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "${method}");`
)
requestString.push(
- `curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}${queryString}");`
+ `curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}?${queryString}");`
)
requestString.push(`struct curl_slist *headers = NULL;`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js b/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js
index 50fe6bbb9..c3452fac1 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/cs-restsharp.js
@@ -48,7 +48,7 @@ export const CsRestsharpCodegen = {
// create client and request
requestString.push(`var client = new RestClient("${url}");\n\n`)
requestString.push(
- `var request = new RestRequest("${pathName}${queryString}", ${requestDataFormat});\n\n`
+ `var request = new RestRequest("${pathName}?${queryString}", ${requestDataFormat});\n\n`
)
// authentification
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/curl.js b/packages/hoppscotch-app/helpers/codegen/generators/curl.js
index 5f6979478..2b7e18d1d 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/curl.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/curl.js
@@ -19,7 +19,7 @@ export const CurlCodegen = {
}) => {
const requestString = []
requestString.push(`curl -X ${method}`)
- requestString.push(` '${url}${pathName}${queryString}'`)
+ requestString.push(` '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
requestString.push(
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/go-native.js b/packages/hoppscotch-app/helpers/codegen/generators/go-native.js
index 539470ca1..3339b265c 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/go-native.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/go-native.js
@@ -25,7 +25,7 @@ export const GoNativeCodegen = {
const requestBody = rawInput ? rawParams : rawRequestBody
if (method === "GET") {
requestString.push(
- `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}")\n`
+ `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}")\n`
)
}
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
@@ -33,11 +33,11 @@ export const GoNativeCodegen = {
if (isJSONContentType(contentType)) {
requestString.push(`var reqBody = []byte(\`${requestBody}\`)\n\n`)
requestString.push(
- `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", bytes.NewBuffer(reqBody))\n`
+ `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", bytes.NewBuffer(reqBody))\n`
)
} else if (contentType.includes("x-www-form-urlencoded")) {
requestString.push(
- `req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", strings.NewReader("${requestBody}"))\n`
+ `req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", strings.NewReader("${requestBody}"))\n`
)
}
}
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js b/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js
index 6061358f2..2225c4060 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/java-okhttp.js
@@ -39,7 +39,7 @@ export const JavaOkhttpCodegen = {
}
requestString.push("Request request = new Request.Builder()")
- requestString.push(`.url("${url}${pathName}${queryString}")`)
+ requestString.push(`.url("${url}${pathName}?${queryString}")`)
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
requestString.push(`.method("${method}", body)`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js b/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js
index d4191513a..c257c76e9 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/java-unirest.js
@@ -32,7 +32,7 @@ export const JavaUnirestCodegen = {
// create client and request
const verb = verbs.find((v) => v.verb === method)
requestString.push(
- `HttpResponse
response = Unirest.${verb.unirestMethod}("${url}${pathName}${queryString}")\n`
+ `HttpResponse response = Unirest.${verb.unirestMethod}("${url}${pathName}?${queryString}")\n`
)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js
index 5d3401b42..7804e8d28 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-fetch.js
@@ -21,7 +21,7 @@ export const JavascriptFetchCodegen = {
}) => {
const requestString = []
let genHeaders = []
- requestString.push(`fetch("${url}${pathName}${queryString}", {\n`)
+ requestString.push(`fetch("${url}${pathName}?${queryString}", {\n`)
requestString.push(` method: "${method}",\n`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js
index 2a2fb8575..b2ca1c0b3 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-jquery.js
@@ -21,7 +21,7 @@ export const JavascriptJqueryCodegen = {
const genHeaders = []
requestString.push(
- `jQuery.ajax({\n url: "${url}${pathName}${queryString}"`
+ `jQuery.ajax({\n url: "${url}${pathName}?${queryString}"`
)
requestString.push(`,\n method: "${method.toUpperCase()}"`)
const requestBody = rawInput ? rawParams : rawRequestBody
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js b/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js
index 650e9b24c..3c752d05a 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/javascript-xhr.js
@@ -25,7 +25,7 @@ export const JavascriptXhrCodegen = {
const user = auth === "Basic Auth" ? `'${httpUser}'` : null
const password = auth === "Basic Auth" ? `'${httpPassword}'` : null
requestString.push(
- `xhr.open('${method}', '${url}${pathName}${queryString}', true, ${user}, ${password})`
+ `xhr.open('${method}', '${url}${pathName}?${queryString}', true, ${user}, ${password})`
)
if (auth === "Bearer Token" || auth === "OAuth 2.0") {
requestString.push(
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js
index 91c4de7da..349e2d259 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-axios.js
@@ -22,7 +22,7 @@ export const NodejsAxiosCodegen = {
const requestBody = rawInput ? rawParams : rawRequestBody
requestString.push(
- `axios.${method.toLowerCase()}('${url}${pathName}${queryString}'`
+ `axios.${method.toLowerCase()}('${url}${pathName}?${queryString}'`
)
if (requestBody.length !== 0) {
requestString.push(", ")
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js
index fba2debdc..cc551ccac 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-native.js
@@ -24,7 +24,7 @@ export const NodejsNativeCodegen = {
requestString.push(`const http = require('http');\n\n`)
- requestString.push(`const url = '${url}${pathName}${queryString}';\n`)
+ requestString.push(`const url = '${url}${pathName}?${queryString}';\n`)
requestString.push(`const options = {\n`)
requestString.push(` method: '${method}',\n`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js
index 1c3361921..cbdf218a0 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-request.js
@@ -25,7 +25,7 @@ export const NodejsRequestCodegen = {
requestString.push(`const request = require('request');\n`)
requestString.push(`const options = {\n`)
requestString.push(` method: '${method.toLowerCase()}',\n`)
- requestString.push(` url: '${url}${pathName}${queryString}'`)
+ requestString.push(` url: '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js
index 45b1e1eb5..dabab4982 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/nodejs-unirest.js
@@ -25,7 +25,7 @@ export const NodejsUnirestCodegen = {
requestString.push(`const unirest = require('unirest');\n`)
requestString.push(`const req = unirest(\n`)
requestString.push(
- `'${method.toLowerCase()}', '${url}${pathName}${queryString}')\n`
+ `'${method.toLowerCase()}', '${url}${pathName}?${queryString}')\n`
)
if (auth === "Basic Auth") {
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js b/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js
index d5d4c5661..3b58d6cfd 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/php-curl.js
@@ -25,7 +25,7 @@ export const PhpCurlCodegen = {
requestString.push(` "${url}${pathName}${queryString}",\n`)
+ requestString.push(` CURLOPT_URL => "${url}${pathName}?${queryString}",\n`)
requestString.push(` CURLOPT_RETURNTRANSFER => true,\n`)
requestString.push(` CURLOPT_ENCODING => "",\n`)
requestString.push(` CURLOPT_MAXREDIRS => 10,\n`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js b/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js
index 8693cf450..40880627c 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/powershell-restmethod.js
@@ -26,7 +26,7 @@ export const PowershellRestmethodCodegen = {
let variables = ""
requestString.push(
- `Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}${queryString}'`
+ `Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}?${queryString}'`
)
const requestBody = rawInput ? rawParams : rawRequestBody
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js b/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js
index 7c7795bfe..e061ac462 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/python-http-client.js
@@ -91,7 +91,7 @@ export const PythonHttpClientCodegen = {
}
}
requestString.push(
- `conn.request("${method}", "${pathName}${queryString}", payload, headers)\n`
+ `conn.request("${method}", "${pathName}?${queryString}", payload, headers)\n`
)
requestString.push(`res = conn.getresponse()\n`)
requestString.push(`data = res.read()\n`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js b/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js
index d5471cbbb..c02062774 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/python-requests.js
@@ -31,7 +31,7 @@ export const PythonRequestsCodegen = {
const genHeaders = []
requestString.push(`import requests\n\n`)
- requestString.push(`url = '${url}${pathName}${queryString}'\n`)
+ requestString.push(`url = '${url}${pathName}?${queryString}'\n`)
// auth headers
if (auth === "Basic Auth") {
@@ -58,7 +58,7 @@ export const PythonRequestsCodegen = {
requestString.push(...printHeaders(genHeaders))
requestString.push(`response = requests.request(\n`)
requestString.push(` '${method}',\n`)
- requestString.push(` '${url}${pathName}${queryString}',\n`)
+ requestString.push(` '${url}${pathName}?${queryString}',\n`)
}
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
genHeaders.push(`'Content-Type': '${contentType}'`)
@@ -83,7 +83,7 @@ export const PythonRequestsCodegen = {
}
requestString.push(`response = requests.request(\n`)
requestString.push(` '${method}',\n`)
- requestString.push(` '${url}${pathName}${queryString}',\n`)
+ requestString.push(` '${url}${pathName}?${queryString}',\n`)
requestString.push(` data=data,\n`)
}
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js b/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js
index e5486bd05..4487891f8 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/ruby-net-http.js
@@ -35,7 +35,7 @@ export const RubyNetHttpCodeGen = {
// create URI and request
const verb = verbs.find((v) => v.verb === method)
- requestString.push(`uri = URI.parse('${url}${pathName}${queryString}')\n`)
+ requestString.push(`uri = URI.parse('${url}${pathName}?${queryString}')\n`)
requestString.push(`request = Net::HTTP::${verb.rbMethod}.new(uri)`)
// content type
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js b/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js
index 9112c766b..250f70313 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/salesforce-apex.js
@@ -30,7 +30,7 @@ export const SalesforceApexCodegen = {
requestString.push(`HttpRequest request = new HttpRequest();\n`)
requestString.push(`request.setMethod('${method}');\n`)
requestString.push(
- `request.setEndpoint('${url}${pathName}${queryString}');\n\n`
+ `request.setEndpoint('${url}${pathName}?${queryString}');\n\n`
)
// authentification
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js b/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js
index dc0284935..0623847b5 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/shell-httpie.js
@@ -37,7 +37,7 @@ export const ShellHttpieCodegen = {
}
// URL
- let escapedUrl = `${url}${pathName}${queryString}`
+ let escapedUrl = `${url}${pathName}?${queryString}`
escapedUrl = escapedUrl.replace(/'/g, "\\'")
requestString.push(` ${method} $'${escapedUrl}'`)
diff --git a/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js b/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js
index 8620a6170..537d6fd97 100644
--- a/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js
+++ b/packages/hoppscotch-app/helpers/codegen/generators/shell-wget.js
@@ -19,7 +19,7 @@ export const ShellWgetCodegen = {
}) => {
const requestString = []
requestString.push(`wget -O - --method=${method}`)
- requestString.push(` '${url}${pathName}${queryString}'`)
+ requestString.push(` '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
requestString.push(
diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts
new file mode 100644
index 000000000..7319c7413
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts
@@ -0,0 +1,215 @@
+import CodeMirror from "codemirror"
+
+import "codemirror-theme-github/theme/github.css"
+import "codemirror/theme/base16-dark.css"
+import "codemirror/theme/tomorrow-night-bright.css"
+
+import "codemirror/lib/codemirror.css"
+import "codemirror/addon/lint/lint.css"
+import "codemirror/addon/dialog/dialog.css"
+import "codemirror/addon/hint/show-hint.css"
+
+import "codemirror/addon/fold/foldgutter.css"
+import "codemirror/addon/fold/foldgutter"
+import "codemirror/addon/fold/brace-fold"
+import "codemirror/addon/fold/comment-fold"
+import "codemirror/addon/fold/indent-fold"
+import "codemirror/addon/display/autorefresh"
+import "codemirror/addon/lint/lint"
+import "codemirror/addon/hint/show-hint"
+import "codemirror/addon/display/placeholder"
+import "codemirror/addon/edit/closebrackets"
+import "codemirror/addon/search/search"
+import "codemirror/addon/search/searchcursor"
+import "codemirror/addon/search/jump-to-line"
+import "codemirror/addon/dialog/dialog"
+import "codemirror/addon/selection/active-line"
+
+import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api"
+import { LinterDefinition } from "./linting/linter"
+import { Completer } from "./completion"
+
+type CodeMirrorOptions = {
+ extendedEditorConfig: Omit
+ linter: LinterDefinition | null
+ completer: Completer | null
+}
+
+const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
+ autoRefresh: true,
+ lineNumbers: true,
+ foldGutter: true,
+ autoCloseBrackets: true,
+ gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+ extraKeys: {
+ "Ctrl-Space": "autocomplete",
+ },
+ viewportMargin: Infinity,
+ styleActiveLine: true,
+}
+
+/**
+ * A Vue composable to mount and use Codemirror
+ *
+ * NOTE: Make sure to import all the necessary Codemirror modules,
+ * as this function doesn't import any other than the core
+ * @param el Reference to the dom node to attach to
+ * @param value Reference to value to read/write to
+ * @param options CodeMirror options to pass
+ */
+export function useCodemirror(
+ el: Ref,
+ value: Ref,
+ options: CodeMirrorOptions
+): { cm: Ref; cursor: Ref } {
+ const { $colorMode } = useContext() as any
+
+ const cm = ref(null)
+ const cursor = ref({ line: 0, ch: 0 })
+
+ const updateEditorConfig = () => {
+ Object.keys(options.extendedEditorConfig).forEach((key) => {
+ // Only update options which need updating
+ if (
+ cm.value &&
+ cm.value?.getOption(key as any) !==
+ (options.extendedEditorConfig as any)[key]
+ ) {
+ cm.value?.setOption(
+ key as any,
+ (options.extendedEditorConfig as any)[key]
+ )
+ }
+ })
+ }
+
+ const updateLinterConfig = () => {
+ if (options.linter) {
+ cm.value?.setOption("lint", options.linter)
+ }
+ }
+
+ const updateCompleterConfig = () => {
+ if (options.completer) {
+ cm.value?.setOption("hintOptions", {
+ completeSingle: false,
+ hint: async (editor: CodeMirror.Editor) => {
+ const pos = editor.getCursor()
+ const text = editor.getValue()
+
+ const token = editor.getTokenAt(pos)
+ // It's not a word token, so, just increment to skip to next
+ if (token.string.toUpperCase() === token.string.toLowerCase())
+ token.start += 1
+
+ const result = await options.completer!(text, pos)
+
+ if (!result) return null
+
+ return {
+ from: { line: pos.line, ch: token.start },
+ to: { line: pos.line, ch: token.end },
+ list: result.completions
+ .sort((a, b) => a.score - b.score)
+ .map((x) => x.text),
+ }
+ },
+ })
+ }
+ }
+
+ const initialize = () => {
+ if (!el.value) return
+
+ cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
+
+ cm.value.setValue(value.value)
+
+ setTheme()
+ updateEditorConfig()
+ updateLinterConfig()
+ updateCompleterConfig()
+
+ cm.value.on("change", (instance) => {
+ // External update propagation (via watchers) should be ignored
+ if (instance.getValue() !== value.value) {
+ value.value = instance.getValue()
+ }
+ })
+
+ cm.value.on("cursorActivity", (instance) => {
+ cursor.value = instance.getCursor()
+ })
+ }
+
+ // Boot-up CodeMirror, set the value and listeners
+ onMounted(() => {
+ initialize()
+ })
+
+ // Reinitialize if the target ref updates
+ watch(el, () => {
+ if (cm.value) {
+ const parent = cm.value.getWrapperElement()
+ parent.remove()
+ cm.value = null
+ }
+ initialize()
+ })
+
+ const setTheme = () => {
+ if (cm.value) {
+ cm.value?.setOption("theme", getThemeName($colorMode.value))
+ }
+ }
+
+ const getThemeName = (mode: string) => {
+ switch (mode) {
+ case "system":
+ return "default"
+ case "light":
+ return "github"
+ case "dark":
+ return "base16-dark"
+ case "black":
+ return "tomorrow-night-bright"
+ default:
+ return "default"
+ }
+ }
+
+ // If the editor properties are reactive, watch for updates
+ watch(() => options.extendedEditorConfig, updateEditorConfig, {
+ immediate: true,
+ deep: true,
+ })
+ watch(() => options.linter, updateLinterConfig, { immediate: true })
+ watch(() => options.completer, updateCompleterConfig, { immediate: true })
+
+ // Watch value updates
+ watch(value, (newVal) => {
+ // Check if we are mounted
+ if (cm.value) {
+ // Don't do anything on internal updates
+ if (cm.value.getValue() !== newVal) {
+ cm.value.setValue(newVal)
+ }
+ }
+ })
+
+ // Push cursor updates
+ watch(cursor, (value) => {
+ if (value !== cm.value?.getCursor()) {
+ cm.value?.focus()
+ cm.value?.setCursor(value)
+ }
+ })
+
+ // Watch color mode updates and update theme
+ watch(() => $colorMode.value, setTheme)
+
+ return {
+ cm,
+ cursor,
+ }
+}
diff --git a/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts b/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts
new file mode 100644
index 000000000..672ef134a
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/completion/gqlQuery.ts
@@ -0,0 +1,27 @@
+import { Ref } from "@nuxtjs/composition-api"
+import { GraphQLSchema } from "graphql"
+import { getAutocompleteSuggestions } from "graphql-language-service-interface"
+import { Completer, CompleterResult, CompletionEntry } from "."
+
+const completer: (schemaRef: Ref) => Completer =
+ (schemaRef: Ref) => (text, completePos) => {
+ if (!schemaRef.value) return Promise.resolve(null)
+
+ const completions = getAutocompleteSuggestions(schemaRef.value, text, {
+ line: completePos.line,
+ character: completePos.ch,
+ } as any)
+
+ return Promise.resolve({
+ completions: completions.map(
+ (x, i) =>
+ {
+ text: x.label!,
+ meta: x.detail!,
+ score: completions.length - i,
+ }
+ ),
+ })
+ }
+
+export default completer
diff --git a/packages/hoppscotch-app/helpers/editor/completion/index.ts b/packages/hoppscotch-app/helpers/editor/completion/index.ts
new file mode 100644
index 000000000..bc78e6193
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/completion/index.ts
@@ -0,0 +1,23 @@
+export type CompletionEntry = {
+ text: string
+ meta: string
+ score: number
+}
+
+export type CompleterResult = {
+ /**
+ * List of completions to display
+ */
+ completions: CompletionEntry[]
+}
+
+export type Completer = (
+ /**
+ * The contents of the editor
+ */
+ text: string,
+ /**
+ * Position where the completer is fired
+ */
+ completePos: { line: number; ch: number }
+) => Promise
diff --git a/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts b/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts
new file mode 100644
index 000000000..cc723155d
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/completion/preRequest.ts
@@ -0,0 +1,24 @@
+import { Completer, CompletionEntry } from "."
+import { getPreRequestScriptCompletions } from "~/helpers/tern"
+
+const completer: Completer = async (text, completePos) => {
+ const results = await getPreRequestScriptCompletions(
+ text,
+ completePos.line,
+ completePos.ch
+ )
+
+ const completions = results.completions.map((completion: any, i: number) => {
+ return {
+ text: completion.name,
+ meta: completion.isKeyword ? "keyword" : completion.type,
+ score: results.completions.length - i,
+ }
+ })
+
+ return {
+ completions,
+ }
+}
+
+export default completer
diff --git a/packages/hoppscotch-app/helpers/editor/completion/testScript.ts b/packages/hoppscotch-app/helpers/editor/completion/testScript.ts
new file mode 100644
index 000000000..88286ac46
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/completion/testScript.ts
@@ -0,0 +1,24 @@
+import { Completer, CompletionEntry } from "."
+import { getTestScriptCompletions } from "~/helpers/tern"
+
+export const completer: Completer = async (text, completePos) => {
+ const results = await getTestScriptCompletions(
+ text,
+ completePos.line,
+ completePos.ch
+ )
+
+ const completions = results.completions.map((completion: any, i: number) => {
+ return {
+ text: completion.name,
+ meta: completion.isKeyword ? "keyword" : completion.type,
+ score: results.completions.length - i,
+ }
+ })
+
+ return {
+ completions,
+ }
+}
+
+export default completer
diff --git a/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts b/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts
new file mode 100644
index 000000000..648cfa732
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/linting/gqlQuery.ts
@@ -0,0 +1,58 @@
+import { Ref } from "@nuxtjs/composition-api"
+import {
+ GraphQLError,
+ GraphQLSchema,
+ parse as gqlParse,
+ validate as gqlValidate,
+} from "graphql"
+import { LinterDefinition, LinterResult } from "./linter"
+
+/**
+ * Creates a Linter function that can lint a GQL query against a given
+ * schema
+ */
+export const createGQLQueryLinter: (
+ schema: Ref
+) => LinterDefinition = (schema: Ref) => (text) => {
+ if (text === "") return Promise.resolve([])
+ if (!schema.value) return Promise.resolve([])
+
+ try {
+ const doc = gqlParse(text)
+
+ const results = gqlValidate(schema.value, doc).map(
+ ({ locations, message }) =>
+ {
+ from: {
+ line: locations![0].line - 1,
+ ch: locations![0].column - 1,
+ },
+ to: {
+ line: locations![0].line - 1,
+ ch: locations![0].column,
+ },
+ message,
+ severity: "error",
+ }
+ )
+
+ return Promise.resolve(results)
+ } catch (e) {
+ const err = e as GraphQLError
+
+ return Promise.resolve([
+ {
+ from: {
+ line: err.locations![0].line - 1,
+ ch: err.locations![0].column - 1,
+ },
+ to: {
+ line: err.locations![0].line - 1,
+ ch: err.locations![0].column,
+ },
+ message: err.message,
+ severity: "error",
+ },
+ ])
+ }
+}
diff --git a/packages/hoppscotch-app/helpers/editor/linting/json.ts b/packages/hoppscotch-app/helpers/editor/linting/json.ts
new file mode 100644
index 000000000..46a690197
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/linting/json.ts
@@ -0,0 +1,21 @@
+import { convertIndexToLineCh } from "../utils"
+import { LinterDefinition, LinterResult } from "./linter"
+import jsonParse from "~/helpers/jsonParse"
+
+const linter: LinterDefinition = (text) => {
+ try {
+ jsonParse(text)
+ return Promise.resolve([])
+ } catch (e: any) {
+ return Promise.resolve([
+ {
+ from: convertIndexToLineCh(text, e.start),
+ to: convertIndexToLineCh(text, e.end),
+ message: e.message,
+ severity: "error",
+ },
+ ])
+ }
+}
+
+export default linter
diff --git a/packages/hoppscotch-app/helpers/editor/linting/linter.ts b/packages/hoppscotch-app/helpers/editor/linting/linter.ts
new file mode 100644
index 000000000..704270cbe
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/linting/linter.ts
@@ -0,0 +1,7 @@
+export type LinterResult = {
+ message: string
+ severity: "warning" | "error"
+ from: { line: number; ch: number }
+ to: { line: number; ch: number }
+}
+export type LinterDefinition = (text: string) => Promise
diff --git a/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts b/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts
new file mode 100644
index 000000000..db42d9864
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/linting/preRequest.ts
@@ -0,0 +1,69 @@
+import * as esprima from "esprima"
+import { LinterDefinition, LinterResult } from "./linter"
+import { performPreRequestLinting } from "~/helpers/tern"
+
+const linter: LinterDefinition = async (text) => {
+ let results: LinterResult[] = []
+
+ // Semantic linting
+ const semanticLints = await performPreRequestLinting(text)
+
+ results = results.concat(
+ semanticLints.map((lint: any) => ({
+ from: lint.from,
+ to: lint.to,
+ severity: "error",
+ message: `[semantic] ${lint.message}`,
+ }))
+ )
+
+ // Syntax linting
+ try {
+ const res: any = esprima.parseScript(text, { tolerant: true })
+ if (res.errors && res.errors.length > 0) {
+ results = results.concat(
+ res.errors.map((err: any) => {
+ const fromPos: { line: number; ch: number } = {
+ line: err.lineNumber - 1,
+ ch: err.column - 1,
+ }
+
+ const toPos: { line: number; ch: number } = {
+ line: err.lineNumber - 1,
+ ch: err.column,
+ }
+
+ return {
+ from: fromPos,
+ to: toPos,
+ message: `[syntax] ${err.description}`,
+ severity: "error",
+ }
+ })
+ )
+ }
+ } catch (e) {
+ const fromPos: { line: number; ch: number } = {
+ line: e.lineNumber - 1,
+ ch: e.column - 1,
+ }
+
+ const toPos: { line: number; ch: number } = {
+ line: e.lineNumber - 1,
+ ch: e.column,
+ }
+
+ results = results.concat([
+ {
+ from: fromPos,
+ to: toPos,
+ message: `[syntax] ${e.description}`,
+ severity: "error",
+ },
+ ])
+ }
+
+ return results
+}
+
+export default linter
diff --git a/packages/hoppscotch-app/helpers/editor/linting/testScript.ts b/packages/hoppscotch-app/helpers/editor/linting/testScript.ts
new file mode 100644
index 000000000..902d1778c
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/linting/testScript.ts
@@ -0,0 +1,69 @@
+import * as esprima from "esprima"
+import { LinterDefinition, LinterResult } from "./linter"
+import { performTestLinting } from "~/helpers/tern"
+
+const linter: LinterDefinition = async (text) => {
+ let results: LinterResult[] = []
+
+ // Semantic linting
+ const semanticLints = await performTestLinting(text)
+
+ results = results.concat(
+ semanticLints.map((lint: any) => ({
+ from: lint.from,
+ to: lint.to,
+ severity: "error",
+ message: `[semantic] ${lint.message}`,
+ }))
+ )
+
+ // Syntax linting
+ try {
+ const res: any = esprima.parseScript(text, { tolerant: true })
+ if (res.errors && res.errors.length > 0) {
+ results = results.concat(
+ res.errors.map((err: any) => {
+ const fromPos: { line: number; ch: number } = {
+ line: err.lineNumber - 1,
+ ch: err.column - 1,
+ }
+
+ const toPos: { line: number; ch: number } = {
+ line: err.lineNumber - 1,
+ ch: err.column,
+ }
+
+ return {
+ from: fromPos,
+ to: toPos,
+ message: `[syntax] ${err.description}`,
+ severity: "error",
+ }
+ })
+ )
+ }
+ } catch (e) {
+ const fromPos: { line: number; ch: number } = {
+ line: e.lineNumber - 1,
+ ch: e.column - 1,
+ }
+
+ const toPos: { line: number; ch: number } = {
+ line: e.lineNumber - 1,
+ ch: e.column,
+ }
+
+ results = results.concat([
+ {
+ from: fromPos,
+ to: toPos,
+ message: `[syntax] ${e.description}`,
+ severity: "error",
+ },
+ ])
+ }
+
+ return results
+}
+
+export default linter
diff --git a/packages/hoppscotch-app/helpers/editor/modes/graphql.ts b/packages/hoppscotch-app/helpers/editor/modes/graphql.ts
new file mode 100644
index 000000000..2c4949882
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/modes/graphql.ts
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+import CodeMirror from "codemirror"
+import {
+ LexRules,
+ ParseRules,
+ isIgnored,
+ onlineParser,
+ State,
+} from "graphql-language-service-parser"
+
+/**
+ * The GraphQL mode is defined as a tokenizer along with a list of rules, each
+ * of which is either a function or an array.
+ *
+ * * Function: Provided a token and the stream, returns an expected next step.
+ * * Array: A list of steps to take in order.
+ *
+ * A step is either another rule, or a terminal description of a token. If it
+ * is a rule, that rule is pushed onto the stack and the parsing continues from
+ * that point.
+ *
+ * If it is a terminal description, the token is checked against it using a
+ * `match` function. If the match is successful, the token is colored and the
+ * rule is stepped forward. If the match is unsuccessful, the remainder of the
+ * rule is skipped and the previous rule is advanced.
+ *
+ * This parsing algorithm allows for incremental online parsing within various
+ * levels of the syntax tree and results in a structured `state` linked-list
+ * which contains the relevant information to produce valuable typeaheads.
+ */
+CodeMirror.defineMode("graphql", (config) => {
+ const parser = onlineParser({
+ eatWhitespace: (stream) => stream.eatWhile(isIgnored),
+ lexRules: LexRules,
+ parseRules: ParseRules,
+ editorConfig: { tabSize: 2 },
+ })
+
+ return {
+ config,
+ startState: parser.startState,
+ token: parser.token as unknown as CodeMirror.Mode["token"], // TODO: Check if the types are indeed compatible
+ indent,
+ electricInput: /^\s*[})\]]/,
+ fold: "brace",
+ lineComment: "#",
+ closeBrackets: {
+ pairs: '()[]{}""',
+ explode: "()[]{}",
+ },
+ }
+})
+
+// Seems the electricInput type in @types/codemirror is wrong (i.e it is written as electricinput instead of electricInput)
+function indent(
+ this: CodeMirror.Mode & {
+ electricInput?: RegExp
+ config?: CodeMirror.EditorConfiguration
+ },
+ state: State,
+ textAfter: string
+) {
+ const levels = state.levels
+ // If there is no stack of levels, use the current level.
+ // Otherwise, use the top level, pre-emptively dedenting for close braces.
+ const level =
+ !levels || levels.length === 0
+ ? state.indentLevel
+ : levels[levels.length - 1] -
+ (this.electricInput?.test(textAfter) ? 1 : 0)
+ return (level || 0) * (this.config?.indentUnit || 0)
+}
diff --git a/packages/hoppscotch-app/helpers/editor/utils.ts b/packages/hoppscotch-app/helpers/editor/utils.ts
new file mode 100644
index 000000000..c7dcb1c12
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/editor/utils.ts
@@ -0,0 +1,38 @@
+export function convertIndexToLineCh(
+ text: string,
+ i: number
+): { line: number; ch: number } {
+ const lines = text.split("\n")
+
+ let line = 0
+ let counter = 0
+
+ while (line < lines.length) {
+ if (i > lines[line].length + counter) {
+ counter += lines[line].length + 1
+ line++
+ } else {
+ return {
+ line: line + 1,
+ ch: i - counter + 1,
+ }
+ }
+ }
+
+ throw new Error("Invalid input")
+}
+
+export function convertLineChToIndex(
+ text: string,
+ lineCh: { line: number; ch: number }
+): number {
+ const textSplit = text.split("\n")
+
+ if (textSplit.length < lineCh.line) throw new Error("Invalid position")
+
+ const tillLineIndex = textSplit
+ .slice(0, lineCh.line)
+ .reduce((acc, line) => acc + line.length + 1, 0)
+
+ return tillLineIndex + lineCh.ch
+}
diff --git a/packages/hoppscotch-app/helpers/editorutils.js b/packages/hoppscotch-app/helpers/editorutils.js
index 0955bbf37..9dd46a4bf 100644
--- a/packages/hoppscotch-app/helpers/editorutils.js
+++ b/packages/hoppscotch-app/helpers/editorutils.js
@@ -1,12 +1,12 @@
const mimeToMode = {
- "text/plain": "plain_text",
- "text/html": "html",
- "application/xml": "xml",
- "application/hal+json": "json",
- "application/vnd.api+json": "json",
- "application/json": "json",
+ "text/plain": "text/x-yaml",
+ "text/html": "htmlmixed",
+ "application/xml": "application/xml",
+ "application/hal+json": "application/ld+json",
+ "application/vnd.api+json": "application/ld+json",
+ "application/json": "application/ld+json",
}
export function getEditorLangForMimeType(mimeType) {
- return mimeToMode[mimeType] || "plain_text"
+ return mimeToMode[mimeType] || "text/x-yaml"
}
diff --git a/packages/hoppscotch-app/helpers/jsonParse.js b/packages/hoppscotch-app/helpers/jsonParse.ts
similarity index 71%
rename from packages/hoppscotch-app/helpers/jsonParse.js
rename to packages/hoppscotch-app/helpers/jsonParse.ts
index d196e3cc1..6262ba278 100644
--- a/packages/hoppscotch-app/helpers/jsonParse.js
+++ b/packages/hoppscotch-app/helpers/jsonParse.ts
@@ -19,7 +19,75 @@
* - end: int - the end exclusive offset of the syntax error
*
*/
-export default function jsonParse(str) {
+type JSONEOFValue = {
+ kind: "EOF"
+ start: number
+ end: number
+}
+
+type JSONNullValue = {
+ kind: "Null"
+ start: number
+ end: number
+}
+
+type JSONNumberValue = {
+ kind: "Number"
+ start: number
+ end: number
+ value: number
+}
+
+type JSONStringValue = {
+ kind: "String"
+ start: number
+ end: number
+ value: string
+}
+
+type JSONBooleanValue = {
+ kind: "Boolean"
+ start: number
+ end: number
+ value: boolean
+}
+
+type JSONPrimitiveValue =
+ | JSONNullValue
+ | JSONEOFValue
+ | JSONStringValue
+ | JSONNumberValue
+ | JSONBooleanValue
+
+export type JSONObjectValue = {
+ kind: "Object"
+ start: number
+ end: number
+ // eslint-disable-next-line no-use-before-define
+ members: JSONObjectMember[]
+}
+
+export type JSONArrayValue = {
+ kind: "Array"
+ start: number
+ end: number
+ // eslint-disable-next-line no-use-before-define
+ values: JSONValue[]
+}
+
+export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
+
+export type JSONObjectMember = {
+ kind: "Member"
+ start: number
+ end: number
+ key: JSONStringValue
+ value: JSONValue
+}
+
+export default function jsonParse(
+ str: string
+): JSONObjectValue | JSONArrayValue {
string = str
strLen = str.length
start = end = lastEnd = -1
@@ -37,15 +105,15 @@ export default function jsonParse(str) {
}
}
-let string
-let strLen
-let start
-let end
-let lastEnd
-let code
-let kind
+let string: string
+let strLen: number
+let start: number
+let end: number
+let lastEnd: number
+let code: number
+let kind: string
-function parseObj() {
+function parseObj(): JSONObjectValue {
const nodeStart = start
const members = []
expect("{")
@@ -63,9 +131,9 @@ function parseObj() {
}
}
-function parseMember() {
+function parseMember(): JSONObjectMember {
const nodeStart = start
- const key = kind === "String" ? curToken() : null
+ const key = kind === "String" ? (curToken() as JSONStringValue) : null
expect("String")
expect(":")
const value = parseVal()
@@ -73,14 +141,14 @@ function parseMember() {
kind: "Member",
start: nodeStart,
end: lastEnd,
- key,
+ key: key!,
value,
}
}
-function parseArr() {
+function parseArr(): JSONArrayValue {
const nodeStart = start
- const values = []
+ const values: JSONValue[] = []
expect("[")
if (!skip("]")) {
do {
@@ -96,7 +164,7 @@ function parseArr() {
}
}
-function parseVal() {
+function parseVal(): JSONValue {
switch (kind) {
case "[":
return parseArr()
@@ -111,14 +179,19 @@ function parseVal() {
lex()
return token
}
- return expect("Value")
+ return expect("Value") as never
}
-function curToken() {
- return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
+function curToken(): JSONPrimitiveValue {
+ return {
+ kind: kind as any,
+ start,
+ end,
+ value: JSON.parse(string.slice(start, end)),
+ }
}
-function expect(str) {
+function expect(str: string) {
if (kind === str) {
lex()
return
@@ -137,11 +210,17 @@ function expect(str) {
throw syntaxError(`Expected ${str} but found ${found}.`)
}
-function syntaxError(message) {
+type SyntaxError = {
+ message: string
+ start: number
+ end: number
+}
+
+function syntaxError(message: string): SyntaxError {
return { message, start, end }
}
-function skip(k) {
+function skip(k: string) {
if (kind === k) {
lex()
return true
@@ -227,7 +306,7 @@ function lex() {
function readString() {
ch()
while (code !== 34 && code > 31) {
- if (code === 92) {
+ if (code === (92 as any)) {
// \
ch()
switch (code) {
@@ -299,7 +378,7 @@ function readNumber() {
if (code === 69 || code === 101) {
// E e
ch()
- if (code === 43 || code === 45) {
+ if (code === (43 as any) || code === (45 as any)) {
// + -
ch()
}
diff --git a/packages/hoppscotch-app/helpers/lenses/htmlLens.js b/packages/hoppscotch-app/helpers/lenses/htmlLens.ts
similarity index 62%
rename from packages/hoppscotch-app/helpers/lenses/htmlLens.js
rename to packages/hoppscotch-app/helpers/lenses/htmlLens.ts
index 7a18570eb..8c4f9da65 100644
--- a/packages/hoppscotch-app/helpers/lenses/htmlLens.js
+++ b/packages/hoppscotch-app/helpers/lenses/htmlLens.ts
@@ -1,10 +1,12 @@
-const htmlLens = {
+import { Lens } from "./lenses"
+
+const htmlLens: Lens = {
lensName: "response.html",
isSupportedContentType: (contentType) =>
/\btext\/html|application\/xhtml\+xml\b/i.test(contentType),
renderer: "htmlres",
rendererImport: () =>
- import("~/components/lenses/renderers/HTMLLensRenderer"),
+ import("~/components/lenses/renderers/HTMLLensRenderer.vue"),
}
export default htmlLens
diff --git a/packages/hoppscotch-app/helpers/lenses/imageLens.js b/packages/hoppscotch-app/helpers/lenses/imageLens.ts
similarity index 67%
rename from packages/hoppscotch-app/helpers/lenses/imageLens.js
rename to packages/hoppscotch-app/helpers/lenses/imageLens.ts
index 2c49b38df..d08d204ad 100644
--- a/packages/hoppscotch-app/helpers/lenses/imageLens.js
+++ b/packages/hoppscotch-app/helpers/lenses/imageLens.ts
@@ -1,4 +1,6 @@
-const imageLens = {
+import { Lens } from "./lenses"
+
+const imageLens: Lens = {
lensName: "response.image",
isSupportedContentType: (contentType) =>
/\bimage\/(?:gif|jpeg|png|bmp|svg\+xml|x-icon|vnd\.microsoft\.icon)\b/i.test(
@@ -6,7 +8,7 @@ const imageLens = {
),
renderer: "imageres",
rendererImport: () =>
- import("~/components/lenses/renderers/ImageLensRenderer"),
+ import("~/components/lenses/renderers/ImageLensRenderer.vue"),
}
export default imageLens
diff --git a/packages/hoppscotch-app/helpers/lenses/jsonLens.js b/packages/hoppscotch-app/helpers/lenses/jsonLens.ts
similarity index 62%
rename from packages/hoppscotch-app/helpers/lenses/jsonLens.js
rename to packages/hoppscotch-app/helpers/lenses/jsonLens.ts
index 1d9135a24..28ad97348 100644
--- a/packages/hoppscotch-app/helpers/lenses/jsonLens.js
+++ b/packages/hoppscotch-app/helpers/lenses/jsonLens.ts
@@ -1,11 +1,12 @@
import { isJSONContentType } from "../utils/contenttypes"
+import { Lens } from "./lenses"
-const jsonLens = {
+const jsonLens: Lens = {
lensName: "response.json",
isSupportedContentType: isJSONContentType,
renderer: "json",
rendererImport: () =>
- import("~/components/lenses/renderers/JSONLensRenderer"),
+ import("~/components/lenses/renderers/JSONLensRenderer.vue"),
}
export default jsonLens
diff --git a/packages/hoppscotch-app/helpers/lenses/lenses.js b/packages/hoppscotch-app/helpers/lenses/lenses.js
deleted file mode 100644
index 674797fce..000000000
--- a/packages/hoppscotch-app/helpers/lenses/lenses.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import jsonLens from "./jsonLens"
-import rawLens from "./rawLens"
-import imageLens from "./imageLens"
-import htmlLens from "./htmlLens"
-import xmlLens from "./xmlLens"
-
-export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
-
-export function getSuitableLenses(response) {
- const contentType = response.headers.find((h) => h.key === "content-type")
-
- if (!contentType) return [rawLens]
-
- const result = []
- for (const lens of lenses) {
- if (lens.isSupportedContentType(contentType.value)) result.push(lens)
- }
-
- return result
-}
-
-export function getLensRenderers() {
- const response = {}
- for (const lens of lenses) {
- response[lens.renderer] = lens.rendererImport
- }
- return response
-}
diff --git a/packages/hoppscotch-app/helpers/lenses/lenses.ts b/packages/hoppscotch-app/helpers/lenses/lenses.ts
new file mode 100644
index 000000000..a6fe10728
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/lenses/lenses.ts
@@ -0,0 +1,42 @@
+import { HoppRESTResponse } from "../types/HoppRESTResponse"
+import jsonLens from "./jsonLens"
+import rawLens from "./rawLens"
+import imageLens from "./imageLens"
+import htmlLens from "./htmlLens"
+import xmlLens from "./xmlLens"
+
+export type Lens = {
+ lensName: string
+ isSupportedContentType: (contentType: string) => boolean
+ renderer: string
+ rendererImport: () => Promise
+}
+
+export const lenses: Lens[] = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
+
+export function getSuitableLenses(response: HoppRESTResponse): Lens[] {
+ // return empty array if response is loading or error
+ if (response.type === "loading" || response.type === "network_fail") return []
+
+ const contentType = response.headers.find((h) => h.key === "content-type")
+
+ if (!contentType) return [rawLens]
+
+ const result = []
+ for (const lens of lenses) {
+ if (lens.isSupportedContentType(contentType.value)) result.push(lens)
+ }
+ return result
+}
+
+type LensRenderers = {
+ [key: string]: Lens["rendererImport"]
+}
+
+export function getLensRenderers(): LensRenderers {
+ const response: LensRenderers = {}
+ for (const lens of lenses) {
+ response[lens.renderer] = lens.rendererImport
+ }
+ return response
+}
diff --git a/packages/hoppscotch-app/helpers/lenses/rawLens.js b/packages/hoppscotch-app/helpers/lenses/rawLens.js
deleted file mode 100644
index f8a18e193..000000000
--- a/packages/hoppscotch-app/helpers/lenses/rawLens.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const rawLens = {
- lensName: "response.raw",
- isSupportedContentType: () => true,
- renderer: "raw",
- rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"),
-}
-
-export default rawLens
diff --git a/packages/hoppscotch-app/helpers/lenses/rawLens.ts b/packages/hoppscotch-app/helpers/lenses/rawLens.ts
new file mode 100644
index 000000000..ec3a7c64f
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/lenses/rawLens.ts
@@ -0,0 +1,11 @@
+import { Lens } from "./lenses"
+
+const rawLens: Lens = {
+ lensName: "response.raw",
+ isSupportedContentType: () => true,
+ renderer: "raw",
+ rendererImport: () =>
+ import("~/components/lenses/renderers/RawLensRenderer.vue"),
+}
+
+export default rawLens
diff --git a/packages/hoppscotch-app/helpers/lenses/xmlLens.js b/packages/hoppscotch-app/helpers/lenses/xmlLens.ts
similarity index 50%
rename from packages/hoppscotch-app/helpers/lenses/xmlLens.js
rename to packages/hoppscotch-app/helpers/lenses/xmlLens.ts
index d393be5f2..2f126bbb7 100644
--- a/packages/hoppscotch-app/helpers/lenses/xmlLens.js
+++ b/packages/hoppscotch-app/helpers/lenses/xmlLens.ts
@@ -1,8 +1,11 @@
-const xmlLens = {
+import { Lens } from "./lenses"
+
+const xmlLens: Lens = {
lensName: "response.xml",
isSupportedContentType: (contentType) => /\bxml\b/i.test(contentType),
renderer: "xmlres",
- rendererImport: () => import("~/components/lenses/renderers/XMLLensRenderer"),
+ rendererImport: () =>
+ import("~/components/lenses/renderers/XMLLensRenderer.vue"),
}
export default xmlLens
diff --git a/packages/hoppscotch-app/helpers/newOutline.ts b/packages/hoppscotch-app/helpers/newOutline.ts
new file mode 100644
index 000000000..b40efaa7b
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/newOutline.ts
@@ -0,0 +1,100 @@
+import {
+ JSONArrayValue,
+ JSONObjectMember,
+ JSONObjectValue,
+ JSONValue,
+} from "./jsonParse"
+
+type RootEntry =
+ | {
+ kind: "RootObject"
+ astValue: JSONObjectValue
+ }
+ | {
+ kind: "RootArray"
+ astValue: JSONArrayValue
+ }
+
+type ObjectMemberEntry = {
+ kind: "ObjectMember"
+ name: string
+ astValue: JSONObjectMember
+ astParent: JSONObjectValue
+}
+
+type ArrayMemberEntry = {
+ kind: "ArrayMember"
+ index: number
+ astValue: JSONValue
+ astParent: JSONArrayValue
+}
+
+type PathEntry = RootEntry | ObjectMemberEntry | ArrayMemberEntry
+
+export function getJSONOutlineAtPos(
+ jsonRootAst: JSONObjectValue | JSONArrayValue,
+ posIndex: number
+): PathEntry[] | null {
+ try {
+ const rootObj = jsonRootAst
+
+ if (posIndex > rootObj.end || posIndex < rootObj.start)
+ throw new Error("Invalid position")
+
+ let current: JSONValue = rootObj
+
+ const path: PathEntry[] = []
+
+ if (rootObj.kind === "Object") {
+ path.push({
+ kind: "RootObject",
+ astValue: rootObj,
+ })
+ } else {
+ path.push({
+ kind: "RootArray",
+ astValue: rootObj,
+ })
+ }
+
+ while (current.kind === "Object" || current.kind === "Array") {
+ if (current.kind === "Object") {
+ const next: JSONObjectMember | undefined = current.members.find(
+ (member) => member.start <= posIndex && member.end >= posIndex
+ )
+
+ if (!next) throw new Error("Couldn't find child")
+
+ path.push({
+ kind: "ObjectMember",
+ name: next.key.value,
+ astValue: next,
+ astParent: current,
+ })
+
+ current = next.value
+ } else {
+ const nextIndex = current.values.findIndex(
+ (value) => value.start <= posIndex && value.end >= posIndex
+ )
+
+ if (nextIndex < 0) throw new Error("Couldn't find child")
+
+ const next: JSONValue = current.values[nextIndex]
+
+ path.push({
+ kind: "ArrayMember",
+ index: nextIndex,
+ astValue: next,
+ astParent: current,
+ })
+
+ current = next
+ }
+ }
+
+ return path
+ } catch (e: any) {
+ return null
+ }
+}
diff --git a/packages/hoppscotch-app/helpers/outline.js b/packages/hoppscotch-app/helpers/outline.js
deleted file mode 100644
index 1eb3e31d5..000000000
--- a/packages/hoppscotch-app/helpers/outline.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import jsonParse from "./jsonParse"
-
-export default () => {
- let jsonAST = {}
- let path = []
-
- const init = (jsonStr) => {
- jsonAST = jsonParse(jsonStr)
- linkParents(jsonAST)
- }
-
- const setNewText = (jsonStr) => {
- init(jsonStr)
- path = []
- }
-
- const linkParents = (node) => {
- if (node.kind === "Object") {
- if (node.members) {
- node.members.forEach((m) => {
- m.parent = node
- linkParents(m)
- })
- }
- } else if (node.kind === "Array") {
- if (node.values) {
- node.values.forEach((v) => {
- v.parent = node
- linkParents(v)
- })
- }
- } else if (node.kind === "Member") {
- if (node.value) {
- node.value.parent = node
- linkParents(node.value)
- }
- }
- }
-
- const genPath = (index) => {
- let output = {}
- path = []
- let current = jsonAST
- if (current.kind === "Object") {
- path.push({ label: "{}", obj: "root" })
- } else if (current.kind === "Array") {
- path.push({ label: "[]", obj: "root" })
- }
- let over = false
-
- try {
- while (!over) {
- if (current.kind === "Object") {
- let i = 0
- let found = false
- while (i < current.members.length) {
- const m = current.members[i]
- if (m.start <= index && m.end >= index) {
- path.push({ label: m.key.value, obj: m })
- current = current.members[i]
- found = true
- break
- }
- i++
- }
- if (!found) over = true
- } else if (current.kind === "Array") {
- if (current.values) {
- let i = 0
- let found = false
- while (i < current.values.length) {
- const m = current.values[i]
- if (m.start <= index && m.end >= index) {
- path.push({ label: `[${i.toString()}]`, obj: m })
- current = current.values[i]
- found = true
- break
- }
- i++
- }
- if (!found) over = true
- } else over = true
- } else if (current.kind === "Member") {
- if (current.value) {
- if (current.value.start <= index && current.value.end >= index) {
- current = current.value
- } else over = true
- } else over = true
- } else if (
- current.kind === "String" ||
- current.kind === "Number" ||
- current.kind === "Boolean" ||
- current.kind === "Null"
- ) {
- if (current.start <= index && current.end >= index) {
- path.push({ label: `${current.value}`, obj: current })
- }
- over = true
- }
- }
- output = { success: true, res: path.map((p) => p.label) }
- } catch (e) {
- output = { success: false, res: e }
- }
- return output
- }
-
- const getSiblings = (index) => {
- const parent = path[index]?.obj?.parent
- if (!parent) return []
- else if (parent.kind === "Object") {
- return parent.members
- } else if (parent.kind === "Array") {
- return parent.values
- } else return []
- }
-
- return {
- init,
- genPath,
- getSiblings,
- setNewText,
- }
-}
diff --git a/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts b/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts
index 6293b583a..f80def03b 100644
--- a/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts
+++ b/packages/hoppscotch-app/helpers/types/HoppRequestSaveContext.ts
@@ -28,4 +28,12 @@ export type HoppRequestSaveContext =
* ID of the request in the team
*/
requestID: string
+ /**
+ * ID of the team
+ */
+ teamID?: string
+ /**
+ * ID of the collection loaded
+ */
+ collectionID?: string
}
diff --git a/packages/hoppscotch-app/helpers/utils/StreamUtils.ts b/packages/hoppscotch-app/helpers/utils/StreamUtils.ts
index fad17f58e..342e9b4b3 100644
--- a/packages/hoppscotch-app/helpers/utils/StreamUtils.ts
+++ b/packages/hoppscotch-app/helpers/utils/StreamUtils.ts
@@ -8,9 +8,9 @@ import { map } from "rxjs/operators"
*
* @returns The constructed object observable
*/
-export function constructFromStreams(
- streamObj: { [key in keyof T]: Observable }
-): Observable {
+export function constructFromStreams(streamObj: {
+ [key in keyof T]: Observable
+}): Observable {
return combineLatest(Object.values>(streamObj)).pipe(
map((streams) => {
const keys = Object.keys(streamObj) as (keyof T)[]
diff --git a/packages/hoppscotch-app/helpers/utils/b64.js b/packages/hoppscotch-app/helpers/utils/b64.ts
similarity index 91%
rename from packages/hoppscotch-app/helpers/utils/b64.js
rename to packages/hoppscotch-app/helpers/utils/b64.ts
index 942b003c4..3b364c0e9 100644
--- a/packages/hoppscotch-app/helpers/utils/b64.js
+++ b/packages/hoppscotch-app/helpers/utils/b64.ts
@@ -1,4 +1,4 @@
-export const decodeB64StringToArrayBuffer = (input) => {
+export function decodeB64StringToArrayBuffer(input: string): ArrayBuffer {
const bytes = Math.floor((input.length / 4) * 3)
const ab = new ArrayBuffer(bytes)
const uarray = new Uint8Array(ab)
diff --git a/packages/hoppscotch-app/helpers/utils/string.js b/packages/hoppscotch-app/helpers/utils/string.js
deleted file mode 100644
index 6967e1c35..000000000
--- a/packages/hoppscotch-app/helpers/utils/string.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export function getSourcePrefix(source) {
- const sourceEmojis = {
- // Source used for info messages.
- info: "\tℹ️ [INFO]:\t",
- // Source used for client to server messages.
- client: "\t⬅️ [SENT]:\t",
- // Source used for server to client messages.
- server: "\t➡️ [RECEIVED]:\t",
- }
- if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source]
- return ""
-}
diff --git a/packages/hoppscotch-app/helpers/utils/string.ts b/packages/hoppscotch-app/helpers/utils/string.ts
new file mode 100644
index 000000000..f23107591
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/utils/string.ts
@@ -0,0 +1,12 @@
+const sourceEmojis = {
+ // Source used for info messages.
+ info: "\tℹ️ [INFO]:\t",
+ // Source used for client to server messages.
+ client: "\t⬅️ [SENT]:\t",
+ // Source used for server to client messages.
+ server: "\t➡️ [RECEIVED]:\t",
+}
+
+export function getSourcePrefix(source: keyof typeof sourceEmojis) {
+ return sourceEmojis[source]
+}
diff --git a/packages/hoppscotch-app/layouts/default.vue b/packages/hoppscotch-app/layouts/default.vue
index 5bc5bfdcd..544568052 100644
--- a/packages/hoppscotch-app/layouts/default.vue
+++ b/packages/hoppscotch-app/layouts/default.vue
@@ -24,7 +24,7 @@
>
-
+
diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json
index a8f11b80c..8af61b8b7 100644
--- a/packages/hoppscotch-app/locales/en.json
+++ b/packages/hoppscotch-app/locales/en.json
@@ -421,6 +421,7 @@
"file_imported": "File imported",
"finished_in": "Finished in {duration}ms",
"history_deleted": "History deleted",
+ "linewrap": "Wrap lines",
"loading": "Loading...",
"none": "None",
"nothing_found": "Nothing found for",
diff --git a/packages/hoppscotch-app/modules/emit-volar-types.ts b/packages/hoppscotch-app/modules/emit-volar-types.ts
new file mode 100644
index 000000000..cad34e011
--- /dev/null
+++ b/packages/hoppscotch-app/modules/emit-volar-types.ts
@@ -0,0 +1,134 @@
+import { resolve } from "path"
+import { Module } from "@nuxt/types"
+import ts from "typescript"
+import chokidar from "chokidar"
+
+const { readdir, writeFile } = require("fs").promises
+
+function titleCase(str: string): string {
+ return str[0].toUpperCase() + str.substring(1)
+}
+
+async function* getFilesInDir(dir: string): AsyncIterable {
+ const dirents = await readdir(dir, { withFileTypes: true })
+ for (const dirent of dirents) {
+ const res = resolve(dir, dirent.name)
+ if (dirent.isDirectory()) {
+ yield* getFilesInDir(res)
+ } else {
+ yield res
+ }
+ }
+}
+
+async function getAllVueComponentPaths(): Promise {
+ const vueFilePaths: string[] = []
+
+ for await (const f of getFilesInDir("./components")) {
+ if (f.endsWith(".vue")) {
+ const componentsIndex = f.split("/").indexOf("components")
+
+ vueFilePaths.push(`./${f.split("/").slice(componentsIndex).join("/")}`)
+ }
+ }
+
+ return vueFilePaths
+}
+
+function resolveComponentName(filename: string): string {
+ const index = filename.split("/").indexOf("components")
+
+ return filename
+ .split("/")
+ .slice(index + 1)
+ .filter((x) => x !== "index.vue") // Remove index.vue
+ .map((x) => x.split(".vue")[0]) // Remove extension
+ .filter((x) => x.toUpperCase() !== x.toLowerCase()) // Remove non-word stuff
+ .map((x) => titleCase(x)) // titlecase it
+ .join("")
+}
+
+function createTSImports(components: [string, string][]) {
+ return components.map(([componentName, componentPath]) => {
+ return ts.factory.createImportDeclaration(
+ undefined,
+ undefined,
+ ts.factory.createImportClause(
+ false,
+ ts.factory.createIdentifier(componentName),
+ undefined
+ ),
+ ts.factory.createStringLiteral(componentPath)
+ )
+ })
+}
+
+function createTSProps(components: [string, string][]) {
+ return components.map(([componentName]) => {
+ return ts.factory.createPropertySignature(
+ undefined,
+ ts.factory.createIdentifier(componentName),
+ undefined,
+ ts.factory.createTypeQueryNode(ts.factory.createIdentifier(componentName))
+ )
+ })
+}
+
+function generateTypeScriptDef(components: [string, string][]) {
+ const statements = [
+ ...createTSImports(components),
+ ts.factory.createModuleDeclaration(
+ undefined,
+ [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
+ ts.factory.createIdentifier("global"),
+ ts.factory.createModuleBlock([
+ ts.factory.createInterfaceDeclaration(
+ undefined,
+ undefined,
+ ts.factory.createIdentifier("__VLS_GlobalComponents"),
+ undefined,
+ undefined,
+ [...createTSProps(components)]
+ ),
+ ]),
+ ts.NodeFlags.ExportContext |
+ ts.NodeFlags.GlobalAugmentation |
+ ts.NodeFlags.ContextFlags
+ ),
+ ]
+
+ const source = ts.factory.createSourceFile(
+ statements,
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ )
+
+ const printer = ts.createPrinter({
+ newLine: ts.NewLineKind.LineFeed,
+ })
+
+ return printer.printFile(source)
+}
+
+async function generateShim() {
+ const results = await getAllVueComponentPaths()
+ const fileComponentNameCombo: [string, string][] = results.map((x) => [
+ resolveComponentName(x),
+ x,
+ ])
+ const typescriptString = generateTypeScriptDef(fileComponentNameCombo)
+
+ await writeFile(resolve("shims-volar.d.ts"), typescriptString)
+}
+
+const module: Module<{}> = async function () {
+ if (!this.nuxt.options.dev) return
+
+ await generateShim()
+
+ chokidar.watch(resolve("../components/")).on("all", async () => {
+ await generateShim()
+ })
+}
+
+export default module
diff --git a/packages/hoppscotch-app/nuxt.config.js b/packages/hoppscotch-app/nuxt.config.js
index b77f39f75..39da30434 100644
--- a/packages/hoppscotch-app/nuxt.config.js
+++ b/packages/hoppscotch-app/nuxt.config.js
@@ -133,6 +133,7 @@ export default {
"@nuxtjs/composition-api/module",
// https://github.com/antfu/unplugin-vue2-script-setup
"unplugin-vue2-script-setup/nuxt",
+ "~/modules/emit-volar-types.ts",
],
// Modules (https://go.nuxtjs.dev/config-modules)
@@ -280,7 +281,7 @@ export default {
config.module.rules.push({
test: /\.js$/,
include: /(node_modules)/,
- exclude: /(node_modules)\/(ace-builds)|(@firebase)/,
+ exclude: /(node_modules)\/(@firebase)/,
loader: "babel-loader",
options: {
plugins: [
diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json
index 4afec6ad8..456b7e797 100644
--- a/packages/hoppscotch-app/package.json
+++ b/packages/hoppscotch-app/package.json
@@ -20,29 +20,26 @@
"lintfix": "eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix",
"test": "jest"
},
- "lint-staged": {
- "*.{ts,js,vue}": "eslint",
- "*.{css,scss,vue}": "stylelint"
- },
"dependencies": {
- "@apollo/client": "^3.4.10",
+ "@apollo/client": "^3.4.11",
"@nuxtjs/axios": "^5.13.6",
- "@nuxtjs/composition-api": "^0.28.0",
+ "@nuxtjs/composition-api": "^0.29.0",
"@nuxtjs/gtm": "^2.4.0",
"@nuxtjs/i18n": "^7.0.3",
"@nuxtjs/robots": "^2.5.0",
"@nuxtjs/sitemap": "^2.4.0",
"@nuxtjs/toast": "^3.3.1",
- "ace-builds": "^1.4.12",
"acorn": "^8.5.0",
"acorn-walk": "^8.2.0",
- "axios": "^0.21.4",
- "core-js": "^3.17.2",
+ "codemirror": "^5.62.3",
+ "codemirror-theme-github": "^1.0.0",
+ "core-js": "^3.17.3",
"esprima": "^4.0.1",
- "firebase": "^9.0.1",
+ "firebase": "^9.0.2",
"fuse.js": "^6.4.6",
"graphql": "^15.5.0",
"graphql-language-service-interface": "^2.8.4",
+ "graphql-language-service-parser": "^1.9.2",
"json-loader": "^0.5.7",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
@@ -65,7 +62,7 @@
},
"devDependencies": {
"@babel/core": "^7.15.5",
- "@babel/preset-env": "^7.15.4",
+ "@babel/preset-env": "^7.15.6",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@nuxt/types": "^2.15.8",
@@ -79,33 +76,34 @@
"@nuxtjs/stylelint-module": "^4.0.0",
"@nuxtjs/svg": "^0.2.0",
"@testing-library/jest-dom": "^5.14.1",
+ "@types/codemirror": "^5.60.2",
"@types/cookie": "^0.4.1",
+ "@types/esprima": "^4.0.3",
"@types/lodash": "^4.14.172",
"@types/splitpanes": "^2.2.1",
- "@vue/runtime-dom": "^3.2.10",
+ "@vue/runtime-dom": "^3.2.11",
"@vue/test-utils": "^1.2.2",
"babel-core": "^7.0.0-bridge.0",
- "babel-jest": "^27.1.0",
+ "babel-jest": "^27.2.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-nuxt": ">=2.0.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.17.0",
- "jest": "^27.1.0",
+ "jest": "^27.2.0",
"jest-serializer-vue": "^2.0.2",
- "lint-staged": "^11.1.2",
- "nuxt-windicss": "^1.2.3",
- "prettier": "^2.3.2",
+ "nuxt-windicss": "^1.2.4",
+ "prettier": "^2.4.0",
"pretty-quick": "^3.1.1",
"raw-loader": "^4.0.2",
- "sass": "^1.39.0",
+ "sass": "^1.40.1",
"sass-loader": "^10.2.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"ts-jest": "^27.0.5",
"typescript": "^4.2",
- "unplugin-vue2-script-setup": "^0.5.8",
+ "unplugin-vue2-script-setup": "^0.6.1",
"vue-jest": "^3.0.7",
"worker-loader": "^3.0.8"
}
diff --git a/packages/hoppscotch-app/pages/documentation.vue b/packages/hoppscotch-app/pages/documentation.vue
index 266a6efa3..2feb14210 100644
--- a/packages/hoppscotch-app/pages/documentation.vue
+++ b/packages/hoppscotch-app/pages/documentation.vue
@@ -61,17 +61,12 @@
@click.native="collectionJSON = '[]'"
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+