🚧 Improve HTTP language parser

This commit is contained in:
2025-11-02 00:01:16 +08:00
parent 94306497a9
commit 7aa3a7e37f
16 changed files with 644 additions and 222 deletions

View File

@@ -5,12 +5,14 @@
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

@@ -1,27 +1,33 @@
import { EditorView } from '@codemirror/view';
import { EditorState, ChangeSpec } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
import { EditorState, ChangeSpec, StateField } from '@codemirror/state';
import { syntaxTree, syntaxTreeAvailable } from '@codemirror/language';
import type { SyntaxNode } from '@lezer/common';
import { blockState } from '../../codeblock/state';
import { getNoteBlockFromPos } from '../../codeblock/state';
/**
* 响应数据模型
*/
export interface HttpResponse {
/** 状态码 */
status: number;
/** 状态文本 */
statusText: string;
/** 状态码和状态文本,如"200 OK" */
status: string;
/** 响应时间(毫秒) */
time: number;
/** 请求大小 */
requestSize?: string;
/** 响应体 */
body: any;
/** 响应头 */
headers?: { [_: string]: string[] };
/** 时间戳 */
timestamp?: Date;
/** 错误信息 */
error?: any;
}
/**
@@ -34,6 +40,44 @@ const NODE_TYPES = {
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;
}
});
/**
* 响应插入位置信息
*/
@@ -55,98 +99,119 @@ export class HttpResponseInserter {
constructor(private view: EditorView) {}
/**
* 在请求后插入响应数据
* 插入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);
return;
}
const insertPos = this.findInsertPosition(state, requestPos);
if (!insertPos) {
console.error('no insert position');
return;
}
// 生成响应文本
const responseText = this.formatResponse(response);
// 创建变更
// 根据是否有旧响应决定插入内容
const insertText = insertPos.hasOldResponse
? responseText // 替换旧响应,不需要额外换行
: `\n${responseText}`; // 新插入,需要换行分隔
const changes: ChangeSpec = {
from: insertPos.from,
to: insertPos.to,
insert: responseText
insert: insertText
};
// 应用变更
this.view.dispatch({
changes,
// 将光标移动到插入内容的末尾
selection: { anchor: insertPos.from + responseText.length },
userEvent: 'http.response.insert',
// 保持光标在请求位置
selection: { anchor: requestPos },
// 滚动到插入位置
scrollIntoView: true
});
}
/**
* 查找插入位置
* 规则:
* 1. 在当前请求后面
* 2. 在下一个请求前面
* 3. 如果已有响应(# Response 开头),删除旧响应
* 查找插入位置(带缓存优化)
*/
private findInsertPosition(state: EditorState, requestPos: number): InsertPosition | null {
const tree = syntaxTree(state);
const blocks = state.field(blockState, false);
if (!blocks) return null;
// 找到当前 HTTP 块
const currentBlock = blocks.find(block =>
block.language.name === 'http' &&
block.content.from <= requestPos &&
requestPos <= block.content.to
);
if (!currentBlock) return null;
const context = this.findInsertionContext(
tree,
state,
requestPos,
currentBlock.content.from,
currentBlock.content.to
);
if (!context.requestNode) return null;
const requestEnd = context.requestNode.to;
if (context.oldResponse) {
// 如果有旧响应,精确替换(从上一行的末尾到响应末尾)
const oldResponseStartLine = state.doc.lineAt(context.oldResponse.from);
const prevLineNum = oldResponseStartLine.number - 1;
let deleteFrom = context.oldResponse.from;
if (prevLineNum >= 1) {
const prevLine = state.doc.line(prevLineNum);
deleteFrom = prevLine.to; // 从上一行的末尾开始删除
}
return {
from: deleteFrom,
to: context.oldResponse.to,
hasOldResponse: true
};
} else {
// 如果没有旧响应,在请求后面插入
const requestEndLine = state.doc.lineAt(requestEnd);
// 在当前行末尾插入formatResponse 会自动添加必要的换行)
return {
from: requestEndLine.to,
to: requestEndLine.to,
hasOldResponse: false
};
// 获取当前代码块
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 };
}
/**
@@ -216,41 +281,47 @@ export class HttpResponseInserter {
// 查找响应注释
if (!responseStartNode && forwardCursor.name === NODE_TYPES.LINE_COMMENT) {
const commentText = state.doc.sliceString(forwardCursor.from, forwardCursor.to);
// 避免不必要的 trim
// 避免不必要的 trim,同时识别普通响应和错误响应
if (commentText.startsWith('# Response') || commentText.startsWith(' # Response')) {
const startNode = forwardCursor.node;
responseStartNode = startNode;
foundResponse = true;
// 继续查找 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;
// 检查是否为错误响应(只有一行)
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;
}
afterJson = afterJson.nextSibling;
break;
}
break;
// 遇到下一个请求,停止
if (nextNode.name === NODE_TYPES.REQUEST_STATEMENT) {
break;
}
nextNode = nextNode.nextSibling;
}
// 遇到下一个请求,停止
if (nextNode.name === NODE_TYPES.REQUEST_STATEMENT) {
break;
}
nextNode = nextNode.nextSibling;
}
}
}
@@ -277,14 +348,22 @@ export class HttpResponseInserter {
* 格式化响应数据
*/
private formatResponse(response: HttpResponse): string {
// 如果有错误,使用最简洁的错误格式
if (response.error) {
return `# Response Error: ${response.error}`;
}
// 正常响应格式
const timestamp = response.timestamp || new Date();
const dateStr = this.formatTimestamp(timestamp);
// 构建响应头行(不带分隔符)
const headerLine = `# Response ${response.status} ${response.statusText} ${response.time}ms ${dateStr}`;
let headerLine = `# Response ${response.status} ${response.time}ms`;
if (response.requestSize) {
headerLine += ` ${response.requestSize}`;
}
headerLine += ` ${dateStr}`;
// 完整的开头行(只有响应头,不带分隔符)
const header = `\n${headerLine}\n`;
// 完整的开头行(不添加前导换行符)
const header = `${headerLine}\n`;
// 格式化响应体
let body: string;
@@ -299,15 +378,15 @@ export class HttpResponseInserter {
}
} else if (response.body === null || response.body === undefined) {
// 空响应(只有响应头和结束分隔线)
const endLine = `# ${'-'.repeat(headerLine.length - 2)}`; // 减去 "# " 的长度
const endLine = `# ${'-'.repeat(Math.max(16, headerLine.length - 2))}`; // 最小16个字符
return header + endLine;
} else {
// 对象或数组
body = JSON.stringify(response.body, null, 2);
}
// 结尾分隔线:和响应头行长度完全一致
const endLine = `# ${'-'.repeat(headerLine.length - 2)}`; // 减去 "# " 的长度
// 结尾分隔线:和响应头行长度一致最小16个字符
const endLine = `# ${'-'.repeat(Math.max(16, headerLine.length - 2))}`;
return header + body + `\n${endLine}`;
}
@@ -335,3 +414,8 @@ export function insertHttpResponse(view: EditorView, requestPos: number, respons
inserter.insertResponse(requestPos, response);
}
/**
* 导出StateField用于扩展配置
*/
export { responseCacheField };

