🚧 Optimized HTTP language parser

This commit is contained in:
2025-11-01 17:42:22 +08:00
parent 8ac78e39f1
commit 93c85b800b
24 changed files with 1945 additions and 582 deletions

View File

@@ -1,27 +1,115 @@
import { EditorView, GutterMarker, gutter } from '@codemirror/view';
import { StateField } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import { getNoteBlockFromPos } from '../../codeblock/state';
import { blockState } from '../../codeblock/state';
import type { SyntaxNode } from '@lezer/common';
import { parseHttpRequest, type HttpRequest } from '../parser/request-parser';
// ==================== 常量定义 ====================
/**
* 语法树节点类型常量
*/
const NODE_TYPES = {
REQUEST_STATEMENT: 'RequestStatement',
METHOD: 'Method',
URL: 'Url',
BLOCK: 'Block',
} as const;
/** 支持的 HTTP 方法(小写) - 使用 Set 以提高查找性能 */
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'connect', 'trace']);
/**
* 有效的 HTTP 方法列表
*/
const VALID_HTTP_METHODS = new Set([
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'HEAD',
'OPTIONS',
'CONNECT',
'TRACE'
]);
/** 匹配 ### Request 标记的正则表达式 */
const REQUEST_MARKER_REGEX = /^###\s+Request(?:\s|$)/i;
/**
* HTTP 请求缓存信息
*/
interface CachedHttpRequest {
lineNumber: number; // 行号(用于快速查找)
position: number; // 字符位置(用于解析)
request: HttpRequest; // 完整的解析结果
}
/** 匹配 ### Response 标记的正则表达式 */
const RESPONSE_MARKER_REGEX = /^###\s+Response/i;
/**
* 预解析所有 HTTP 块中的请求
* 只在文档改变时调用,结果缓存在 StateField 中
*
* 优化:一次遍历完成验证和解析,避免重复工作
*/
function parseHttpRequests(state: any): Map<number, CachedHttpRequest> {
const requestsMap = new Map<number, CachedHttpRequest>();
const blocks = state.field(blockState, false);
if (!blocks) return requestsMap;
const tree = syntaxTree(state);
// 只遍历 HTTP 块
for (const block of blocks) {
if (block.language.name !== 'http') continue;
// 在块范围内查找所有 RequestStatement
tree.iterate({
from: block.content.from,
to: block.content.to,
enter: (node) => {
if (node.name === NODE_TYPES.REQUEST_STATEMENT) {
// 检查是否包含错误节点
let hasError = false;
node.node.cursor().iterate((nodeRef) => {
if (nodeRef.name === '⚠') {
hasError = true;
return false;
}
});
if (hasError) return;
// 直接解析请求
const request = parseHttpRequest(state, node.from);
if (request) {
const line = state.doc.lineAt(request.position.from);
requestsMap.set(line.number, {
lineNumber: line.number,
position: request.position.from,
request: request,
});
}
}
}
});
}
return requestsMap;
}
/** 匹配 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;
/**
* StateField缓存所有 HTTP 请求的完整解析结果
* 只在文档改变时重新解析
*/
const httpRequestsField = StateField.define<Map<number, CachedHttpRequest>>({
create(state) {
return parseHttpRequests(state);
},
update(requests, transaction) {
// 只有文档改变或缓存为空时才重新解析
if (transaction.docChanged || requests.size === 0) {
return parseHttpRequests(transaction.state);
}
return requests;
}
});
// ==================== 运行按钮 Marker ====================
@@ -29,7 +117,10 @@ const MAX_REQUEST_MARKER_DISTANCE = 10;
* 运行按钮 Gutter Marker
*/
class RunButtonMarker extends GutterMarker {
constructor(private readonly linePosition: number) {
constructor(
private readonly lineNumber: number,
private readonly cachedRequest: HttpRequest
) {
super();
}
@@ -45,124 +136,46 @@ class RunButtonMarker extends GutterMarker {
e.stopPropagation();
this.executeRequest(view);
};
return button;
}
private async executeRequest(view: EditorView) {
console.log(`\n============ 执行 HTTP 请求 ============`);
console.log(`位置: ${this.linePosition}`);
console.log('行号:', this.lineNumber);
// 直接使用缓存的解析结果,无需重新解析!
console.log('解析结果:', JSON.stringify(this.cachedRequest, null, 2));
// TODO: 调用后端 API 执行请求
// const response = await executeHttpRequest(this.cachedRequest);
// renderResponse(response);
}
}
/**
* 使用语法树检查一行是否是 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);
},
lineMarker(view, line) {
// O(1) 查找:从缓存中获取请求
const requestsMap = view.state.field(httpRequestsField, false);
if (!requestsMap) return null;
const lineNumber = view.state.doc.lineAt(line.from).number;
const cached = requestsMap.get(lineNumber);
if (!cached) {
return null;
}
// 创建并返回运行按钮,传递缓存的解析结果
return new RunButtonMarker(cached.lineNumber, cached.request);
},
});
export const httpRunButtonTheme = EditorView.baseTheme({
@@ -183,7 +196,7 @@ export const httpRunButtonTheme = EditorView.baseTheme({
padding: '0',
transition: 'color 0.15s ease',
},
// 悬停效果
'.cm-http-run-button:hover': {
color: '#45a049', // 深绿色
@@ -197,3 +210,5 @@ export const httpRunButtonTheme = EditorView.baseTheme({
},
});
// 导出 StateField 供扩展系统使用
export { httpRequestsField };