🎨 Add variable support for http client

This commit is contained in:
2025-11-02 17:20:22 +08:00
parent 4380ad010c
commit 5688304817
12 changed files with 1704 additions and 391 deletions

View File

@@ -5,14 +5,12 @@
import {Extension} from '@codemirror/state';
import {httpRequestsField, httpRunButtonGutter, httpRunButtonTheme} from './widgets/run-gutter';
import {responseCacheField} from "@/views/editor/extensions/httpclient/parser/response-inserter";
/**
* 创建 HTTP Client 扩展
*/
export function createHttpClientExtension(): Extension[] {
return [
responseCacheField,
httpRequestsField,
httpRunButtonGutter,
httpRunButtonTheme,

View File

@@ -9,14 +9,25 @@
// @json - JSON 格式(属性必须用逗号分隔)
// @formdata - 表单数据(属性必须用逗号分隔)
// @urlencoded - URL 编码格式(属性必须用逗号分隔)
// @text - 纯文本内容(使用 content 字段)
// @text - 纯文本内容
//
// 3. 响应数据
// 3. 变量定义
// @var {
// baseUrl: "https://api.example.com",
// version: "v1",
// timeout: 30000
// }
//
// 4. 变量引用:
// {{variableName}} - 简单引用
// {{variableName:default}} - 带默认值引用
//
// 5. 响应数据:
// 使用独立的 JSON 块
// # Response 200 OK 234ms
// { "code": 200, "message": "success" }
//
// 4. 注释:
// 6. 注释:
// # 单行注释
//
// 示例 1 - JSON 请求:
@@ -83,12 +94,55 @@
@top Document { item* }
item {
VarDeclaration |
RequestStatement |
ResponseDeclaration |
AtRule |
JsonObject |
JsonArray
}
// 变量声明
VarDeclaration {
@specialize[@name=VarKeyword]<AtKeyword, "@var">
JsonBlock
}
// 响应声明
// 格式:@response <status> <time>ms <timestamp> { <json> }
// 示例:@response 200 123ms 2025-10-31T10:30:31 { "data": "..." }
// 错误:@response error 0ms 2025-10-31T10:30:31 { "error": "..." }
ResponseDeclaration {
@specialize[@name=ResponseKeyword]<AtKeyword, "@response">
ResponseStatus
ResponseTime
ResponseTimestamp
ResponseBlock
}
// 响应状态状态码200 或 200-OK或 "error" 关键字
// 数字开头的状态码作为一个整体 token
ResponseStatus {
StatusCode |
@specialize[@name=ErrorStatus]<identifier, "error">
}
// 响应时间:数字 + "ms" 作为一个整体 token
ResponseTime {
TimeValue
}
// 响应时间戳ISO 8601 格式字符串
// 格式2025-10-31T10:30:31
ResponseTimestamp {
Timestamp
}
// 响应块:标准 JSON 对象或数组(支持带引号的 key
ResponseBlock {
JsonObject | JsonArray
}
// HTTP 请求 - URL 必须是字符串
RequestStatement {
Method Url Block
@@ -109,12 +163,12 @@ Method {
// URL 必须是字符串
Url { StringLiteral }
// @ 规则(支持多种请求体格式)
// @ 规则(支持多种请求体格式,后面可选逗号
AtRule {
JsonRule |
(JsonRule |
FormDataRule |
UrlEncodedRule |
TextRule
TextRule) ","?
}
// @json 块JSON 格式请求体(属性必须用逗号分隔)
@@ -141,13 +195,17 @@ TextRule {
JsonBlock
}
// 普通块结构(属性逗号可选)
// 普通块结构(属性逗号可选,最多一个请求体
Block {
"{" blockContent "}"
"{" blockContent? "}"
}
// 块内容:
// - 选项1: 只有属性
// - 选项2: 属性 + 请求体
// - 选项3: 属性 + 请求体 + 属性
blockContent {
(Property | AtRule)*
Property+ | Property* AtRule Property*
}
// HTTP 属性(逗号可选)
@@ -176,18 +234,20 @@ NumberLiteral {
numberLiteralInner Unit?
}
// HTTP 属性值(支持块嵌套)
// HTTP 属性值(支持块嵌套和变量引用
value {
StringLiteral |
NumberLiteral |
VariableRef |
Block |
identifier
}
// JSON 属性值(严格的 JSON 语法:字符串必须用引号)
// JSON 属性值(严格的 JSON 语法:字符串必须用引号,支持变量引用
jsonValue {
StringLiteral |
NumberLiteral |
VariableRef |
JsonBlock |
JsonTrue |
JsonFalse |
@@ -221,10 +281,11 @@ jsonArrayContent {
JsonValue ("," JsonValue)* ","?
}
// JSON 值(完整的 JSON 值类型)
// JSON 值(完整的 JSON 值类型,支持变量引用
JsonValue {
StringLiteral |
NumberLiteral |
VariableRef |
JsonObject |
JsonArray |
JsonTrue |
@@ -244,6 +305,14 @@ JsonNull { @specialize[@name=Null]<identifier, "null"> }
AtKeyword { "@" "-"? @asciiLetter (@asciiLetter | @digit | "-")* }
// 变量引用: {{variableName}} 或 {{variableName:default}} 或 {{obj.nested.path}}
VariableRef[isolate] {
"{{"
(@asciiLetter | $[_$]) (@asciiLetter | @digit | $[-_$] | ".")*
(":" (![\n}] | "}" ![}])*)?
"}}"
}
// 标识符(属性名,支持连字符)
identifier {
(@asciiLetter | $[_$])
@@ -253,9 +322,25 @@ JsonNull { @specialize[@name=Null]<identifier, "null"> }
// 单位(必须跟在数字后面,所以不单独匹配)
Unit { @asciiLetter+ }
// 时间戳ISO 8601 格式YYYY-MM-DDTHH:MM:SS
Timestamp[isolate] {
@digit @digit @digit @digit "-" @digit @digit "-" @digit @digit
"T" @digit @digit ":" @digit @digit ":" @digit @digit
}
// 状态码:纯数字或数字-字母组合200, 200-OK, 404-Not-Found
StatusCode {
@digit+ ("-" @asciiLetter (@asciiLetter | "-")*)?
}
// 时间值:数字 + ms123ms
TimeValue {
@digit+ "ms"
}
whitespace { @whitespace+ }
@precedence { identifier, Unit }
@precedence { Timestamp, TimeValue, StatusCode, numberLiteralInner, VariableRef, identifier, Unit }
numberLiteralInner {
("+" | "-")? (@digit+ ("." @digit*)? | "." @digit+)

View File

@@ -0,0 +1,263 @@
import { describe, it, expect } from 'vitest';
import { EditorState } from '@codemirror/state';
import { httpLanguage } from './index';
import { syntaxTree } from '@codemirror/language';
/**
* 创建测试用的 EditorState
*/
function createTestState(content: string): EditorState {
return EditorState.create({
doc: content,
extensions: [httpLanguage]
});
}
/**
* 检查节点是否存在
*/
function hasNode(state: EditorState, nodeName: string): boolean {
const tree = syntaxTree(state);
let found = false;
tree.iterate({
enter: (node) => {
if (node.name === nodeName) {
found = true;
return false;
}
}
});
return found;
}
/**
* 获取节点文本
*/
function getNodeText(state: EditorState, nodeName: string): string | null {
const tree = syntaxTree(state);
let text: string | null = null;
tree.iterate({
enter: (node) => {
if (node.name === nodeName) {
text = state.doc.sliceString(node.from, node.to);
return false;
}
}
});
return text;
}
describe('HTTP Grammar - @response 响应语法', () => {
it('✅ 成功响应 - 完整格式', () => {
const content = `@response 200 123ms 2025-10-31T10:30:31 {
"message": "success",
"data": [1, 2, 3]
}`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'ResponseStatus')).toBe(true);
expect(hasNode(state, 'ResponseTime')).toBe(true);
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
expect(hasNode(state, 'ResponseBlock')).toBe(true);
});
it('✅ 错误响应 - error 关键字', () => {
const content = `@response error 0ms 2025-10-31T10:30:31 {
"error": "Network timeout"
}`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'ErrorStatus')).toBe(true);
expect(hasNode(state, 'TimeUnit')).toBe(true);
});
it('✅ 响应与请求结合', () => {
const content = `GET "https://api.example.com/users" {}
@response 200 123ms 2025-10-31T10:30:31 {
"users": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}`;
const state = createTestState(content);
expect(hasNode(state, 'RequestStatement')).toBe(true);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
});
it('✅ 多个请求和响应', () => {
const content = `GET "https://api.example.com/users" {}
@response 200 100ms 2025-10-31T10:30:31 {
"users": []
}
POST "https://api.example.com/users" {
@json { "name": "Alice" }
}
@response 201 50ms 2025-10-31T10:30:32 {
"id": 1,
"name": "Alice"
}`;
const state = createTestState(content);
const tree = syntaxTree(state);
// 统计 ResponseDeclaration 数量
let responseCount = 0;
tree.iterate({
enter: (node) => {
if (node.name === 'ResponseDeclaration') {
responseCount++;
}
}
});
expect(responseCount).toBe(2);
});
it('✅ 响应状态码类型', () => {
const testCases = [
{ status: '200', shouldParse: true },
{ status: '201', shouldParse: true },
{ status: '404', shouldParse: true },
{ status: '500', shouldParse: true },
{ status: 'error', shouldParse: true }
];
testCases.forEach(({ status, shouldParse }) => {
const content = `@response ${status} 0ms 2025-10-31T10:30:31 { "data": null }`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(shouldParse);
});
});
it('✅ 响应时间单位', () => {
const content = `@response 200 12345ms 2025-10-31T10:30:31 {
"data": "test"
}`;
const state = createTestState(content);
expect(hasNode(state, 'TimeUnit')).toBe(true);
expect(getNodeText(state, 'TimeUnit')).toBe('ms');
});
it('✅ 响应块包含复杂 JSON', () => {
const content = `@response 200 150ms 2025-10-31T10:30:31 {
"status": "success",
"data": {
"users": [
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"active": true
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"active": false
}
],
"pagination": {
"page": 1,
"pageSize": 10,
"total": 100
}
}
}`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'JsonObject')).toBe(true);
expect(hasNode(state, 'JsonArray')).toBe(true);
});
it('✅ 空响应体', () => {
const content = `@response 204 50ms 2025-10-31T10:30:31 {}`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'ResponseBlock')).toBe(true);
});
it('✅ 响应体为数组', () => {
const content = `@response 200 80ms 2025-10-31T10:30:31 [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
expect(hasNode(state, 'JsonArray')).toBe(true);
});
it('✅ 时间戳格式', () => {
const testCases = [
'2025-10-31T10:30:31',
'2025-01-01T00:00:00',
'2025-12-31T23:59:59'
];
testCases.forEach(timestamp => {
const content = `@response 200 100ms ${timestamp} { "data": null }`;
const state = createTestState(content);
expect(hasNode(state, 'ResponseTimestamp')).toBe(true);
});
});
it('❌ 缺少必填字段应该有错误', () => {
const invalidCases = [
'@response 200 { "data": null }', // 缺少时间和时间戳
'@response 200 100ms { "data": null }', // 缺少时间戳
];
invalidCases.forEach(content => {
const state = createTestState(content);
const tree = syntaxTree(state);
// 检查是否有错误节点
let hasError = false;
tree.iterate({
enter: (node) => {
if (node.name === '⚠') {
hasError = true;
return false;
}
}
});
expect(hasError).toBe(true);
});
});
it('✅ 与变量结合', () => {
const content = `@var {
apiUrl: "https://api.example.com"
}
GET "https://api.example.com/users" {}
@response 200 123ms 2025-10-31T10:30:31 {
"users": []
}`;
const state = createTestState(content);
expect(hasNode(state, 'VarDeclaration')).toBe(true);
expect(hasNode(state, 'RequestStatement')).toBe(true);
expect(hasNode(state, 'ResponseDeclaration')).toBe(true);
});
});

View File

@@ -0,0 +1,350 @@
import { describe, it, expect } from 'vitest';
import { parser } from './http.parser';
/**
* HTTP 变量功能测试
*
* 测试变量定义 @var 和变量引用 {{variableName}} 语法
*/
describe('HTTP 变量功能测试', () => {
/**
* 辅助函数:解析代码并返回语法树
*/
function parseCode(code: string) {
const tree = parser.parse(code);
return tree;
}
/**
* 辅助函数:检查语法树中是否有错误节点
*/
function hasErrorNodes(tree: any): { hasError: boolean; errors: Array<{ name: string; from: number; to: number; text: string }> } {
const errors: Array<{ name: string; from: number; to: number; text: string }> = [];
tree.iterate({
enter: (node: any) => {
if (node.name === '⚠') {
errors.push({
name: node.name,
from: node.from,
to: node.to,
text: tree.toString().substring(node.from, node.to)
});
}
}
});
return {
hasError: errors.length > 0,
errors
};
}
/**
* 辅助函数:打印语法树结构(用于调试)
*/
function printTree(tree: any, code: string, maxDepth = 5) {
const lines: string[] = [];
tree.iterate({
enter: (node: any) => {
const depth = getNodeDepth(tree, node);
if (depth > maxDepth) return false;
const indent = ' '.repeat(depth);
const text = code.substring(node.from, Math.min(node.to, node.from + 30));
const displayText = text.length === 30 ? text + '...' : text;
lines.push(`${indent}${node.name} [${node.from}-${node.to}]: "${displayText.replace(/\n/g, '\\n')}"`);
}
});
return lines.join('\n');
}
function getNodeDepth(tree: any, targetNode: any): number {
let depth = 0;
let current = targetNode;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
it('✅ @var - 变量声明', () => {
const code = `@var {
baseUrl: "https://api.example.com",
version: "v1",
timeout: 30000
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ @var 变量声明格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
// 验证是否有 VarDeclaration 节点
let hasVarDeclaration = false;
let hasVarKeyword = false;
tree.iterate({
enter: (node: any) => {
if (node.name === 'VarDeclaration') hasVarDeclaration = true;
if (node.name === 'VarKeyword') hasVarKeyword = true;
}
});
expect(hasVarDeclaration).toBe(true);
expect(hasVarKeyword).toBe(true);
});
it('✅ 变量引用 - 在属性值中使用', () => {
const code = `GET "http://example.com" {
timeout: {{timeout}}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 变量引用格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
// 验证是否有 VariableRef 节点
let hasVariableRef = false;
tree.iterate({
enter: (node: any) => {
if (node.name === 'VariableRef') hasVariableRef = true;
}
});
expect(hasVariableRef).toBe(true);
});
it('✅ 变量引用 - 带默认值 {{variableName:default}}', () => {
const code = `GET "http://example.com" {
authorization: "Bearer {{token:default-token}}"
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 带默认值的变量引用格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 完整示例 - 变量定义和使用(用户提供的示例)', () => {
const code = `@var {
baseUrl: "https://api.example.com",
version: "v1",
timeout: 30000
}
GET "{{baseUrl}}/{{version}}/users" {
timeout: {{timeout}},
authorization: "Bearer {{token:default-token}}"
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 完整示例格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 变量在 JSON 请求体中使用', () => {
const code = `@var {
userId: "12345",
userName: "张三"
}
POST "http://api.example.com/users" {
@json {
id: {{userId}},
name: {{userName}},
email: "{{userName}}@example.com"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ JSON 中使用变量格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 变量在多个请求中使用', () => {
const code = `@var {
baseUrl: "https://api.example.com",
token: "Bearer abc123"
}
GET "{{baseUrl}}/users" {
authorization: {{token}}
}
POST "{{baseUrl}}/users" {
authorization: {{token}},
@json {
name: "test"
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 多请求中使用变量格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ URL 中包含多个变量', () => {
const code = `GET "{{protocol}}://{{host}}:{{port}}/{{path}}/{{resource}}" {
host: "example.com"
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ URL 多变量格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 变量名包含下划线和数字', () => {
const code = `@var {
api_base_url_v2: "https://api.example.com",
timeout_30s: 30000,
user_id_123: "123"
}
GET "{{api_base_url_v2}}/users/{{user_id_123}}" {
timeout: {{timeout_30s}}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 变量名包含下划线和数字格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 变量默认值包含特殊字符', () => {
const code = `GET "http://example.com" {
authorization: "Bearer {{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0}}"
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 变量默认值特殊字符格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
it('✅ 混合变量和普通值', () => {
const code = `@var {
baseUrl: "https://api.example.com",
version: "v1"
}
POST "{{baseUrl}}/{{version}}/users" {
content-type: "application/json",
authorization: "Bearer {{token:default}}",
user-agent: "Mozilla/5.0",
@json {
name: "张三",
age: 25,
apiUrl: {{baseUrl}},
apiVersion: {{version}}
}
}`;
const tree = parseCode(code);
const result = hasErrorNodes(tree);
if (result.hasError) {
console.log('\n❌ 混合变量和普通值格式错误:');
result.errors.forEach(err => {
console.log(` - ${err.name} at ${err.from}-${err.to}: "${err.text}"`);
});
console.log('\n完整语法树:');
console.log(printTree(tree, code));
}
expect(result.hasError).toBe(false);
});
});

View File

@@ -17,16 +17,26 @@ export const httpHighlighting = styleTags({
// 其他方法 - 使用修饰关键字
"TRACE CONNECT": t.modifier,
// ========== @ 规则(请求体格式)==========
// ========== @ 规则(请求体格式和变量声明==========
// @json, @formdata, @urlencoded - 使用类型名
"JsonKeyword FormDataKeyword UrlEncodedKeyword": t.typeName,
// @text - 使用特殊类型
"TextKeyword": t.special(t.typeName),
// @var - 变量声明关键字
"VarKeyword": t.definitionKeyword,
// @response - 响应关键字
"ResponseKeyword": t.keyword,
// @ 符号本身 - 使用元标记
"AtKeyword": t.meta,
// ========== 变量引用 ==========
// {{variableName}} - 使用特殊变量名
"VariableRef": t.special(t.definitionKeyword),
// ========== URL特殊处理==========
// URL 节点 - 使用链接颜色
"Url": t.link,
@@ -48,6 +58,22 @@ export const httpHighlighting = styleTags({
// 单位 - 单位颜色
"Unit": t.unit,
// ========== 响应相关 ==========
// 响应状态码 - 数字颜色
"StatusCode": t.number,
"ResponseStatus/StatusCode": t.number,
// 响应错误状态 - 关键字
"ErrorStatus": t.operatorKeyword,
// 响应时间 - 数字颜色
"TimeValue": t.number,
"ResponseTime": t.number,
// 时间戳 - 字符串颜色
"Timestamp": t.string,
"ResponseTimestamp": t.string,
// ========== 注释 ==========
// # 单行注释 - 行注释颜色
"LineComment": t.lineComment,
@@ -57,9 +83,13 @@ export const httpHighlighting = styleTags({
"JsonObject": t.brace,
"JsonArray": t.squareBracket,
// JSON 成员(属性名)
"JsonMember/StringLiteral": t.definition(t.propertyName),
"JsonMember/identifier": t.definition(t.propertyName),
// JSON 属性名 - 使用属性名颜色
"JsonProperty/PropertyName": t.propertyName,
"JsonProperty/StringLiteral": t.propertyName,
// JSON 成员(属性名)- 使用属性名颜色(适用于独立 JSON 对象)
"JsonMember/StringLiteral": t.propertyName,
"JsonMember/identifier": t.propertyName,
// JSON 字面量值
"True False": t.bool,

View File

@@ -2,42 +2,55 @@
export const
LineComment = 1,
Document = 2,
RequestStatement = 3,
Method = 4,
GET = 5,
POST = 6,
PUT = 7,
DELETE = 8,
PATCH = 9,
HEAD = 10,
OPTIONS = 11,
CONNECT = 12,
TRACE = 13,
Url = 14,
StringLiteral = 15,
Block = 18,
Property = 19,
NumberLiteral = 22,
Unit = 23,
AtRule = 25,
JsonRule = 26,
AtKeyword = 27,
JsonKeyword = 28,
JsonBlock = 29,
JsonProperty = 30,
JsonTrue = 32,
True = 33,
JsonFalse = 34,
False = 35,
JsonNull = 36,
Null = 37,
FormDataRule = 38,
FormDataKeyword = 39,
UrlEncodedRule = 40,
UrlEncodedKeyword = 41,
TextRule = 42,
TextKeyword = 43,
JsonObject = 44,
JsonMember = 45,
JsonValue = 46,
JsonArray = 49
VarDeclaration = 3,
AtKeyword = 4,
VarKeyword = 5,
JsonBlock = 8,
JsonProperty = 9,
StringLiteral = 12,
NumberLiteral = 13,
Unit = 14,
VariableRef = 15,
JsonTrue = 16,
True = 17,
JsonFalse = 18,
False = 19,
JsonNull = 20,
Null = 21,
RequestStatement = 23,
Method = 24,
GET = 25,
POST = 26,
PUT = 27,
DELETE = 28,
PATCH = 29,
HEAD = 30,
OPTIONS = 31,
CONNECT = 32,
TRACE = 33,
Url = 34,
Block = 35,
Property = 36,
AtRule = 38,
JsonRule = 39,
JsonKeyword = 40,
FormDataRule = 41,
FormDataKeyword = 42,
UrlEncodedRule = 43,
UrlEncodedKeyword = 44,
TextRule = 45,
TextKeyword = 46,
ResponseDeclaration = 47,
ResponseKeyword = 48,
ResponseStatus = 49,
StatusCode = 50,
ErrorStatus = 51,
ResponseTime = 52,
TimeValue = 53,
ResponseTimestamp = 54,
Timestamp = 55,
ResponseBlock = 56,
JsonObject = 57,
JsonMember = 58,
JsonValue = 59,
JsonArray = 62

View File

@@ -1,26 +1,26 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {httpHighlighting} from "./http.highlight"
const spec_identifier = {__proto__:null,GET:10, POST:12, PUT:14, DELETE:16, PATCH:18, HEAD:20, OPTIONS:22, CONNECT:24, TRACE:26, true:66, false:70, null:74}
const spec_AtKeyword = {__proto__:null,"@json":56, "@formdata":78, "@urlencoded":82, "@text":86}
const spec_AtKeyword = {__proto__:null,"@var":10, "@json":80, "@formdata":84, "@urlencoded":88, "@text":92, "@response":96}
const spec_identifier = {__proto__:null,true:34, false:38, null:42, GET:50, POST:52, PUT:54, DELETE:56, PATCH:58, HEAD:60, OPTIONS:62, CONNECT:64, TRACE:66, error:102}
export const parser = LRParser.deserialize({
version: 14,
states: "+WQYQPOOOOQO'#C`'#C`O!ZQPO'#CvO!ZQPO'#DSO!ZQPO'#DUO!ZQPO'#DWOOQO'#Cu'#CuO!`QPO'#C_O!|QPO'#D_O#TQPO'#DYOOQO'#Dh'#DhOOQO'#D`'#D`QYQPOOO#`QPO'#CyOOQO,59b,59bOOQO,59n,59nOOQO,59p,59pOOQO,59r,59rOOQO'#Cj'#CjO#hQPO,58yO#mQPO'#CrOOQO'#C|'#C|OOQO'#DO'#DOOOQO'#DQ'#DQO$[QPO'#DpOOQO,59y,59yO$dQPO,59yOOQO'#D['#D[O$iQPO'#DZO$nQPO'#DoOOQO,59t,59tO$vQPO,59tOOQO-E7^-E7^OOQO'#C{'#C{O${QPO'#CzO%QQPO'#DmOOQO,59e,59eO%YQPO,59eO%pQPO'#CnOOQO1G.e1G.eOOQO,59^,59^O%wQPO,5:[O&OQPO,5:[OOQO1G/e1G/eO!eQPO,59uO&WQPO,5:ZO&cQPO,5:ZOOQO1G/`1G/`O&kQPO,59fO'PQPO,5:XO'XQPO,5:XOOQO1G/P1G/POOQO'#Cp'#CpO'aQPO'#CoOOQO'#Da'#DaO'fQPO'#DjO'mQPO,59YOOQO,59},59}O'rQPO1G/vOOQO-E7a-E7aOOQO1G/a1G/aOOQO,5:O,5:OO'yQPO1G/uOOQO-E7b-E7bOOQO'#Dn'#DnOOQO1G/Q1G/QOOQO,59|,59|O(UQPO1G/sOOQO-E7`-E7`O(^QPO,59ZOOQO-E7_-E7_OOQO1G.t1G.tP!eQPO'#DcP(lQPO'#DdP#cQPO'#DbOOQO'#Dk'#DkO(tQPO1G.uOOQO7+$a7+$a",
stateData: ")`~O!ZOSPOS~OTPOUPOVPOWPOXPOYPOZPO[PO]POaXOlQOwROySO{TO!QWO~Oa]O~O_bO~O_kOaXOqeOsfOugO!QWO!`dO~O!PiO~P!eO_lO`nO!]lO~O`tO!]qO~OavO~OgxOhfX!PfX`fXlfXwfXyfX{fX!]fX~OhyO!P!dX~O!P{O~Oe|O~Oh}O`!cX~O`!PO~Oe!QO~Oh!RO`!aX~O`!TO~OlQOwROySO{TO!]!UO~O`!^P~P%_O!P!da~P!eOh![O!P!da~O_lO!]lO`!ca~Oh!`O`!ca~O_!bOa]OqeOsfOugO!`dO~O!]qO`!aa~Oh!eO`!aa~Oe!gO~O`!^X~P%_O`!iO~O!P!di~P!eO_lO!]lO`!ci~O!]qO`!ai~O_!mOavO!]!mO!`dO~O_lO!]lO~Oh!oO`cilciwciyci{ci!]ci~O!]g~",
goto: "&Z!ePPP!f!jPPPPPPPPP!nPPP!q!w!{P#PPP#^#fPP#l#{$T$ZP$ZP$ZP#fP#fP#fP$e$p$xPP$e%T%Z%a%g%mPPP%sP%w%zP%}&Q&T&WTYO[TVO[RcVQwcR!m!gT!Wv!XT!Vv!XYkWy|![!jQ!b!QR!m!gSYO[T!Wv!XXUO[v!XQ^QQ_RQ`SQaTR!b!QQs]V!d!R!e!lXr]!R!e!lYkWy|![!jR!b!QSYO[ZkWy|![!jQmXV!_}!`!kQhWU!Zy![!jR!^|Q[ORp[Q!XvR!h!XQ!SsR!f!SQzhR!]zQ!OmR!a!OTZO[R!YvR!n!gRu]R!c!QRoXRjW",
nodeNames: "⚠ LineComment Document RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url StringLiteral } { Block Property PropertyName : NumberLiteral Unit , AtRule JsonRule AtKeyword JsonKeyword JsonBlock JsonProperty PropertyName JsonTrue True JsonFalse False JsonNull Null FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword JsonObject JsonMember JsonValue ] [ JsonArray",
maxTerm: 66,
states: "-bQYQPOOO!aQPO'#C_OOQO'#Ct'#CtO!aQPO'#DTO!aQPO'#DVO!aQPO'#DXO!aQPO'#DZO!fQPO'#DSO#yQPO'#CsO$jQPO'#DlO$qQPO'#DgO$|QPO'#D]OOQO'#Du'#DuOOQO'#Dm'#DmQYQPOOO%UQPO'#CdOOQO,58y,58yOOQO,59o,59oOOQO,59q,59qOOQO,59s,59sOOQO,59u,59uOOQO,59n,59nOOQO'#DO'#DOO%^QPO,59_O%cQPO'#CiOOQO'#Cl'#ClOOQO'#Cn'#CnOOQO'#Cp'#CpO&QQPO'#D}OOQO,5:W,5:WO&YQPO,5:WOOQO'#Di'#DiO&_QPO'#DhO&dQPO'#D|OOQO,5:R,5:RO&lQPO,5:ROOQO'#D_'#D_O&qQPO,59wOOQO-E7k-E7kOOQO'#Cf'#CfO&vQPO'#CeO&{QPO'#DvOOQO,59O,59OO'TQPO,59OO'kQPO'#DPOOQO1G.y1G.yOOQO,59T,59TO'rQPO,5:iO'yQPO,5:iOOQO1G/r1G/rO$OQPO,5:SO(RQPO,5:hO(^QPO,5:hOOQO1G/m1G/mOOQO'#Db'#DbO(fQPO1G/cO(kQPO,59PO)SQPO,5:bO)[QPO,5:bOOQO1G.j1G.jOOQO'#DR'#DRO)dQPO'#DQOOQO'#Do'#DoO)iQPO'#DzO)pQPO'#DzOOQO,59k,59kO)xQPO,59kOOQO,5:[,5:[O)}QPO1G0TOOQO-E7n-E7nOOQO1G/n1G/nOOQO,5:],5:]O*UQPO1G0SOOQO-E7o-E7oOOQO'#Dd'#DdO*aQPO7+$}OOQO'#Dx'#DxOOQO1G.k1G.kOOQO,5:Y,5:YO*iQPO1G/|OOQO-E7l-E7lO*qQPO,59lOOQO-E7m-E7mO+SQPO,5:fO+SQPO,5:fOOQO1G/V1G/VP$OQPO'#DpP$tQPO'#DqOOQO'#Df'#DfOOQO<<Hi<<HiP%XQPO'#DnOOQO'#D{'#D{O+[QPO1G/WO+sQPO1G0QOOQO7+$r7+$r",
stateData: ",U~O!hOSPOS~OTPOVYOiQOjQOkQOlQOmQOnQOoQOpQOqQOxROzSO|TO!OUO!QZO!_XO~OV_O~OfeOTvXVvXivXjvXkvXlvXmvXnvXovXpvXqvXxvXzvX|vX!OvX!QvX!_vX!fvXUvX!kvX~O[fO~OVYO[oO_oOaiOcjOekO!_XO!mhO~O!^mO~P$OOUrO[pO!kpO~O!StO!TtO~OUzO!kwO~OV|O~O^!OOf]X!^]XU]Xx]Xz]X|]X!O]X!k]X~Of!PO!^!qX~O!^!RO~OZ!SO~Of!TOU!pX~OU!VO~O!V!WO~OZ!YO~Of!ZOU!jX~OU!]O~OxROzSO|TO!OUO!k!^O~OU!cO~P'YO!^!qa~P$OOf!fO!^!qa~O[pO!kpOU!pa~Of!jOU!pa~O!X!lO~OV_O[!nO_!nOaiOcjOekO!mhO~O!kwOU!ja~Of!qOU!ja~OZ!sO~OU!nX~P'YO!k!^OU!nX~OU!wO~O!^!qi~P$OO[pO!kpOU!pi~OVYO!_XO~O!kwOU!ji~OV|O[!}O_!}O!k!}O!mhO~O!k!^OU!na~Of#QOUtixtizti|ti!Oti!kti~O!k!^OU!ni~O!X!V!S!m_!k^!m~",
goto: "'^!rPPP!sPPPP!w#Z#cPP#iPP#vP#vP#vPP!s$QPPPPPPPPP$U$X$_$g$o$yP$yP$yP$yP!sP%PPP%SP%VP%Y%]%k%sPP%]&O&U&[&j&pPPP&v&zP&}P'Q'T'W'ZT[O^Q`PQaRQbSQcTQdUR!n!YQy_V!p!Z!q!|Xx_!Z!q!|YoX!P!S!f!xQ!n!YR!}!sYoX!P!S!f!xR!n!YTWO^RgWQ}gR!}!s]!`|!a!b!u!v#P]!_|!a!b!u!v#PS[O^Q!b|R!u!aXVO^|!aRuZR!XuR!m!XR!{!mS[O^YoX!P!S!f!xR!z!mQqYV!i!T!j!yQlXU!e!P!f!xR!h!SQ^ORv^Q![yR!r![Q!a|U!t!a!v#PQ!v!bR#P!uQ!QlR!g!QQ!UqR!k!UT]O^R{_R!o!YR!d|R#O!sRsYRnX",
nodeNames: "⚠ LineComment Document VarDeclaration AtKeyword VarKeyword } { JsonBlock JsonProperty PropertyName : StringLiteral NumberLiteral Unit VariableRef JsonTrue True JsonFalse False JsonNull Null , RequestStatement Method GET POST PUT DELETE PATCH HEAD OPTIONS CONNECT TRACE Url Block Property PropertyName AtRule JsonRule JsonKeyword FormDataRule FormDataKeyword UrlEncodedRule UrlEncodedKeyword TextRule TextKeyword ResponseDeclaration ResponseKeyword ResponseStatus StatusCode ErrorStatus ResponseTime TimeValue ResponseTimestamp Timestamp ResponseBlock JsonObject JsonMember JsonValue ] [ JsonArray",
maxTerm: 79,
nodeProps: [
["isolate", 15,""],
["openedBy", 16,"{",47,"["],
["closedBy", 17,"}",48,"]"]
["openedBy", 6,"{",60,"["],
["closedBy", 7,"}",61,"]"],
["isolate", -3,12,15,55,""]
],
propSources: [httpHighlighting],
skippedNodes: [0,1,27],
skippedNodes: [0,1,4],
repeatNodeCount: 5,
tokenData: "+l~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O(P!O!P(Y!Q![)Y![!])p!b!c)u!c!}*m!}#O+W#P#Q+]#R#S%y#T#o*m#o#p+b#q#r+g#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!Z~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O_~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!]~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&b~(SQ!O!P(Y!Q![)Y~(]P!Q![(`~(eR!`~!Q![(`!g!h(n#X#Y(n~(qR{|(z}!O(z!Q![)Q~(}P!Q![)Q~)VP!`~!Q![)Q~)_S!`~!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOh~~)uOe~~)xR}!O*R!c!}*[#T#o*[~*UQ!c!}*[#T#o*[~*aSk~}!O*[!Q![*[!c!}*[#T#o*[~*tU!]~g~tu%y}!O%y!Q![%y!c!}*m#R#S%y#T#o*m~+]O!Q~~+bO!P~~+gOa~~+lO`~",
tokenData: "3b~RlX^!ypq!yrs#nst%btu%ywx&b{|(P|})k}!O(P!O!P(Y!Q![)p![!]/Y!b!c/_!c!}0V!}#O0p#P#Q0u#R#S%y#T#o0V#o#p0z#q#r3]#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#OY!h~X^!ypq!y#y#z!y$f$g!y#BY#BZ!y$IS$I_!y$I|$JO!y$JT$JU!y$KV$KW!y&FU&FV!y~#qWOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[<%lO#n~$`O[~~$cRO;'S#n;'S;=`$l;=`O#n~$oXOY#nZr#nrs$Zs#O#n#O#P$`#P;'S#n;'S;=`%[;=`<%l#n<%lO#n~%_P;=`<%l#n~%gSP~OY%bZ;'S%b;'S;=`%s<%lO%b~%vP;=`<%l%b~&OU!k~tu%y}!O%y!Q![%y!c!}%y#R#S%y#T#o%y~&eWOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y<%lO&b~'QRO;'S&b;'S;=`'Z;=`O&b~'^XOY&bZw&bwx$Zx#O&b#O#P&}#P;'S&b;'S;=`'y;=`<%l&b<%lO&b~'|P;=`<%l&b~(SQ!O!P(Y!Q![)Y~(]P!Q![(`~(eR!m~!Q![(`!g!h(n#X#Y(n~(qR{|(z}!O(z!Q![)Q~(}P!Q![)Q~)VP!m~!Q![)Q~)_S!m~!O!P(`!Q![)Y!g!h(n#X#Y(n~)pOf~~)wU!S~!m~}!O*Z!O!P(`!Q![*r!g!h(n#X#Y(n#a#b.}~*^Q!c!}*d#T#o*d~*iR!S~}!O*d!c!}*d#T#o*d~*yU!S~!m~}!O*Z!O!P(`!Q![+]!g!h(n#X#Y(n#a#b.}~+dU!S~!m~}!O*Z!O!P(`!Q![+v!g!h(n#X#Y(n#a#b.}~+}U!S~!m~}!O,a!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~,dR!Q![,m!c!}*d#T#o*d~,pP!Q![,s~,vP}!O,y~,|P!Q![-P~-SP!Q![-V~-YP!v!w-]~-`P!Q![-c~-fP!Q![-i~-lP![!]-o~-rP!Q![-u~-xP!Q![-{~.OP![!].R~.UP!Q![.X~.[P!Q![._~.dO!X~~.kU!S~!m~}!O*Z!O!P(`!Q![.d!g!h(n#X#Y(n#a#b.}~/QP#g#h/T~/YO!V~~/_OZ~~/bR}!O/k!c!}/t#T#o/t~/nQ!c!}/t#T#o/t~/ySS~}!O/t!Q![/t!c!}/t#T#o/t~0^U!k~^~tu%y}!O%y!Q![%y!c!}0V#R#S%y#T#o0V~0uO!_~~0zO!^~~1PPV~#o#p1S~1VStu1c!c!}1c#R#S1c#T#o1c~1fXtu1c}!O1c!O!P1c!Q![1c![!]2R!c!}1c#R#S1c#T#o1c#q#r3V~2UUOY2RZ#q2R#q#r2h#r;'S2R;'S;=`3P<%lO2R~2kTO#q2R#q#r2z#r;'S2R;'S;=`3P<%lO2R~3PO_~~3SP;=`<%l2R~3YP#q#r2z~3bOU~",
tokenizers: [0],
topRules: {"Document":[0,2]},
specialized: [{term: 59, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1},{term: 27, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1}],
tokenPrec: 381
specialized: [{term: 4, get: (value: keyof typeof spec_AtKeyword) => spec_AtKeyword[value] || -1},{term: 73, get: (value: keyof typeof spec_identifier) => spec_identifier[value] || -1}],
tokenPrec: 503
})

View File

@@ -1,6 +1,7 @@
import { EditorState } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import type { SyntaxNode } from '@lezer/common';
import { VariableResolver } from './variable-resolver';
/**
* HTTP 请求模型
@@ -53,13 +54,36 @@ const NODE_TYPES = {
TEXT_KEYWORD: 'TextKeyword',
JSON_BLOCK: 'JsonBlock',
JSON_PROPERTY: 'JsonProperty',
VARIABLE_REF: 'VariableRef',
} as const;
/**
* HTTP 请求解析器
*/
export class HttpRequestParser {
constructor(private state: EditorState) {}
private variableResolver: VariableResolver | null = null;
/**
* 构造函数
* @param state EditorState
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
*/
constructor(
private state: EditorState,
private blockRange?: { from: number; to: number }
) {
}
/**
* 获取或创建变量解析器(懒加载)
*/
private getVariableResolver(): VariableResolver {
if (!this.variableResolver) {
this.variableResolver = new VariableResolver(this.state, this.blockRange);
}
return this.variableResolver;
}
/**
* 解析指定位置的 HTTP 请求
@@ -152,7 +176,9 @@ export class HttpRequestParser {
private parseUrl(node: SyntaxNode): string {
const urlText = this.getNodeText(node);
// 移除引号
return urlText.replace(/^["']|["']$/g, '');
const url = urlText.replace(/^["']|["']$/g, '');
// 替换变量
return this.getVariableResolver().replaceVariables(url);
}
/**
@@ -236,12 +262,13 @@ export class HttpRequestParser {
const name = this.getNodeText(nameNode);
// 尝试获取值节点String, Number, JsonBlock
// 尝试获取值节点String, Number, JsonBlock, VariableRef
let value: any = null;
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.name === NODE_TYPES.STRING_LITERAL ||
child.name === NODE_TYPES.NUMBER_LITERAL ||
child.name === NODE_TYPES.JSON_BLOCK ||
child.name === NODE_TYPES.VARIABLE_REF ||
child.name === NODE_TYPES.IDENTIFIER) {
value = this.parseValue(child);
return { name, value };
@@ -305,20 +332,36 @@ export class HttpRequestParser {
}
}
// HTTP Header 的值必须转换为字符串
if (value !== null && value !== undefined) {
value = String(value);
}
return { name, value };
}
/**
* 解析值节点(字符串、数字、标识符、嵌套块)
* 解析值节点(字符串、数字、标识符、嵌套块、变量引用
*/
private parseValue(node: SyntaxNode): any {
if (node.name === NODE_TYPES.STRING_LITERAL) {
const text = this.getNodeText(node);
// 移除引号
return text.replace(/^["']|["']$/g, '');
const value = text.replace(/^["']|["']$/g, '');
// 替换字符串中的变量
return this.getVariableResolver().replaceVariables(value);
} else if (node.name === NODE_TYPES.NUMBER_LITERAL) {
const text = this.getNodeText(node);
return parseFloat(text);
} else if (node.name === NODE_TYPES.VARIABLE_REF) {
// 处理变量引用节点
const text = this.getNodeText(node);
const resolver = this.getVariableResolver();
const ref = resolver.parseVariableRef(text);
if (ref) {
return resolver.resolveVariable(ref.name, ref.defaultValue);
}
return text;
} else if (node.name === NODE_TYPES.IDENTIFIER) {
const text = this.getNodeText(node);
// 处理布尔值
@@ -347,9 +390,16 @@ export class HttpRequestParser {
/**
* 便捷函数:解析指定位置的 HTTP 请求
* @param state EditorState
* @param pos 光标位置
* @param blockRange 块的范围(可选),如果提供则只解析该块内的变量
*/
export function parseHttpRequest(state: EditorState, pos: number): HttpRequest | null {
const parser = new HttpRequestParser(state);
export function parseHttpRequest(
state: EditorState,
pos: number,
blockRange?: { from: number; to: number }
): HttpRequest | null {
const parser = new HttpRequestParser(state, blockRange);
return parser.parseRequestAt(pos);
}

View File

@@ -1,6 +1,6 @@
import { EditorView } from '@codemirror/view';
import { EditorState, ChangeSpec, StateField } from '@codemirror/state';
import { syntaxTree, syntaxTreeAvailable } from '@codemirror/language';
import { EditorState, ChangeSpec } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import type { SyntaxNode } from '@lezer/common';
import { getNoteBlockFromPos } from '../../codeblock/state';
@@ -30,68 +30,6 @@ export interface HttpResponse {
error?: any;
}
/**
* 节点类型常量
*/
const NODE_TYPES = {
REQUEST_STATEMENT: 'RequestStatement',
LINE_COMMENT: 'LineComment',
JSON_OBJECT: 'JsonObject',
JSON_ARRAY: 'JsonArray',
} as const;
/**
* 缓存接口
*/
interface ParseCache {
version: number;
blockId: string;
requestPositions: Map<number, {
requestNode: SyntaxNode | null;
nextRequestPos: number | null;
oldResponse: { from: number; to: number } | null;
}>;
}
/**
* StateField用于缓存解析结果
*/
const responseCacheField = StateField.define<ParseCache>({
create(): ParseCache {
return {
version: 0,
blockId: '',
requestPositions: new Map()
};
},
update(cache, tr): ParseCache {
// 如果有文档变更,清空缓存
if (tr.docChanged) {
return {
version: cache.version + 1,
blockId: '',
requestPositions: new Map()
};
}
return cache;
}
});
/**
* 响应插入位置信息
*/
interface InsertPosition {
/** 插入位置 */
from: number;
/** 删除结束位置(如果需要删除旧响应) */
to: number;
/** 是否需要删除旧响应 */
hasOldResponse: boolean;
}
/**
* HTTP 响应插入器
*/
@@ -99,302 +37,204 @@ export class HttpResponseInserter {
constructor(private view: EditorView) {}
/**
* 插入HTTP响应(优化版本)
* 插入HTTP响应
* @param requestPos 请求的起始位置
* @param response 响应数据
*/
insertResponse(requestPos: number, response: HttpResponse): void {
const state = this.view.state;
// 检查语法树是否可用避免阻塞UI
if (!syntaxTreeAvailable(state)) {
// 延迟执行,等待语法树可用
setTimeout(() => {
if (syntaxTreeAvailable(this.view.state)) {
this.insertResponse(requestPos, response);
}
}, 10);
// 获取当前代码块
const blockInfo = getNoteBlockFromPos(state, requestPos);
if (!blockInfo) {
return;
}
const insertPos = this.findInsertPosition(state, requestPos);
const blockFrom = blockInfo.range.from;
const blockTo = blockInfo.range.to;
if (!insertPos) {
// 查找请求节点和旧响应
const context = this.findRequestAndResponse(state, requestPos, blockFrom, blockTo);
if (!context.requestNode) {
return;
}
// 生成响应文本
// 生成响应文本
const responseText = this.formatResponse(response);
// 根据是否有旧响应决定插入内容
const insertText = insertPos.hasOldResponse
? responseText // 替换旧响应,不需要额外换行
: `\n${responseText}`; // 新插入,需要换行分隔
// 定插入位置
let insertFrom: number;
let insertTo: number;
if (context.oldResponseNode) {
// 替换旧响应
insertFrom = context.oldResponseNode.from;
insertTo = context.oldResponseNode.to;
} else {
// 在请求后插入新响应
// 使用 requestNode.to - 1 定位到请求的最后一个字符所在行
const lastCharPos = Math.max(context.requestNode.from, context.requestNode.to - 1);
const requestEndLine = state.doc.lineAt(lastCharPos);
insertFrom = requestEndLine.to;
insertTo = insertFrom;
}
const changes: ChangeSpec = {
from: insertPos.from,
to: insertPos.to,
insert: insertText
from: insertFrom,
to: insertTo,
insert: context.oldResponseNode ? responseText : `\n\n${responseText}`
};
this.view.dispatch({
changes,
userEvent: 'http.response.insert',
// 保持光标在请求位置
selection: { anchor: requestPos },
// 滚动到插入位置
scrollIntoView: true
});
}
/**
* 查找插入位置(带缓存优化)
*/
private findInsertPosition(state: EditorState, requestPos: number): InsertPosition | null {
// 获取当前代码块
const blockInfo = getNoteBlockFromPos(state, requestPos);
if (!blockInfo) {
return null;
}
const blockFrom = blockInfo.range.from;
const blockTo = blockInfo.range.to;
const blockId = `${blockFrom}-${blockTo}`; // 使用位置作为唯一ID
// 检查缓存
const cache = state.field(responseCacheField, false);
if (cache && cache.blockId === blockId) {
const cachedResult = cache.requestPositions.get(requestPos);
if (cachedResult) {
// 使用缓存结果
const { requestNode, nextRequestPos, oldResponse } = cachedResult;
if (requestNode) {
const insertFrom = oldResponse ? oldResponse.from : requestNode.to + 1;
const insertTo = oldResponse ? oldResponse.to : insertFrom;
return {
from: insertFrom,
to: insertTo,
hasOldResponse: !!oldResponse
};
}
}
}
// 缓存未命中,执行解析
const tree = syntaxTree(state);
const context = this.findInsertionContext(tree, state, requestPos, blockFrom, blockTo);
// 更新缓存
if (cache) {
cache.blockId = blockId;
cache.requestPositions.set(requestPos, context);
}
if (!context.requestNode) {
return null;
}
// 计算插入位置
let insertFrom: number;
let insertTo: number;
let hasOldResponse = false;
if (context.oldResponse) {
// 有旧响应,替换
insertFrom = context.oldResponse.from;
insertTo = context.oldResponse.to;
hasOldResponse = true;
} else {
// 没有旧响应,在请求后插入
const requestEndLine = state.doc.lineAt(context.requestNode.to);
// 在请求行末尾插入,添加换行符分隔
insertFrom = requestEndLine.to;
insertTo = insertFrom;
}
return { from: insertFrom, to: insertTo, hasOldResponse };
}
/**
* 单次遍历查找插入上下文
* 查找请求节点和旧响应节点(使用 tree.iterate
*/
private findInsertionContext(
tree: any,
private findRequestAndResponse(
state: EditorState,
requestPos: number,
blockFrom: number,
blockTo: number
): {
requestNode: SyntaxNode | null;
nextRequestPos: number | null;
oldResponse: { from: number; to: number } | null;
oldResponseNode: { node: SyntaxNode; from: number; to: number } | null;
} {
const tree = syntaxTree(state);
let requestNode: SyntaxNode | null = null;
let nextRequestPos: number | null = null;
let responseStartNode: SyntaxNode | null = null;
let responseEndPos: number | null = null;
let requestNodeTo = -1;
let oldResponseNode: { node: SyntaxNode; from: number; to: number } | null = null;
let nextRequestFrom = -1;
// 第一步:向上查找当前请求节点
const cursor = tree.cursorAt(requestPos);
do {
if (cursor.name === NODE_TYPES.REQUEST_STATEMENT) {
if (cursor.from >= blockFrom && cursor.to <= blockTo) {
requestNode = cursor.node;
break;
// 遍历查找:请求节点、旧响应、下一个请求
tree.iterate({
from: blockFrom,
to: blockTo,
enter: (node) => {
// 1. 找到包含 requestPos 的 RequestStatement
if (node.name === 'RequestStatement' &&
node.from <= requestPos &&
node.to >= requestPos) {
requestNode = node.node;
requestNodeTo = node.to;
}
}
} while (cursor.parent());
// 如果向上查找失败,从块开始位置查找
if (!requestNode) {
const blockCursor = tree.cursorAt(blockFrom);
do {
if (blockCursor.name === NODE_TYPES.REQUEST_STATEMENT) {
if (blockCursor.from <= requestPos && requestPos <= blockCursor.to) {
requestNode = blockCursor.node;
break;
}
// 2. 找到请求后的第一个 ResponseDeclaration
if (requestNode && !oldResponseNode &&
node.name === 'ResponseDeclaration' &&
node.from >= requestNodeTo) {
oldResponseNode = {
node: node.node,
from: node.from,
to: node.to
};
}
} while (blockCursor.next() && blockCursor.from < blockTo);
}
if (!requestNode) {
return { requestNode: null, nextRequestPos: null, oldResponse: null };
}
// 3. 记录下一个请求的起始位置(用于确定响应范围)
if (requestNode && nextRequestFrom === -1 &&
node.name === 'RequestStatement' &&
node.from > requestNodeTo) {
nextRequestFrom = node.from;
}
const requestEnd = requestNode.to;
// 第二步:从请求结束位置向后遍历,查找响应和下一个请求
const forwardCursor = tree.cursorAt(requestEnd);
let foundResponse = false;
do {
if (forwardCursor.from <= requestEnd) continue;
if (forwardCursor.from >= blockTo) break;
// 查找下一个请求
if (!nextRequestPos && forwardCursor.name === NODE_TYPES.REQUEST_STATEMENT) {
nextRequestPos = forwardCursor.from;
// 如果已经找到响应,可以提前退出
if (foundResponse) break;
}
// 查找响应注释
if (!responseStartNode && forwardCursor.name === NODE_TYPES.LINE_COMMENT) {
const commentText = state.doc.sliceString(forwardCursor.from, forwardCursor.to);
// 避免不必要的 trim同时识别普通响应和错误响应
if (commentText.startsWith('# Response') || commentText.startsWith(' # Response')) {
const startNode = forwardCursor.node;
responseStartNode = startNode;
foundResponse = true;
// 检查是否为错误响应(只有一行)
if (commentText.includes('Error:')) {
// 错误响应只有一行,直接设置结束位置
responseEndPos = startNode.to;
} else {
// 继续查找 JSON 和结束分隔线(正常响应)
let nextNode = startNode.nextSibling;
while (nextNode && nextNode.from < (nextRequestPos || blockTo)) {
// 找到 JSON
if (nextNode.name === NODE_TYPES.JSON_OBJECT || nextNode.name === NODE_TYPES.JSON_ARRAY) {
responseEndPos = nextNode.to;
// 查找结束分隔线
let afterJson = nextNode.nextSibling;
while (afterJson && afterJson.from < (nextRequestPos || blockTo)) {
if (afterJson.name === NODE_TYPES.LINE_COMMENT) {
const text = state.doc.sliceString(afterJson.from, afterJson.to);
// 使用更快的正则匹配
if (/^#?\s*-+$/.test(text)) {
responseEndPos = afterJson.to;
break;
}
}
afterJson = afterJson.nextSibling;
}
break;
}
// 遇到下一个请求,停止
if (nextNode.name === NODE_TYPES.REQUEST_STATEMENT) {
break;
}
nextNode = nextNode.nextSibling;
}
// 4. 早期退出优化:如果已找到请求节点,且满足以下任一条件,则停止遍历
// - 找到了旧响应节点
// - 找到了下一个请求(说明当前请求没有响应)
if (requestNode !== null) {
if (oldResponseNode !== null || nextRequestFrom !== -1) {
return false; // 停止遍历
}
}
}
} while (forwardCursor.next() && forwardCursor.from < blockTo);
});
// 构建旧响应信息
let oldResponse: { from: number; to: number } | null = null;
if (responseStartNode) {
const startLine = state.doc.lineAt(responseStartNode.from);
if (responseEndPos !== null) {
const endLine = state.doc.lineAt(responseEndPos);
oldResponse = { from: startLine.from, to: endLine.to };
} else {
const commentEndLine = state.doc.lineAt(responseStartNode.to);
oldResponse = { from: startLine.from, to: commentEndLine.to };
// 如果找到了下一个请求,且旧响应超出范围,则清除旧响应
if (oldResponseNode && nextRequestFrom !== -1) {
// TypeScript 类型收窄问题,使用非空断言
if ((oldResponseNode as { from: number; to: number; node: SyntaxNode }).from >= nextRequestFrom) {
oldResponseNode = null;
}
}
return { requestNode, nextRequestPos, oldResponse };
return { requestNode, oldResponseNode };
}
/**
* 格式化响应数据
* 格式化响应数据(新格式:@response
* 格式:@response <status> <time>ms <timestamp> { <json> }
* 状态格式200 或 200-OK支持完整状态文本
* 错误:@response error 0ms <timestamp> { "error": "..." }
*/
private formatResponse(response: HttpResponse): string {
// 如果有错误,使用最简洁的错误格式
if (response.error) {
return `# Response Error: ${response.error}`;
}
// 正常响应格式
// 时间戳格式ISO 8601YYYY-MM-DDTHH:MM:SS
const timestamp = response.timestamp || new Date();
const dateStr = this.formatTimestamp(timestamp);
const timestampStr = this.formatTimestampISO(timestamp);
let headerLine = `# Response ${response.status} ${response.time}ms`;
if (response.requestSize) {
headerLine += ` ${response.requestSize}`;
}
headerLine += ` ${dateStr}`;
// 完整的开头行(不添加前导换行符)
const header = `${headerLine}\n`;
// 格式化响应体
let body: string;
if (typeof response.body === 'string') {
// 格式化响应体为 JSON
let bodyJson: string;
if (response.error) {
// 错误响应
bodyJson = JSON.stringify({ error: String(response.error) }, null, 2);
} else if (typeof response.body === 'string') {
// 尝试解析 JSON 字符串
try {
const parsed = JSON.parse(response.body);
body = JSON.stringify(parsed, null, 2);
bodyJson = JSON.stringify(parsed, null, 2);
} catch {
// 如果不是 JSON直接使用字符串
body = response.body;
// 如果不是 JSON包装为对象
bodyJson = JSON.stringify({ data: response.body }, null, 2);
}
} else if (response.body === null || response.body === undefined) {
// 空响应(只有响应头和结束分隔线)
const endLine = `# ${'-'.repeat(Math.max(16, headerLine.length - 2))}`; // 最小16个字符
return header + endLine;
// 空响应
bodyJson = '{}';
} else {
// 对象或数组
body = JSON.stringify(response.body, null, 2);
bodyJson = JSON.stringify(response.body, null, 2);
}
// 结尾分隔线和响应头行长度一致最小16个字符
const endLine = `# ${'-'.repeat(Math.max(16, headerLine.length - 2))}`;
return header + body + `\n${endLine}`;
// 构建响应
if (response.error) {
// 错误格式:@response error 0ms <timestamp> { ... }
return `@response error 0ms ${timestampStr} ${bodyJson}`;
} else {
// 成功格式:@response <status> <time>ms <timestamp> { ... }
// 支持完整状态200-OK 或 200
const statusDisplay = this.formatStatus(response.status);
return `@response ${statusDisplay} ${response.time}ms ${timestampStr} ${bodyJson}`;
}
}
/**
* 格式化时间戳
* 格式化状态码显示
* 输入:"200 OK" 或 "404 Not Found" 或 "200"
* 输出:"200-OK" 或 "404-Not-Found" 或 "200"
*/
private formatTimestamp(date: Date): string {
private formatStatus(status: string): string {
// 提取状态码和状态文本
const parts = status.trim().split(/\s+/);
if (parts.length === 1) {
// 只有状态码200
return parts[0];
} else {
// 有状态码和文本200 OK -> 200-OK
const code = parts[0];
const text = parts.slice(1).join('-');
return `${code}-${text}`;
}
}
/**
* 格式化时间戳为 ISO 8601 格式YYYY-MM-DDTHH:MM:SS
*/
private formatTimestampISO(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
@@ -402,7 +242,7 @@ export class HttpResponseInserter {
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
}
}
@@ -413,9 +253,3 @@ export function insertHttpResponse(view: EditorView, requestPos: number, respons
const inserter = new HttpResponseInserter(view);
inserter.insertResponse(requestPos, response);
}
/**
* 导出StateField用于扩展配置
*/
export { responseCacheField };

View File

@@ -0,0 +1,367 @@
import { describe, it, expect } from 'vitest';
import { EditorState } from '@codemirror/state';
import { httpLanguage } from '../language/http-language';
import { VariableResolver } from './variable-resolver';
import { HttpRequestParser } from './request-parser';
/**
* 创建测试用的 EditorState
*/
function createTestState(content: string): EditorState {
return EditorState.create({
doc: content,
extensions: [httpLanguage],
});
}
describe('VariableResolver 测试', () => {
it('✅ 解析 @var 声明(无块范围)', () => {
const content = `@var {
baseUrl: "https://api.example.com",
version: "v1",
timeout: 30000
}`;
const state = createTestState(content);
const resolver = new VariableResolver(state);
const variables = resolver.getAllVariables();
expect(variables.get('baseUrl')).toBe('https://api.example.com');
expect(variables.get('version')).toBe('v1');
expect(variables.get('timeout')).toBe(30000);
});
it('✅ 解析 @var 声明(指定块范围)', () => {
const content = `@var {
baseUrl: "https://api.example.com",
version: "v1",
timeout: 30000
}`;
const state = createTestState(content);
// 指定块范围(整个内容)
const blockRange = { from: 0, to: content.length };
const resolver = new VariableResolver(state, blockRange);
const variables = resolver.getAllVariables();
expect(variables.get('baseUrl')).toBe('https://api.example.com');
expect(variables.get('version')).toBe('v1');
expect(variables.get('timeout')).toBe(30000);
});
it('✅ 解析变量引用', () => {
const resolver = new VariableResolver(createTestState(''));
const ref1 = resolver.parseVariableRef('{{baseUrl}}');
expect(ref1).toEqual({
name: 'baseUrl',
defaultValue: undefined,
raw: '{{baseUrl}}',
});
const ref2 = resolver.parseVariableRef('{{token:default-token}}');
expect(ref2).toEqual({
name: 'token',
defaultValue: 'default-token',
raw: '{{token:default-token}}',
});
});
it('✅ 替换字符串中的变量', () => {
const content = `@var {
baseUrl: "https://api.example.com",
version: "v1"
}`;
const state = createTestState(content);
const resolver = new VariableResolver(state);
const result = resolver.replaceVariables('{{baseUrl}}/{{version}}/users');
expect(result).toBe('https://api.example.com/v1/users');
});
it('✅ 使用默认值', () => {
const content = `@var {
baseUrl: "https://api.example.com"
}`;
const state = createTestState(content);
const resolver = new VariableResolver(state);
const result = resolver.replaceVariables('{{baseUrl}}/{{version:v1}}/users');
expect(result).toBe('https://api.example.com/v1/users');
});
it('✅ 变量未定义且无默认值,保持原样', () => {
const state = createTestState('');
const resolver = new VariableResolver(state);
const result = resolver.replaceVariables('{{undefined}}/users');
expect(result).toBe('{{undefined}}/users');
});
it('✅ 嵌套对象变量 - 路径访问', () => {
const content = `@var {
config: {
nested: {
deep: {
value: 123
}
}
},
simple: "test"
}`;
const state = createTestState(content);
const resolver = new VariableResolver(state);
// 测试简单变量
expect(resolver.resolveVariable('simple')).toBe('test');
// 测试嵌套路径访问
expect(resolver.resolveVariable('config.nested.deep.value')).toBe(123);
// 测试部分路径访问
const nestedObj = resolver.resolveVariable('config.nested');
expect(nestedObj).toEqual({ deep: { value: 123 } });
// 测试不存在的路径
expect(resolver.resolveVariable('config.notExist', 'default')).toBe('default');
// 测试字符串替换
const result = resolver.replaceVariables('Value is {{config.nested.deep.value}}');
expect(result).toBe('Value is 123');
});
it('✅ 嵌套对象变量 - 整个对象引用', () => {
const content = `@var {
config: {
host: "example.com",
port: 8080
}
}`;
const state = createTestState(content);
const resolver = new VariableResolver(state);
// 引用整个对象
const configObj = resolver.resolveVariable('config');
expect(configObj).toEqual({
host: 'example.com',
port: 8080
});
// 字符串中引用对象会转换为 JSON
const result = resolver.replaceVariables('Config: {{config}}');
expect(result).toBe('Config: {"host":"example.com","port":8080}');
});
it('✅ 块范围限制 - 只解析块内的变量', () => {
// 模拟多块文档
const content = `@var {
block1Var: "value1"
}
GET "http://example.com" {}
--- 块分隔 ---
@var {
block2Var: "value2"
}
POST "http://example.com" {}`;
const state = createTestState(content);
// 第一个块:只包含 block1Var
const block1Range = { from: 0, to: 60 };
const resolver1 = new VariableResolver(state, block1Range);
expect(resolver1.getAllVariables().get('block1Var')).toBe('value1');
expect(resolver1.getAllVariables().get('block2Var')).toBeUndefined();
// 第二个块:只包含 block2Var
const block2Start = content.indexOf('@var {', 60);
const block2Range = { from: block2Start, to: content.length };
const resolver2 = new VariableResolver(state, block2Range);
expect(resolver2.getAllVariables().get('block1Var')).toBeUndefined();
expect(resolver2.getAllVariables().get('block2Var')).toBe('value2');
});
});
describe('HttpRequestParser 集成变量测试', () => {
it('✅ 解析带变量的 URL', () => {
const content = `@var {
baseUrl: "https://api.example.com",
version: "v1"
}
GET "{{baseUrl}}/{{version}}/users" {
host: "example.com"
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
// 查找 GET 请求的位置
const getPos = content.indexOf('GET');
const request = parser.parseRequestAt(getPos);
expect(request).not.toBeNull();
expect(request?.url).toBe('https://api.example.com/v1/users');
});
it('✅ 解析 HTTP 头中的变量', () => {
const content = `@var {
token: "Bearer abc123",
timeout: 5000
}
GET "http://example.com" {
authorization: {{token}},
timeout: {{timeout}}
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const getPos = content.indexOf('GET');
const request = parser.parseRequestAt(getPos);
expect(request).not.toBeNull();
expect(request?.headers.authorization).toBe('Bearer abc123');
// HTTP Header 值会被转换为字符串
expect(request?.headers.timeout).toBe('5000');
});
it('✅ 解析 JSON 请求体中的变量', () => {
const content = `@var {
userId: "12345",
userName: "张三"
}
POST "http://api.example.com/users" {
@json {
id: {{userId}},
name: {{userName}}
}
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const postPos = content.indexOf('POST');
const request = parser.parseRequestAt(postPos);
expect(request).not.toBeNull();
expect(request?.body).toEqual({
id: '12345',
name: '张三',
});
});
it('✅ 字符串中嵌入变量', () => {
const content = `@var {
userName: "张三"
}
POST "http://api.example.com/users" {
@json {
name: {{userName}},
email: "{{userName}}@example.com"
}
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const postPos = content.indexOf('POST');
const request = parser.parseRequestAt(postPos);
expect(request).not.toBeNull();
expect(request?.body).toEqual({
name: '张三',
email: '张三@example.com',
});
});
it('✅ 使用变量默认值', () => {
const content = `@var {
baseUrl: "https://api.example.com"
}
GET "{{baseUrl}}/users" {
timeout: {{timeout:30000}},
authorization: "Bearer {{token:default-token}}"
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const getPos = content.indexOf('GET');
const request = parser.parseRequestAt(getPos);
expect(request).not.toBeNull();
// HTTP Header 值会被转换为字符串
expect(request?.headers.timeout).toBe('30000');
expect(request?.headers.authorization).toBe('Bearer default-token');
});
it('✅ 嵌套变量在 HTTP 请求中使用', () => {
const content = `@var {
api: {
base: "https://api.example.com",
version: "v1",
endpoints: {
users: "/users",
posts: "/posts"
}
},
config: {
timeout: 5000
}
}
GET "{{api.base}}/{{api.version}}{{api.endpoints.users}}" {
timeout: {{config.timeout}}
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const getPos = content.indexOf('GET');
const request = parser.parseRequestAt(getPos);
expect(request).not.toBeNull();
expect(request?.url).toBe('https://api.example.com/v1/users');
expect(request?.headers.timeout).toBe('5000');
});
it('✅ 完整示例 - 用户提供的场景', () => {
const content = `@var {
baseUrl: "https://api.example.com",
version: "v1",
timeout: 30000
}
GET "{{baseUrl}}/{{version}}/users" {
timeout: {{timeout}},
authorization: "Bearer {{token:default-token}}"
}`;
const state = createTestState(content);
const parser = new HttpRequestParser(state);
const getPos = content.indexOf('GET');
const request = parser.parseRequestAt(getPos);
expect(request).not.toBeNull();
expect(request?.url).toBe('https://api.example.com/v1/users');
// HTTP Header 值会被转换为字符串Go 后端要求)
expect(request?.headers.timeout).toBe('30000');
expect(request?.headers.authorization).toBe('Bearer default-token');
});
});

View File

@@ -0,0 +1,325 @@
import { EditorState } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import type { SyntaxNode } from '@lezer/common';
/**
* 变量引用模型
*/
export interface VariableReference {
/** 变量名 */
name: string;
/** 默认值(可选) */
defaultValue?: string;
/** 原始文本 */
raw: string;
}
/**
* 节点类型常量
*/
const NODE_TYPES = {
VAR_DECLARATION: 'VarDeclaration',
VAR_KEYWORD: 'VarKeyword',
JSON_BLOCK: 'JsonBlock',
JSON_PROPERTY: 'JsonProperty',
PROPERTY_NAME: 'PropertyName',
STRING_LITERAL: 'StringLiteral',
NUMBER_LITERAL: 'NumberLiteral',
IDENTIFIER: 'identifier',
VARIABLE_REF: 'VariableRef',
} as const;
/**
* 变量解析器
* 负责解析 @var 块和变量引用(块级作用域)
*/
export class VariableResolver {
/** 变量存储 */
private variables: Map<string, any> = new Map();
/**
* 构造函数
* @param state EditorState
* @param blockRange 块的范围(可选),如果提供则只解析该范围内的变量
*/
constructor(
private state: EditorState,
private blockRange?: { from: number; to: number }
) {
// 初始化时解析变量定义
this.parseBlockVariables();
}
/**
* 解析块范围内的 @var 声明
* 如果提供了 blockRange只解析该范围内的变量
* 否则解析整个文档(向后兼容)
*/
private parseBlockVariables(): void {
const tree = syntaxTree(this.state);
tree.iterate({
enter: (node: any) => {
// 如果指定了块范围,检查节点是否在范围内
if (this.blockRange) {
// 节点完全在块范围外,跳过
if (node.to < this.blockRange.from || node.from > this.blockRange.to) {
return false; // 停止遍历此分支
}
}
if (node.name === NODE_TYPES.VAR_DECLARATION) {
this.parseVarDeclaration(node.node);
}
}
});
}
/**
* 解析单个 @var 声明
*/
private parseVarDeclaration(node: SyntaxNode): void {
// 获取 JsonBlock 节点
const jsonBlockNode = node.getChild(NODE_TYPES.JSON_BLOCK);
if (!jsonBlockNode) {
return;
}
// 解析 JsonBlock 中的所有属性
const variables = this.parseJsonBlock(jsonBlockNode);
// 存储变量
for (const [name, value] of Object.entries(variables)) {
this.variables.set(name, value);
}
}
/**
* 解析 JsonBlock
*/
private parseJsonBlock(node: SyntaxNode): Record<string, any> {
const result: Record<string, any> = {};
// 遍历所有 JsonProperty 子节点
let child = node.firstChild;
while (child) {
if (child.name === NODE_TYPES.JSON_PROPERTY) {
const { name, value } = this.parseJsonProperty(child);
if (name) {
result[name] = value;
}
}
child = child.nextSibling;
}
return result;
}
/**
* 解析 JsonProperty
*/
private parseJsonProperty(node: SyntaxNode): { name: string | null; value: any } {
// 获取属性名节点
const nameNode = node.getChild(NODE_TYPES.PROPERTY_NAME);
if (!nameNode) {
return { name: null, value: null };
}
const name = this.getNodeText(nameNode);
// 获取值节点
let valueNode = nameNode.nextSibling;
// 跳过冒号
while (valueNode && valueNode.name === ':') {
valueNode = valueNode.nextSibling;
}
if (!valueNode) {
return { name, value: null };
}
const value = this.parseValue(valueNode);
return { name, value };
}
/**
* 解析值节点
*/
private parseValue(node: SyntaxNode): any {
switch (node.name) {
case NODE_TYPES.STRING_LITERAL: {
const text = this.getNodeText(node);
// 移除引号
return text.replace(/^["']|["']$/g, '');
}
case NODE_TYPES.NUMBER_LITERAL: {
const text = this.getNodeText(node);
return parseFloat(text);
}
case NODE_TYPES.IDENTIFIER: {
const text = this.getNodeText(node);
// 处理布尔值和 null
if (text === 'true') return true;
if (text === 'false') return false;
if (text === 'null') return null;
return text;
}
case NODE_TYPES.JSON_BLOCK: {
// 嵌套对象
return this.parseJsonBlock(node);
}
default:
// 其他类型返回原始文本
return this.getNodeText(node);
}
}
/**
* 解析变量引用
* 从 {{variableName}} 或 {{variableName:default}} 或 {{obj.nested.path}} 中提取信息
*/
public parseVariableRef(text: string): VariableReference | null {
// 正则匹配:
// - {{variableName}}
// - {{variableName:default}}
// - {{obj.nested.path}}
// - {{obj.nested.path:default}}
const match = text.match(/^\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:(.*))?\}\}$/);
if (!match) {
return null;
}
return {
name: match[1],
defaultValue: match[3],
raw: text,
};
}
/**
* 解析字符串中的所有变量引用
* 例如: "{{baseUrl}}/{{version}}/users" 或 "{{config.nested.value}}"
*/
public parseVariableRefsInString(text: string): VariableReference[] {
const refs: VariableReference[] = [];
// 支持路径访问:允许变量名中包含 "."
const regex = /\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:[^}]+)?\}\}/g;
let match;
while ((match = regex.exec(text)) !== null) {
refs.push({
name: match[1],
defaultValue: match[2] ? match[2].substring(1) : undefined, // 去掉冒号
raw: match[0],
});
}
return refs;
}
/**
* 解析变量值(支持路径访问)
* @param name 变量名,支持路径访问如 "config.nested.deep.value"
* @param defaultValue 默认值
* @returns 解析后的值
*/
public resolveVariable(name: string, defaultValue?: string): any {
// 检查是否包含路径访问符 "."
if (name.includes('.')) {
return this.resolveNestedVariable(name, defaultValue);
}
// 简单变量名直接查找
if (this.variables.has(name)) {
return this.variables.get(name);
}
return defaultValue;
}
/**
* 解析嵌套变量(路径访问)
* @param path 变量路径,如 "config.nested.deep.value"
* @param defaultValue 默认值
* @returns 解析后的值
*/
private resolveNestedVariable(path: string, defaultValue?: string): any {
const parts = path.split('.');
const rootName = parts[0];
// 获取根变量
if (!this.variables.has(rootName)) {
return defaultValue;
}
let current: any = this.variables.get(rootName);
// 遍历路径访问嵌套属性
for (let i = 1; i < parts.length; i++) {
const key = parts[i];
// 检查当前值是否是对象
if (current === null || current === undefined || typeof current !== 'object') {
return defaultValue;
}
// 访问嵌套属性
if (!(key in current)) {
return defaultValue;
}
current = current[key];
}
return current;
}
/**
* 替换字符串中的所有变量引用
* 支持:
* - 简单变量: "{{baseUrl}}/{{version}}/users"
* - 路径访问: "{{config.nested.deep.value}}"
* - 默认值: "{{timeout:30000}}"
*/
public replaceVariables(text: string): string {
return text.replace(
/\{\{([a-zA-Z_$][a-zA-Z0-9_$.-]*)(:[^}]+)?\}\}/g,
(match, name, defaultPart) => {
const defaultValue = defaultPart ? defaultPart.substring(1) : undefined;
const value = this.resolveVariable(name, defaultValue);
// 如果值是对象或数组,转换为 JSON 字符串
if (value !== undefined && value !== null) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
// 如果没有找到变量,保持原样
return match;
}
);
}
/**
* 获取所有已定义的变量
*/
public getAllVariables(): Map<string, any> {
return new Map(this.variables);
}
/**
* 获取节点的文本内容
*/
private getNodeText(node: SyntaxNode): string {
return this.state.doc.sliceString(node.from, node.to);
}
}

View File

@@ -62,7 +62,7 @@ function parseHttpRequests(state: any): Map<number, CachedHttpRequest> {
if (hasError) return;
// 直接解析请求
const request = parseHttpRequest(state, node.from);
const request = parseHttpRequest(state, node.from,{from: block.content.from, to: block.content.to});
if (request) {
const line = state.doc.lineAt(request.position.from);
@@ -109,7 +109,6 @@ class RunButtonMarker extends GutterMarker {
private readonly debouncedExecute: ((view: EditorView) => void) | null = null;
constructor(
private readonly lineNumber: number,
private readonly cachedRequest: HttpRequest
) {
super();
@@ -155,7 +154,6 @@ class RunButtonMarker extends GutterMarker {
if (this.isLoading) return;
this.setLoadingState(true);
try {
const response = await ExecuteRequest(this.cachedRequest);
if (!response) {
@@ -230,7 +228,7 @@ export const httpRunButtonGutter = gutter({
}
// 创建并返回运行按钮,传递缓存的解析结果
return new RunButtonMarker(cached.lineNumber, cached.request);
return new RunButtonMarker(cached.request);
},
});