View File

@@ -5,6 +5,7 @@ import { blockState } from '../../codeblock/state';
import { parseHttpRequest, type HttpRequest } from '../parser/request-parser';
import { insertHttpResponse, type HttpResponse } from '../parser/response-inserter';
import { createDebounce } from '@/common/utils/debounce';
import { ExecuteRequest } from '@/../bindings/voidraft/internal/services/httpclientservice';
/**
* 语法树节点类型常量
@@ -156,34 +157,36 @@ class RunButtonMarker extends GutterMarker {
this.setLoadingState(true);
try {
console.log(`\n============ 执行 HTTP 请求 ============`);
console.log('行号:', this.lineNumber);
console.log('解析结果:', JSON.stringify(this.cachedRequest, null, 2));
const response = await ExecuteRequest(this.cachedRequest);
if (!response) {
throw new Error('No response');
}
// TODO: 调用后端 API 执行请求
// 临时模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
// 临时模拟响应数据用于测试
const mockResponse: HttpResponse = {
status: 200,
statusText: 'OK',
time: Math.floor(Math.random() * 500) + 50, // 50-550ms
body: {
code: 200,
message: "请求成功",
data: {
id: 1001,
timestamp: new Date().toISOString()
}
},
timestamp: new Date()
// 转换后端响应为前端格式
const httpResponse: HttpResponse = {
status: response.status, // 后端已返回完整状态如"200 OK"
time: response.time,
requestSize: response.requestSize,
body: response.body,
headers: response.headers,
timestamp: response.timestamp ? new Date(response.timestamp) : new Date(),
error: response.error
};
// 插入响应数据
insertHttpResponse(view, this.cachedRequest.position.from, mockResponse);
insertHttpResponse(view, this.cachedRequest.position.from, httpResponse);
} catch (error) {
console.error('HTTP 请求执行失败:', error);
// 创建错误响应
const errorResponse: HttpResponse = {
status: 'Request Failed',
time: 0,
body: `Error: ${error instanceof Error ? error.message : String(error)}`,
timestamp: new Date(),
error: error
};
// 插入错误响应
insertHttpResponse(view, this.cachedRequest.position.from, errorResponse);
} finally {
this.setLoadingState(false);
}

View File

@@ -9,6 +9,7 @@ import {
} from '@codemirror/view';
import { Extension, Range } from '@codemirror/state';
import * as runtime from "@wailsio/runtime";
import { getNoteBlockFromPos } from '../codeblock/state';
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi;
@@ -53,6 +54,13 @@ function hyperLinkDecorations(view: EditorView, anchor?: HyperLinkExtensionOptio
const from = match.index;
const to = from + match[0].length;
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
continue;
}
const linkMark = Decoration.mark({
class: 'cm-hyper-link-text',
attributes: {
@@ -84,6 +92,13 @@ const linkDecorator = (
new MatchDecorator({
regexp: regexp || defaultRegexp,
decorate: (add, from, to, match, view) => {
// 检查当前位置是否在 HTTP 代码块中
const block = getNoteBlockFromPos(view.state, from);
if (block && block.language.name === 'http') {
// 如果在 HTTP 代码块中,跳过超链接装饰
return;
}
const url = match[0];
let urlStr = matchFn && typeof matchFn === 'function' ? matchFn(url, match.input, from, to) : url;
if (matchData && matchData[url]) {