From f45f4d1bb9c5fbf481aff707e493d2ea24d8409e Mon Sep 17 00:00:00 2001 From: Youngster_yj <907985037@qq.com> Date: Mon, 13 Jul 2026 15:49:06 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20YakitEditor=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yakitUI/YakitEditor/YakitEditor.tsx | 1271 ++--------------- .../yakitUI/YakitEditor/constants/index.ts | 95 ++ .../decorations/generateDecorations.ts | 441 ++++++ .../YakitEditor/fizzMenu/editerMenuFun.tsx | 453 ++++++ .../YakitEditor/hooks/useBinaryFold.ts | 98 ++ .../YakitEditor/hooks/usePluginSearch.ts | 121 ++ .../yakitUI/YakitEditor/hooks/useYakFormat.ts | 93 ++ .../yakitUI/YakitEditor/menus/menuHelpers.tsx | 141 ++ 8 files changed, 1523 insertions(+), 1190 deletions(-) create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/constants/index.ts create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts create mode 100644 app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 31bd17aee9..9a2a18387d 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're import { useDebounceFn, useGetState, - useKeyPress, useMemoizedFn, useThrottleFn, useUpdateEffect, @@ -20,7 +19,6 @@ import '@/utils/monacoSpec/html' import { YakitIMonacoEditor, YakitEditorProps, - YakitITextModel, KeyboardToFuncProps, YakitIModelDecoration, OperationRecord, @@ -28,54 +26,26 @@ import { OperationRecordRes, } from './YakitEditorType' import { showByRightContext } from '../YakitMenu/showByRightContext' -import { ConvertYakStaticAnalyzeErrorToMarker, YakStaticAnalyzeErrorResult } from '@/utils/editorMarkers' -import { StringToUint8Array } from '@/utils/str' import { baseMenuLists, extraMenuLists } from './contextMenus' -import { EditorMenu, EditorMenuItemDividerProps, EditorMenuItemProps, EditorMenuItemType } from './EditorMenu' +import { EditorMenuItemProps, EditorMenuItemType } from './EditorMenu' import cloneDeep from 'lodash/cloneDeep' import { getRemoteValue, setRemoteValue } from '@/utils/kv' import classNames from 'classnames' import styles from './YakitEditor.module.scss' import './StaticYakitEditor.scss' -import { queryYakScriptList } from '@/pages/yakitStore/network' -import { YakScript } from '@/pages/invoker/schema' import { failed } from '@/utils/notification' import { randomString } from '@/utils/randomUtil' import { v4 as uuidv4 } from 'uuid' -import { editor as newEditor } from 'monaco-editor' -import IModelDecoration = newEditor.IModelDecoration -import { - CountDirectionProps, - EditorDetailInfoProps, - HTTPFuzzerClickEditorMenu, - HTTPFuzzerRangeEditorMenu, - HTTPFuzzerRangeReadOnlyEditorMenu, - SmartDecodeAnchorRect, - SmartDecodeFloatPanel, - snapshotRect, -} from '@/pages/fuzzer/HTTPFuzzerEditorMenu' -import { QueryFuzzerLabelResponseProps } from '@/pages/fuzzer/StringFuzzer' -import { insertFileFuzzTag, insertTemporaryFileFuzzTag } from '@/pages/fuzzer/InsertFileFuzzTag' -import { monacoEditorWrite } from '@/pages/fuzzer/fuzzerTemplates' -import { onInsertYakFuzzer, showDictsAndSelect } from '@/pages/fuzzer/HTTPFuzzerPage' import { openExternalWebsite } from '@/utils/openWebsite' import emiter from '@/utils/eventBus/eventBus' import { GetPluginLanguage } from '@/pages/plugins/builtInData' -import { createRoot } from 'react-dom/client' -import { setEditorContext, YaklangMonacoSpec } from '@/utils/monacoSpec/yakEditor' +import { setEditorContext } from '@/utils/monacoSpec/yakEditor' import { YakParamProps } from '@/pages/plugins/pluginsType' -import { usePageInfo } from '@/store/pageInfo' -import { shallow } from 'zustand/shallow' -import { YakitRoute } from '@/enums/yakitRoute' -import { useStore } from '@/store/editorState' import { CloudDownloadIcon } from '@/assets/newIcon' import { IconSolidAIIcon, IconSolidAIWhiteIcon } from '@/assets/icon/colors' -import { PluginSwitchToTag } from '@/pages/pluginEditor/defaultconstants' -import { SyntaxFlowMonacoSpec } from '@/utils/monacoSpec/syntaxflowEditor' import { getStorageYakEditorShortcutKeyEvents, - getYakEditorShortcutKeyEvents, isPageOrGlobalShortcut, isYakEditorDefaultShortcut, isYakEditorShortcut, @@ -84,27 +54,22 @@ import ShortcutKeyFocusHook from '@/utils/globalShortcutKey/shortcutKeyFocusHook import useFocusContextStore from '@/utils/globalShortcutKey/shortcutKeyFocusHook/hooks/useStore' import { ShortcutKeyFocusType } from '@/utils/globalShortcutKey/events/global' import { - convertKeyboardToUIKey, convertKeyEventToKeyCombination, sortKeysCombination, } from '@/utils/globalShortcutKey/utils' -import { YakitKeyBoard, YakitKeyMod } from '@/utils/globalShortcutKey/keyboard' import { applyYakitMonacoTheme } from '@/utils/monacoSpec/theme' import { useTheme } from '@/hook/useTheme' import { keepSearchNameMapStore, useKeepSearchNameMap } from '@/store/keepSearchName' -import type { IEvent } from 'monaco-editor' -import { TFunction, useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { fontSizeOptions, useEditorFontSize } from '@/store/editorFontSize' import { editorActionBarStore, useShowActionBar } from '@/store/editorActionBar' import { JSONParseLog } from '@/utils/tool' import { BinaryFuzztagEntry, - buildChipLabel, collapseBinaryFuzztag, decodeBinaryTag, encodeBytesToTag, expandBinaryFuzztag, - findPlaceholderOffsets, registerBinaryFoldEntries, unregisterBinaryFoldEntries, } from './binaryFuzztag' @@ -112,91 +77,27 @@ import { BinaryFuzztagHexModal, BinaryFuzztagSubmitResult } from './BinaryFuzzta import { Base64HexFuzztagModal } from './Base64HexFuzztagModal' import { showYakitModal } from '../YakitModal/YakitModalConfirm' -// 二进制 Fuzztag 折叠侧表的内存上限:累积保留历史项以支持占位被破坏后再补回的恢复, -// 用上限淘汰最旧项防止长会话内存膨胀 -const MAX_BINARY_FOLD_ENTRIES = 500 - -export interface CodecTypeProps { - key?: string - verbose: string - subTypes?: CodecTypeProps[] - params?: YakParamProps[] - help?: React.ReactNode - isYakScript?: boolean -} - -export interface contextMenuProps { - key: string - value: string - isAiPlugin: boolean - params: YakParamProps[] -} - -interface IFindReplaceState { - isRevealed: boolean - searchString: string - change(update: { searchString: string }, moveCursor: boolean): void - onFindReplaceStateChange: IEvent<() => void> -} - -interface IFindController extends monaco.editor.IStandaloneCodeEditor { - getState(): IFindReplaceState - start(opts?: { - forceRevealReplace?: boolean - seedSearchStringFromSelection?: boolean - shouldFocus?: boolean - seedSearchStringFromGlobalClipboard?: boolean - }): void -} - -const { ipcRenderer } = window.require('electron') - -/** @name 字体key值对应字体大小 */ -const keyToFontSize: Record = { - 'font-size-small': 12, - 'font-size-middle': 16, - 'font-size-large': 20, -} - -/** 编辑器右键默认菜单 - 顶部 */ -const DefaultMenuTop: (t: TFunction, nowFontsize: number) => EditorMenuItemType[] = (t, nowFontsize) => { - return [ - { - key: 'font-size', - label: t('YakitEditor.fontSize'), - children: fontSizeOptions.map((val) => ({ - key: val + '', - label: `${val}${nowFontsize === val ? '\u00A0\u00A0\u00A0✓' : ''}`, - })), - }, - ] -} - -/** 编辑器右键默认菜单 - 底部 */ -const DefaultMenuBottom: (t: TFunction) => EditorMenuItemType[] = (t) => { - return [ - { key: 'cut', label: t('YakitEditor.cut') }, - { key: 'copy', label: t('YakitEditor.copy') }, - { key: 'paste', label: t('YakitEditor.paste') }, - ] -} - -function openFind(editor: YakitIMonacoEditor, keyword: string) { - // editor.focus() - const findController = editor.getContribution('editor.contrib.findController') - const state = findController?.getState() - if (!state?.isRevealed) { - findController?.start({ - seedSearchStringFromSelection: false, - shouldFocus: true, - }) - } +// ===== 拆分模块导入 ===== +import { + CodecTypeProps, + contextMenuProps, + DefaultMenuTop, + DefaultMenuBottom, + openFind, + PLUGIN_PREFIX, + IFindController, +} from './constants' +import { useBinaryFold } from './hooks/useBinaryFold' +import { usePluginSearch } from './hooks/usePluginSearch' +import { useYakFormat } from './hooks/useYakFormat' +import { generateDecorations as generateDecorationsFn } from './decorations/generateDecorations' +import { editerMenuFun } from './fizzMenu/editerMenuFun' +import { menuReduce, sortMenuFun, contextMenuKeybindingHandle } from './menus/menuHelpers' + +// re-export 保持外部导入路径兼容 +export { PLUGIN_PREFIX } +export type { CodecTypeProps, contextMenuProps } - if (state?.searchString !== keyword) { - state?.change({ searchString: keyword }, false) - } -} -export const PLUGIN_PREFIX = 'pluginExtension_' export const YakitEditor: React.FC = React.memo((props) => { const { forceRenderMenu = false, @@ -296,63 +197,19 @@ export const YakitEditor: React.FC = React.memo((props) => { const disableUnicodeDecodeRef = useRef(props.disableUnicodeDecode) // ===== 二进制 Fuzztag 折叠:翻译边界 ===== - // 仅在 foldBinaryFuzztag 且 http 类型下启用;模型存短占位,向上抛真实值,下游消费者无感知 - const foldBinaryCapable = props.foldBinaryFuzztag !== undefined - const [foldBinaryOpen, setFoldBinaryOpen, getFoldBinaryOpen] = useGetState(!!props.foldBinaryFuzztag) - useEffect(() => { - if (props.foldBinaryFuzztag !== undefined) { - setFoldBinaryOpen(!!props.foldBinaryFuzztag) - } - }, [props.foldBinaryFuzztag]) - const foldBinaryEnabled = foldBinaryCapable && foldBinaryOpen && type === 'http' - // 侧表:占位 id -> 原始标签信息;handleBinaryChange 据此 expand 还原真实文本 - const binaryFoldEntriesRef = useRef>(new Map()) - // 占位范围(仿 privacyMaskRangesRef),用于点击命中打开 HEX 编辑弹窗 - const binaryFoldRangesRef = useRef<{ id: string; range: monaco.Range; ordinal: number }[]>([]) - // "被修改"记录:按编辑器中第 N 个二进制标签(文档顺序的序号)记录,只记是否改过(布尔)。 - // 与内容/占位 id 解耦,保证复制粘贴出去的永远是纯内容、不含任何改动元数据。 - const binaryModifiedOrdinalsRef = useRef>(new Set()) - // 计算传给 MonacoEditor 的展示文本(真实值 -> 占位) - const displayValue = useMemo(() => { - if (!foldBinaryEnabled) { - // 原地清空而非替换对象,保持注册表里登记的 map 引用一直有效 - binaryFoldEntriesRef.current.clear() - binaryModifiedOrdinalsRef.current.clear() - return value - } - const { text, entries } = collapseBinaryFuzztag(value ?? '') - // 累积合并而非整表替换:保留历史占位映射。 - // 原因:用户在占位上 backspace 会把占位破坏成非法 fuzztag(如缺一个 }), - // 此时 expand 无法匹配 -> 真实二进制会被破坏文本顶替丢失;若再整表替换映射, - // 即使补回 }} 也找不到 id 对应的原始标签,Binary 小块与内容永久无法恢复。 - // 保留映射后,补回完整占位即可由 expand 还原真实内容并重新折叠出小块。 - const map = binaryFoldEntriesRef.current - entries.forEach((v, k) => { - // 重新插入以将当前文本中的项标记为最新,避免被下方内存淘汰 - map.delete(k) - map.set(k, v) - }) - // 限制内存上限,淘汰最旧项;当前文本中的项已在上面置为最新,不会被淘汰 - while (map.size > MAX_BINARY_FOLD_ENTRIES) { - const oldest = map.keys().next().value - if (oldest === undefined) { - break - } - map.delete(oldest) - } - return text - }, [value, foldBinaryEnabled]) - // 向上回调:占位 -> 真实值 - const handleBinaryChange = useMemoizedFn((content: string) => { - const emit = setValue || onChange - if (!emit) { - return - } - if (!foldBinaryEnabled) { - emit(content) - return - } - emit(expandBinaryFuzztag(content, binaryFoldEntriesRef.current)) + const { + foldBinaryEnabled, + binaryFoldEntriesRef, + binaryFoldRangesRef, + binaryModifiedOrdinalsRef, + displayValue, + handleBinaryChange, + } = useBinaryFold({ + value, + setValue, + onChange, + foldBinaryFuzztag: props.foldBinaryFuzztag, + type, }) useLayoutEffect(() => { @@ -409,101 +266,11 @@ export const YakitEditor: React.FC = React.memo((props) => { onOperationRecord('noWordWrap', noWordWrap) }, [noWordWrap]) - // 自定义HTTP数据包变形处理 - const { customHTTPMutatePlugin, contextMenuPlugin, setCustomHTTPMutatePlugin, setContextMenuPlugin } = useStore() - const searchCodecCustomHTTPMutatePlugin = useMemoizedFn(() => { - queryYakScriptList( - 'codec', - (i: YakScript[], total) => { - if (!total || total === 0) { - return - } - setCustomHTTPMutatePlugin( - i.map((script) => { - return { - key: script.ScriptName, - verbose: 'CODEC 社区插件: ' + script.ScriptName, - isYakScript: true, - } as CodecTypeProps - }), - ) - }, - undefined, - 10, - undefined, - undefined, - undefined, - undefined, - [PluginSwitchToTag.PluginCodecHttpSwitch], - ) - }) - - // 插件扩展 - const searchCodecCustomContextMenuPlugin = useMemoizedFn(() => { - queryYakScriptList( - 'codec', - (i: YakScript[], total) => { - if (!total || total === 0) { - return - } - setContextMenuPlugin( - i.map((script) => { - const isAiPlugin: boolean = script.Tags.includes('AI工具') - return { - key: script.ScriptName, - value: script.ScriptName, - isAiPlugin, - params: script.Params, - } as contextMenuProps - }), - ) - }, - undefined, - 10, - undefined, - undefined, - undefined, - undefined, - [PluginSwitchToTag.PluginCodecContextMenuExecuteSwitch], - ) - }) - + // 自定义HTTP数据包变形处理 + 插件扩展 const ref = useRef(null) const [inViewport] = useInViewport(ref) - useEffect(() => { - if (inViewport && menuType.length > 0) { - searchCodecCustomHTTPMutatePlugin() - searchCodecCustomContextMenuPlugin() - } - }, [inViewport]) - - const onRefreshPluginCodecMenu = useMemoizedFn(() => { - if (inViewport && menuType.length > 0) { - searchCodecCustomHTTPMutatePlugin() - searchCodecCustomContextMenuPlugin() - } - }) - - useEffect(() => { - emiter.on('onRefPluginCodecMenu', onRefreshPluginCodecMenu) - return () => { - emiter.off('onRefPluginCodecMenu', onRefreshPluginCodecMenu) - } - }, []) - - // 菜单数组去重 - const menuReduce = useMemoizedFn((array: any[]) => { - let newArr: any[] = [] - let arr: string[] = [] - array.forEach((item) => { - if (!arr.includes(item.key)) { - arr.push(item.key) - newArr.push(item) - } - }) - return newArr - }) + const { customHTTPMutatePlugin, contextMenuPlugin } = usePluginSearch({ menuType, inViewport }) /** * 整理右键菜单的对应关系 @@ -818,94 +585,6 @@ export const YakitEditor: React.FC = React.memo((props) => { } }) - /** 菜单自定义快捷键渲染处理事件 */ - const contextMenuKeybindingHandle = useMemoizedFn((parentKey: string, data: EditorMenuItemType[]) => { - const menus: EditorMenuItemType[] = [] - for (let item of data) { - /** 屏蔽菜单分割线选项 */ - if (typeof (data as any as EditorMenuItemDividerProps)['type'] !== 'undefined') { - const info: EditorMenuItemDividerProps = { type: 'divider' } - menus.push(info) - } else { - /** 处理带快捷键的菜单项 */ - const info = { ...item } as EditorMenuItemProps - if (info.children && info.children.length > 0) { - info.children = contextMenuKeybindingHandle(info.key, info.children) - } else { - if (info.key === 'cut' && info.label === t('YakitEditor.cut')) { - const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_X]) - - info.label = keysContent ? ( -
-
{t('YakitEditor.cut')}
-
{keysContent}
-
- ) : ( - info.label - ) - } - if (info.key === 'copy' && info.label === t('YakitEditor.copy')) { - const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_C]) - info.label = keysContent ? ( -
-
{t('YakitEditor.copy')}
-
{keysContent}
-
- ) : ( - info.label - ) - } - if (info.key === 'paste' && info.label === t('YakitEditor.paste')) { - const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_V]) - info.label = keysContent ? ( -
-
{t('YakitEditor.paste')}
-
{keysContent}
-
- ) : ( - info.label - ) - } - - if (info.keybindings && info.keybindings.length > 0) { - const keyArr = getYakEditorShortcutKeyEvents()[info.keybindings].keys - const keysContent = convertKeyboardToUIKey(keyArr) - // 记录自定义快捷键映射按键的回调事件 - if (keysContent) { - let sortKeys = sortKeysCombination(keyArr) - keyBindingRef.current[sortKeys.join('-')] = parentKey ? [info.key, parentKey] : [info.key] - } - - info.label = keysContent ? ( -
-
{info.label}
-
{keysContent}
-
- ) : ( - info.label - ) - } - } - menus.push(info) - } - } - return menus - }) - - const sortMenuFun = useMemoizedFn((dataSource, sortData) => { - const result = sortData.reduce( - (acc, item) => { - if (item.order >= 0) { - acc.splice(item.order, 0, ...item.menu) - } else { - acc.push(...item.menu) - } - return acc - }, - [...dataSource], - ) - return result - }) /** yak后缀文件中,右键菜单增加'Yak 代码格式化'功能 */ useEffect(() => { /** @@ -971,7 +650,7 @@ export const YakitEditor: React.FC = React.memo((props) => { if (sortContextMenu.length > 0) { rightContextMenu.current = sortMenuFun(rightContextMenu.current, sortContextMenu) } - rightContextMenu.current = contextMenuKeybindingHandle('', rightContextMenu.current) + rightContextMenu.current = contextMenuKeybindingHandle(t, keyBindingRef, '', rightContextMenu.current) if (!forceRenderMenu) isInitRef.current = true }, [ @@ -1049,361 +728,30 @@ export const YakitEditor: React.FC = React.memo((props) => { // 检查 model 是否已被释放 if (!model || isModelDisposedRef.current) return [] try { - const endsp = model.getPositionAt(1800) - const dec: YakitIModelDecoration[] = [] - const text = - endsp.lineNumber === 1 - ? model.getValueInRange({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: endsp.column, - }) - : model.getValueInRange({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: endsp.lineNumber, - endColumn: endsp.column, - }) - - if (props.type === 'http') { - ;(() => { - try { - //http - ;[{ regexp: /\nContent-Length:\s*?\d+/, classType: 'content-length' }].map((detail) => { - // handle content-length - const match = detail.regexp.exec(text) - if (!match) { - return - } - const start = model.getPositionAt(match.index) - const end = model.getPositionAt(match.index + match[0].indexOf(':')) - dec.push({ - id: detail.classType + match.index, - ownerId: 0, - range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { - afterContentClassName: `${detail.classType} lang-${i18n.language}`, - }, - } as YakitIModelDecoration) - }) - } catch (e) {} - })() - ;(() => { - try { - if (!props.showHostHint) return - const fullText = model.getValue() - const hostRegex = /\nHost:\s*?([^\r\n]+)/ - const hostMatch = hostRegex.exec(fullText) - if (!hostMatch) return - - const fullMatch = hostMatch[0] - const hostColonIndex = hostMatch.index + fullMatch.indexOf(':') - const hostLabelStart = model.getPositionAt(hostMatch.index) - const hostLabelEnd = model.getPositionAt(hostColonIndex + 1) - - // 添加 Host 标签装饰器(? 标记) - dec.push({ - id: `host-label-${hostMatch.index}`, - ownerId: 0, - range: new monaco.Range( - hostLabelStart.lineNumber, - hostLabelStart.column, - hostLabelEnd.lineNumber, - hostLabelEnd.column, - ), - options: { - afterContentClassName: `host lang-${i18n.language}`, - }, - } as YakitIModelDecoration) - - if (!privacyFun()) return - - // 提取并处理 Host 值 - const hostValue = hostMatch[1].trim() - if (!hostValue) return - - const colonIndex = hostValue.indexOf(':') - const hostname = colonIndex > 0 ? hostValue.substring(0, colonIndex) : hostValue - if (!hostname) return - - // 构建搜索模式(hostname、hostname:port、不带www的域名) - const patterns = [hostname] - if (colonIndex > 0) patterns.push(hostValue) - if (hostname.toLowerCase().startsWith('www.')) { - const withoutWww = hostname.substring(4) - if (withoutWww) patterns.push(withoutWww) - } - - // 使用正则一次性查找所有匹配 - const escapedPatterns = patterns.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - const searchRegex = new RegExp(escapedPatterns.join('|'), 'g') - - const matches: { index: number; length: number }[] = [] - let match: RegExpExecArray | null - while ((match = searchRegex.exec(fullText)) !== null) { - matches.push({ index: match.index, length: match[0].length }) - } - - // 去重重叠匹配(保留较长的) - const filtered = matches - .sort((a, b) => (a.index !== b.index ? a.index - b.index : b.length - a.length)) - .filter((curr, idx, arr) => idx === 0 || curr.index >= arr[idx - 1].index + arr[idx - 1].length) - - // 获取光标位置用于判断是否显示遮罩 - const cursorPos = editor.getPosition() - const newPrivacyRanges: { id: string; range: monaco.Range }[] = [] - - filtered.forEach((m, idx) => { - const start = model.getPositionAt(m.index) - const end = model.getPositionAt(m.index + m.length) - const range = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column) - const decorationId = `host-privacy-${idx}` - - newPrivacyRanges.push({ id: decorationId, range }) - - // 光标在范围内则不显示遮罩 - const isCursorIn = - cursorPos?.lineNumber === start.lineNumber && - cursorPos.column >= start.column && - cursorPos.column <= end.column - - if (!isCursorIn) { - dec.push({ - id: decorationId, - ownerId: 0, - range, - options: { - inlineClassName: `host-privacy-mask-hidden lang-${i18n.language}`, - stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }, - } as YakitIModelDecoration) - } - }) - - privacyMaskRangesRef.current = newPrivacyRanges - } catch (e) {} - })() - } - const needDecode = props.type && ['html', 'http', 'json'].includes(props.type) - if (needDecode && !disableUnicodeDecodeRef.current) { - ;(() => { - // http html json - const text = model.getValue() - let match - const regex = /(\\u[\dabcdef]{4})+/gi - - while ((match = regex.exec(text)) !== null) { - const start = model.getPositionAt(match.index) - const end = model.getPositionAt(match.index + match[0].length) - const decoded = match[0] - .split('\\u') - .filter(Boolean) - .map((hex) => String.fromCharCode(parseInt(hex, 16))) - .join('') - dec.push({ - id: 'decode' + match.index, - ownerId: 1, - range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { - className: 'unicode-decode', - hoverMessage: { value: decoded }, - afterContentClassName: 'unicode-decode', - after: { content: decoded, inlineClassName: 'unicode-decode-after' }, - }, - } as IModelDecoration) - } - })() - } - - ;(() => { - const targetValue = fixContentTypeFun() - if (!targetValue) return - const text = model.getValue() - let match - - // 匹配 Content-Type: 后面的值 - const regex = /Content-Type:\s*([^\r\n]*)/gi - - while ((match = regex.exec(text)) !== null) { - const contentTypeValue = match[1].trim() // 获取 Content-Type 后的值并去除多余空格 - if (contentTypeValue === targetValue) { - // 计算 Content-Type: 后具体值的起始位置,避免空格问题 - const textBeforeMatch = text.substring(match.index, regex.lastIndex) // 获取匹配到的完整文本 - const contentStartIndex = match.index + textBeforeMatch.indexOf(contentTypeValue) // 确保起始位置精确匹配 - - const start = model.getPositionAt(contentStartIndex) - const end = model.getPositionAt(match.index + match[0].length) - - dec.push({ - id: 'decode' + match.index, - ownerId: 1, - range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { - className: 'unicode-decode', - hoverMessage: { value: fixContentTypeHoverMessageFun() }, - afterContentClassName: 'unicode-decode', - after: { - content: - originalContentTypeFun() === '' - ? t('YakitEditor.emptyContentTypeAutoDetected') - : originalContentTypeFun(), - inlineClassName: 'unicode-decode-after', - }, - }, - } as IModelDecoration) - } - } - })() - ;(() => { - // all - const keywordRegExp = /\r?\n/g - let match - let count = 0 - while ((match = keywordRegExp.exec(text)) !== null) { - count++ - const start = model.getPositionAt(match.index) - const className: 'crlf' | 'lf' = match[0] === '\r\n' ? 'crlf' : 'lf' - const end = model.getPositionAt(match.index + match[0].length) - dec.push({ - id: 'keyword' + match.index, - ownerId: 2, - range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), - options: { beforeContentClassName: className }, - } as YakitIModelDecoration) - if (count > 19) { - return - } - } - })() - - function highLightRange(item) { - const { startOffset = 0, highlightLength = 0, startLineNumber, startColumn, endLineNumber, endColumn } = item - let range = { - startLineNumber: 0, - startColumn: 0, - endLineNumber: 0, - endColumn: 0, - } - if (typeof startLineNumber === 'number') { - range.startLineNumber = startLineNumber - range.startColumn = startColumn - range.endLineNumber = endLineNumber - range.endColumn = endColumn - } else { - if (model) { - // 获取偏移量对应的位置 - const startPosition = model.getPositionAt(Number(startOffset)) - const endPosition = model.getPositionAt(Number(startOffset) + Number(highlightLength)) - range.startLineNumber = startPosition.lineNumber - range.startColumn = startPosition.column - range.endLineNumber = endPosition.lineNumber - range.endColumn = endPosition.column - } - } - return range - } - - ;(() => { - //all - highLightTextFun().forEach((item) => { - const range = highLightRange(item) - // 创建装饰选项 - dec.push({ - id: - 'hight-light-text_' + - range.startLineNumber + - '_' + - range.startColumn + - '_' + - range.endLineNumber + - '_' + - range.endColumn, - ownerId: 3, - range: new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), - options: { - isWholeLine: false, - className: highLightClass ? highLightClass : 'hight-light-default-bg-color', - hoverMessage: [{ value: item.hoverVal, isTrusted: true }], - }, - } as IModelDecoration) - }) - - highLightFindFun().forEach((item) => { - const range = highLightRange(item) - // 创建装饰选项 - dec.push({ - id: - 'hight-light-find_' + - range.startLineNumber + - '_' + - range.startColumn + - '_' + - range.endLineNumber + - '_' + - range.endColumn, - ownerId: 3, - range: new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), - options: { - isWholeLine: false, - className: highLightFindClass ? highLightFindClass : 'hight-light-find-default-bg-color', - hoverMessage: [{ value: '', isTrusted: true }], - }, - } as IModelDecoration) - }) - })() - - // 二进制 Fuzztag 折叠:为占位渲染 Binary[..]/File[..] 小块,并记录范围用于点击 - ;(() => { - if (!foldBinaryEnabled) { - binaryFoldRangesRef.current = [] - return - } - try { - const fullText = model.getValue() - const offsets = findPlaceholderOffsets(fullText) - const newRanges: { id: string; range: monaco.Range; ordinal: number }[] = [] - // index 即"编辑器中第 N 个二进制标签"的序号,按文档顺序;据此判断是否被修改过 - offsets.forEach((off, index) => { - const entry = binaryFoldEntriesRef.current.get(off.id) - if (!entry) { - return - } - const changed = binaryModifiedOrdinalsRef.current.has(index) - const start = model.getPositionAt(off.start) - const end = model.getPositionAt(off.end) - const range = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column) - newRanges.push({ id: off.id, range, ordinal: index }) - dec.push({ - id: 'binary-fold-' + off.id + '-' + off.start, - ownerId: 0, - range, - options: { - inlineClassName: 'binary-fuzz-hidden', - // 关键:声明该 inlineClassName 会改变字符宽度(font-size:0), - // 强制 monaco 放弃等宽快速路径(FastRenderedViewLine)、改用 DOM 实测, - // 否则隐藏占位仍按 charWidth*列数 占据光标/选区宽度,表现为 chip 后的可选中空白 - inlineClassNameAffectsLetterSpacing: true, - after: { - content: buildChipLabel(entry, changed), - inlineClassName: changed ? 'binary-fuzz-chip binary-fuzz-chip-changed' : 'binary-fuzz-chip', - // chip 带 padding/不同字号,宽度非等宽,同样需 DOM 实测以保证光标/选区贴合 - inlineClassNameAffectsLetterSpacing: true, - }, - stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - hoverMessage: { - value: entry.editable - ? 'Click to modify: open the helper editor to edit content of any size' - : 'Click to view (read-only reference)', - }, - }, - } as YakitIModelDecoration) - }) - binaryFoldRangesRef.current = newRanges - } catch (e) {} - })() - - return dec + return generateDecorationsFn({ + model, + editor, + type: props.type, + showHostHint: props.showHostHint, + privacy: privacyFun(), + disableUnicodeDecode: disableUnicodeDecodeRef.current, + highLightText: highLightTextFun(), + highLightClass, + highLightFind: highLightFindFun(), + highLightFindClass, + fixContentType: fixContentTypeFun(), + originalContentType: originalContentTypeFun(), + fixContentTypeHoverMessage: fixContentTypeHoverMessageFun(), + foldBinaryEnabled, + binaryFoldEntriesRef, + binaryFoldRangesRef, + binaryModifiedOrdinalsRef, + language: i18n.language, + t, + onPrivacyRanges: (ranges) => { + privacyMaskRangesRef.current = ranges + }, + }) } catch (e) { // model 可能已被释放 return [] @@ -1773,90 +1121,8 @@ export const YakitEditor: React.FC = React.memo((props) => { }) }) - /** 计算编辑器的高度 有点问题,为什么用state记录而不是ref记录,测试过后删除该问题 */ - const handleEditorMount = (editor: YakitIMonacoEditor, monaco: any) => { - editor.onDidChangeModelDecorations(() => { - updateEditorHeight() // typing - /** - * @description 浏览器自带函数(IE9以上版本特性),自动获取屏幕刷新率,计算出计时器的执行时间,并触发传入的回调函数 - */ - requestAnimationFrame(updateEditorHeight) // folding - }) - - const updateEditorHeight = () => { - const editorElement = editor.getDomNode() - - if (!editorElement) { - return - } - - const padding = 40 - - const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) - const lineCount = editor.getModel()?.getLineCount() || 1 - const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight + padding - - if (preHeightRef.current !== height) { - preHeightRef.current = height - // setPreHeight(height) - editorElement.style.height = `${height}px` - editor.layout() - } - } - } - - /** Yak 代码格式化功能实现 */ - const yakCompileAndFormat = useDebounceFn( - useMemoizedFn((editor: YakitIMonacoEditor, model: YakitITextModel) => { - const allContent = model.getValue() - ipcRenderer - .invoke('YaklangCompileAndFormat', { Code: allContent }) - .then((e: { Errors: YakStaticAnalyzeErrorResult[]; Code: string }) => { - if (e.Code !== '') { - model.setValue(e.Code) - } - - /** 编辑器中错误提示的标记 */ - if (e && e.Errors.length > 0) { - const markers = e.Errors.map(ConvertYakStaticAnalyzeErrorToMarker) - monaco.editor.setModelMarkers(model, 'owner', markers) - } else { - monaco.editor.setModelMarkers(model, 'owner', []) - } - }) - .catch((e) => { - console.info(e) - }) - }), - { wait: 500, leading: true, trailing: false }, - ) - - const AnalyzeSessionIDRef = useRef(uuidv4()) - /** Yak语言 代码错误检查并显示提示标记 */ - const yakStaticAnalyze = useDebounceFn( - useMemoizedFn((editor: YakitIMonacoEditor, model: YakitITextModel) => { - if (language === YaklangMonacoSpec || language === SyntaxFlowMonacoSpec) { - const allContent = model.getValue() - ipcRenderer - .invoke('StaticAnalyzeError', { - Code: StringToUint8Array(allContent), - PluginType: type, - SessionID: AnalyzeSessionIDRef.current, - }) - .then((e: { Result: YakStaticAnalyzeErrorResult[] }) => { - if (e && e.Result.length > 0) { - const markers = e.Result.map(ConvertYakStaticAnalyzeErrorToMarker) - monaco.editor.setModelMarkers(model, 'owner', markers) - } else { - monaco.editor.setModelMarkers(model, 'owner', []) - } - }) - } else { - monaco.editor.setModelMarkers(model, 'owner', []) - } - }), - { wait: 300 }, - ) + // ===== Yak 代码格式化 + 静态分析 ===== + const { yakCompileAndFormat, yakStaticAnalyze } = useYakFormat({ language, type }) const downPosY = useRef() const upPosY = useRef() @@ -1865,7 +1131,22 @@ export const YakitEditor: React.FC = React.memo((props) => { const editorInfo = useRef() useEffect(() => { if (editor && isShowSelectRangeMenu) { - editerMenuFun(editor) + editerMenuFun({ + editor, + selectNode, + rangeNode, + readOnly, + value, + overLine, + getShowActionBar, + execAutoDecodeCallback, + editorInfoRef: editorInfo, + fizzSelectTimeoutIdRef: fizzSelectTimeoutId, + fizzRangeTimeoutIdRef: fizzRangeTimeoutId, + downPosYRef: downPosY, + upPosYRef: upPosY, + onScrollTopRef: onScrollTop, + }) } }, [editor, isShowSelectRangeMenu]) // 定时消失的定时器 @@ -1888,396 +1169,6 @@ export const YakitEditor: React.FC = React.memo((props) => { }) } // 编辑器菜单 - const editerMenuFun = (editor: YakitIMonacoEditor) => { - // 编辑器点击弹窗的唯一Id - const selectId: string = `monaco.fizz.select.widget-${uuidv4()}` - // 编辑器选中弹窗的唯一Id - const rangeId: string = `monaco.fizz.range.widget-${uuidv4()}` - // 插入标签 - const insertLabelFun = (v: QueryFuzzerLabelResponseProps) => { - if (v.Label) { - editor && editor.trigger('keyboard', 'type', { text: v.Label }) - } else if (v.DefaultDescription === '插入文件-fixed') { - editor && insertFileFuzzTag((i) => monacoEditorWrite(editor, i), 'file:line') - } else if (v.DefaultDescription === '插入字典-fixed') { - editor && - showDictsAndSelect((i) => { - monacoEditorWrite(editor, i, editor.getSelection()) - }) - } else if (v.DefaultDescription === '插入临时字典-fixed') { - editor && insertTemporaryFileFuzzTag((i) => monacoEditorWrite(editor, i)) - } - } - const toOpenAiChat = (scriptName: string, params?: YakParamProps[]) => { - if (scriptName === 'aiplugin-Get*plug-in') { - emiter.emit('onOpenFuzzerModal', JSON.stringify({ scriptName, isAiPlugin: 'isGetPlugin' })) - closeFizzRangeWidget() - return - } - - if (editor) { - const selectedText = editor.getModel()?.getValueInRange(editor.getSelection() as any) || value - emiter.emit( - 'onOpenFuzzerModal', - JSON.stringify({ text: selectedText, scriptName, isAiPlugin: true, params, isExec: false }), - ) - closeFizzRangeWidget() - } - } - // 编辑器点击显示的菜单 - const fizzSelectWidget = { - isOpen: false, - getId: function () { - return selectId - }, - getDomNode: function () { - // 将TSX转换为DOM节点 - const domNode = document.createElement('div') - // 解决弹窗内鼠标滑轮无法滚动的问题 - domNode.onwheel = (e) => e.stopPropagation() - if (selectNode) { - createRoot(domNode).render(selectNode(closeFizzSelectWidget, editorInfo.current)) - } else { - createRoot(domNode).render( - closeFizzSelectWidget()} - fizzSelectTimeoutId={fizzSelectTimeoutId} - toOpenAiChat={toOpenAiChat} - insert={(v: QueryFuzzerLabelResponseProps) => { - insertLabelFun(v) - closeFizzSelectWidget() - }} - addLabel={() => { - closeFizzSelectWidget() - onInsertYakFuzzer(editor) - }} - />, - ) - } - return domNode - }, - getPosition: function () { - const currentPos = editor.getPosition() - return { - position: { - lineNumber: currentPos?.lineNumber || 0, - column: currentPos?.column || 0, - }, - preference: [1, 2], - } - }, - update: function () { - // 更新小部件的位置 - this.getPosition() - editor.layoutContentWidget(this) - }, - } - // 编辑器选中显示的菜单 - const fizzRangeWidget = { - isOpen: false, - getId: function () { - return rangeId - }, - getDomNode: function () { - // 将TSX转换为DOM节点 - const domNode = document.createElement('div') - // 解决弹窗内鼠标滑轮无法滚动的问题 - domNode.onwheel = (e) => e.stopPropagation() - if (rangeNode) { - createRoot(domNode).render(rangeNode(closeFizzRangeWidget, editorInfo.current)) - } else { - readOnly - ? createRoot(domNode).render( - closeFizzRangeWidget()} - fizzRangeTimeoutId={fizzRangeTimeoutId} - execAutoDecodeCallback={execAutoDecodeCallback} - />, - ) - : createRoot(domNode).render( - closeFizzRangeWidget()} - insert={(fun: any) => { - if (editor) { - const selectedText = editor.getModel()?.getValueInRange(editor.getSelection() as any) || '' - if (selectedText.length > 0) { - ipcRenderer - .invoke('QueryFuzzerLabel') - .then((data: { Data: QueryFuzzerLabelResponseProps[] }) => { - const { Data } = data - let newSelectedText: string = selectedText - if (Array.isArray(Data) && Data.length > 0) { - // 选中项是否存在于标签中 - let isHave: boolean = Data.map((item) => item.Label).includes(selectedText) - if (isHave) { - newSelectedText = selectedText.replace(/{{|}}/g, '') - } - } - const text: string = fun(newSelectedText) - // editor.trigger("keyboard", "type", {text})// 选择范围大会卡死 - monacoEditorWrite(editor, text) - }) - } - } - }} - replace={(text: string) => { - if (editor) { - editor.trigger('keyboard', 'paste', { text }) - closeFizzRangeWidget() - } - }} - toOpenAiChat={toOpenAiChat} - rangeValue={(editor && editor.getModel()?.getValueInRange(editor.getSelection() as any)) || ''} - fizzRangeTimeoutId={fizzRangeTimeoutId} - onOpenSmartDecode={(rangeValue, anchorRect) => { - openSmartDecodeRef.current?.(rangeValue, editorInfo.current, anchorRect) - }} - hTTPFuzzerClickEditorMenuProps={ - readOnly - ? undefined - : { - editorInfo: editorInfo.current, - close: () => closeFizzRangeWidget(), - insert: (v: QueryFuzzerLabelResponseProps) => { - insertLabelFun(v) - closeFizzRangeWidget() - }, - addLabel: () => { - closeFizzRangeWidget() - onInsertYakFuzzer(editor) - }, - } - } - />, - ) - } - return domNode - }, - getPosition: function () { - const currentPos = editor.getPosition() - - return { - position: { - lineNumber: currentPos?.lineNumber || 0, - column: currentPos?.column || 0, - }, - preference: [2, 1], - } - }, - update: function () { - // 更新小部件的位置 - this.getPosition() - editor.layoutContentWidget(this) - }, - } - // 是否展示菜单 - // if (false) { - // closeFizzSelectWidget() - // return - // } - - // 关闭点击的菜单 - const closeFizzSelectWidget = () => { - fizzSelectWidget.isOpen = false - fizzSelectTimeoutId.current && clearTimeout(fizzSelectTimeoutId.current) - editor.removeContentWidget(fizzSelectWidget) - } - // 关闭选中的菜单 - const closeFizzRangeWidget = () => { - fizzRangeWidget.isOpen = false - fizzRangeTimeoutId.current && clearTimeout(fizzRangeTimeoutId.current) - editor.removeContentWidget(fizzRangeWidget) - } - - // 编辑器更新 关闭之前展示 - closeFizzSelectWidget() - closeFizzRangeWidget() - - editor?.getModel()?.pushEOL(newEditor.EndOfLineSequence.CRLF) - editor.onMouseMove((e) => { - try { - // const pos = e.target.position - // if (pos?.lineNumber) { - // const lineOffset = pos.lineNumber - (editor.getPosition()?.lineNumber || 0) - // // 超出范围移除菜单 - // if (lineOffset > 2 || lineOffset < -2) { - // // console.log("移出两行内"); - // closeFizzSelectWidget() - // closeFizzRangeWidget() - // } - // } - - const { target, event } = e - const { posy } = event - const detail = - target.type === newEditor.MouseTargetType.CONTENT_WIDGET || - target.type === newEditor.MouseTargetType.OVERLAY_WIDGET - ? target.detail - : undefined - const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) - if (detail !== selectId && detail !== rangeId && downPosY.current && upPosY.current) { - const overHeight = overLine * lineHeight - if (fizzSelectWidget.isOpen) { - if (posy < upPosY.current - overHeight || posy > upPosY.current + overHeight) { - closeFizzSelectWidget() - } - } else if (fizzRangeWidget.isOpen) { - // 从上到下的选择范围 - if ( - downPosY.current < upPosY.current && - (posy < downPosY.current - overHeight || posy > upPosY.current + overHeight) - ) { - closeFizzRangeWidget() - } - // 从下到上的选择范围 - else if ( - downPosY.current > upPosY.current && - (posy < upPosY.current - overHeight || posy > downPosY.current + overHeight) - ) { - closeFizzRangeWidget() - } - } - } - } catch (e) {} - }) - - // 移出编辑器时触发 - // editor.onMouseLeave(() => { - // closeFizzSelectWidget() - // closeFizzRangeWidget() - // }) - - editor.onMouseDown((e) => { - const { leftButton, posy } = e.event - // 当两者都没有打开时 - if (leftButton && !fizzSelectWidget.isOpen && !fizzRangeWidget.isOpen) { - // 记录posy位置 - downPosY.current = posy - } - }) - - editor.onMouseUp((e) => { - // @ts-ignore - const { leftButton, rightButton, posx, posy, editorPos } = e.event - // 获取编辑器所处x,y轴,并获取其长宽 - const { x, y } = editorPos - const editorHeight = editorPos.height - const editorWidth = editorPos.width - - // 计算焦点的坐标位置 - let a: any = editor.getPosition() - const position = editor.getScrolledVisiblePosition(a) - if (position) { - // 获取焦点在编辑器中所处位置,height为每行所占高度(随字体大小改变) - const { top, left, height } = position - - // 解决方法1 - // 获取焦点位置判断焦点所处于编辑器的位置(上下左右)从而决定弹出层显示方向 - // 问题 需要焦点位置进行计算 如何获取焦点位置? 目前仅找到行列号 无法定位到其具体坐标位置 - // console.log("焦点位置:", e, x, left, y, top, x + left, y + top) - const focusX = x + left - const focusY = y + top - - // 焦点与抬起坐标是否超出限制 - const isOver: boolean = overLine * height < Math.abs(focusY - posy) - if (leftButton && !isOver) { - // 获取编辑器容器的相关信息并判断其处于编辑器的具体方位 - const editorContainer = editor.getDomNode() - if (editorContainer) { - const editorContainerInfo = editorContainer.getBoundingClientRect() - const { top, bottom, left, right } = editorContainerInfo - // 通过判断编辑器长宽限制是否显示 (宽度小于250或者长度小于200则不展示) - const isShowByLimit = right - left > 250 && bottom - top > 200 - // 判断焦点位置 - const isTopHalf = focusY < (top + bottom) / 2 - const isLeftHalf = focusX < (left + right) / 2 - // 行高 - // const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) - - let countDirection: CountDirectionProps = {} - if (isTopHalf) { - // 鼠标位于编辑器上半部分 - countDirection.y = 'top' - } else { - // 鼠标位于编辑器下半部分 - countDirection.y = 'bottom' - } - if (Math.abs(focusX - (left + right) / 2) < 50) { - // 鼠标位于编辑器中间部分 - countDirection.x = 'middle' - } else if (isLeftHalf) { - // 鼠标位于编辑器左半部分 - countDirection.x = 'left' - } else { - // 鼠标位于编辑器右半部分 - countDirection.x = 'right' - } - - editorInfo.current = { - direction: countDirection, - top, - bottom, - left, - right, - focusX, - focusY, - lineHeight: height, - scrollTop: onScrollTop.current, - } - - upPosY.current = posy - const selection = editor.getSelection() - if (selection && isShowByLimit) { - const selectedText = editor.getModel()?.getValueInRange(selection) || '' - if (fizzSelectWidget.isOpen && selectedText.length === 0) { - // 更新点击菜单小部件的位置 - fizzSelectWidget.update() - } else if (fizzRangeWidget.isOpen && selectedText.length !== 0) { - fizzRangeWidget.update() - } else if (selectedText.length === 0) { - if (!readOnly && getShowActionBar()) { - closeFizzRangeWidget() - // 展示点击的菜单 - selectId && editor.addContentWidget(fizzSelectWidget) - fizzSelectWidget.isOpen = true - } - } else { - closeFizzSelectWidget() - if (getShowActionBar()) { - // 展示选中的菜单 - rangeId && editor.addContentWidget(fizzRangeWidget) - fizzRangeWidget.isOpen = true - } - } - } else { - closeFizzRangeWidget() - closeFizzSelectWidget() - } - } - } - if (rightButton) { - closeFizzRangeWidget() - closeFizzSelectWidget() - } - } - }) - editor.onDidScrollChange((e) => { - const { scrollTop } = e - onScrollTop.current = scrollTop - }) - - // 监听光标移动 - editor.onDidChangeCursorPosition((e) => { - closeFizzRangeWidget() - closeFizzSelectWidget() - // const { position } = e; - // console.log('当前光标位置:', position); - }) - } useEffect(() => { // 此处一个页面可能存在多个monaco diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/constants/index.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/constants/index.ts new file mode 100644 index 0000000000..d34b3176ad --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/constants/index.ts @@ -0,0 +1,95 @@ +import React from 'react' +import { monaco } from 'react-monaco-editor' +import { fontSizeOptions } from '@/store/editorFontSize' +import { YakParamProps } from '@/pages/plugins/pluginsType' +import { TFunction } from '@/i18n/useI18nNamespaces' +import { EditorMenuItemType } from '../EditorMenu' +import { YakitIMonacoEditor } from '../YakitEditorType' + +export interface CodecTypeProps { + key?: string + verbose: string + subTypes?: CodecTypeProps[] + params?: YakParamProps[] + help?: React.ReactNode + isYakScript?: boolean +} + +export interface contextMenuProps { + key: string + value: string + isAiPlugin: boolean + params: YakParamProps[] +} + +interface IFindReplaceState { + isRevealed: boolean + searchString: string + change(update: { searchString: string }, moveCursor: boolean): void + onFindReplaceStateChange: monaco.IEvent<() => void> +} + +interface IFindController extends monaco.editor.IStandaloneCodeEditor { + getState(): IFindReplaceState + start(opts?: { + forceRevealReplace?: boolean + seedSearchStringFromSelection?: boolean + shouldFocus?: boolean + seedSearchStringFromGlobalClipboard?: boolean + }): void +} + +// 二进制 Fuzztag 折叠侧表的内存上限:累积保留历史项以支持占位被破坏后再补回的恢复, +// 用上限淘汰最旧项防止长会话内存膨胀 +export const MAX_BINARY_FOLD_ENTRIES = 500 + +/** 插件扩展前缀 */ +export const PLUGIN_PREFIX = 'pluginExtension_' + +/** @name 字体key值对应字体大小 */ +export const keyToFontSize: Record = { + 'font-size-small': 12, + 'font-size-middle': 16, + 'font-size-large': 20, +} + +/** 编辑器右键默认菜单 - 顶部 */ +export const DefaultMenuTop: (t: TFunction, nowFontsize: number) => EditorMenuItemType[] = (t, nowFontsize) => { + return [ + { + key: 'font-size', + label: t('YakitEditor.fontSize'), + children: fontSizeOptions.map((val) => ({ + key: val + '', + label: `${val}${nowFontsize === val ? '\u00A0\u00A0\u00A0✓' : ''}`, + })), + }, + ] +} + +/** 编辑器右键默认菜单 - 底部 */ +export const DefaultMenuBottom: (t: TFunction) => EditorMenuItemType[] = (t) => { + return [ + { key: 'cut', label: t('YakitEditor.cut') }, + { key: 'copy', label: t('YakitEditor.copy') }, + { key: 'paste', label: t('YakitEditor.paste') }, + ] +} + +export function openFind(editor: YakitIMonacoEditor, keyword: string) { + // editor.focus() + const findController = editor.getContribution('editor.contrib.findController') + const state = findController?.getState() + if (!state?.isRevealed) { + findController?.start({ + seedSearchStringFromSelection: false, + shouldFocus: true, + }) + } + + if (state?.searchString !== keyword) { + state?.change({ searchString: keyword }, false) + } +} + +export type { IFindReplaceState, IFindController } diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts new file mode 100644 index 0000000000..f9b625fb4e --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts @@ -0,0 +1,441 @@ +import { monaco } from 'react-monaco-editor' +import { editor as newEditor } from 'monaco-editor' +import IModelDecoration = newEditor.IModelDecoration +import { + BinaryFuzztagEntry, + buildChipLabel, + findPlaceholderOffsets, +} from '../binaryFuzztag' +import { YakitIModelDecoration, YakitIMonacoEditor } from '../YakitEditorType' +import { HighLightText } from '../YakitEditorType' +import { Selection } from '@/pages/yakRunner/RunnerTabs/RunnerTabsType' +import { TFunction } from '@/i18n/useI18nNamespaces' + +export interface GenerateDecorationsParams { + model: monaco.editor.ITextModel + editor: YakitIMonacoEditor + type?: string + showHostHint?: boolean + /** 隐私模式 */ + privacy: boolean | undefined + /** 是否禁用Unicode解码 */ + disableUnicodeDecode: boolean | undefined + /** 高亮显示配置 */ + highLightText: (HighLightText[] | Selection[]) | (() => HighLightText[] | Selection[]) + highLightClass?: string + highLightFind: (HighLightText[] | Selection[]) | (() => HighLightText[] | Selection[]) + highLightFindClass?: string + /** Content-Type 修正 */ + fixContentType: string | undefined + originalContentType: string | undefined + fixContentTypeHoverMessage: string | undefined + /** 二进制 Fuzztag 折叠 */ + foldBinaryEnabled: boolean + binaryFoldEntriesRef: React.MutableRefObject> + binaryFoldRangesRef: React.MutableRefObject<{ id: string; range: monaco.Range; ordinal: number }[]> + binaryModifiedOrdinalsRef: React.MutableRefObject> + /** i18n */ + language: string + /** i18n t function */ + t: TFunction + /** 隐私遮挡范围更新回调(原代码直接赋值 privacyMaskRangesRef.current) */ + onPrivacyRanges?: (ranges: { id: string; range: monaco.Range }[]) => void +} + +/** + * 生成编辑器装饰器(decorations) + * + * 包含: + * 1. HTTP content-length / Host 提示 / 隐私打码 + * 2. Unicode 解码提示 + * 3. Content-Type 修正提示 + * 4. 换行符可视字符 + * 5. 高亮文本/查找 + * 6. 二进制 Fuzztag 折叠小块 + */ +export const generateDecorations = (params: GenerateDecorationsParams): YakitIModelDecoration[] => { + const { + model, + editor, + type, + showHostHint, + privacy, + disableUnicodeDecode, + highLightText, + highLightClass, + highLightFind, + highLightFindClass, + fixContentType, + originalContentType, + fixContentTypeHoverMessage, + foldBinaryEnabled, + binaryFoldEntriesRef, + binaryFoldRangesRef, + binaryModifiedOrdinalsRef, + language, + t, + } = params + + const dec: YakitIModelDecoration[] = [] + + const endsp = model.getPositionAt(1800) + const text = + endsp.lineNumber === 1 + ? model.getValueInRange({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: endsp.column, + }) + : model.getValueInRange({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: endsp.lineNumber, + endColumn: endsp.column, + }) + + if (type === 'http') { + ;(() => { + try { + //http + ;[{ regexp: /\nContent-Length:\s*?\d+/, classType: 'content-length' }].map((detail) => { + // handle content-length + const match = detail.regexp.exec(text) + if (!match) { + return + } + const start = model.getPositionAt(match.index) + const end = model.getPositionAt(match.index + match[0].indexOf(':')) + dec.push({ + id: detail.classType + match.index, + ownerId: 0, + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { + afterContentClassName: `${detail.classType} lang-${language}`, + }, + } as YakitIModelDecoration) + }) + } catch (e) {} + })() + ;(() => { + try { + if (!showHostHint) return + const fullText = model.getValue() + const hostRegex = /\nHost:\s*?([^\r\n]+)/ + const hostMatch = hostRegex.exec(fullText) + if (!hostMatch) return + + const fullMatch = hostMatch[0] + const hostColonIndex = hostMatch.index + fullMatch.indexOf(':') + const hostLabelStart = model.getPositionAt(hostMatch.index) + const hostLabelEnd = model.getPositionAt(hostColonIndex + 1) + + // 添加 Host 标签装饰器(? 标记) + dec.push({ + id: `host-label-${hostMatch.index}`, + ownerId: 0, + range: new monaco.Range( + hostLabelStart.lineNumber, + hostLabelStart.column, + hostLabelEnd.lineNumber, + hostLabelEnd.column, + ), + options: { + afterContentClassName: `host lang-${language}`, + }, + } as YakitIModelDecoration) + + if (!privacy) return + + // 提取并处理 Host 值 + const hostValue = hostMatch[1].trim() + if (!hostValue) return + + const colonIndex = hostValue.indexOf(':') + const hostname = colonIndex > 0 ? hostValue.substring(0, colonIndex) : hostValue + if (!hostname) return + + // 构建搜索模式(hostname、hostname:port、不带www的域名) + const patterns = [hostname] + if (colonIndex > 0) patterns.push(hostValue) + if (hostname.toLowerCase().startsWith('www.')) { + const withoutWww = hostname.substring(4) + if (withoutWww) patterns.push(withoutWww) + } + + // 使用正则一次性查找所有匹配 + const escapedPatterns = patterns.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + const searchRegex = new RegExp(escapedPatterns.join('|'), 'g') + + const matches: { index: number; length: number }[] = [] + let match: RegExpExecArray | null + while ((match = searchRegex.exec(fullText)) !== null) { + matches.push({ index: match.index, length: match[0].length }) + } + + // 去重重叠匹配(保留较长的) + const filtered = matches + .sort((a, b) => (a.index !== b.index ? a.index - b.index : b.length - a.length)) + .filter((curr, idx, arr) => idx === 0 || curr.index >= arr[idx - 1].index + arr[idx - 1].length) + + // 获取光标位置用于判断是否显示遮罩 + const cursorPos = editor.getPosition() + const newPrivacyRanges: { id: string; range: monaco.Range }[] = [] + + filtered.forEach((m, idx) => { + const start = model.getPositionAt(m.index) + const end = model.getPositionAt(m.index + m.length) + const range = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column) + const decorationId = `host-privacy-${idx}` + + newPrivacyRanges.push({ id: decorationId, range }) + + // 光标在范围内则不显示遮罩 + const isCursorIn = + cursorPos?.lineNumber === start.lineNumber && + cursorPos.column >= start.column && + cursorPos.column <= end.column + + if (!isCursorIn) { + dec.push({ + id: decorationId, + ownerId: 0, + range, + options: { + inlineClassName: `host-privacy-mask-hidden lang-${language}`, + stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + }, + } as YakitIModelDecoration) + } + }) + + // 注意:privacyMaskRangesRef 的更新由调用方处理 + // 这里通过返回值无法直接更新 ref,所以调用方需要在外层处理 + // 但原代码是直接赋值 privacyMaskRangesRef.current = newPrivacyRanges + // 为了保持行为一致,我们需要一个回调 + params.onPrivacyRanges?.(newPrivacyRanges) + } catch (e) {} + })() + } + const needDecode = type && ['html', 'http', 'json'].includes(type) + if (needDecode && !disableUnicodeDecode) { + ;(() => { + // http html json + const text = model.getValue() + let match + const regex = /(\\u[\dabcdef]{4})+/gi + + while ((match = regex.exec(text)) !== null) { + const start = model.getPositionAt(match.index) + const end = model.getPositionAt(match.index + match[0].length) + const decoded = match[0] + .split('\\u') + .filter(Boolean) + .map((hex) => String.fromCharCode(parseInt(hex, 16))) + .join('') + dec.push({ + id: 'decode' + match.index, + ownerId: 1, + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { + className: 'unicode-decode', + hoverMessage: { value: decoded }, + afterContentClassName: 'unicode-decode', + after: { content: decoded, inlineClassName: 'unicode-decode-after' }, + }, + } as IModelDecoration) + } + })() + } + + ;(() => { + const targetValue = fixContentType + if (!targetValue) return + const text = model.getValue() + let match + + // 匹配 Content-Type: 后面的值 + const regex = /Content-Type:\s*([^\r\n]*)/gi + + while ((match = regex.exec(text)) !== null) { + const contentTypeValue = match[1].trim() // 获取 Content-Type 后的值并去除多余空格 + if (contentTypeValue === targetValue) { + // 计算 Content-Type: 后具体值的起始位置,避免空格问题 + const textBeforeMatch = text.substring(match.index, regex.lastIndex) // 获取匹配到的完整文本 + const contentStartIndex = match.index + textBeforeMatch.indexOf(contentTypeValue) // 确保起始位置精确匹配 + + const start = model.getPositionAt(contentStartIndex) + const end = model.getPositionAt(match.index + match[0].length) + + dec.push({ + id: 'decode' + match.index, + ownerId: 1, + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { + className: 'unicode-decode', + hoverMessage: { value: fixContentTypeHoverMessage }, + afterContentClassName: 'unicode-decode', + after: { + content: + originalContentType === '' + ? t('YakitEditor.emptyContentTypeAutoDetected') + : originalContentType, + inlineClassName: 'unicode-decode-after', + }, + }, + } as IModelDecoration) + } + } + })() + ;(() => { + // all + const keywordRegExp = /\r?\n/g + let match + let count = 0 + while ((match = keywordRegExp.exec(text)) !== null) { + count++ + const start = model.getPositionAt(match.index) + const className: 'crlf' | 'lf' = match[0] === '\r\n' ? 'crlf' : 'lf' + const end = model.getPositionAt(match.index + match[0].length) + dec.push({ + id: 'keyword' + match.index, + ownerId: 2, + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { beforeContentClassName: className }, + } as YakitIModelDecoration) + if (count > 19) { + return + } + } + })() + + function highLightRange(item) { + const { startOffset = 0, highlightLength = 0, startLineNumber, startColumn, endLineNumber, endColumn } = item + let range = { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + } + if (typeof startLineNumber === 'number') { + range.startLineNumber = startLineNumber + range.startColumn = startColumn + range.endLineNumber = endLineNumber + range.endColumn = endColumn + } else { + if (model) { + // 获取偏移量对应的位置 + const startPosition = model.getPositionAt(Number(startOffset)) + const endPosition = model.getPositionAt(Number(startOffset) + Number(highlightLength)) + range.startLineNumber = startPosition.lineNumber + range.startColumn = startPosition.column + range.endLineNumber = endPosition.lineNumber + range.endColumn = endPosition.column + } + } + return range + } + + ;(() => { + //all + const highLightTextArr = typeof highLightText === 'function' ? highLightText() : highLightText + highLightTextArr.forEach((item) => { + const range = highLightRange(item) + // 创建装饰选项 + dec.push({ + id: + 'hight-light-text_' + + range.startLineNumber + + '_' + + range.startColumn + + '_' + + range.endLineNumber + + '_' + + range.endColumn, + ownerId: 3, + range: new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), + options: { + isWholeLine: false, + className: highLightClass ? highLightClass : 'hight-light-default-bg-color', + hoverMessage: [{ value: item.hoverVal, isTrusted: true }], + }, + } as IModelDecoration) + }) + + const highLightFindArr = typeof highLightFind === 'function' ? highLightFind() : highLightFind + highLightFindArr.forEach((item) => { + const range = highLightRange(item) + // 创建装饰选项 + dec.push({ + id: + 'hight-light-find_' + + range.startLineNumber + + '_' + + range.startColumn + + '_' + + range.endLineNumber + + '_' + + range.endColumn, + ownerId: 3, + range: new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), + options: { + isWholeLine: false, + className: highLightFindClass ? highLightFindClass : 'hight-light-find-default-bg-color', + hoverMessage: [{ value: '', isTrusted: true }], + }, + } as IModelDecoration) + }) + })() + + // 二进制 Fuzztag 折叠:为占位渲染 Binary[..]/File[..] 小块,并记录范围用于点击 + ;(() => { + if (!foldBinaryEnabled) { + binaryFoldRangesRef.current = [] + return + } + try { + const fullText = model.getValue() + const offsets = findPlaceholderOffsets(fullText) + const newRanges: { id: string; range: monaco.Range; ordinal: number }[] = [] + // index 即"编辑器中第 N 个二进制标签"的序号,按文档顺序;据此判断是否被修改过 + offsets.forEach((off, index) => { + const entry = binaryFoldEntriesRef.current.get(off.id) + if (!entry) { + return + } + const changed = binaryModifiedOrdinalsRef.current.has(index) + const start = model.getPositionAt(off.start) + const end = model.getPositionAt(off.end) + const range = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column) + newRanges.push({ id: off.id, range, ordinal: index }) + dec.push({ + id: 'binary-fold-' + off.id + '-' + off.start, + ownerId: 0, + range, + options: { + inlineClassName: 'binary-fuzz-hidden', + // 关键:声明该 inlineClassName 会改变字符宽度(font-size:0), + // 强制 monaco 放弃等宽快速路径(FastRenderedViewLine)、改用 DOM 实测, + // 否则隐藏占位仍按 charWidth*列数 占据光标/选区宽度,表现为 chip 后的可选中空白 + inlineClassNameAffectsLetterSpacing: true, + after: { + content: buildChipLabel(entry, changed), + inlineClassName: changed ? 'binary-fuzz-chip binary-fuzz-chip-changed' : 'binary-fuzz-chip', + // chip 带 padding/不同字号,宽度非等宽,同样需 DOM 实测以保证光标/选区贴合 + inlineClassNameAffectsLetterSpacing: true, + }, + stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + hoverMessage: { + value: entry.editable + ? 'Click to modify: open the helper editor to edit content of any size' + : 'Click to view (read-only reference)', + }, + }, + } as YakitIModelDecoration) + }) + binaryFoldRangesRef.current = newRanges + } catch (e) {} + })() + + return dec +} \ No newline at end of file diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx new file mode 100644 index 0000000000..5177effffd --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx @@ -0,0 +1,453 @@ +import React from 'react' +import { v4 as uuidv4 } from 'uuid' +import { monaco } from 'react-monaco-editor' +import { editor as newEditor } from 'monaco-editor' +import { createRoot } from 'react-dom/client' +import { QueryFuzzerLabelResponseProps } from '@/pages/fuzzer/StringFuzzer' +import { + CountDirectionProps, + HTTPFuzzerClickEditorMenu, + HTTPFuzzerRangeEditorMenu, + HTTPFuzzerRangeReadOnlyEditorMenu, +} from '@/pages/fuzzer/HTTPFuzzerEditorMenu' +import { insertFileFuzzTag, insertTemporaryFileFuzzTag } from '@/pages/fuzzer/InsertFileFuzzTag' +import { monacoEditorWrite } from '@/pages/fuzzer/fuzzerTemplates' +import { onInsertYakFuzzer, showDictsAndSelect } from '@/pages/fuzzer/HTTPFuzzerPage' +import emiter from '@/utils/eventBus/eventBus' +import { EditorDetailInfoProps } from '@/pages/fuzzer/HTTPFuzzerEditorMenu' +import { YakParamProps } from '@/pages/plugins/pluginsType' +import { YakitIMonacoEditor } from '../YakitEditorType' + +const { ipcRenderer } = window.require('electron') + +export interface EditerMenuFunParams { + editor: YakitIMonacoEditor + selectNode?: (close: () => void, editorInfo?: EditorDetailInfoProps) => React.ReactElement + rangeNode?: (close: () => void, editorInfo?: EditorDetailInfoProps) => React.ReactElement + readOnly: boolean + value?: string + overLine: number + getShowActionBar: () => boolean + execAutoDecodeCallback?: () => void + /** 编辑器信息 ref(长宽等) */ + editorInfoRef: React.MutableRefObject + /** 定时消失的定时器 */ + fizzSelectTimeoutIdRef: React.MutableRefObject + fizzRangeTimeoutIdRef: React.MutableRefObject + /** 鼠标位置记录 refs */ + downPosYRef: React.MutableRefObject + upPosYRef: React.MutableRefObject + onScrollTopRef: React.MutableRefObject +} + +/** + * 编辑器内点击/选中浮层菜单 + * + * 包含: + * 1. 点击菜单(插入标签) + * 2. 选中菜单(编/解码) + * 3. 鼠标移动/点击/滚动事件绑定 + * 4. 浮层位置计算与显示/隐藏 + */ +export const editerMenuFun = (params: EditerMenuFunParams) => { + const { + editor, + selectNode, + rangeNode, + readOnly, + value, + overLine, + getShowActionBar, + execAutoDecodeCallback, + editorInfoRef, + fizzSelectTimeoutIdRef, + fizzRangeTimeoutIdRef, + downPosYRef, + upPosYRef, + onScrollTopRef, + } = params + + // 编辑器点击弹窗的唯一Id + const selectId: string = `monaco.fizz.select.widget-${uuidv4()}` + // 编辑器选中弹窗的唯一Id + const rangeId: string = `monaco.fizz.range.widget-${uuidv4()}` + + // 插入标签 + const insertLabelFun = (v: QueryFuzzerLabelResponseProps) => { + if (v.Label) { + editor && editor.trigger('keyboard', 'type', { text: v.Label }) + } else if (v.DefaultDescription === '插入文件-fixed') { + editor && insertFileFuzzTag((i) => monacoEditorWrite(editor, i), 'file:line') + } else if (v.DefaultDescription === '插入字典-fixed') { + editor && + showDictsAndSelect((i) => { + monacoEditorWrite(editor, i, editor.getSelection()) + }) + } else if (v.DefaultDescription === '插入临时字典-fixed') { + editor && insertTemporaryFileFuzzTag((i) => monacoEditorWrite(editor, i)) + } + } + + const toOpenAiChat = (scriptName: string, params?: YakParamProps[]) => { + if (scriptName === 'aiplugin-Get*plug-in') { + emiter.emit('onOpenFuzzerModal', JSON.stringify({ scriptName, isAiPlugin: 'isGetPlugin' })) + closeFizzRangeWidget() + return + } + + if (editor) { + const selectedText = editor.getModel()?.getValueInRange(editor.getSelection() as any) || value + emiter.emit( + 'onOpenFuzzerModal', + JSON.stringify({ text: selectedText, scriptName, isAiPlugin: true, params, isExec: false }), + ) + closeFizzRangeWidget() + } + } + + // 关闭点击的菜单 + const closeFizzSelectWidget = () => { + fizzSelectWidget.isOpen = false + fizzSelectTimeoutIdRef.current && clearTimeout(fizzSelectTimeoutIdRef.current) + editor.removeContentWidget(fizzSelectWidget) + } + // 关闭选中的菜单 + const closeFizzRangeWidget = () => { + fizzRangeWidget.isOpen = false + fizzRangeTimeoutIdRef.current && clearTimeout(fizzRangeTimeoutIdRef.current) + editor.removeContentWidget(fizzRangeWidget) + } + + // 编辑器点击显示的菜单 + const fizzSelectWidget = { + isOpen: false, + getId: function () { + return selectId + }, + getDomNode: function () { + // 将TSX转换为DOM节点 + const domNode = document.createElement('div') + // 解决弹窗内鼠标滑轮无法滚动的问题 + domNode.onwheel = (e) => e.stopPropagation() + if (selectNode) { + createRoot(domNode).render(selectNode(closeFizzSelectWidget, editorInfoRef.current)) + } else { + createRoot(domNode).render( + closeFizzSelectWidget()} + fizzSelectTimeoutId={fizzSelectTimeoutIdRef} + toOpenAiChat={toOpenAiChat} + insert={(v: QueryFuzzerLabelResponseProps) => { + insertLabelFun(v) + closeFizzSelectWidget() + }} + addLabel={() => { + closeFizzSelectWidget() + onInsertYakFuzzer(editor) + }} + />, + ) + } + return domNode + }, + getPosition: function () { + const currentPos = editor.getPosition() + return { + position: { + lineNumber: currentPos?.lineNumber || 0, + column: currentPos?.column || 0, + }, + preference: [1, 2], + } + }, + update: function () { + // 更新小部件的位置 + this.getPosition() + editor.layoutContentWidget(this) + }, + } + + // 编辑器选中显示的菜单 + const fizzRangeWidget = { + isOpen: false, + getId: function () { + return rangeId + }, + getDomNode: function () { + // 将TSX转换为DOM节点 + const domNode = document.createElement('div') + // 解决弹窗内鼠标滑轮无法滚动的问题 + domNode.onwheel = (e) => e.stopPropagation() + if (rangeNode) { + createRoot(domNode).render(rangeNode(closeFizzRangeWidget, editorInfoRef.current)) + } else { + readOnly + ? createRoot(domNode).render( + closeFizzRangeWidget()} + fizzRangeTimeoutId={fizzRangeTimeoutIdRef} + execAutoDecodeCallback={execAutoDecodeCallback} + />, + ) + : createRoot(domNode).render( + closeFizzRangeWidget()} + insert={(fun: any) => { + if (editor) { + const selectedText = editor.getModel()?.getValueInRange(editor.getSelection() as any) || '' + if (selectedText.length > 0) { + ipcRenderer + .invoke('QueryFuzzerLabel') + .then((data: { Data: QueryFuzzerLabelResponseProps[] }) => { + const { Data } = data + let newSelectedText: string = selectedText + if (Array.isArray(Data) && Data.length > 0) { + // 选中项是否存在于标签中 + let isHave: boolean = Data.map((item) => item.Label).includes(selectedText) + if (isHave) { + newSelectedText = selectedText.replace(/{{|}}/g, '') + } + } + const text: string = fun(newSelectedText) + // editor.trigger("keyboard", "type", {text})// 选择范围大会卡死 + monacoEditorWrite(editor, text) + }) + } + } + }} + replace={(text: string) => { + if (editor) { + editor.trigger('keyboard', 'paste', { text }) + closeFizzRangeWidget() + } + }} + toOpenAiChat={toOpenAiChat} + rangeValue={(editor && editor.getModel()?.getValueInRange(editor.getSelection() as any)) || ''} + fizzRangeTimeoutId={fizzRangeTimeoutIdRef} + hTTPFuzzerClickEditorMenuProps={ + readOnly + ? undefined + : { + editorInfo: editorInfoRef.current, + close: () => closeFizzRangeWidget(), + insert: (v: QueryFuzzerLabelResponseProps) => { + insertLabelFun(v) + closeFizzRangeWidget() + }, + addLabel: () => { + closeFizzRangeWidget() + onInsertYakFuzzer(editor) + }, + } + } + />, + ) + } + return domNode + }, + getPosition: function () { + const currentPos = editor.getPosition() + + return { + position: { + lineNumber: currentPos?.lineNumber || 0, + column: currentPos?.column || 0, + }, + preference: [2, 1], + } + }, + update: function () { + // 更新小部件的位置 + this.getPosition() + editor.layoutContentWidget(this) + }, + } + + // 编辑器更新 关闭之前展示 + closeFizzSelectWidget() + closeFizzRangeWidget() + + editor?.getModel()?.pushEOL(newEditor.EndOfLineSequence.CRLF) + editor.onMouseMove((e) => { + try { + // const pos = e.target.position + // if (pos?.lineNumber) { + // const lineOffset = pos.lineNumber - (editor.getPosition()?.lineNumber || 0) + // // 超出范围移除菜单 + // if (lineOffset > 2 || lineOffset < -2) { + // // console.log("移出两行内"); + // closeFizzSelectWidget() + // closeFizzRangeWidget() + // } + // } + + const { target, event } = e + const { posy } = event + const detail = + target.type === newEditor.MouseTargetType.CONTENT_WIDGET || + target.type === newEditor.MouseTargetType.OVERLAY_WIDGET + ? target.detail + : undefined + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) + if (detail !== selectId && detail !== rangeId && downPosYRef.current && upPosYRef.current) { + const overHeight = overLine * lineHeight + if (fizzSelectWidget.isOpen) { + if (posy < upPosYRef.current - overHeight || posy > upPosYRef.current + overHeight) { + closeFizzSelectWidget() + } + } else if (fizzRangeWidget.isOpen) { + // 从上到下的选择范围 + if ( + downPosYRef.current < upPosYRef.current && + (posy < downPosYRef.current - overHeight || posy > upPosYRef.current + overHeight) + ) { + closeFizzRangeWidget() + } + // 从下到上的选择范围 + else if ( + downPosYRef.current > upPosYRef.current && + (posy < upPosYRef.current - overHeight || posy > downPosYRef.current + overHeight) + ) { + closeFizzRangeWidget() + } + } + } + } catch (e) {} + }) + + // 移出编辑器时触发 + // editor.onMouseLeave(() => { + // closeFizzSelectWidget() + // closeFizzRangeWidget() + // }) + + editor.onMouseDown((e) => { + const { leftButton, posy } = e.event + // 当两者都没有打开时 + if (leftButton && !fizzSelectWidget.isOpen && !fizzRangeWidget.isOpen) { + // 记录posy位置 + downPosYRef.current = posy + } + }) + + editor.onMouseUp((e) => { + // @ts-ignore + const { leftButton, rightButton, posy, editorPos } = e.event + // 获取编辑器所处x,y轴,并获取其长宽 + const { x, y } = editorPos + + // 计算焦点的坐标位置 + let a: monaco.Position | null = editor.getPosition() + if (!a) return + const position = editor.getScrolledVisiblePosition(a) + if (position) { + // 获取焦点在编辑器中所处位置,height为每行所占高度(随字体大小改变) + const { top, left, height } = position + + // 解决方法1 + // 获取焦点位置判断焦点所处于编辑器的位置(上下左右)从而决定弹出层显示方向 + // 问题 需要焦点位置进行计算 如何获取焦点位置? 目前仅找到行列号 无法定位到其具体坐标位置 + // console.log("焦点位置:", e, x, left, y, top, x + left, y + top) + const focusX = x + left + const focusY = y + top + + // 焦点与抬起坐标是否超出限制 + const isOver: boolean = overLine * height < Math.abs(focusY - posy) + if (leftButton && !isOver) { + // 获取编辑器容器的相关信息并判断其处于编辑器的具体方位 + const editorContainer = editor.getDomNode() + if (editorContainer) { + const editorContainerInfo = editorContainer.getBoundingClientRect() + const { top, bottom, left, right } = editorContainerInfo + // 通过判断编辑器长宽限制是否显示 (宽度小于250或者长度小于200则不展示) + const isShowByLimit = right - left > 250 && bottom - top > 200 + // 判断焦点位置 + const isTopHalf = focusY < (top + bottom) / 2 + const isLeftHalf = focusX < (left + right) / 2 + // 行高 + // const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight) + + let countDirection: CountDirectionProps = {} + if (isTopHalf) { + // 鼠标位于编辑器上半部分 + countDirection.y = 'top' + } else { + // 鼠标位于编辑器下半部分 + countDirection.y = 'bottom' + } + if (Math.abs(focusX - (left + right) / 2) < 50) { + // 鼠标位于编辑器中间部分 + countDirection.x = 'middle' + } else if (isLeftHalf) { + // 鼠标位于编辑器左半部分 + countDirection.x = 'left' + } else { + // 鼠标位于编辑器右半部分 + countDirection.x = 'right' + } + + editorInfoRef.current = { + direction: countDirection, + top, + bottom, + left, + right, + focusX, + focusY, + lineHeight: height, + scrollTop: onScrollTopRef.current, + } + + upPosYRef.current = posy + const selection = editor.getSelection() + if (selection && isShowByLimit) { + const selectedText = editor.getModel()?.getValueInRange(selection) || '' + if (fizzSelectWidget.isOpen && selectedText.length === 0) { + // 更新点击菜单小部件的位置 + fizzSelectWidget.update() + } else if (fizzRangeWidget.isOpen && selectedText.length !== 0) { + fizzRangeWidget.update() + } else if (selectedText.length === 0) { + if (!readOnly && getShowActionBar()) { + closeFizzRangeWidget() + // 展示点击的菜单 + selectId && editor.addContentWidget(fizzSelectWidget) + fizzSelectWidget.isOpen = true + } + } else { + closeFizzSelectWidget() + if (getShowActionBar()) { + // 展示选中的菜单 + rangeId && editor.addContentWidget(fizzRangeWidget) + fizzRangeWidget.isOpen = true + } + } + } else { + closeFizzRangeWidget() + closeFizzSelectWidget() + } + } + } + if (rightButton) { + closeFizzRangeWidget() + closeFizzSelectWidget() + } + } + }) + editor.onDidScrollChange((e) => { + const { scrollTop } = e + onScrollTopRef.current = scrollTop + }) + + // 监听光标移动 + editor.onDidChangeCursorPosition((e) => { + closeFizzRangeWidget() + closeFizzSelectWidget() + // const { position } = e; + // console.log('当前光标位置:', position); + }) +} \ No newline at end of file diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts new file mode 100644 index 0000000000..247607bcb0 --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts @@ -0,0 +1,98 @@ +import { useMemo, useRef } from 'react' +import { useMemoizedFn } from 'ahooks' +import { monaco } from 'react-monaco-editor' +import { + BinaryFuzztagEntry, + collapseBinaryFuzztag, + expandBinaryFuzztag, +} from '../binaryFuzztag' +import { MAX_BINARY_FOLD_ENTRIES } from '../constants' + +export interface UseBinaryFoldParams { + value?: string + setValue?: (content: string) => any + onChange?: (content: string) => any + foldBinaryFuzztag?: boolean + type?: string +} + +export interface UseBinaryFoldResult { + foldBinaryEnabled: boolean + binaryFoldEntriesRef: React.MutableRefObject> + binaryFoldRangesRef: React.MutableRefObject<{ id: string; range: monaco.Range; ordinal: number }[]> + binaryModifiedOrdinalsRef: React.MutableRefObject> + displayValue: string | undefined + handleBinaryChange: (content: string) => void +} + +/** + * 二进制 Fuzztag 折叠:翻译边界 + * + * 仅在 foldBinaryFuzztag 且 http 类型下启用;模型存短占位,向上抛真实值,下游消费者无感知 + */ +export const useBinaryFold = (params: UseBinaryFoldParams): UseBinaryFoldResult => { + const { value, setValue, onChange, foldBinaryFuzztag, type } = params + + // 仅在 foldBinaryFuzztag 且 http 类型下启用;模型存短占位,向上抛真实值,下游消费者无感知 + const foldBinaryEnabled = !!foldBinaryFuzztag && type === 'http' + // 侧表:占位 id -> 原始标签信息;handleBinaryChange 据此 expand 还原真实文本 + const binaryFoldEntriesRef = useRef>(new Map()) + // 占位范围(仿 privacyMaskRangesRef),用于点击命中打开 HEX 编辑弹窗 + const binaryFoldRangesRef = useRef<{ id: string; range: monaco.Range; ordinal: number }[]>([]) + // "被修改"记录:按编辑器中第 N 个二进制标签(文档顺序的序号)记录,只记是否改过(布尔)。 + // 与内容/占位 id 解耦,保证复制粘贴出去的永远是纯内容、不含任何改动元数据。 + const binaryModifiedOrdinalsRef = useRef>(new Set()) + + // 计算传给 MonacoEditor 的展示文本(真实值 -> 占位) + const displayValue = useMemo(() => { + if (!foldBinaryEnabled) { + // 原地清空而非替换对象,保持注册表里登记的 map 引用一直有效 + binaryFoldEntriesRef.current.clear() + binaryModifiedOrdinalsRef.current.clear() + return value + } + const { text, entries } = collapseBinaryFuzztag(value ?? '') + // 累积合并而非整表替换:保留历史占位映射。 + // 原因:用户在占位上 backspace 会把占位破坏成非法 fuzztag(如缺一个 }), + // 此时 expand 无法匹配 -> 真实二进制会被破坏文本顶替丢失;若再整表替换映射, + // 即使补回 }} 也找不到 id 对应的原始标签,Binary 小块与内容永久无法恢复。 + // 保留映射后,补回完整占位即可由 expand 还原真实内容并重新折叠出小块。 + const map = binaryFoldEntriesRef.current + entries.forEach((v, k) => { + // 重新插入以将当前文本中的项标记为最新,避免被下方内存淘汰 + map.delete(k) + map.set(k, v) + }) + // 限制内存上限,淘汰最旧项;当前文本中的项已在上面置为最新,不会被淘汰 + while (map.size > MAX_BINARY_FOLD_ENTRIES) { + const oldest = map.keys().next().value + if (oldest === undefined) { + break + } + map.delete(oldest) + } + return text + }, [value, foldBinaryEnabled]) + + // 向上回调:占位 -> 真实值 + const handleBinaryChange = useMemoizedFn((content: string) => { + const emit = setValue || onChange + if (!emit) { + return + } + if (!foldBinaryEnabled) { + emit(content) + return + } + emit(expandBinaryFuzztag(content, binaryFoldEntriesRef.current)) + }) + + return { + foldBinaryEnabled, + binaryFoldEntriesRef, + binaryFoldRangesRef, + binaryModifiedOrdinalsRef, + displayValue, + handleBinaryChange, + } +} \ No newline at end of file diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts new file mode 100644 index 0000000000..57c8e626da --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts @@ -0,0 +1,121 @@ +import { useEffect } from 'react' +import { useMemoizedFn } from 'ahooks' +import { queryYakScriptList } from '@/pages/yakitStore/network' +import { YakScript } from '@/pages/invoker/schema' +import { failed } from '@/utils/notification' +import emiter from '@/utils/eventBus/eventBus' +import { useStore } from '@/store/editorState' +import { PluginSwitchToTag } from '@/pages/pluginEditor/defaultconstants' +import { YakitEditorExtraRightMenuType } from '../YakitEditorType' +import { CodecTypeProps, contextMenuProps } from '../constants' + +export interface UsePluginSearchParams { + menuType: YakitEditorExtraRightMenuType[] + inViewport: boolean | undefined +} + +export interface UsePluginSearchResult { + customHTTPMutatePlugin: CodecTypeProps[] + contextMenuPlugin: contextMenuProps[] + setCustomHTTPMutatePlugin: (info: CodecTypeProps[]) => void + setContextMenuPlugin: (info: contextMenuProps[]) => void + onRefreshPluginCodecMenu: () => void +} + +/** + * Codec 插件搜索逻辑 + * + * 自定义HTTP数据包变形处理 + 插件扩展 + */ +export const usePluginSearch = (params: UsePluginSearchParams): UsePluginSearchResult => { + const { menuType, inViewport } = params + + // 自定义HTTP数据包变形处理 + const { customHTTPMutatePlugin, contextMenuPlugin, setCustomHTTPMutatePlugin, setContextMenuPlugin } = useStore() + + const searchCodecCustomHTTPMutatePlugin = useMemoizedFn(() => { + queryYakScriptList( + 'codec', + (i: YakScript[], total) => { + if (!total || total === 0) { + return + } + setCustomHTTPMutatePlugin( + i.map((script) => { + return { + key: script.ScriptName, + verbose: 'CODEC 社区插件: ' + script.ScriptName, + isYakScript: true, + } as CodecTypeProps + }), + ) + }, + undefined, + 10, + undefined, + undefined, + undefined, + undefined, + [PluginSwitchToTag.PluginCodecHttpSwitch], + ) + }) + + // 插件扩展 + const searchCodecCustomContextMenuPlugin = useMemoizedFn(() => { + queryYakScriptList( + 'codec', + (i: YakScript[], total) => { + if (!total || total === 0) { + return + } + setContextMenuPlugin( + i.map((script) => { + const isAiPlugin: boolean = script.Tags.includes('AI工具') + return { + key: script.ScriptName, + value: script.ScriptName, + isAiPlugin, + params: script.Params, + } as contextMenuProps + }), + ) + }, + undefined, + 10, + undefined, + undefined, + undefined, + undefined, + [PluginSwitchToTag.PluginCodecContextMenuExecuteSwitch], + ) + }) + + useEffect(() => { + if (inViewport && menuType.length > 0) { + searchCodecCustomHTTPMutatePlugin() + searchCodecCustomContextMenuPlugin() + } + }, [inViewport]) + + const onRefreshPluginCodecMenu = useMemoizedFn(() => { + if (inViewport && menuType.length > 0) { + searchCodecCustomHTTPMutatePlugin() + searchCodecCustomContextMenuPlugin() + } + }) + + useEffect(() => { + emiter.on('onRefPluginCodecMenu', onRefreshPluginCodecMenu) + return () => { + emiter.off('onRefPluginCodecMenu', onRefreshPluginCodecMenu) + } + }, []) + + return { + customHTTPMutatePlugin, + contextMenuPlugin, + setCustomHTTPMutatePlugin, + setContextMenuPlugin, + onRefreshPluginCodecMenu, + } +} \ No newline at end of file diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts new file mode 100644 index 0000000000..bf738f14d0 --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts @@ -0,0 +1,93 @@ +import { useRef } from 'react' +import { useDebounceFn, useMemoizedFn } from 'ahooks' +import { v4 as uuidv4 } from 'uuid' +import { monaco } from 'react-monaco-editor' +import { ConvertYakStaticAnalyzeErrorToMarker, YakStaticAnalyzeErrorResult } from '@/utils/editorMarkers' +import { StringToUint8Array } from '@/utils/str' +import { YaklangMonacoSpec } from '@/utils/monacoSpec/yakEditor' +import { SyntaxFlowMonacoSpec } from '@/utils/monacoSpec/syntaxflowEditor' +import { YakitIMonacoEditor, YakitITextModel } from '../YakitEditorType' + +const { ipcRenderer } = window.require('electron') + +export interface UseYakFormatParams { + language?: string + type?: string +} + +export interface UseYakFormatResult { + yakCompileAndFormat: { + run: (editor: YakitIMonacoEditor, model: YakitITextModel) => void + } + yakStaticAnalyze: { + run: (editor: YakitIMonacoEditor, model: YakitITextModel) => void + } + AnalyzeSessionIDRef: React.MutableRefObject +} + +/** + * Yak 代码格式化 + 静态分析 + */ +export const useYakFormat = (params: UseYakFormatParams): UseYakFormatResult => { + const { language, type } = params + + const AnalyzeSessionIDRef = useRef(uuidv4()) + + /** Yak 代码格式化功能实现 */ + const yakCompileAndFormat = useDebounceFn( + useMemoizedFn((editor: YakitIMonacoEditor, model: YakitITextModel) => { + const allContent = model.getValue() + ipcRenderer + .invoke('YaklangCompileAndFormat', { Code: allContent }) + .then((e: { Errors: YakStaticAnalyzeErrorResult[]; Code: string }) => { + if (e.Code !== '') { + model.setValue(e.Code) + } + + /** 编辑器中错误提示的标记 */ + if (e && e.Errors.length > 0) { + const markers = e.Errors.map(ConvertYakStaticAnalyzeErrorToMarker) + monaco.editor.setModelMarkers(model, 'owner', markers) + } else { + monaco.editor.setModelMarkers(model, 'owner', []) + } + }) + .catch((e) => { + console.info(e) + }) + }), + { wait: 500, leading: true, trailing: false }, + ) + + /** Yak语言 代码错误检查并显示提示标记 */ + const yakStaticAnalyze = useDebounceFn( + useMemoizedFn((editor: YakitIMonacoEditor, model: YakitITextModel) => { + if (language === YaklangMonacoSpec || language === SyntaxFlowMonacoSpec) { + const allContent = model.getValue() + ipcRenderer + .invoke('StaticAnalyzeError', { + Code: StringToUint8Array(allContent), + PluginType: type, + SessionID: AnalyzeSessionIDRef.current, + }) + .then((e: { Result: YakStaticAnalyzeErrorResult[] }) => { + if (e && e.Result.length > 0) { + const markers = e.Result.map(ConvertYakStaticAnalyzeErrorToMarker) + monaco.editor.setModelMarkers(model, 'owner', markers) + } else { + monaco.editor.setModelMarkers(model, 'owner', []) + } + }) + } else { + monaco.editor.setModelMarkers(model, 'owner', []) + } + }), + { wait: 300 }, + ) + + return { + yakCompileAndFormat, + yakStaticAnalyze, + AnalyzeSessionIDRef, + } +} \ No newline at end of file diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx new file mode 100644 index 0000000000..fb49e7074a --- /dev/null +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx @@ -0,0 +1,141 @@ +import classNames from 'classnames' +import styles from '../YakitEditor.module.scss' +import { + EditorMenuItemDividerProps, + EditorMenuItemProps, + EditorMenuItemType, +} from '../EditorMenu' +import { KeyboardToFuncProps } from '../YakitEditorType' +import { TFunction } from '@/i18n/useI18nNamespaces' +import { + convertKeyboardToUIKey, + sortKeysCombination, +} from '@/utils/globalShortcutKey/utils' +import { + getYakEditorShortcutKeyEvents, +} from '@/utils/globalShortcutKey/events/page/yakEditor' +import { YakitKeyBoard, YakitKeyMod } from '@/utils/globalShortcutKey/keyboard' + +/** + * 菜单数组去重 + */ +export const menuReduce = (array: any[]) => { + let newArr: any[] = [] + let arr: string[] = [] + array.forEach((item) => { + if (!arr.includes(item.key)) { + arr.push(item.key) + newArr.push(item) + } + }) + return newArr +} + +/** + * 菜单排序 + * @param dataSource 原始菜单数组 + * @param sortData 需要排序的菜单项(带 order 字段) + */ +export const sortMenuFun = (dataSource, sortData) => { + const result = sortData.reduce( + (acc, item) => { + if (item.order >= 0) { + acc.splice(item.order, 0, ...item.menu) + } else { + acc.push(...item.menu) + } + return acc + }, + [...dataSource], + ) + return result +} + +/** + * 菜单自定义快捷键渲染处理事件 + * + * 为 cut/copy/paste 及带 keybindings 的菜单项添加快捷键文字展示 + * @param t i18n 翻译函数 + * @param keyBindingRef 记录快捷键映射的 ref + * @param parentKey 父级菜单 key + * @param data 菜单项数组 + */ +export const contextMenuKeybindingHandle = ( + t: TFunction, + keyBindingRef: React.MutableRefObject, + parentKey: string, + data: EditorMenuItemType[], +): EditorMenuItemType[] => { + const menus: EditorMenuItemType[] = [] + for (let item of data) { + /** 屏蔽菜单分割线选项 */ + if (typeof (data as any as EditorMenuItemDividerProps)['type'] !== 'undefined') { + const info: EditorMenuItemDividerProps = { type: 'divider' } + menus.push(info) + } else { + /** 处理带快捷键的菜单项 */ + const info = { ...item } as EditorMenuItemProps + if (info.children && info.children.length > 0) { + info.children = contextMenuKeybindingHandle(t, keyBindingRef, info.key, info.children) + } else { + if (info.key === 'cut' && info.label === t('YakitEditor.cut')) { + const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_X]) + + info.label = keysContent ? ( +
+
{t('YakitEditor.cut')}
+
{keysContent}
+
+ ) : ( + info.label + ) + } + if (info.key === 'copy' && info.label === t('YakitEditor.copy')) { + const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_C]) + + info.label = keysContent ? ( +
+
{t('YakitEditor.copy')}
+
{keysContent}
+
+ ) : ( + info.label + ) + } + if (info.key === 'paste' && info.label === t('YakitEditor.paste')) { + const keysContent = convertKeyboardToUIKey([YakitKeyMod.CtrlCmd, YakitKeyBoard.KEY_V]) + + info.label = keysContent ? ( +
+
{t('YakitEditor.paste')}
+
{keysContent}
+
+ ) : ( + info.label + ) + } + + if (info.keybindings && info.keybindings.length > 0) { + const keyArr = getYakEditorShortcutKeyEvents()[info.keybindings].keys + const keysContent = convertKeyboardToUIKey(keyArr) + // 记录自定义快捷键映射按键的回调事件 + if (keysContent) { + let sortKeys = sortKeysCombination(keyArr) + keyBindingRef.current[sortKeys.join('-')] = parentKey ? [info.key, parentKey] : [info.key] + } + + info.label = keysContent ? ( +
+
{info.label}
+
{keysContent}
+
+ ) : ( + info.label + ) + } + } + menus.push(info) + } + } + return menus +} \ No newline at end of file From be734695b72f3dbd646676f8587bd247cf7a1e61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 13 Jul 2026 08:16:40 +0000 Subject: [PATCH 2/9] style: apply prettier formatting [skip ci] --- .../yakitUI/YakitEditor/YakitEditor.tsx | 5 +--- .../decorations/generateDecorations.ts | 13 ++------ .../YakitEditor/fizzMenu/editerMenuFun.tsx | 30 +++++++++---------- .../YakitEditor/hooks/useBinaryFold.ts | 8 ++--- .../YakitEditor/hooks/usePluginSearch.ts | 2 +- .../yakitUI/YakitEditor/hooks/useYakFormat.ts | 2 +- .../yakitUI/YakitEditor/menus/menuHelpers.tsx | 17 +++-------- 7 files changed, 26 insertions(+), 51 deletions(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 9a2a18387d..23dcd79b4a 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -53,10 +53,7 @@ import { import ShortcutKeyFocusHook from '@/utils/globalShortcutKey/shortcutKeyFocusHook/ShortcutKeyFocusHook' import useFocusContextStore from '@/utils/globalShortcutKey/shortcutKeyFocusHook/hooks/useStore' import { ShortcutKeyFocusType } from '@/utils/globalShortcutKey/events/global' -import { - convertKeyEventToKeyCombination, - sortKeysCombination, -} from '@/utils/globalShortcutKey/utils' +import { convertKeyEventToKeyCombination, sortKeysCombination } from '@/utils/globalShortcutKey/utils' import { applyYakitMonacoTheme } from '@/utils/monacoSpec/theme' import { useTheme } from '@/hook/useTheme' import { keepSearchNameMapStore, useKeepSearchNameMap } from '@/store/keepSearchName' diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts index f9b625fb4e..593305de2e 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts @@ -1,11 +1,7 @@ import { monaco } from 'react-monaco-editor' import { editor as newEditor } from 'monaco-editor' import IModelDecoration = newEditor.IModelDecoration -import { - BinaryFuzztagEntry, - buildChipLabel, - findPlaceholderOffsets, -} from '../binaryFuzztag' +import { BinaryFuzztagEntry, buildChipLabel, findPlaceholderOffsets } from '../binaryFuzztag' import { YakitIModelDecoration, YakitIMonacoEditor } from '../YakitEditorType' import { HighLightText } from '../YakitEditorType' import { Selection } from '@/pages/yakRunner/RunnerTabs/RunnerTabsType' @@ -276,10 +272,7 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo hoverMessage: { value: fixContentTypeHoverMessage }, afterContentClassName: 'unicode-decode', after: { - content: - originalContentType === '' - ? t('YakitEditor.emptyContentTypeAutoDetected') - : originalContentType, + content: originalContentType === '' ? t('YakitEditor.emptyContentTypeAutoDetected') : originalContentType, inlineClassName: 'unicode-decode-after', }, }, @@ -438,4 +431,4 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo })() return dec -} \ No newline at end of file +} diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx index 5177effffd..3a336590e1 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx @@ -200,22 +200,20 @@ export const editerMenuFun = (params: EditerMenuFunParams) => { if (editor) { const selectedText = editor.getModel()?.getValueInRange(editor.getSelection() as any) || '' if (selectedText.length > 0) { - ipcRenderer - .invoke('QueryFuzzerLabel') - .then((data: { Data: QueryFuzzerLabelResponseProps[] }) => { - const { Data } = data - let newSelectedText: string = selectedText - if (Array.isArray(Data) && Data.length > 0) { - // 选中项是否存在于标签中 - let isHave: boolean = Data.map((item) => item.Label).includes(selectedText) - if (isHave) { - newSelectedText = selectedText.replace(/{{|}}/g, '') - } + ipcRenderer.invoke('QueryFuzzerLabel').then((data: { Data: QueryFuzzerLabelResponseProps[] }) => { + const { Data } = data + let newSelectedText: string = selectedText + if (Array.isArray(Data) && Data.length > 0) { + // 选中项是否存在于标签中 + let isHave: boolean = Data.map((item) => item.Label).includes(selectedText) + if (isHave) { + newSelectedText = selectedText.replace(/{{|}}/g, '') } - const text: string = fun(newSelectedText) - // editor.trigger("keyboard", "type", {text})// 选择范围大会卡死 - monacoEditorWrite(editor, text) - }) + } + const text: string = fun(newSelectedText) + // editor.trigger("keyboard", "type", {text})// 选择范围大会卡死 + monacoEditorWrite(editor, text) + }) } } }} @@ -450,4 +448,4 @@ export const editerMenuFun = (params: EditerMenuFunParams) => { // const { position } = e; // console.log('当前光标位置:', position); }) -} \ No newline at end of file +} diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts index 247607bcb0..44e41ddf65 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts @@ -1,11 +1,7 @@ import { useMemo, useRef } from 'react' import { useMemoizedFn } from 'ahooks' import { monaco } from 'react-monaco-editor' -import { - BinaryFuzztagEntry, - collapseBinaryFuzztag, - expandBinaryFuzztag, -} from '../binaryFuzztag' +import { BinaryFuzztagEntry, collapseBinaryFuzztag, expandBinaryFuzztag } from '../binaryFuzztag' import { MAX_BINARY_FOLD_ENTRIES } from '../constants' export interface UseBinaryFoldParams { @@ -95,4 +91,4 @@ export const useBinaryFold = (params: UseBinaryFoldParams): UseBinaryFoldResult displayValue, handleBinaryChange, } -} \ No newline at end of file +} diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts index 57c8e626da..2e6c6bf555 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/usePluginSearch.ts @@ -118,4 +118,4 @@ export const usePluginSearch = (params: UsePluginSearchParams): UsePluginSearchR setContextMenuPlugin, onRefreshPluginCodecMenu, } -} \ No newline at end of file +} diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts index bf738f14d0..4096ed28c0 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useYakFormat.ts @@ -90,4 +90,4 @@ export const useYakFormat = (params: UseYakFormatParams): UseYakFormatResult => yakStaticAnalyze, AnalyzeSessionIDRef, } -} \ No newline at end of file +} diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx index fb49e7074a..4659fa6928 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/menus/menuHelpers.tsx @@ -1,19 +1,10 @@ import classNames from 'classnames' import styles from '../YakitEditor.module.scss' -import { - EditorMenuItemDividerProps, - EditorMenuItemProps, - EditorMenuItemType, -} from '../EditorMenu' +import { EditorMenuItemDividerProps, EditorMenuItemProps, EditorMenuItemType } from '../EditorMenu' import { KeyboardToFuncProps } from '../YakitEditorType' import { TFunction } from '@/i18n/useI18nNamespaces' -import { - convertKeyboardToUIKey, - sortKeysCombination, -} from '@/utils/globalShortcutKey/utils' -import { - getYakEditorShortcutKeyEvents, -} from '@/utils/globalShortcutKey/events/page/yakEditor' +import { convertKeyboardToUIKey, sortKeysCombination } from '@/utils/globalShortcutKey/utils' +import { getYakEditorShortcutKeyEvents } from '@/utils/globalShortcutKey/events/page/yakEditor' import { YakitKeyBoard, YakitKeyMod } from '@/utils/globalShortcutKey/keyboard' /** @@ -138,4 +129,4 @@ export const contextMenuKeybindingHandle = ( } } return menus -} \ No newline at end of file +} From c165f5712dc19a6360cdcd9a068f5996a32d6c5d Mon Sep 17 00:00:00 2001 From: Youngster_yj <907985037@qq.com> Date: Tue, 14 Jul 2026 12:07:50 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20YakitEditor=E5=B0=8F=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yakitUI/YakitEditor/YakitEditor.tsx | 135 +++++++++++------- .../decorations/generateDecorations.ts | 17 ++- 2 files changed, 100 insertions(+), 52 deletions(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 23dcd79b4a..c054687cb3 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -28,7 +28,6 @@ import { import { showByRightContext } from '../YakitMenu/showByRightContext' import { baseMenuLists, extraMenuLists } from './contextMenus' import { EditorMenuItemProps, EditorMenuItemType } from './EditorMenu' -import cloneDeep from 'lodash/cloneDeep' import { getRemoteValue, setRemoteValue } from '@/utils/kv' import classNames from 'classnames' @@ -95,6 +94,23 @@ import { menuReduce, sortMenuFun, contextMenuKeybindingHandle } from './menus/me export { PLUGIN_PREFIX } export type { CodecTypeProps, contextMenuProps } +/** 右键菜单浅拷贝:避免 cloneDeep(ReactNode) 的性能开销,同时防止原地改写 props */ +const shallowCloneMenuItems = (items: EditorMenuItemType[]): EditorMenuItemType[] => { + return items.map((item) => { + if (!item || typeof item !== 'object') { + return item + } + if ((item as { type?: string }).type === 'divider') { + return { ...item } + } + const next = { ...(item as EditorMenuItemProps) } + if (Array.isArray(next.children)) { + next.children = next.children.map((child) => ({ ...child })) + } + return next + }) +} + export const YakitEditor: React.FC = React.memo((props) => { const { forceRenderMenu = false, @@ -157,7 +173,7 @@ export const YakitEditor: React.FC = React.memo((props) => { } }, [type]) - useMemo(() => { + useEffect(() => { if (editor) { setEditorContext(editor, 'plugin', props.type || '') } @@ -211,7 +227,7 @@ export const YakitEditor: React.FC = React.memo((props) => { useLayoutEffect(() => { applyYakitMonacoTheme(propsTheme ?? themeGlobal) - }, [themeGlobal, editor, propsTheme]) + }, [themeGlobal, propsTheme]) useEffect(() => { // 控制编辑器失焦 @@ -633,10 +649,15 @@ export const YakitEditor: React.FC = React.memo((props) => { for (let menus in contextMenu) { /* 需要排序项 */ if (typeof contextMenu[menus].order === 'number') { - sortContextMenu = sortContextMenu.concat(cloneDeep(contextMenu[menus]) as any as OtherMenuListProps[]) + sortContextMenu = sortContextMenu.concat([ + { + ...contextMenu[menus], + menu: shallowCloneMenuItems(contextMenu[menus].menu), + } as any, + ]) } else { - /** 当cloneDeep里面存在reactnode时,执行会产生性能问题 */ - rightContextMenu.current = rightContextMenu.current.concat(cloneDeep(contextMenu[menus].menu)) + /** 浅拷贝即可,避免 cloneDeep 遍历 ReactNode 导致的性能问题 */ + rightContextMenu.current = rightContextMenu.current.concat(shallowCloneMenuItems(contextMenu[menus].menu)) } } @@ -674,6 +695,9 @@ export const YakitEditor: React.FC = React.memo((props) => { { wait: 500 }, ) + // ===== Yak 代码格式化 + 静态分析(需在 decoration effect 之前声明) ===== + const { yakCompileAndFormat, yakStaticAnalyze } = useYakFormat({ language, type }) + const rafIdRef = useRef(null) // RAF ID const deltaDecorationsRef = useRef<() => any>() const highLightTextFun = useMemoizedFn(() => highLightText) @@ -775,13 +799,14 @@ export const YakitEditor: React.FC = React.memo((props) => { scheduleDecorations() } - let lastValue = model.getValue() - editor.onDidChangeModelContent((e) => { - const newValue = model.getValue() - if (newValue === lastValue) { + // 用 versionId 判断内容是否变化,避免每次 getValue 全量字符串比较 + let lastVersionId = model.getVersionId() + const contentChangeDisposable = editor.onDidChangeModelContent(() => { + const versionId = model.getVersionId() + if (versionId === lastVersionId) { return } - lastValue = newValue + lastVersionId = versionId scheduleDecorations() }) scheduleDecorations() @@ -796,7 +821,7 @@ export const YakitEditor: React.FC = React.memo((props) => { // 监听查找面板变化 const findController = editor.getContribution('editor.contrib.findController') const state = findController?.getState() - state?.onFindReplaceStateChange(() => { + const findStateDisposable = state?.onFindReplaceStateChange(() => { if (!keepSearchName) return if (state.isRevealed) { keepSearchNameMapStore.setKeepSearchNameMap(keepSearchName, state.searchString || '') @@ -805,6 +830,12 @@ export const YakitEditor: React.FC = React.memo((props) => { } }) + // Yak 静态分析:挂在同一 effect 内以便 cleanup dispose + yakStaticAnalyze.run(editor, model) + const yakAnalyzeDisposable = model.onDidChangeContent(() => { + yakStaticAnalyze.run(editor, model) + }) + // 添加点击事件处理,用于临时解除 Host 值的打码 let isHandlingPrivacyClick = false const handleHostPrivacyClick = (e: monaco.editor.IEditorMouseEvent) => { @@ -1023,7 +1054,14 @@ export const YakitEditor: React.FC = React.memo((props) => { return () => { try { isModelDisposedRef.current = true + if (rafIdRef.current !== null) { + cancelAnimationFrame(rafIdRef.current) + rafIdRef.current = null + } + contentChangeDisposable.dispose() cursorPositionDisposable.dispose() + findStateDisposable?.dispose() + yakAnalyzeDisposable.dispose() mouseDownDisposable.dispose() binaryFoldMouseDownDisposable.dispose() editorDomNode?.removeEventListener('copy', handleEditorClipboard, true) @@ -1118,9 +1156,6 @@ export const YakitEditor: React.FC = React.memo((props) => { }) }) - // ===== Yak 代码格式化 + 静态分析 ===== - const { yakCompileAndFormat, yakStaticAnalyze } = useYakFormat({ language, type }) - const downPosY = useRef() const upPosY = useRef() const onScrollTop = useRef() @@ -1192,10 +1227,45 @@ export const YakitEditor: React.FC = React.memo((props) => { const filterItem = (arr) => arr.filter((item, index) => arr.indexOf(item) === index) useEffect(() => { if (Array.isArray(shortcutIds)) { - setFocusIds(filterItem([...focusIds, ...shortcutIds])) + setFocusIds((prev) => filterItem([...prev, ...shortcutIds])) } }, [shortcutIds]) + const editorOptions = useMemo( + () => ({ + readOnly: readOnly, + scrollBeyondLastLine: false, + fontWeight: '500', + fontSize: nowFontsize || 12, + showFoldingControls: 'always' as const, + showUnused: true, + wordWrap: (noWordWrap ? 'off' : 'on') as 'off' | 'on', + renderLineHighlight, + lineNumbers: (noLineNumber ? 'off' : 'on') as 'off' | 'on', + minimap: noMiniMap ? { enabled: false } : undefined, + lineNumbersMinChars: lineNumbersMinChars || 5, + contextmenu: false, + // 保持 all:与换行符 decoration / binary chip 空格策略一致,避免改变既有视觉行为 + renderWhitespace: 'all' as const, + bracketPairColorization: { + enabled: true, + independentColorPoolPerBracketType: true, + }, + fixedOverflowWidgets: true, + renderValidationDecorations: renderValidationDecorations, + }), + [ + readOnly, + nowFontsize, + noWordWrap, + renderLineHighlight, + noLineNumber, + noMiniMap, + lineNumbersMinChars, + renderValidationDecorations, + ], + ) + return (
= React.memo((props) => { endLineNumber: 0, }) - if (editor) { - /** Yak语言 代码错误检查 */ - const model = editor.getModel() - if (model) { - yakStaticAnalyze.run(editor, model) - model.onDidChangeContent(() => { - yakStaticAnalyze.run(editor, model) - }) - } - } - editor.onKeyDown((e) => { // 是否直接使用编辑器快捷键 不走自定义逻辑 const isUseDefaultShortcut = isYakEditorDefaultShortcut(e.browserEvent) @@ -1310,27 +1369,7 @@ export const YakitEditor: React.FC = React.memo((props) => { if (editorDidMount) editorDidMount(editor, monaco) }} - options={{ - readOnly: readOnly, - scrollBeyondLastLine: false, - fontWeight: '500', - fontSize: nowFontsize || 12, - showFoldingControls: 'always', - showUnused: true, - wordWrap: noWordWrap ? 'off' : 'on', - renderLineHighlight, - lineNumbers: noLineNumber ? 'off' : 'on', - minimap: noMiniMap ? { enabled: false } : undefined, - lineNumbersMinChars: lineNumbersMinChars || 5, - contextmenu: false, - renderWhitespace: 'all', - bracketPairColorization: { - enabled: true, - independentColorPoolPerBracketType: true, - }, - fixedOverflowWidgets: true, - renderValidationDecorations: renderValidationDecorations, - }} + options={editorOptions} />
diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts index 593305de2e..3ae3f3fb10 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/decorations/generateDecorations.ts @@ -74,6 +74,15 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo const dec: YakitIModelDecoration[] = [] + // 同一次 decoration 生成内复用 getValue,避免多次全量拷贝 + let fullTextCache: string | undefined + const getFullText = () => { + if (fullTextCache === undefined) { + fullTextCache = model.getValue() + } + return fullTextCache + } + const endsp = model.getPositionAt(1800) const text = endsp.lineNumber === 1 @@ -116,7 +125,7 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo ;(() => { try { if (!showHostHint) return - const fullText = model.getValue() + const fullText = getFullText() const hostRegex = /\nHost:\s*?([^\r\n]+)/ const hostMatch = hostRegex.exec(fullText) if (!hostMatch) return @@ -217,7 +226,7 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo if (needDecode && !disableUnicodeDecode) { ;(() => { // http html json - const text = model.getValue() + const text = getFullText() let match const regex = /(\\u[\dabcdef]{4})+/gi @@ -247,7 +256,7 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo ;(() => { const targetValue = fixContentType if (!targetValue) return - const text = model.getValue() + const text = getFullText() let match // 匹配 Content-Type: 后面的值 @@ -387,7 +396,7 @@ export const generateDecorations = (params: GenerateDecorationsParams): YakitIMo return } try { - const fullText = model.getValue() + const fullText = getFullText() const offsets = findPlaceholderOffsets(fullText) const newRanges: { id: string; range: monaco.Range; ordinal: number }[] = [] // index 即"编辑器中第 N 个二进制标签"的序号,按文档顺序;据此判断是否被修改过 From f679d9c0d8d4e123dac7e0ce370952a68e57d900 Mon Sep 17 00:00:00 2001 From: Youngster_yj <907985037@qq.com> Date: Wed, 29 Jul 2026 15:12:42 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20rebase=E5=86=B2=E7=AA=81=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yakitUI/YakitEditor/YakitEditor.tsx | 15 +++++++++++++ .../YakitEditor/fizzMenu/editerMenuFun.tsx | 4 ++++ .../YakitEditor/hooks/useBinaryFold.ts | 22 +++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index c054687cb3..660f486b75 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -89,6 +89,12 @@ import { useYakFormat } from './hooks/useYakFormat' import { generateDecorations as generateDecorationsFn } from './decorations/generateDecorations' import { editerMenuFun } from './fizzMenu/editerMenuFun' import { menuReduce, sortMenuFun, contextMenuKeybindingHandle } from './menus/menuHelpers' +import { + EditorDetailInfoProps, + SmartDecodeAnchorRect, + SmartDecodeFloatPanel, + snapshotRect, +} from '@/pages/fuzzer/HTTPFuzzerEditorMenu' // re-export 保持外部导入路径兼容 export { PLUGIN_PREFIX } @@ -211,12 +217,16 @@ export const YakitEditor: React.FC = React.memo((props) => { // ===== 二进制 Fuzztag 折叠:翻译边界 ===== const { + foldBinaryCapable, foldBinaryEnabled, binaryFoldEntriesRef, binaryFoldRangesRef, binaryModifiedOrdinalsRef, displayValue, handleBinaryChange, + foldBinaryOpen, + setFoldBinaryOpen, + getFoldBinaryOpen, } = useBinaryFold({ value, setValue, @@ -1178,6 +1188,7 @@ export const YakitEditor: React.FC = React.memo((props) => { downPosYRef: downPosY, upPosYRef: upPosY, onScrollTopRef: onScrollTop, + onOpenSmartDecode, }) } }, [editor, isShowSelectRangeMenu]) @@ -1200,6 +1211,10 @@ export const YakitEditor: React.FC = React.memo((props) => { anchorRect: snapshotRect(rect), }) } + + const onOpenSmartDecode = useMemoizedFn((rangeValue: string, anchorRect?: DOMRect) => { + openSmartDecodeRef.current?.(rangeValue, editorInfo.current, anchorRect) + }) // 编辑器菜单 useEffect(() => { diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx index 3a336590e1..9cfb3771ee 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/fizzMenu/editerMenuFun.tsx @@ -38,6 +38,8 @@ export interface EditerMenuFunParams { downPosYRef: React.MutableRefObject upPosYRef: React.MutableRefObject onScrollTopRef: React.MutableRefObject + + onOpenSmartDecode: (rangeValue: string, anchorRect?: DOMRect) => void } /** @@ -65,6 +67,7 @@ export const editerMenuFun = (params: EditerMenuFunParams) => { downPosYRef, upPosYRef, onScrollTopRef, + onOpenSmartDecode, } = params // 编辑器点击弹窗的唯一Id @@ -226,6 +229,7 @@ export const editerMenuFun = (params: EditerMenuFunParams) => { toOpenAiChat={toOpenAiChat} rangeValue={(editor && editor.getModel()?.getValueInRange(editor.getSelection() as any)) || ''} fizzRangeTimeoutId={fizzRangeTimeoutIdRef} + onOpenSmartDecode={onOpenSmartDecode} hTTPFuzzerClickEditorMenuProps={ readOnly ? undefined diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts index 44e41ddf65..31213f9dee 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/hooks/useBinaryFold.ts @@ -1,5 +1,5 @@ -import { useMemo, useRef } from 'react' -import { useMemoizedFn } from 'ahooks' +import { Dispatch, SetStateAction, useEffect, useMemo, useRef } from 'react' +import { useGetState, useMemoizedFn } from 'ahooks' import { monaco } from 'react-monaco-editor' import { BinaryFuzztagEntry, collapseBinaryFuzztag, expandBinaryFuzztag } from '../binaryFuzztag' import { MAX_BINARY_FOLD_ENTRIES } from '../constants' @@ -13,12 +13,16 @@ export interface UseBinaryFoldParams { } export interface UseBinaryFoldResult { + foldBinaryCapable: boolean foldBinaryEnabled: boolean binaryFoldEntriesRef: React.MutableRefObject> binaryFoldRangesRef: React.MutableRefObject<{ id: string; range: monaco.Range; ordinal: number }[]> binaryModifiedOrdinalsRef: React.MutableRefObject> displayValue: string | undefined handleBinaryChange: (content: string) => void + foldBinaryOpen: boolean + setFoldBinaryOpen: Dispatch> + getFoldBinaryOpen: () => boolean } /** @@ -29,8 +33,14 @@ export interface UseBinaryFoldResult { export const useBinaryFold = (params: UseBinaryFoldParams): UseBinaryFoldResult => { const { value, setValue, onChange, foldBinaryFuzztag, type } = params - // 仅在 foldBinaryFuzztag 且 http 类型下启用;模型存短占位,向上抛真实值,下游消费者无感知 - const foldBinaryEnabled = !!foldBinaryFuzztag && type === 'http' + const foldBinaryCapable = foldBinaryFuzztag !== undefined + const [foldBinaryOpen, setFoldBinaryOpen, getFoldBinaryOpen] = useGetState(!!foldBinaryFuzztag) + useEffect(() => { + if (foldBinaryFuzztag !== undefined) { + setFoldBinaryOpen(!!foldBinaryFuzztag) + } + }, [foldBinaryFuzztag]) + const foldBinaryEnabled = foldBinaryCapable && foldBinaryOpen && type === 'http' // 侧表:占位 id -> 原始标签信息;handleBinaryChange 据此 expand 还原真实文本 const binaryFoldEntriesRef = useRef>(new Map()) // 占位范围(仿 privacyMaskRangesRef),用于点击命中打开 HEX 编辑弹窗 @@ -84,11 +94,15 @@ export const useBinaryFold = (params: UseBinaryFoldParams): UseBinaryFoldResult }) return { + foldBinaryCapable, foldBinaryEnabled, binaryFoldEntriesRef, binaryFoldRangesRef, binaryModifiedOrdinalsRef, displayValue, handleBinaryChange, + foldBinaryOpen, + setFoldBinaryOpen, + getFoldBinaryOpen, } } From 0799aad693c002ee10320e2cea070563f3ad7f9b Mon Sep 17 00:00:00 2001 From: HJH <75428400+hjhke@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:24:58 +0800 Subject: [PATCH 5/9] =?UTF-8?q?perf:=20YakitEditor=20=E7=94=A8=E8=BD=BB?= =?UTF-8?q?=E9=87=8F=E6=8C=87=E7=BA=B9=E6=9B=BF=E4=BB=A3=E9=AB=98=E4=BA=AE?= =?UTF-8?q?=20JSON.stringify=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../yakitUI/YakitEditor/YakitEditor.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 660f486b75..5defd92ab4 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -117,6 +117,22 @@ const shallowCloneMenuItems = (items: EditorMenuItemType[]): EditorMenuItemType[ }) } +/** 高亮数组轻量指纹:只拼位置字段,避免 JSON.stringify 序列化 hoverVal 等大字段 */ +const highLightFingerprint = (list?: readonly any[]): string => { + if (!list?.length) return '' + let s = String(list.length) + for (let i = 0; i < list.length; i++) { + const item = list[i] + if (item == null) continue + if ('startOffset' in item) { + s += `|${item.startOffset},${item.highlightLength ?? 0}` + } else { + s += `|${item.startLineNumber},${item.startColumn},${item.endLineNumber},${item.endColumn}` + } + } + return s +} + export const YakitEditor: React.FC = React.memo((props) => { const { forceRenderMenu = false, @@ -1081,14 +1097,16 @@ export const YakitEditor: React.FC = React.memo((props) => { } catch (e) {} } }, [editor]) + const highLightTextKey = highLightFingerprint(highLightText) + const highLightFindKey = highLightFingerprint(highLightFind) useEffect(() => { if (deltaDecorationsRef.current) { disableUnicodeDecodeRef.current = props.disableUnicodeDecode deltaDecorationsRef.current() } }, [ - JSON.stringify(highLightText), - JSON.stringify(highLightFind), + highLightTextKey, + highLightFindKey, props.disableUnicodeDecode, props.fixContentType, props.originalContentType, @@ -1116,7 +1134,7 @@ export const YakitEditor: React.FC = React.memo((props) => { } } catch (error) {} }, - [editor, isPositionHighLightCursor, JSON.stringify(highLightFind)], + [editor, isPositionHighLightCursor, highLightFindKey], { wait: 300 }, ) From 61581ec20be5988247230588d8fc0a8df9a32bc3 Mon Sep 17 00:00:00 2001 From: HJH <75428400+hjhke@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:31:30 +0800 Subject: [PATCH 6/9] =?UTF-8?q?perf:=20YakitEditor=20HTTP=20=E5=85=89?= =?UTF-8?q?=E6=A0=87=E4=BB=85=E5=9C=A8=E9=9A=90=E7=A7=81=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E9=87=8D=E7=AE=97=E8=A3=85=E9=A5=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../main/src/components/yakitUI/YakitEditor/YakitEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 5defd92ab4..a20c47a1f4 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -837,9 +837,9 @@ export const YakitEditor: React.FC = React.memo((props) => { }) scheduleDecorations() - // 监听光标位置变化,用于隐私模式的动态显示/隐藏 + // 监听光标位置变化,仅隐私模式需要按光标动态显示/隐藏打码 const cursorPositionDisposable = editor.onDidChangeCursorPosition(() => { - if (props.type === 'http') { + if (props.type === 'http' && privacyFun()) { scheduleDecorations() } }) From 07235a23d443fb1841bf06667b8c8bb06c5f50f8 Mon Sep 17 00:00:00 2001 From: HJH <75428400+hjhke@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:04:01 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20YakitEditor=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E4=BB=85=E6=B3=A8=E5=86=8C=E4=B8=80=E6=AC=A1=20Monaco=20linkOp?= =?UTF-8?q?ener?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../main/src/components/yakitUI/YakitEditor/YakitEditor.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index a20c47a1f4..30e0fe6fc2 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -133,6 +133,9 @@ const highLightFingerprint = (list?: readonly any[]): string => { return s } +/** Monaco link opener 全局只注册一次,避免多编辑器实例重复注册 */ +let linkOpenerRegistered = false + export const YakitEditor: React.FC = React.memo((props) => { const { forceRenderMenu = false, @@ -278,6 +281,8 @@ export const YakitEditor: React.FC = React.memo((props) => { // 阻止编辑器点击URL默认打开行为 自定义外部系统默认浏览器打开URL useEffect(() => { + if (linkOpenerRegistered) return + linkOpenerRegistered = true monaco.editor.registerLinkOpener({ open: (link) => { // 在系统默认浏览器中打开链接 From b08a6ae716bf8a02b4bdcab8d994a5e7e082bc82 Mon Sep 17 00:00:00 2001 From: HJH <75428400+hjhke@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:11:12 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20YakitEditor=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=20dispose=20=E9=81=BF=E5=85=8D=E5=8F=8C?= =?UTF-8?q?=E9=87=8A=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx index 30e0fe6fc2..12085e946e 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitEditor/YakitEditor.tsx @@ -1098,7 +1098,7 @@ export const YakitEditor: React.FC = React.memo((props) => { editorDomNode?.removeEventListener('copy', handleEditorClipboard, true) editorDomNode?.removeEventListener('cut', handleEditorClipboard, true) unregisterBinaryFoldEntries(model) - editor.dispose() + // 不手动 editor.dispose(),交由 react-monaco-editor 卸载处理,避免双释放 } catch (e) {} } }, [editor]) From 434192688450adccc739ef5201d4e654bb70193a Mon Sep 17 00:00:00 2001 From: HJH <75428400+hjhke@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:47:40 +0800 Subject: [PATCH 9/9] =?UTF-8?q?perf:=20=E6=8B=86=E5=88=86=20getAllRows=20?= =?UTF-8?q?=E5=B9=B6=E7=A7=BB=E9=99=A4=20md-editor=20=E6=AD=BB=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E4=BB=A5=E5=87=8F=E5=B0=8F=E5=90=AF=E5=8A=A8=E4=BD=93?= =?UTF-8?q?=E7=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../configNetwork/CustomizeCode.tsx | 19 +------------------ .../configNetwork/CustomizeCodeTypes.ts | 18 ++++++++++++++++++ .../utils/monacoSpec/fuzzHTTPMonacoSpec.ts | 2 +- .../utils/monacoSpec/yakCompletionSchema.ts | 2 +- .../main/src/utils/monacoSpec/yakEditor.ts | 1 - 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/renderer/src/main/src/components/configNetwork/CustomizeCode.tsx b/app/renderer/src/main/src/components/configNetwork/CustomizeCode.tsx index 8f8fcb156a..55bbe6e47f 100644 --- a/app/renderer/src/main/src/components/configNetwork/CustomizeCode.tsx +++ b/app/renderer/src/main/src/components/configNetwork/CustomizeCode.tsx @@ -15,12 +15,12 @@ import { yakitNotify } from '@/utils/notification' import { invalidateCustomSnippetsCache } from '@/utils/monacoSpec/yakCompletionSchema' import type { CodeCustomizeModalProps, - RowOf, TCodeCustomizeTagProps, TCustomCodeGeneral, TCustomEditorCodeGeneral, TQueryCustomCodeRequest, } from './CustomizeCodeTypes' +import { getAllRows } from './CustomizeCodeTypes' import { YakitSpin } from '../yakitUI/YakitSpin/YakitSpin' import { OutlineXIcon } from '@/assets/icon/outline' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' @@ -156,23 +156,6 @@ const LevelOptions = [ }, ] -export const getAllRows = >(data: T): RowOf[] => { - const keys = Object.keys(data) as (keyof T)[] - const length = Math.max(...keys.map((k) => data[k].length)) - - return Array.from({ length }, (_, index) => - keys.reduce((obj, key) => { - const arr = data[key] - if (Array.isArray(arr) && index < arr.length) { - // 将 key 末尾的 s 去掉 - const singular = (key as string).replace(/s$/, '') - ;(obj as any)[singular] = arr[index] - } - return obj - }, {} as RowOf), - ) -} - const CodeCustomize: FC> = ({ value }) => { const [form] = useForm() const { theme } = useTheme() diff --git a/app/renderer/src/main/src/components/configNetwork/CustomizeCodeTypes.ts b/app/renderer/src/main/src/components/configNetwork/CustomizeCodeTypes.ts index 20ce80285e..a11d1f4b07 100644 --- a/app/renderer/src/main/src/components/configNetwork/CustomizeCodeTypes.ts +++ b/app/renderer/src/main/src/components/configNetwork/CustomizeCodeTypes.ts @@ -41,6 +41,24 @@ type RowOf> = { [K in keyof T as K extends `${infer S}s` ? S : K]: T[K][number] } +/** 将「列数组」结构转为行对象数组(与 UI 解耦,供 monacoSpec 等轻量引用) */ +export const getAllRows = >(data: T): RowOf[] => { + const keys = Object.keys(data) as (keyof T)[] + const length = Math.max(...keys.map((k) => data[k].length)) + + return Array.from({ length }, (_, index) => + keys.reduce((obj, key) => { + const arr = data[key] + if (Array.isArray(arr) && index < arr.length) { + // 将 key 末尾的 s 去掉 + const singular = (key as string).replace(/s$/, '') + ;(obj as any)[singular] = arr[index] + } + return obj + }, {} as RowOf), + ) +} + export type { TCodeCustomizeTagProps, CodeCustomizeModalProps, diff --git a/app/renderer/src/main/src/utils/monacoSpec/fuzzHTTPMonacoSpec.ts b/app/renderer/src/main/src/utils/monacoSpec/fuzzHTTPMonacoSpec.ts index 2fd57426a4..e84c3c894a 100644 --- a/app/renderer/src/main/src/utils/monacoSpec/fuzzHTTPMonacoSpec.ts +++ b/app/renderer/src/main/src/utils/monacoSpec/fuzzHTTPMonacoSpec.ts @@ -11,7 +11,7 @@ import { import { getModelContext } from '@/utils/monacoSpec/yakEditor' import IWordAtPosition = editor.IWordAtPosition import { TCustomCodeGeneral } from '@/components/configNetwork/CustomizeCodeTypes' -import { getAllRows } from '@/components/configNetwork/CustomizeCode' +import { getAllRows } from '@/components/configNetwork/CustomizeCodeTypes' const { ipcRenderer } = window.require('electron') export const highlightKinds: string[] = [ diff --git a/app/renderer/src/main/src/utils/monacoSpec/yakCompletionSchema.ts b/app/renderer/src/main/src/utils/monacoSpec/yakCompletionSchema.ts index fecc57428d..4417972092 100644 --- a/app/renderer/src/main/src/utils/monacoSpec/yakCompletionSchema.ts +++ b/app/renderer/src/main/src/utils/monacoSpec/yakCompletionSchema.ts @@ -2,7 +2,7 @@ import { editor, IRange, languages, Position } from 'monaco-editor' import { CancellationToken } from 'typescript' import { monaco } from 'react-monaco-editor' import { getModelContext } from './yakEditor' -import { getAllRows } from '@/components/configNetwork/CustomizeCode' +import { getAllRows } from '@/components/configNetwork/CustomizeCodeTypes' import { TCustomCodeGeneral } from '@/components/configNetwork/CustomizeCodeTypes' import { highlightKinds } from './fuzzHTTPMonacoSpec' diff --git a/app/renderer/src/main/src/utils/monacoSpec/yakEditor.ts b/app/renderer/src/main/src/utils/monacoSpec/yakEditor.ts index 5343c88394..f1950918f4 100644 --- a/app/renderer/src/main/src/utils/monacoSpec/yakEditor.ts +++ b/app/renderer/src/main/src/utils/monacoSpec/yakEditor.ts @@ -16,7 +16,6 @@ import { setupCompletionHint } from './yakCompletionHint' import { KeyCode, KeyMod, languages } from 'monaco-editor' import CodeAction = languages.CodeAction import CodeActionList = languages.CodeActionList -import { EditorContext } from '@uiw/react-md-editor' export const YaklangMonacoSpec = 'yak' // export const GolangMonacoSpec = "go";