🚧 Added HTTP language parser
This commit is contained in:
31
frontend/src/views/editor/extensions/httpclient/index.ts
Normal file
31
frontend/src/views/editor/extensions/httpclient/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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"]<word, "GET"> |
|
||||
@specialize[@name="POST"]<word, "POST"> |
|
||||
@specialize[@name="PUT"]<word, "PUT"> |
|
||||
@specialize[@name="DELETE"]<word, "DELETE"> |
|
||||
@specialize[@name="PATCH"]<word, "PATCH"> |
|
||||
@specialize[@name="HEAD"]<word, "HEAD"> |
|
||||
@specialize[@name="OPTIONS"]<word, "OPTIONS"> |
|
||||
@specialize[@name="CONNECT"]<word, "CONNECT"> |
|
||||
@specialize[@name="TRACE"]<word, "TRACE">
|
||||
}
|
||||
|
||||
Url { urlPart+ }
|
||||
|
||||
urlPart { urlContent | TemplateExpression }
|
||||
|
||||
HttpVersion { httpVersionToken }
|
||||
|
||||
// ==================== Header 语句 ====================
|
||||
|
||||
HeaderStatement {
|
||||
HeaderKeyword HeaderName colon HeaderValue ";"
|
||||
}
|
||||
|
||||
colon { ":" }
|
||||
|
||||
HeaderKeyword { @specialize[@name="HEADER"]<word, "HEADER"> }
|
||||
|
||||
HeaderName { word }
|
||||
|
||||
HeaderValue { headerValuePart* }
|
||||
|
||||
headerValuePart { headerValueContent | TemplateExpression }
|
||||
|
||||
// ==================== Body 语句 ====================
|
||||
|
||||
BodyStatement {
|
||||
BodyKeyword BodyType BodyContent ";"
|
||||
}
|
||||
|
||||
BodyKeyword { @specialize[@name="BODY"]<word, "BODY"> }
|
||||
|
||||
BodyType {
|
||||
@specialize[@name="JSON"]<word, "JSON"> |
|
||||
@specialize[@name="FORM"]<word, "FORM"> |
|
||||
@specialize[@name="URLENCODED"]<word, "URLENCODED"> |
|
||||
@specialize[@name="GRAPHQL"]<word, "GRAPHQL"> |
|
||||
@specialize[@name="XML"]<word, "XML"> |
|
||||
@specialize[@name="TEXT"]<word, "TEXT"> |
|
||||
@specialize[@name="BINARY"]<word, "BINARY"> |
|
||||
@specialize[@name="MULTIPART"]<word, "MULTIPART">
|
||||
}
|
||||
|
||||
BodyContent { bodyContentPart* }
|
||||
|
||||
bodyContentPart { bodyText | TemplateExpression | FileReference }
|
||||
|
||||
FileReference {
|
||||
"@file" FilePath
|
||||
}
|
||||
|
||||
FilePath { filePathContent }
|
||||
|
||||
// ==================== Variables 语句 ====================
|
||||
|
||||
VariablesStatement {
|
||||
VariablesKeyword VariablesContent ";"
|
||||
}
|
||||
|
||||
VariablesKeyword { @specialize[@name="VARIABLES"]<word, "VARIABLES"> }
|
||||
|
||||
VariablesContent[isolate] { variablesContent }
|
||||
|
||||
// ==================== 响应 ====================
|
||||
|
||||
// 响应行 - 固定格式:RESPONSE <状态码> <状态文本> <大小> <时间戳>;
|
||||
ResponseLine {
|
||||
ResponseKeyword StatusCode StatusText Size Timestamp ";"
|
||||
}
|
||||
|
||||
ResponseKeyword { @specialize[@name="RESPONSE"]<word, "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
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
export { http, httpLanguage, httpHighlighting } from './http-language';
|
||||
export { parser } from './http.grammar.js';
|
||||
|
||||
1
frontend/src/views/editor/extensions/httpclient/types.ts
Normal file
1
frontend/src/views/editor/extensions/httpclient/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user