🚧 Added HTTP language parser

This commit is contained in:
2025-10-31 19:39:44 +08:00
parent 61a23fe7f2
commit 8ac78e39f1
18 changed files with 1200 additions and 13 deletions

View File

@@ -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 {

View File

@@ -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"]),
];

View File

@@ -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

View File

@@ -27,6 +27,11 @@ export const blockState = StateField.define<Block[]>({
* 获取当前活动的块
*/
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
);

View File

@@ -65,6 +65,7 @@ export type SupportedLanguage =
| 'less'
| 'angular'
| 'svelte'
| 'http' // HTTP Client
/**
* 创建块的选项

View 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;
}

View File

@@ -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);
}

View File

@@ -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,
});

View File

@@ -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 };

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export { http, httpLanguage, httpHighlighting } from './http-language';
export { parser } from './http.grammar.js';

View File

@@ -0,0 +1 @@

View File

@@ -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)',
},
});