🚧 Optimized HTTP language parser
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user