From 8ac78e39f1659619c6e1d98beb655258dfeb5b0e Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 31 Oct 2025 19:39:44 +0800 Subject: [PATCH] :construction: Added HTTP language parser --- frontend/package-lock.json | 333 +++++++++++++++++- frontend/package.json | 4 +- frontend/src/stores/editorStore.ts | 6 +- .../codeblock/lang-parser/codeblock.grammar | 3 +- .../codeblock/lang-parser/languages.ts | 2 + .../codeblock/lang-parser/parser.js | 8 +- .../editor/extensions/codeblock/state.ts | 14 + .../editor/extensions/codeblock/types.ts | 1 + .../editor/extensions/httpclient/index.ts | 31 ++ .../httpclient/language/build-parser.js | 53 +++ .../httpclient/language/http-highlight.ts | 75 ++++ .../httpclient/language/http-language.ts | 130 +++++++ .../httpclient/language/http.grammar | 267 ++++++++++++++ .../httpclient/language/http.grammar.js | 23 ++ .../httpclient/language/http.grammar.terms.js | 60 ++++ .../extensions/httpclient/language/index.ts | 3 + .../editor/extensions/httpclient/types.ts | 1 + .../httpclient/widgets/run-gutter.ts | 199 +++++++++++ 18 files changed, 1200 insertions(+), 13 deletions(-) create mode 100644 frontend/src/views/editor/extensions/httpclient/index.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/build-parser.js create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http-language.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar.js create mode 100644 frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js create mode 100644 frontend/src/views/editor/extensions/httpclient/language/index.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/types.ts create mode 100644 frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2409502..f91f3b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -80,6 +80,7 @@ "unplugin-vue-components": "^30.0.0", "vite": "^7.1.12", "vite-plugin-node-polyfills": "^0.24.0", + "vitest": "^4.0.4", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.1.1" } @@ -2293,6 +2294,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@toml-tools/lexer": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz", @@ -2312,6 +2320,24 @@ "chevrotain": "^11.0.1" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -2618,6 +2644,127 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.4.tgz", + "integrity": "sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "chai": "^6.0.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.4.tgz", + "integrity": "sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.19" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.4.tgz", + "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.4.tgz", + "integrity": "sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.4.tgz", + "integrity": "sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.4", + "magic-string": "^0.30.19", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.4.tgz", + "integrity": "sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.4.tgz", + "integrity": "sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.4", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.23", "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.23.tgz", @@ -2945,6 +3092,16 @@ "util": "^0.12.5" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/autolinker": { "version": "3.16.2", "resolved": "https://registry.npmmirror.com/autolinker/-/autolinker-3.16.2.tgz", @@ -3325,6 +3482,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -3902,6 +4069,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4182,6 +4356,16 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", @@ -6298,6 +6482,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6322,13 +6513,19 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.9.0.tgz", "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "license": "MIT", - "optional": true, - "peer": true + "devOptional": true, + "license": "MIT" }, "node_modules/stream-browserify": { "version": "3.0.0", @@ -6448,13 +6645,19 @@ "node": ">=0.6.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "license": "MIT", - "optional": true, - "peer": true + "devOptional": true, + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -6504,6 +6707,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.1.tgz", @@ -7049,6 +7262,97 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.4.tgz", + "integrity": "sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.4", + "@vitest/mocker": "4.0.4", + "@vitest/pretty-format": "4.0.4", + "@vitest/runner": "4.0.4", + "@vitest/snapshot": "4.0.4", + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.19", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.4", + "@vitest/browser-preview": "4.0.4", + "@vitest/browser-webdriverio": "4.0.4", + "@vitest/ui": "4.0.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -7236,6 +7540,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index d7ced64..29e2020 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,8 @@ "preview": "vite preview", "lint": "eslint", "lint:fix": "eslint --fix", - "build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js" + "build:lang-parser": "node src/views/editor/extensions/codeblock/lang-parser/build-parser.js", + "test": "vitest" }, "dependencies": { "@codemirror/autocomplete": "^6.19.0", @@ -85,6 +86,7 @@ "unplugin-vue-components": "^30.0.0", "vite": "^7.1.12", "vite-plugin-node-polyfills": "^0.24.0", + "vitest": "^4.0.4", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.1.1" } diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index b5811cf..0bd6fa5 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -23,6 +23,7 @@ import {AsyncManager} from '@/common/utils/asyncManager'; import {generateContentHash} from "@/common/utils/hashUtils"; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {EDITOR_CONFIG} from '@/common/constant/editor'; +import {createHttpClientExtension} from "@/views/editor/extensions/httpclient"; export interface DocumentStats { lines: number; @@ -154,6 +155,8 @@ export const useEditorStore = defineStore('editor', () => { enableAutoDetection: true }); + const httpExtension = createHttpClientExtension(); + // 再次检查操作有效性 if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); @@ -185,7 +188,8 @@ export const useEditorStore = defineStore('editor', () => { statsExtension, contentChangeExtension, codeBlockExtension, - ...dynamicExtensions + ...dynamicExtensions, + ...httpExtension ]; // 创建编辑器状态 diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar index f4513ff..64ed357 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/codeblock.grammar @@ -17,7 +17,8 @@ BlockLanguage { "css" | "xml" | "cpp" | "rs" | "cs" | "rb" | "sh" | "yaml" | "toml" | "go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" | "ps1" | "dart" | "scala" | "math" | "dockerfile" | "lua" | "vue" | "lezer" | - "liquid" | "wast" | "sass" | "less" | "angular" | "svelte" + "liquid" | "wast" | "sass" | "less" | "angular" | "svelte" | + "http" } @tokens { diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index c90ae97..807ef1e 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -23,6 +23,7 @@ import {sassLanguage} from "@codemirror/lang-sass"; import {lessLanguage} from "@codemirror/lang-less"; import {angularLanguage} from "@codemirror/lang-angular"; import { svelteLanguage } from "@replit/codemirror-lang-svelte"; +import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language"; import {StreamLanguage} from "@codemirror/language"; import {ruby} from "@codemirror/legacy-modes/mode/ruby"; @@ -224,6 +225,7 @@ export const LANGUAGES: LanguageInfo[] = [ filename: "index.svelte" } }), + new LanguageInfo("http", "Http", httpLanguage.parser, ["http"]), ]; diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js index 2d9fe78..326be17 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/parser.js @@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr" import {blockContent} from "./external-tokens.js" export const parser = LRParser.deserialize({ version: 14, - states: "!jQQOQOOOVOQO'#C`O#uOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#zOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$SOSO1G.fOOOP7+$Q7+$Q", - stateData: "$X~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO~OPVO~OUYO!SXO~O!SZO~O", + states: "!jQQOQOOOVOQO'#C`O#xOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#}OSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO$VOSO1G.fOOOP7+$Q7+$Q", + stateData: "$[~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTOwTOxTOyTOzTO{TO|TO}TO!OTO!PTO!QTO!RTO!STO~OPVO~OUYO!TXO~O!TZO~O", goto: "jWPPPX]aPdTROSTQOSRUPQSORWS", nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto", - maxTerm: 50, + maxTerm: 51, skippedNodes: [0], repeatNodeCount: 1, - tokenData: "3g~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_(s#_#`)r#`#a)}#a#b+z#d#e,k#f#g-d#g#h-w#h#i0n#j#k1s#k#l2U#l#m2m#m#n3OR!fP!SQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eP#a#b(h~(kP#`#a(n~(sO]~~(vQ#T#U(|#g#h)_~)PP#j#k)S~)VP#T#U)Y~)_O`~~)dPo~#c#d)g~)jP#b#c)m~)rOZ~~)uP#h#i)x~)}Or~~*QR#X#Y*Z#]#^+Q#i#j+o~*^Q#g#h*d#n#o*o~*gP#g#h*j~*oO!P~~*rP#X#Y*u~*xP#f#g*{~+QO{~~+TP#e#f+W~+ZP#i#j+^~+aP#]#^+d~+gP#W#X+j~+oO|~~+rP#T#U+u~+zOy~~+}Q#T#U,T#W#X,f~,WP#h#i,Z~,^P#[#],a~,fOw~~,kO_~~,nR#[#],w#g#h-S#m#n-_~,zP#d#e,}~-SOa~~-VP!R!S-Y~-_Ot~~-dO[~~-gQ#U#V-m#g#h-r~-rOg~~-wOe~~-zU#T#U.^#V#W.o#[#]/W#e#f/]#j#k/h#k#l0V~.aP#g#h.d~.gP#g#h.j~.oO!O~~.rP#T#U.u~.xP#`#a.{~/OP#T#U/R~/WOv~~/]Oh~~/`P#`#a/c~/hO^~~/kP#X#Y/n~/qP#`#a/t~/wP#h#i/z~/}P#X#Y0Q~0VO!R~~0YP#]#^0]~0`P#Y#Z0c~0fP#h#i0i~0nOq~~0qR#X#Y0z#c#d1]#g#h1n~0}P#l#m1Q~1TP#h#i1W~1]OY~~1`P#a#b1c~1fP#`#a1i~1nOj~~1sOp~~1vP#i#j1y~1|P#X#Y2P~2UOz~~2XP#T#U2[~2_P#g#h2b~2eP#h#i2h~2mO}~~2pP#a#b2s~2vP#`#a2y~3OOc~~3RP#T#U3U~3XP#a#b3[~3_P#`#a3b~3gOi~", + tokenData: "3u~RdYZ!a}!O!z#T#U#V#V#W$Q#W#X%R#X#Y&t#Z#['_#[#]([#^#_)R#_#`*Q#`#a*]#a#b,Y#d#e,y#f#g-r#g#h.V#h#i0|#j#k2R#k#l2d#l#m2{#m#n3^R!fP!TQ%&x%&y!iP!lP%&x%&y!oP!rP%&x%&y!uP!zOXP~!}P#T#U#Q~#VOU~~#YP#b#c#]~#`P#Z#[#c~#fP#i#j#i~#lP#`#a#o~#rP#T#U#u~#xP#f#g#{~$QO!Q~~$TR#`#a$^#d#e$i#g#h$t~$aP#^#_$d~$iOl~~$lP#d#e$o~$tOd~~$yPf~#g#h$|~%ROb~~%UQ#T#U%[#c#d%m~%_P#f#g%b~%eP#h#i%h~%mOu~~%pP#V#W%s~%vP#_#`%y~%|P#X#Y&P~&SP#f#g&V~&YP#Y#Z&]~&`P#]#^&c~&fP#`#a&i~&lP#X#Y&o~&tOx~~&wQ#f#g&}#l#m'Y~'QP#`#a'T~'YOn~~'_Om~~'bQ#c#d'h#f#g'm~'mOk~~'pP#c#d's~'vP#c#d'y~'|P#j#k(P~(SP#m#n(V~([Os~~(_P#h#i(b~(eQ#a#b(k#h#i(v~(nP#`#a(q~(vO]~~(yP#d#e(|~)RO!S~~)UQ#T#U)[#g#h)m~)_P#j#k)b~)eP#T#U)h~)mO`~~)rPo~#c#d)u~)xP#b#c){~*QOZ~~*TP#h#i*W~*]Or~~*`R#X#Y*i#]#^+`#i#j+}~*lQ#g#h*r#n#o*}~*uP#g#h*x~*}O!P~~+QP#X#Y+T~+WP#f#g+Z~+`O{~~+cP#e#f+f~+iP#i#j+l~+oP#]#^+r~+uP#W#X+x~+}O|~~,QP#T#U,T~,YOy~~,]Q#T#U,c#W#X,t~,fP#h#i,i~,lP#[#],o~,tOw~~,yO_~~,|R#[#]-V#g#h-b#m#n-m~-YP#d#e-]~-bOa~~-eP!R!S-h~-mOt~~-rO[~~-uQ#U#V-{#g#h.Q~.QOg~~.VOe~~.YU#T#U.l#V#W.}#[#]/f#e#f/k#j#k/v#k#l0e~.oP#g#h.r~.uP#g#h.x~.}O!O~~/QP#T#U/T~/WP#`#a/Z~/^P#T#U/a~/fOv~~/kOh~~/nP#`#a/q~/vO^~~/yP#X#Y/|~0PP#`#a0S~0VP#h#i0Y~0]P#X#Y0`~0eO!R~~0hP#]#^0k~0nP#Y#Z0q~0tP#h#i0w~0|Oq~~1PR#X#Y1Y#c#d1k#g#h1|~1]P#l#m1`~1cP#h#i1f~1kOY~~1nP#a#b1q~1tP#`#a1w~1|Oj~~2ROp~~2UP#i#j2X~2[P#X#Y2_~2dOz~~2gP#T#U2j~2mP#g#h2p~2sP#h#i2v~2{O}~~3OP#a#b3R~3UP#`#a3X~3^Oc~~3aP#T#U3d~3gP#a#b3j~3mP#`#a3p~3uOi~", tokenizers: [blockContent, 0, 1], topRules: {"Document":[0,2]}, tokenPrec: 0 diff --git a/frontend/src/views/editor/extensions/codeblock/state.ts b/frontend/src/views/editor/extensions/codeblock/state.ts index ff71df0..ca32b3d 100644 --- a/frontend/src/views/editor/extensions/codeblock/state.ts +++ b/frontend/src/views/editor/extensions/codeblock/state.ts @@ -27,6 +27,11 @@ export const blockState = StateField.define({ * 获取当前活动的块 */ export function getActiveNoteBlock(state: EditorState): Block | undefined { + // 检查 blockState 字段是否存在 + if (!state.field(blockState, false)) { + return undefined; + } + // 找到光标所在的块 const range = state.selection.asSingle().ranges[0]; return state.field(blockState).find(block => @@ -38,6 +43,9 @@ export function getActiveNoteBlock(state: EditorState): Block | undefined { * 获取第一个块 */ export function getFirstNoteBlock(state: EditorState): Block | undefined { + if (!state.field(blockState, false)) { + return undefined; + } return state.field(blockState)[0]; } @@ -45,6 +53,9 @@ export function getFirstNoteBlock(state: EditorState): Block | undefined { * 获取最后一个块 */ export function getLastNoteBlock(state: EditorState): Block | undefined { + if (!state.field(blockState, false)) { + return undefined; + } const blocks = state.field(blockState); return blocks[blocks.length - 1]; } @@ -53,6 +64,9 @@ export function getLastNoteBlock(state: EditorState): Block | undefined { * 根据位置获取块 */ export function getNoteBlockFromPos(state: EditorState, pos: number): Block | undefined { + if (!state.field(blockState, false)) { + return undefined; + } return state.field(blockState).find(block => block.range.from <= pos && block.range.to >= pos ); diff --git a/frontend/src/views/editor/extensions/codeblock/types.ts b/frontend/src/views/editor/extensions/codeblock/types.ts index 5c45111..562442f 100644 --- a/frontend/src/views/editor/extensions/codeblock/types.ts +++ b/frontend/src/views/editor/extensions/codeblock/types.ts @@ -65,6 +65,7 @@ export type SupportedLanguage = | 'less' | 'angular' | 'svelte' + | 'http' // HTTP Client /** * 创建块的选项 diff --git a/frontend/src/views/editor/extensions/httpclient/index.ts b/frontend/src/views/editor/extensions/httpclient/index.ts new file mode 100644 index 0000000..733fcfa --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/index.ts @@ -0,0 +1,31 @@ +/** + * HTTP Client 扩展 + */ + +import {Extension} from '@codemirror/state'; + +import {httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter'; + +/** + * 创建 HTTP Client 扩展 + */ +export function createHttpClientExtension(): Extension[] { + + const extensions: Extension[] = []; + + // HTTP 语言解析器 + // extensions.push(httpLanguage); + + // 运行按钮 Gutte + extensions.push(httpRunButtonGutter); + extensions.push(httpRunButtonTheme); + + + // TODO: 后续阶段添加 + // - 自动补全(可选) + // - 变量支持(可选) + + return extensions; +} + + diff --git a/frontend/src/views/editor/extensions/httpclient/language/build-parser.js b/frontend/src/views/editor/extensions/httpclient/language/build-parser.js new file mode 100644 index 0000000..f3878bf --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/build-parser.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +/** + * HTTP Grammar Parser Builder + * 编译 Lezer grammar 文件为 JavaScript parser + * 使用命令行方式编译 + */ + +import { execSync } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +console.log('🚀 开始编译 HTTP grammar parser...'); + +try { + // 检查语法文件是否存在 + const grammarFile = path.join(__dirname, 'http.grammar'); + if (!fs.existsSync(grammarFile)) { + throw new Error('语法文件 http.grammar 未找到'); + } + + console.log('📄 语法文件:', grammarFile); + + // 运行 lezer-generator + console.log('⚙️ 编译 parser...'); + execSync('npx lezer-generator http.grammar -o http.grammar.js', { + cwd: __dirname, + stdio: 'inherit' + }); + + // 检查生成的文件 + const parserFile = path.join(__dirname, 'http.grammar.js'); + const termsFile = path.join(__dirname, 'http.grammar.terms.js'); + + if (fs.existsSync(parserFile) && fs.existsSync(termsFile)) { + console.log('✅ Parser 文件成功生成!'); + console.log('📦 生成的文件:'); + console.log(' - http.grammar.js'); + console.log(' - http.grammar.terms.js'); + } else { + throw new Error('Parser 生成失败'); + } + + console.log('🎉 编译成功!'); + +} catch (error) { + console.error('❌ 编译失败:', error.message); + process.exit(1); +} + diff --git a/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts b/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts new file mode 100644 index 0000000..4176be3 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http-highlight.ts @@ -0,0 +1,75 @@ +import { styleTags, tags as t } from '@lezer/highlight'; + +/** + * HTTP Client 语法高亮配置 + */ +export const httpHighlighting = styleTags({ + // 注释 + LineComment: t.lineComment, + + // HTTP 方法关键字 + "GET POST PUT DELETE PATCH HEAD OPTIONS": t.keyword, + + // 关键字 + "HEADER BODY VARIABLES RESPONSE": t.keyword, + + // Body 类型 + "TEXT JSON XML FORM URLENCODED GRAPHQL BINARY": t.keyword, + + // 变量名 + VariableName: t.variableName, + VariableValue: t.string, + + // URL 和版本 + Url: t.url, + UrlText: t.url, + HttpVersion: t.literal, + + // Header + HeaderName: t.propertyName, + HeaderValue: t.string, + + // Body 内容 + BodyContent: t.content, + + // Variables 内容 + VariablesContent: t.content, + + // 响应 + StatusCode: t.number, + StatusText: t.string, + Duration: t.literal, + Size: t.literal, + Timestamp: t.literal, + + // 文件引用 + FilePath: t.string, + + // 模板表达式 + "{{ }}": t.special(t.brace), + "TemplateExpression/VariableName": t.variableName, + + // 成员访问 + "MemberExpression/VariableName": t.variableName, + "MemberExpression/PropertyName": t.propertyName, + PropertyName: t.propertyName, + + // 函数调用 + FunctionName: t.function(t.variableName), + + // 基础类型 + Number: t.number, + String: t.string, + + // 符号 + ": Spread": t.punctuation, + "( )": t.paren, + "[ ]": t.squareBracket, + "{ }": t.brace, + ".": t.derefOperator, + ", ;": t.separator, + "@": t.meta, + "$": t.meta, + "=": t.definitionOperator, +}); + diff --git a/frontend/src/views/editor/extensions/httpclient/language/http-language.ts b/frontend/src/views/editor/extensions/httpclient/language/http-language.ts new file mode 100644 index 0000000..af77aa2 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http-language.ts @@ -0,0 +1,130 @@ +import { LRLanguage, LanguageSupport, foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language'; +import { parser } from './http.grammar.js'; +import { httpHighlighting } from './http-highlight'; + +/** + * HTTP Client 语言定义 + */ + +// 配置折叠规则和高亮 +const httpParserWithMetadata = parser.configure({ + props: [ + // 应用语法高亮 + httpHighlighting, + + // 折叠规则:允许折叠多行 Body、Variables、Headers 等 + foldNodeProp.add({ + BodyStatement: foldInside, + VariablesStatement: foldInside, + Document: foldInside, + }), + + // 缩进规则 + indentNodeProp.add({ + BodyStatement: () => 2, + HeaderStatement: () => 0, + VariableDeclaration: () => 0, + }), + ], +}); + +// 创建 LR 语言实例 +export const httpLanguage = LRLanguage.define({ + parser: httpParserWithMetadata, + languageData: { + // 注释配置 + commentTokens: { line: '#' }, + + // 自动闭合括号 + closeBrackets: { brackets: ['(', '[', '{', '"', "'"] }, + + // 单词字符定义 + wordChars: '-_', + }, +}); + +/** + * HTTP Client 语言支持 + * 包含语法高亮、折叠、缩进等完整功能 + */ +export function http() { + return new LanguageSupport(httpLanguage, [ + httpLanguage.data.of({ + autocomplete: httpCompletion, + }), + ]); +} + +/** + * HTTP Client 自动补全 + */ +function httpCompletion(context: any) { + const word = context.matchBefore(/\w*/); + if (!word || (word.from === word.to && !context.explicit)) { + return null; + } + + return { + from: word.from, + options: [ + // HTTP 方法 + { label: 'GET', type: 'keyword', detail: 'HTTP Method' }, + { label: 'POST', type: 'keyword', detail: 'HTTP Method' }, + { label: 'PUT', type: 'keyword', detail: 'HTTP Method' }, + { label: 'DELETE', type: 'keyword', detail: 'HTTP Method' }, + { label: 'PATCH', type: 'keyword', detail: 'HTTP Method' }, + { label: 'HEAD', type: 'keyword', detail: 'HTTP Method' }, + { label: 'OPTIONS', type: 'keyword', detail: 'HTTP Method' }, + + // 关键字 + { label: 'HEADER', type: 'keyword', detail: 'Header Statement' }, + { label: 'BODY', type: 'keyword', detail: 'Body Statement' }, + { label: 'VARIABLES', type: 'keyword', detail: 'Variables Statement' }, + + // Body 类型 + { label: 'TEXT', type: 'keyword', detail: 'Body Type' }, + { label: 'JSON', type: 'keyword', detail: 'Body Type' }, + { label: 'XML', type: 'keyword', detail: 'Body Type' }, + { label: 'FORM', type: 'keyword', detail: 'Body Type' }, + { label: 'URLENCODED', type: 'keyword', detail: 'Body Type' }, + { label: 'GRAPHQL', type: 'keyword', detail: 'Body Type' }, + { label: 'BINARY', type: 'keyword', detail: 'Body Type' }, + + // HTTP 版本 + { label: 'HTTP/1.0', type: 'constant', detail: 'HTTP Version' }, + { label: 'HTTP/1.1', type: 'constant', detail: 'HTTP Version' }, + { label: 'HTTP/2.0', type: 'constant', detail: 'HTTP Version' }, + + // 常用 Headers + { label: 'Content-Type', type: 'property', detail: 'Header Name' }, + { label: 'Authorization', type: 'property', detail: 'Header Name' }, + { label: 'Accept', type: 'property', detail: 'Header Name' }, + { label: 'User-Agent', type: 'property', detail: 'Header Name' }, + { label: 'Cookie', type: 'property', detail: 'Header Name' }, + + // 常用 Content-Type + { label: 'application/json', type: 'constant', detail: 'Content Type' }, + { label: 'application/xml', type: 'constant', detail: 'Content Type' }, + { label: 'text/html', type: 'constant', detail: 'Content Type' }, + { label: 'text/plain', type: 'constant', detail: 'Content Type' }, + { label: 'multipart/form-data', type: 'constant', detail: 'Content Type' }, + { label: 'application/x-www-form-urlencoded', type: 'constant', detail: 'Content Type' }, + + // 特殊标记 + { label: '@timestamp', type: 'keyword', detail: 'Timestamp' }, + { label: '@file', type: 'keyword', detail: 'File Reference' }, + + // 内置函数 + { label: '$timestamp()', type: 'function', detail: 'Current Timestamp' }, + { label: '$uuid()', type: 'function', detail: 'Generate UUID' }, + { label: '$randomInt()', type: 'function', detail: 'Random Integer' }, + { label: '$hash()', type: 'function', detail: 'Hash Function' }, + ], + }; +} + +/** + * 导出语言定义和高亮配置 + */ +export { httpHighlighting }; + diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar b/frontend/src/views/editor/extensions/httpclient/language/http.grammar new file mode 100644 index 0000000..10b9948 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.grammar @@ -0,0 +1,267 @@ +@precedence { + member, + call +} + +@top Document { statement* } + +statement { + VariableDeclaration | + ResponseLine | + RequestLine | + HeaderStatement | + BodyStatement | + VariablesStatement +} + +// ==================== 变量定义 ==================== + +VariableDeclaration { + "@" VariableName "=" VariableValue ";" +} + +VariableName { word } + +VariableValue[isolate] { anyContent } + +// ==================== 请求行 ==================== + +RequestLine { + Method Url HttpVersion? ";" +} + +Method { + @specialize[@name="GET"] | + @specialize[@name="POST"] | + @specialize[@name="PUT"] | + @specialize[@name="DELETE"] | + @specialize[@name="PATCH"] | + @specialize[@name="HEAD"] | + @specialize[@name="OPTIONS"] | + @specialize[@name="CONNECT"] | + @specialize[@name="TRACE"] +} + +Url { urlPart+ } + +urlPart { urlContent | TemplateExpression } + +HttpVersion { httpVersionToken } + +// ==================== Header 语句 ==================== + +HeaderStatement { + HeaderKeyword HeaderName colon HeaderValue ";" +} + +colon { ":" } + +HeaderKeyword { @specialize[@name="HEADER"] } + +HeaderName { word } + +HeaderValue { headerValuePart* } + +headerValuePart { headerValueContent | TemplateExpression } + +// ==================== Body 语句 ==================== + +BodyStatement { + BodyKeyword BodyType BodyContent ";" +} + +BodyKeyword { @specialize[@name="BODY"] } + +BodyType { + @specialize[@name="JSON"] | + @specialize[@name="FORM"] | + @specialize[@name="URLENCODED"] | + @specialize[@name="GRAPHQL"] | + @specialize[@name="XML"] | + @specialize[@name="TEXT"] | + @specialize[@name="BINARY"] | + @specialize[@name="MULTIPART"] +} + +BodyContent { bodyContentPart* } + +bodyContentPart { bodyText | TemplateExpression | FileReference } + +FileReference { + "@file" FilePath +} + +FilePath { filePathContent } + +// ==================== Variables 语句 ==================== + +VariablesStatement { + VariablesKeyword VariablesContent ";" +} + +VariablesKeyword { @specialize[@name="VARIABLES"] } + +VariablesContent[isolate] { variablesContent } + +// ==================== 响应 ==================== + +// 响应行 - 固定格式:RESPONSE <状态码> <状态文本> <大小> <时间戳>; +ResponseLine { + ResponseKeyword StatusCode StatusText Size Timestamp ";" +} + +ResponseKeyword { @specialize[@name="RESPONSE"] } + +StatusCode { Number } + +StatusText { word+ } + +Size { Number sizeUnit } + +// 时间戳格式:YYYY-MM-DD HH:MM:SS 或 ISO8601 格式 +Timestamp { timestampContent } + +// ==================== 模板表达式 ==================== + +TemplateExpression { + "{{" templateContent "}}" +} + +templateContent { + VariableName | + MemberExpression | + FunctionCall +} + +MemberExpression { + VariableName !member ("." PropertyName)+ +} + +PropertyName { word } + +FunctionCall { + "$" FunctionName !call "(" argumentList? ")" +} + +FunctionName { word } + +argumentList { + argument ("," argument)* +} + +argument { Number | String | word } + +// ==================== Tokens ==================== + +@skip { spaces | newline | LineComment } + +@tokens { + // 空白字符 + spaces[@export] { $[ \t]+ } + + newline[@export] { $[\r\n] } + + // 注释 + LineComment[@export,isolate] { "#" ![\n]* } + + // 标识符 + identifierChar { @asciiLetter | $[_$] } + + word { identifierChar (identifierChar | @digit | $[-])* } + + // 数字 + Number { + @digit+ ("." @digit+)? + } + + // 字符串 + String { + '"' stringContentDouble* '"' | + "'" stringContentSingle* "'" + } + + stringContentDouble { ![\\\n"]+ | "\\" _ } + + stringContentSingle { ![\\\n']+ | "\\" _ } + + // URL 内容 - 排除空格、换行、特殊前缀字符 + // 允许 # (用于锚点),但不允许以 # 开头(避免和注释冲突) + urlContent { ![ \t\n\r@{};]+ } + + // HTTP 版本 + httpVersionToken { "HTTP/" @digit+ "." @digit+ } + + // Header 值内容 - 排除所有 skip tokens 和特殊字符 + headerValueContent { ![ \t\n\r#{};]+ } + + // Body 文本内容 - 排除空格、换行、分号 + // 允许大括号、#、@、$ 等,因为JSON/XML/GraphQL/email等需要它们 + // {{ 和 @file 是独立token,有更高优先级 + bodyText { ![ \t\n\r;]+ } + + // 任意内容(直到分号) - 只排除换行和分号 + // 允许空格、#、数字等所有字符 + anyContent { ![\n\r;]+ } + + // 文件路径内容 - 排除空格、换行、分号 + filePathContent { ![ \t\n\r;]+ } + + // Variables 内容 - 只排除分号,允许所有字符包括换行 + variablesContent { ![;]+ } + + // 时间戳内容 - 日期时间格式,包含日期时间字符 + timestampContent { timestampChar+ } + + timestampChar { @digit | $[-:TZ.+] } + + // Duration 单位 + durationUnit { "ms" | "s" | "m" } + + // Size 单位 + sizeUnit { "B" | "KB" | "MB" | "GB" } + + // 优先级声明 + @precedence { urlContent, LineComment } + + @precedence { "{{", bodyText } + + @precedence { "@file", bodyText } + + @precedence { "$", bodyText } + + @precedence { bodyText, LineComment } + + @precedence { anyContent, LineComment } + + @precedence { anyContent, spaces } + + @precedence { filePathContent, LineComment } + + @precedence { variablesContent, newline } + + @precedence { variablesContent, LineComment } + + @precedence { variablesContent, spaces } + + @precedence { "$", word } + + @precedence { spaces, newline, word } + + @precedence { httpVersionToken, urlContent, word } + + @precedence { "{{", bodyText } + + @precedence { word, bodyText } + + @precedence { Number, word } + + // 符号 + "(" ")" "[" "]" "{" "}" + + "." "," ":" "=" "@" "$" ";" "{{" "}}" + + // 组合 tokens + "@file" +} + +@detectDelim diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js new file mode 100644 index 0000000..ffc2d9c --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.js @@ -0,0 +1,23 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser} from "@lezer/lr" +const spec_word = {__proto__:null,RESPONSE:22, GET:38, POST:40, PUT:42, DELETE:44, PATCH:46, HEAD:48, OPTIONS:50, CONNECT:52, TRACE:54, HEADER:90, BODY:102, JSON:106, FORM:108, URLENCODED:110, GRAPHQL:112, XML:114, TEXT:116, BINARY:118, MULTIPART:120, VARIABLES:134} +export const parser = LRParser.deserialize({ + version: 14, + states: ",lQ]QPOOO!ZQPO'#C`OOQO'#Cf'#CfO!`QPO'#CeOOQO'#Cn'#CnO!eQQO'#CmOOQO'#DY'#DYOOQO'#D`'#D`O!mQPO'#D_OOQO'#Dp'#DpO#XQSO'#DoOOQO'#EO'#EOOOQO'#Ds'#DsQ]QPOOO#^QPO'#DXOOQO'#Cb'#CbO#cQPO,58zOOQO'#Ch'#ChO#hQPO,59PO#mQPO'#C{OOQO'#ET'#ETOOQO'#Du'#DuO#uQQO'#CxO$TQPO,59XOOQO'#Db'#DbO$]QWO,59yOOQO'#Dr'#DrO$kQPO,5:ZOOQO-E7q-E7qOOQO'#D['#D[O$pQPO,59sO$uQ`O1G.fOOQO'#Dt'#DtO$zQPO'#CjO%SQPO1G.kO%XQPO'#EVO%aQPO'#DROOQO'#EV'#EVO%fQPO,59gOOQO-E7s-E7sOOQO'#DW'#DWOOQO1G.s1G.sO%kQPO1G.sO%pQpO'#DlOOQO'#E]'#E]OOQO'#Dy'#DyO%uQWO'#DkO&TQPO1G/eOOQO1G/u1G/uO&YQ!bO1G/_OOQO'#Cd'#CdO&eQPO7+$QOOQO-E7r-E7rO&jQ#tO'#CkO&oQ#tO7+$VO&tQPO'#DvO&yQPO,59hOOQO'#DS'#DSO'RQPO,59mOOQO1G/R1G/ROOQO7+$_7+$_OOQO'#Dn'#DnOOQO,5:W,5:WOOQO-E7w-E7wOOQO7+%P7+%POOQO'#EZ'#EZOOQO'#Dx'#DxO'WQ!bO'#D^O'cQPO7+$yOOQO<a;=`<%l0z<%lO;O!_>dXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l;O<%lO0z!_?SP;=`<%l;O!a?dc!xQ#SS#Rp!t`#QWOX7wXY.oYZ/oZ]7w]^/o^p7wpq.oq!]7w!]!^0z!^!b7w!b!c;O!c#o7w#o#p;O#p#q7w#q#r;O#r;'S7w;'S;=`@o;=`<%l0z<%lO7w!a@rXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l7w<%lO0z!aAbP;=`<%l7w#qAre#O!b#SS#Rp!t`#QWOXAeXY.oYZ&mZ]Ae]^/o^pAepq.oqrAersCTst;Ot!]Ae!]!^0z!^#OAe#O#PDi#P#oAe#o#p;O#p#qAe#q#r;O#r;'SAe;'S;=`Fq<%lOAe#qCdaxP#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#qDvc#O!b#SS#Rp!t`#QWOXAeXY.oYZ/oZ]Ae]^/o^pAepq.oqsAest;Ot!]Ae!]!^0z!^#oAe#o#p;O#p#qAe#q#r;O#r;'SAe;'S;=`FR;=`<%l0z<%lOAe#qFUXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%lAe<%lO0z#qFtP;=`<%lAe#sGWe#O!b!xQ#SS#Rp!t`#QWOX,wXY.oYZ/oZ],w]^/o^p,wpq.oqs,wst7wt!],w!]!^0z!^!b,w!b!cAe!c#o,w#o#p;O#p#q,w#q#r;O#r;'S,w;'S;=`Hi;=`<%l0z<%lO,w#sHlXOY0zZr0zrs1gs#O0z#O#P1l#P;'S0z;'S;=`2h;=`<%l,w<%lO0z#sI[P;=`<%l,w~Inb!xQ#SS#Rp!t`#QWP~OXI_XYJvYZ&mZ]I_]^Kl^pI_pqJvq!]I_!]!^LY!^!bI_!b!cL}!c#oI_#o#pL}#p#qI_#q#rL}#r;'SI_;'S;=`NW<%lOI_~KPX#SS!t`P~OYJvYZ&mZ]Jv]^Kl^!]Jv!]!^LY!^;'SJv;'S;=`Lw<%lOJv~KsV#SSP~OYKlYZ&mZ!]Kl!]!^LY!^;'SKl;'S;=`Lq<%lOKl~L_SP~OYLYZ;'SLY;'S;=`Lk<%lOLY~LnP;=`<%lLY~LtP;=`<%lKl~LzP;=`<%lJv~M[[#SS#Rp!t`#QWP~OXL}XYJvYZ&mZ]L}]^Kl^pL}pqJvq!]L}!]!^LY!^;'SL};'S;=`NQ<%lOL}~NTP;=`<%lL}~NZP;=`<%lI_#sNql#O!b!xQ#SS#RptP!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!!zl#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!%Rh#O!b!xQ#SS#Rp!t`#QWOX!$rXY!&mYZ&mZ]!$r]^!'m^p!$rpq!&mqs!$rst!,|tw!$rwx6Zx!]!$r!]!^!(e!^!b!$r!b!c!4U!c#O!$r#O#P!8V#P#o!$r#o#p!.o#p#q!$r#q#r!.o#r;'S!$r;'S;=`!:g<%lO!$re!&t]#SS!t`OY!&mYZ&mZ]!&m]^!'m^w!&mwx3{x!]!&m!]!^!(e!^#O!&m#O#P!+a#P;'S!&m;'S;=`!,v<%lO!&mT!'rZ#SSOY!'mYZ&mZw!'mwx0gx!]!'m!]!^!(e!^#O!'m#O#P!*S#P;'S!'m;'S;=`!+Z<%lO!'mP!(hWOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|<%lO!(eP!)TRO;'S!(e;'S;=`!)^;=`O!(eP!)aXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!(e<%lO!(eP!*PP;=`<%l!(eT!*XU#SSO!]!'m!]!^!(e!^;'S!'m;'S;=`!*k;=`<%l!(e<%lO!'mT!*nXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!'m<%lO!(eT!+^P;=`<%l!'me!+hY#SS!t`OY!&mYZ!'mZ]!&m]^!'m^!]!&m!]!^!(e!^;'S!&m;'S;=`!,W;=`<%l!(e<%lO!&me!,ZXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!&m<%lO!(ee!,yP;=`<%l!&m!a!-Zf!xQ#SS#Rp!t`#QWOX!,|XY!&mYZ&mZ]!,|]^!'m^p!,|pq!&mqw!,|wx9jx!]!,|!]!^!(e!^!b!,|!b!c!.o!c#O!,|#O#P!1v#P#o!,|#o#p!.o#p#q!,|#q#r!.o#r;'S!,|;'S;=`!4O<%lO!,|!_!.z`#SS#Rp!t`#QWOX!.oXY!&mYZ&mZ]!.o]^!'m^p!.opq!&mqw!.owx<]x!]!.o!]!^!(e!^#O!.o#O#P!/|#P;'S!.o;'S;=`!1p<%lO!.o!_!0X]#SS#Rp!t`#QWOX!.oXY!&mYZ!'mZ]!.o]^!'m^p!.opq!&mq!]!.o!]!^!(e!^;'S!.o;'S;=`!1Q;=`<%l!(e<%lO!.o!_!1TXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!.o<%lO!(e!_!1sP;=`<%l!.o!a!2Tc!xQ#SS#Rp!t`#QWOX!,|XY!&mYZ!'mZ]!,|]^!'m^p!,|pq!&mq!]!,|!]!^!(e!^!b!,|!b!c!.o!c#o!,|#o#p!.o#p#q!,|#q#r!.o#r;'S!,|;'S;=`!3`;=`<%l!(e<%lO!,|!a!3cXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!,|<%lO!(e!a!4RP;=`<%l!,|#q!4cf#O!b#SS#Rp!t`#QWOX!4UXY!&mYZ&mZ]!4U]^!'m^p!4Upq!&mqs!4Ust!.otw!4UwxCTx!]!4U!]!^!(e!^#O!4U#O#P!5w#P#o!4U#o#p!.o#p#q!4U#q#r!.o#r;'S!4U;'S;=`!8P<%lO!4U#q!6Uc#O!b#SS#Rp!t`#QWOX!4UXY!&mYZ!'mZ]!4U]^!'m^p!4Upq!&mqs!4Ust!.ot!]!4U!]!^!(e!^#o!4U#o#p!.o#p#q!4U#q#r!.o#r;'S!4U;'S;=`!7a;=`<%l!(e<%lO!4U#q!7dXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!4U<%lO!(e#q!8SP;=`<%l!4U#s!8fe#O!b!xQ#SS#Rp!t`#QWOX!$rXY!&mYZ!'mZ]!$r]^!'m^p!$rpq!&mqs!$rst!,|t!]!$r!]!^!(e!^!b!$r!b!c!4U!c#o!$r#o#p!.o#p#q!$r#q#r!.o#r;'S!$r;'S;=`!9w;=`<%l!(e<%lO!$r#s!9zXOY!(eZw!(ewx1gx#O!(e#O#P!)Q#P;'S!(e;'S;=`!)|;=`<%l!$r<%lO!(e#s!:jP;=`<%l!$r#s!;OcwP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!Yn#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s!@icyP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!BXnqP#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Djn#O!b!v#t]P!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!Fh!P!Q$b!Q![!DV![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Fyn#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!Hw![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!I[n#O!b!v#t]P!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!Hw![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i!Kmn!PP#O!b!v#t!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t{$b{|!=w|}$b}!O!=w!O!P!=w!P!Q$b!Q![!=w![!]!=w!^!b$b!b!c)x!c!v$b!v!w!=w!w!|$b!|!}!=w!}#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b~!MpOR~#s!NRcVP#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#q# mcTP#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#Y)x#Y#Z#!x#Z#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p##Vc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#])x#]#^#$b#^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#$oc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#`)x#`#a#%z#a#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#&Xc#O!b#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#X)x#X#Y#'d#Y#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x#p#'sa#O!b!aW#SS#Rp!t`#QWOX)xXY%|YZ&mZ])x]^&m^p)xpq%|qs)xst(nt!])x!^#o)x#o#p(n#p#q)x#q#r(n#r;'S)x;'S;=`+[<%lO)x&i#)]l#O!b!u#t!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i#+fn#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!d!!i!d!e#(x!e!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#-un#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!v!!i!v!w#/s!w!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#0Un#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!v!!i!v!w#2S!w!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#2en#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!Q$b!Q![!!i![!]$b!^!b$b!b!c)x!c!r!!i!r!s#4c!s!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#4tm#O!b!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu}$b}!O!!i!O!P$b!P!Q#6o!Q![!!i![!]$b!^!b$b!b!c)x!c!}!!i!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#7Oe#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#8a![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#8pg#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!O$b!O!P#:X!P!Q$b!Q![#8a![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#:he#O!b!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#;y![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#s#<[e#O!b!|R!xQ#SS#Rp!t`#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[t!Q$b!Q![#;y![!]$b!^!b$b!b!c)x!c#o$b#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b&i#>Qr#O!b!v#t!xQ#SS#Rp!t`!sP#QWOX$bXY%|YZ&mZ]$b]^&m^p$bpq%|qs$bst'[tu!!iu{$b{|!=w|}$b}!O#=m!O!P!=w!P!Q$b!Q![#=m![!]!=w!^!b$b!b!c)x!c!v!!i!v!w#=m!w!|!!i!|!}#=m!}#R$b#R#S!!i#S#T$b#T#o!!i#o#p(n#p#q$b#q#r(n#r;'S$b;'S;=`+b<%lO$b#r#@g]#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^#o(n#o#p#A`#p;'S(n;'S;=`)l<%lO(n#r#AmZn!l#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^;'S(n;'S;=`)l<%lO(n!_#Bk]#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^#q(n#q#r#Cd#r;'S(n;'S;=`)l<%lO(n!_#CqZmP#SS#Rp!t`#QWOX(nXY%|YZ&mZ](n]^&m^p(npq%|q!](n!^;'S(n;'S;=`)l<%lO(n", + tokenizers: [0, 1, 2, 3, 4, 5, 6, 7], + topRules: {"Document":[0,2]}, + specialized: [{term: 81, get: (value) => spec_word[value] || -1}], + tokenPrec: 351 +}) diff --git a/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js new file mode 100644 index 0000000..7f9f96d --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/http.grammar.terms.js @@ -0,0 +1,60 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + spaces = 78, + newline = 79, + LineComment = 1, + Document = 2, + VariableDeclaration = 4, + VariableName = 6, + VariableValue = 8, + ResponseLine = 9, + ResponseKeyword = 10, + RESPONSE = 11, + StatusCode = 12, + Number = 13, + StatusText = 14, + Size = 15, + Timestamp = 16, + RequestLine = 17, + Method = 18, + GET = 19, + POST = 20, + PUT = 21, + DELETE = 22, + PATCH = 23, + HEAD = 24, + OPTIONS = 25, + CONNECT = 26, + TRACE = 27, + Url = 28, + TemplateExpression = 31, + MemberExpression = 32, + PropertyName = 34, + FunctionCall = 37, + FunctionName = 38, + String = 40, + HttpVersion = 42, + HeaderStatement = 43, + HeaderKeyword = 44, + HEADER = 45, + HeaderName = 46, + HeaderValue = 48, + BodyStatement = 49, + BodyKeyword = 50, + BODY = 51, + BodyType = 52, + JSON = 53, + FORM = 54, + URLENCODED = 55, + GRAPHQL = 56, + XML = 57, + TEXT = 58, + BINARY = 59, + MULTIPART = 60, + BodyContent = 61, + FileReference = 62, + FilePath = 64, + VariablesStatement = 65, + VariablesKeyword = 66, + VARIABLES = 67, + VariablesContent = 68 diff --git a/frontend/src/views/editor/extensions/httpclient/language/index.ts b/frontend/src/views/editor/extensions/httpclient/language/index.ts new file mode 100644 index 0000000..6f0e7f5 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/language/index.ts @@ -0,0 +1,3 @@ +export { http, httpLanguage, httpHighlighting } from './http-language'; +export { parser } from './http.grammar.js'; + diff --git a/frontend/src/views/editor/extensions/httpclient/types.ts b/frontend/src/views/editor/extensions/httpclient/types.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/types.ts @@ -0,0 +1 @@ + diff --git a/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts new file mode 100644 index 0000000..41ebe4d --- /dev/null +++ b/frontend/src/views/editor/extensions/httpclient/widgets/run-gutter.ts @@ -0,0 +1,199 @@ +import { EditorView, GutterMarker, gutter } from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; +import { getNoteBlockFromPos } from '../../codeblock/state'; +import type { SyntaxNode } from '@lezer/common'; + +// ==================== 常量定义 ==================== + +/** 支持的 HTTP 方法(小写) - 使用 Set 以提高查找性能 */ +const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'connect', 'trace']); + +/** 匹配 ### Request 标记的正则表达式 */ +const REQUEST_MARKER_REGEX = /^###\s+Request(?:\s|$)/i; + +/** 匹配 ### Response 标记的正则表达式 */ +const RESPONSE_MARKER_REGEX = /^###\s+Response/i; + +/** 匹配 HTTP 方法的正则表达式 */ +const HTTP_METHOD_REGEX = /^\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+/i; + +/** HTTP 方法在行首的最大偏移位置(字符数) */ +const MAX_METHOD_POSITION_OFFSET = 20; + +/** 向上查找 ### Request 标记的最大行数 */ +const MAX_REQUEST_MARKER_DISTANCE = 10; + +// ==================== 运行按钮 Marker ==================== + +/** + * 运行按钮 Gutter Marker + */ +class RunButtonMarker extends GutterMarker { + constructor(private readonly linePosition: number) { + super(); + } + + toDOM(view: EditorView) { + const button = document.createElement('button'); + button.className = 'cm-http-run-button'; + button.innerHTML = '▶'; + button.title = 'Run HTTP Request'; + button.setAttribute('aria-label', 'Run HTTP Request'); + + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.executeRequest(view); + }; + + return button; + } + + + + private async executeRequest(view: EditorView) { + console.log(`\n============ 执行 HTTP 请求 ============`); + console.log(`位置: ${this.linePosition}`); + + } +} + +/** + * 使用语法树检查一行是否是 HTTP 请求行(更可靠) + * 必须符合规则:前面有 ### Request,然后才是 GET/POST 等请求行 + */ +function isRequestLineInSyntaxTree(view: EditorView, lineFrom: number, lineTo: number): boolean { + const tree = syntaxTree(view.state); + let hasHttpMethod = false; + + // 遍历该行的语法树节点 + tree.iterate({ + from: lineFrom, + to: lineTo, + enter: (node: SyntaxNode) => { + // HTTP 解析器将 HTTP 方法(GET、POST 等)标记为 "keyword" + // 并且该节点应该在行首附近 + if (node.name === 'keyword' && + node.from >= lineFrom && + node.from < lineFrom + MAX_METHOD_POSITION_OFFSET) { + const text = view.state.sliceDoc(node.from, node.to); + if (HTTP_METHODS.has(text.toLowerCase())) { + // 检查前面是否有 ### Request 标记 + if (hasPrecedingRequestMarker(view, lineFrom)) { + hasHttpMethod = true; + } + } + } + } + }); + + return hasHttpMethod; +} + +/** + * 检查前面是否有 ### Request 标记 + * 只要包含 "### Request",后面可以跟任何描述文字 + */ +function hasPrecedingRequestMarker(view: EditorView, lineFrom: number): boolean { + const currentLineNum = view.state.doc.lineAt(lineFrom).number; + + // 向上查找前面的几行(最多往上找指定行数) + for (let i = currentLineNum - 1; + i >= Math.max(1, currentLineNum - MAX_REQUEST_MARKER_DISTANCE); + i--) { + const line = view.state.doc.line(i); + const lineText = view.state.sliceDoc(line.from, line.to).trim(); + + if (REQUEST_MARKER_REGEX.test(lineText)) { + return true; + } + + // 如果遇到 ### Response,停止查找 + if (RESPONSE_MARKER_REGEX.test(lineText)) { + return false; + } + + // 如果是空行,继续往上找 + if (lineText === '') { + continue; + } + + // 如果遇到另一个请求方法,停止查找 + if (HTTP_METHOD_REGEX.test(lineText)) { + return false; + } + } + + return false; +} + +/** + * 检查位置是否在 HTTP 块内 + */ +function isInHttpBlock(view: EditorView, pos: number): boolean { + try { + const block = getNoteBlockFromPos(view.state, pos); + return block?.language.name === 'http' || block?.language.name === 'rest'; + } catch { + return false; + } +} + +/** + * 创建运行按钮 Gutter + */ +export const httpRunButtonGutter = gutter({ + class: 'cm-http-gutter', + + // 为每一行决定是否显示 marker + lineMarker(view, line) { + const linePos = line.from; + + // 第一步:检查是否在 HTTP 块内 + if (!isInHttpBlock(view, linePos)) { + return null; + } + + // 第二步:使用语法树检查是否是请求行 + if (!isRequestLineInSyntaxTree(view, line.from, line.to)) { + return null; + } + + // 创建运行按钮 + return new RunButtonMarker(linePos); + }, + +}); + +export const httpRunButtonTheme = EditorView.baseTheme({ + // 运行按钮样式 + '.cm-http-run-button': { + // width: '18px', + // height: '18px', + border: 'none', + borderRadius: '2px', + backgroundColor: 'transparent', + color: '#4CAF50', // 绿色三角 + // fontSize: '13px', + // lineHeight: '16px', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '0', + transition: 'color 0.15s ease', + }, + + // 悬停效果 + '.cm-http-run-button:hover': { + color: '#45a049', // 深绿色 + // backgroundColor: 'rgba(76, 175, 80, 0.1)', // 淡绿色背景 + }, + + // 激活效果 + '.cm-http-run-button:active': { + color: '#3d8b40', + // backgroundColor: 'rgba(76, 175, 80, 0.2)', + }, +}); +