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