- 桌面应用
- 开发工具
【免费下载链接】Notepad3
Notepad like text editor based on the Scintilla source code. Notepad3 based on code from Notepad2 and MiniPath on code from metapath. Download Notepad3:
导读
本文基于 Notepad3 仓库中的重构计划文档 plans/notepad3_refactoring.md,完整梳理该项目对核心模块src/Notepad3.c的大型重构:包括将 12,985 行的单体文件拆分为独立工具模块Notepad3Util.c/.h,以及把 2,994 行的MsgCommand()巨型 switch 改造为 73 行薄调度器。读者将掌握大型 C 语言 Win32 项目"行为不变前提下拆分巨型文件"的完整方法论——从模块边界划分、风险分级、源码级调用点迁移,到构建验证与冒烟测试策略。
一、重构背景:单体文件的维护困境
1.1 重构前的核心数据
根据 plans/notepad3_refactoring.md 的 Context 部分,重构前src/Notepad3.c是项目中最大的源文件,规模数据如下:
| 指标 | 重构前数值 |
|---|---|
Notepad3.c总行数 | 12,985 行 |
| 静态函数数量 | 约 55 个 |
| 静态变量数量 | 约 60 个 |
MsgCommand()行数 | 2,994 行 |
MsgCommand()switch case 数 | 360+ 个 |
这些代码横跨多个互不相关的关注点:自动滚动(Auto-Scroll)、文件观察(File Observation)、位图加载(Bitmap Loading)、文本输入辅助(Text Input Helpers)、TinyExpr 表达式求值等,全部堆叠在同一文件中。
1.2 四大重构动机
文档明确列出了推动本次重构的四点动机:
- 可导航性(Navigability):在 13K 行的文件中查找代码效率低下;逻辑上成组的辅助函数应当归属各自独立的模块。
- 可维护性(Maintainability):
MsgCommand()单体巨型函数使得难以对单个命令组进行推理和修改。 - 封装性(Encapsulation):自动滚动、文件观察、TinyExpr 等无关子系统的静态变量全部处于全局文件作用域,掩盖了它们真实的作用域边界。
- 一致性(Consistency):项目中其他模块(如 src/Edit.c、src/Styles.c、src/Dialogs.c)已经遵循清晰的
.c/.h成对文件边界,Notepad3.c是例外。
1.3 不拆什么:刻意保留在 Notepad3.c 的代码
重构计划特别强调"What NOT to extract",以下代码被刻意保留在原文件,因为它们与整个应用生命周期或全局状态深度耦合:
_InitGlobals()/_CleanUpResources()—— 核心应用生命周期函数,触及所有子系统。- 消息队列辅助函数(
_MQ_AppendCmd、MQ_ExecuteNext)—— 与定时器系统和 UI 更新紧密集成。 - UI 更新辅助函数(
_UpdateStatusbarDelayed、_UpdateToolbarDelayed、_UpdateTitlebarDelayed)—— 依赖消息队列和复杂全局状态。 _EditSubclassProc()—— Scintilla 子类化胶水代码,应当紧邻MainWndProc。ParseCmdLnOption()—— 命令行解析,与程序启动流程归属一处。MsgInitMenu()—— 读取 20+ 个状态变量来启用/禁用菜单项,本质上是全局应用状态。
这一"明确划定不拆范围"的决策,恰恰是重构成功的关键——不是所有代码都适合拆分,只有内聚性足够强的子系统才值得独立成模块。
二、已完成的 Part 2:MsgCommand() 薄调度器改造
2.1 从 2,994 行 switch 到 73 行分发链
文档记录MsgCommand()从2,994 行的巨型 switch 重构为73 行的薄调度器,将 360+ 个 case 委派给10 个静态处理器函数(仍保留在Notepad3.c中)。这一改造已在当前仓库中落地,见 src/Notepad3.c。
调度器实际结构(对应 src/Notepad3.c)如下:
LRESULT MsgCommand(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam) { // Language/theme menu range checks (inline) // Timer/notification cases (inline, return immediately) switch(iLoWParam) { case SCEN_CHANGE: ... return FALSE; ... } // Handler dispatch chain if (_HandleFileCommands(hwnd, umsg, wParam, lParam)) { return FALSE; } if (_HandleEncodingCommands(...)) { return FALSE; } ... return DefWindowProc(hwnd, umsg, wParam, lParam); }2.2 10 个处理器函数的职责划分
文档给出的处理器分工表(当前仓库中 10 个 handler 已全部实现,行号见 src/Notepad3.c):
| Handler | Case 数 | 覆盖的命令范围 |
|---|---|---|
_HandleFileCommands | ~27 | IDM_FILE_*—— 打开/保存/打印/收藏夹/grepWin(源码位置 src/Notepad3.c) |
_HandleEncodingCommands | ~10 | IDM_ENCODING_*、IDM_LINEENDINGS_*(src/Notepad3.c) |
_HandleEditBasicCommands | ~30 | IDM_EDIT_UNDO..CMD_VK_INSERT—— 撤销/重做/剪切/复制/粘贴/缩进(src/Notepad3.c) |
_HandleEditLineManipulation | ~42 | IDM_EDIT_ENCLOSESELECTION..IDM_EDIT_INSERT_GUID—— 行修改/排序/合并/大小写(src/Notepad3.c) |
_HandleEditTextTransform | ~45 | IDM_EDIT_LINECOMMENT..IDM_EDIT_HEX2CHAR—— 注释/编解码/转义/十六进制(src/Notepad3.c) |
_HandleEditFind | ~21 | IDM_EDIT_FINDMATCHINGBRACE..IDM_EDIT_GOTOLINE—— 查找/替换/书签(src/Notepad3.c) |
_HandleViewAndSettingsCommands | ~99 | IDM_VIEW_*、IDM_SET_*—— 视图/设置/渲染(src/Notepad3.c) |
_HandleHelpCommands | ~5 | IDM_HELP_*、IDM_SETPASS(src/Notepad3.c) |
_HandleCmdCommands | ~90 | CMD_*—— 键盘快捷键/导航/窗口定位(src/Notepad3.c) |
_HandleToolbarCommands | ~30 | IDT_*—— 通过s_ToolbarDispatch[]查找表分发(src/Notepad3.c) |
2.3 调度链的短路语义
每个处理器遵循统一约定:返回true表示已处理,返回false则交给下一个处理器尝试。从 src/Notepad3.c 的分发链可以看到,这是一个典型的"责任链(Chain of Responsibility)"模式:命令 ID 按文件 → 编码 → 编辑基础 → 行操作 → 文本变换 → 查找 → 视图设置 → 帮助 → 快捷键 → 工具栏的顺序依次尝试匹配,全部未命中才落入DefWindowProc。
2.4 工具栏分发表:消灭 40 个重复 case
重构前有 40 个重复的IDT_*工具栏 case,重构后用静态查找表s_ToolbarDispatch[]取代(见 src/Notepad3.c)。表中包含29 个标准条目,将工具栏命令 ID 映射为菜单命令 ID,例如:
static const struct { unsigned idt; unsigned idm; } s_ToolbarDispatch[] = { { IDT_FILE_NEW, IDM_FILE_NEW }, { IDT_FILE_OPEN, IDM_FILE_OPEN }, { IDT_FILE_SAVE, IDM_FILE_SAVE }, { IDT_EDIT_UNDO, IDM_EDIT_UNDO }, { IDT_EDIT_CUT, IDM_EDIT_CUT }, { IDT_GREP_WIN_TOOL, IDM_GREP_WIN_SEARCH }, { IDT_VIEW_WORDWRAP, IDM_VIEW_WORDWRAP }, { IDT_VIEW_CHASING_DOCTAIL, IDM_VIEW_CHASING_DOCTAIL }, { IDT_VIEW_PIN_ON_TOP, IDM_SET_ALWAYSONTOP }, // ... 其余条目 };查找表分发的核心逻辑(src/Notepad3.c):遍历表,命中后先通过IsCmdEnabled()检查菜单命令是否可用,可用则SendWMCommand()转发,不可用则SimpleBeep()提示。
另外保留2 个特殊 case(不经过查找表):
IDT_EDIT_COPY:当复制命令不可用时降级为 COPYALL(复制全部内容);IDT_EDIT_CLEAR:当清除命令不可用时降级为SciCall_ClearAll()。
这种"查找表 + 特例兜底"的组合,既消除了重复代码,又保留了原有异常分支的行为语义,是重构中"行为零变化"原则的典型体现。
三、已完成的 Part 1(Phases 1-3):Notepad3Util 模块
3.1 新模块总览
Part 1 的前三个阶段已全部完成,新增了 src/Notepad3Util.c(349 行)与 src/Notepad3Util.h(50 行)两个文件。项目文件 src/Notepad3.vcxproj 与 src/Notepad3.vcxproj.filters 中已注册<ClCompile>和<ClInclude>条目,CLAUDE.md的核心模块表与.github/copilot-instructions.md的核心模块列表也同步更新。
该模块被定位为"从 Notepad3.c 提取的实用函数集合",头文件 src/Notepad3Util.h 通过#include "TypeDefs.h"和#include "SciCall.h"保持与核心类型的关联。
3.2 Phase 1:位图/图片加载(低风险,约 100 行)
Phase 1 移入的三个位图加载函数:
| 函数 | 职责 |
|---|---|
NP3Util_LoadBitmapFile() | 从文件加载工具栏位图并校验尺寸合法性 |
NP3Util_CreateScaledImageListFromBitmap() | 从位图创建 DPI 缩放的图像列表 |
NP3Util_XXX_CreateScaledImageListFromBitmap() | 使用固定NUMTOOLBITMAPS的旧版变体 |
同时,NUMTOOLBITMAPS宏从Notepad3.c迁移到 src/Notepad3Util.h,当前值为31。
从源码看(src/Notepad3Util.c),NP3Util_LoadBitmapFile()的实现逻辑是:先通过Path_IsExistingFile()检查文件存在,再用LoadImage(..., LR_CREATEDIBSECTION | LR_LOADFROMFILE)加载,随后用GetObject()读取位图尺寸,校验bmWidth >= height * NUMTOOLBITMAPS——即位图宽度必须能容纳 31 个高度为 16 像素的图标单元,否则弹出IDS_MUI_ERR_BITMAP警告框;文件不存在则弹出IDS_MUI_ERR_LOADFILE错误提示。
而两个*CreateScaledImageListFromBitmap()函数(src/Notepad3Util.c)展示了 DPI 适配的关键细节:
UINT const dpi = Scintilla_GetWindowDPI(hWnd); if (!Settings.DpiScaleToolBar || (dpi == USER_DEFAULT_SCREEN_DPI)) { return himl; // default DPI, we are done } // Scale button icons/images int const scx = ScaleIntToDPI(hWnd, cx); int const scy = ScaleIntToDPI(hWnd, cy); HIMAGELIST hsciml = ImageList_Create(scx, scy, ILC_COLOR32 | ILC_MASK | ILC_HIGHQUALITYSCALE, ...);即:仅在Settings.DpiScaleToolBar开启且系统 DPI 非 96 时才执行缩放,逐个图标通过ImageList_GetIcon(..., ILD_TRANSPARENT | ILD_PRESERVEALPHA | ILD_SCALE)提取后再加入缩放后的图像列表——这解释了Settings.DpiScaleToolBar配置项在底层图像处理上的实际作用。
3.3 Phase 2:自动换行配置(低风险,约 100 行)
移入的三个换行配置函数:
| 函数 | 职责 |
|---|---|
NP3Util_SetWrapStartIndent() | 根据Settings.WordWrapIndent设置换行起始缩进 |
NP3Util_SetWrapIndentMode() | 设置换行缩进模式(same/indent/deep/fixed) |
NP3Util_SetWrapVisualFlags(HWND) | 设置换行视觉标志符号 |
从 src/Notepad3Util.c 可以看到WordWrapIndent配置值(1-7)的完整语义映射:
void NP3Util_SetWrapStartIndent(void) { int i = 0; switch (Settings.WordWrapIndent) { case 1: i = 1; break; // 固定 1 字符 case 2: i = 2; break; // 固定 2 字符 case 3: i = 1 * (iIndentWidth ? iIndentWidth : iTabWidth); break; // 1×缩进/制表宽度 case 4: i = 2 * (iIndentWidth ? iIndentWidth : iTabWidth); break; // 2×缩进/制表宽度 default: break; } SciCall_SetWrapStartIndent(i); }而NP3Util_SetWrapIndentMode()(src/Notepad3Util.c)将WordWrapIndent == 5/6/7分别映射为 Scintilla 的SC_WRAPINDENT_SAME/SC_WRAPINDENT_INDENT/SC_WRAPINDENT_DEEPINDENT,其余值则走NP3Util_SetWrapStartIndent()并设置SC_WRAPINDENT_FIXED。可见Settings.WordWrapIndent是一个 1-7 的多模式枚举:1/2 为固定字符数缩进、3/4 为按制表宽度倍数缩进、5/6/7 为 Scintilla 内建模式。
NP3Util_SetWrapVisualFlags()(src/Notepad3Util.c)则负责Settings.ShowWordWrapSymbols与Settings.WordWrapSymbols的解析:当WordWrapSymbols为 0 时自动回退为 22,然后按个位数字(1=行尾箭头贴文本、2=行尾箭头贴边缘)和十位数字(1=行首箭头贴文本、2=行首箭头贴边缘)组合出SC_WRAPVISUALFLAG_*标志。
3.4 Phase 3:自动滚动(中低风险,约 200 行)
Phase 3 将中键自动滚动(Firefox 风格的连续滚动)完整移入Notepad3Util:
- 6 个静态变量迁移:
s_bAutoScrollMode、s_bAutoScrollHeld、s_dwAutoScrollStartTick、s_ptAutoScrollOrigin、s_ptAutoScrollMouse、s_dAutoScrollAccumY——全部封装进 src/Notepad3Util.c,不再污染Notepad3.c的全局作用域。 - 4 个常量迁移:
AUTOSCROLL_TIMER_MS(30)、AUTOSCROLL_DEADZONE(15)、AUTOSCROLL_DIVISOR(60.0)、AUTOSCROLL_CLICK_THRESHOLD_MS(200),定义于 src/Notepad3Util.h。 - 核心滚动逻辑:
NP3Util_AutoScrollStart/Stop()、NP3Util_AutoScrollTimerProc()。 - 状态访问器:
NP3Util_IsAutoScrollMode()、NP3Util_IsAutoScrollHeld()、NP3Util_GetAutoScrollStartTick()、NP3Util_SetAutoScrollHeld()、NP3Util_AutoScrollUpdateMouse()——专供_EditSubclassProc读取/修改状态。
滚动算法细节(src/Notepad3Util.c)值得关注:定时器每 30ms 触发一次,先计算鼠标当前 Y 与起始 Y 的差值deltaY,死区(15 像素)内的移动不产生滚动;超出死区后,速度与超出量成正比(speed = (deltaY - deadzone) / 60.0),累积在s_dAutoScrollAccumY中,累积量达到整行数时调用SciCall_LineScroll()滚动并扣除已滚动的部分。这种"速度随距离递增 + 余数累积"的设计实现了平滑的加速度滚动效果。
3.5 Part 1 的净收益
文档记录:拆分后Notepad3.c从12,985 行降至 12,713 行。
3.6 修改文件清单
Part 1/Part 2 涉及的文件改动:
- src/Notepad3.c —— 删除被提取的代码、更新调用点、新增
#include "Notepad3Util.h" - src/Notepad3Util.c —— 新建实现文件
- src/Notepad3Util.h —— 新建头文件
- src/Notepad3.vcxproj —— 新增
<ClCompile>与<ClInclude>条目 - src/Notepad3.vcxproj.filters —— 新增 Source Files / Header Files 过滤器条目
- CLAUDE.md —— 更新核心模块表、新增 Menu/Command Architecture 章节
.github/copilot-instructions.md—— 更新核心模块列表
四、未完成的 Phases 4-6:下一步迁移计划
文档详细规划了剩余三个阶段的迁移方案(当前仓库中这些函数仍在Notepad3.c内,尚未迁移)。
4.1 Phase 4:TinyExpr 表达式求值(约 130 行,中等风险)
待迁移的静态变量(src/Notepad3.c):
s_dExpression(double)—— 最近一次表达式求值结果s_iExprError(te_int_t)—— 最近一次表达式错误码
待迁移函数与目标名:
| 当前函数 | 新函数 | 当前行号 | 规模 |
|---|---|---|---|
_EvalTinyExpr(bool qmark) | NP3Util_EvalTinyExpr(bool) | 2968 | ~150 |
_InterpMultiSelectionTinyExpr(te_int_t*) | NP3Util_InterpMultiSelectionTinyExpr(te_int_t*) | 11238 | ~50 |
需要新增的访问器:
NP3Util_GetLastExpression()—— 返回s_dExpression(由_UpdateStatusbarDelayed读取)NP3Util_GetLastExprError()—— 返回s_iExprError(由_UpdateStatusbarDelayed读取)
需要更新的 3 处调用点:
- 行 7874(文档记为 6839):
_EvalTinyExpr(false)—— 位于_HandleCmdCommands的CMD_ENTER_RETURNcase - 行 10057(文档记为 8921):
_EvalTinyExpr(true)—— 位于_MsgNotifyFromEdit,输入?字符时触发 - 行 11601(文档记为 10399):
s_dExpression = _InterpMultiSelectionTinyExpr(&s_iExprError)—— 位于_UpdateStatusbarDelayed
依赖:Settings.EvalTinyExprOnSelection、Encoding_SciCP、SciCall_*(均通过头文件)、te_interp()(需在 Notepad3Util.c 中#include "tinyexpr/tinyexpr.h")、AllocMem/FreeMem(通过Helpers.h)。
风险点:状态栏代码目前直接读取s_dExpression/s_iExprError,迁移后必须改用 getter;te_interp()作用于 Scintilla 的原始字符缓冲区,对编码敏感,但迁移本身机制上是机械的。
4.2 Phase 5:文本输入辅助(约 300 行,中等风险)
待迁移的静态变量(行 189):
s_SelectionBuffer(char*)—— 用于自动闭合括号/引号跟踪的动态分配缓冲区
待迁移函数:
| 当前函数 | 新函数 | 当前行号 | 规模 |
|---|---|---|---|
_HandleAutoIndent(int) | NP3Util_HandleAutoIndent(int) | 9401 | ~45 |
_HandleAutoCloseTags() | NP3Util_HandleAutoCloseTags() | 9448 | ~58 |
_SaveSelectionToBuffer() | NP3Util_SaveSelectionToBuffer() | 9508 | ~16 |
_EncloseSelectionBuffer(char,char) | NP3Util_EncloseSelectionBuffer(char,char) | 9526 | ~17 |
_HandleInsertCheck(SCNotification*) | NP3Util_HandleInsertCheck(...) | 9545 | ~89 |
_HandleDeleteCheck(SCNotification*) | NP3Util_HandleDeleteCheck(...) | 9636 | ~60 |
明确跳过:_IsIMEOpenInNoNativeMode()(行 9696)是#if 0包裹的死代码,不迁移。
需要新增的生命周期函数:
NP3Util_TextInputInit()—— 分配s_SelectionBuffer;在MsgCreate中调用NP3Util_TextInputCleanup()—— 释放s_SelectionBuffer;在_CleanUpResources中调用
需要更新的 5 处调用点(均位于_MsgNotifyFromEdit,见 src/Notepad3.c):
- 行 9745:
_HandleInsertCheck(scn) - 行 9752:
_SaveSelectionToBuffer() - 行 9768:
_HandleDeleteCheck(scn) - 行 10048:
_HandleAutoIndent(ich) - 行 10053:
_HandleAutoCloseTags()
依赖:Settings.AutoIndent、Settings.AutoCloseQuotes、Settings.AutoCloseBrackets、Settings.AutoCloseTags(全局配置)、SciCall_*/Sci_*(头文件)、EditReplaceSelection()(通过 src/Edit.h)、AllocMem/FreeMem/ReAllocMem/SizeOfMem(通过 src/Helpers.h)。
风险分析(文档明确指出):这些函数运行在 Scintilla 通知的热路径上(SCN_MODIFIED、SCN_CHARADDED),但每次按键只调用一次而非逐字符批量调用,因此函数调用开销可忽略。主要风险在于确保s_SelectionBuffer的生命周期始终正确。
4.3 Phase 6:文件观察(约 450 行,高风险,留到最后)
待迁移的静态变量(行 531):
s_FileChgObsvrData(FCOBSRVDATA_T)—— 包含事件句柄(hEventFileChanged、hEventFileDeleted)、文件元数据(fdCurFile)、代次计数器(iObservationGeneration,采用InterlockedCompareExchange/InterlockedIncrement的 seqlock 模式)、后台工作线程句柄。在 Notepad3.c 中有 48 处非注释引用。
待迁移函数:
| 当前函数 | 新函数 | 当前行号 | 规模 |
|---|---|---|---|
IsFileReadOnly() | NP3Util_IsFileReadOnly() | 519 | ~15 |
IsFileChangedFlagSet() | NP3Util_IsFileChangedFlagSet() | 535 | ~4 |
IsFileDeletedFlagSet() | NP3Util_IsFileDeletedFlagSet() | 540 | ~4 |
RaiseFlagIfCurrentFileChanged() | NP3Util_RaiseFlagIfCurrentFileChanged() | 545 | ~50 |
ResetFileObservationData(bool) | NP3Util_ResetFileObservationData(bool) | 596 | ~20 |
IsFileVarLogFile() | NP3Util_IsFileVarLogFile() | — | ~10 |
_ResetFileWatchingMode() | NP3Util_ResetFileWatchingMode() | — | ~10 |
NotifyIfFileHasChanged() | NP3Util_NotifyIfFileHasChanged() | — | ~20 |
WatchTimerProc(...) | NP3Util_WatchTimerProc(...) | — | ~15 |
LogRotateTimerProc(...) | NP3Util_LogRotateTimerProc(...) | — | ~25 |
AtomicSaveTimerProc(...) | NP3Util_AtomicSaveTimerProc(...) | — | ~50 |
需要新增的生命周期函数:
NP3Util_FileObservationInit()—— 创建事件句柄;替换InitInstance()中约 1843-1852 行的代码NP3Util_FileObservationCleanup()—— 销毁工作线程与事件句柄;替换_CleanUpResources()中约 824-833 行的代码NP3Util_GetFileObservationData()—— 返回PFCOBSRVDATA_T指针,供InstallFileWatching()访问结构体
循环依赖及解决方案:定时器回调会反向调用Notepad3.c中的函数——AtomicSaveTimerProc调用InstallFileWatching(false)和FileSave(FSF_SaveAlways);LogRotateTimerProc调用PostWMCommand(Globals.hwndMain, IDM_VIEW_CHASING_DOCTAIL)和InstallFileWatching(true);_ResetFileWatchingMode调用CheckCmd(GetMenu(...))。文档给出的解决方案是:Notepad3Util.c已#include声明了这些函数的 src/Notepad3.h,由链接器解析跨模块调用——这与Edit.c调用FileLoad()的模式完全相同。
主要调用点更新(约 48 处):
InitInstance()—— 事件创建改为NP3Util_FileObservationInit()(src/Notepad3.c)_CleanUpResources()—— 清理改为NP3Util_FileObservationCleanup()(src/Notepad3.c)InstallFileWatching()—— 直接结构体字段访问改为NP3Util_GetFileObservationData()->(src/Notepad3.c)MsgFileChangeNotify()—— 读取标志、重置观察数据_UpdateTitlebarDelayed()—— 调用IsFileChangedFlagSet()/IsFileDeletedFlagSet()MsgInitMenu()—— 调用IsFileReadOnly()_HandleViewAndSettingsCommands、_HandleCmdCommands—— 各种标志检查
线程安全注意事项:代次计数器使用InterlockedCompareExchange/InterlockedIncrement实现 seqlock 模式(后台工作线程 vs UI 线程)。从源码看(src/Notepad3.c),RaiseFlagIfCurrentFileChanged()正是通过"读前取 generation → 比较文件元数据 → 读后再取 generation 比对"的方式检测fdCurFile在并发ResetFileObservationData期间是否发生撕裂读取。迁移结构体不改变线程安全性,但文档特别提醒:NP3Util_GetFileObservationData()返回裸指针,调用者不得在可能触发重新分配的操作之间缓存该指针。
风险评级:高—— 48 处引用点(多为机械改名),但InstallFileWatching()直接操纵结构体字段(工作线程启动/取消、事件等待),SetTimer()调用中的定时器过程函数指针必须同步更新,线程正确性是关键。
五、验证策略:如何证明"行为零变化"
重构的核心承诺是"不改变任何行为",文档为此设计了分阶段验证策略:
5.1 每阶段三重验证
- 构建验证:至少执行
Build\Build_x64.cmd Debug,确保无编译和链接错误; - Diff 审计:执行
git diff,确认改动是纯机械移动,不含任何逻辑变化; - 按命令组冒烟测试:
- Phase 4(TinyExpr):选中
1+2后按?键,验证结果被插入;用列选择检查状态栏表达式显示; - Phase 5(文本输入):输入
"验证自动闭合引号;输入{验证自动闭合括号;在if (...) {后按 Enter 验证自动缩进;输入<div>验证自动闭合</div>;在""上按 Backspace 验证成对删除; - Phase 6(文件观察):在其他编辑器中修改文件 → Notepad3 必须提示重新加载;启用日志跟踪模式(Ctrl+Shift+L)→ 验证自动刷新;测试原子保存(
Settings2.AtomicFileSave=1);测试文件删除检测;快速开关文件 → 验证无定时器泄漏。
- Phase 4(TinyExpr):选中
5.2 全量验证
全部阶段完成后执行Build\BuildAll.cmd Release,覆盖全部 4 个平台(对应 Build/BuildAll.cmd 及 Build 目录下的 x64/Win32/ARM64 构建脚本)。
六、预期最终结果:重构前后的量化对比
文档给出的最终预估指标:
| 指标 | 重构前 | 重构后(全部阶段完成) |
|---|---|---|
Notepad3.c行数 | 12,985 | ~11,700 |
Notepad3Util.c行数 | 0 | ~1,250 |
MsgCommand()行数 | 2,994 | ~73(薄调度器) |
| 工具栏 switch case | 40 个重复 | 查找表 |
Notepad3.c静态函数数 | ~55 | ~35 |
七、可复用的重构方法论总结
从 plans/notepad3_refactoring.md 和当前仓库源码中,可以提炼出适用于大型 C 语言 Win32 项目的行为保持型重构要点:
- 先划边界,再动手:明确"拆什么"与"不拆什么"。生命周期函数、UI 状态密集型函数、与消息循环深度耦合的代码应留在原文件;只有内聚性强、状态自包含的子系统(位图、换行、自动滚动、表达式求值、文本输入、文件观察)才适合提取。
- 按风险分级排序:低风险先行(位图加载、换行配置),中风险居中(自动滚动、TinyExpr、文本输入),高风险留到最后(文件观察,涉及 48 处引用和线程安全)。
- 每阶段独立验证:构建 + diff 审计 + 针对该功能组的冒烟测试,把大规模重构分解为可独立回滚的小步骤。
- 状态封装成访问器:被提取模块的静态变量不暴露全局符号,通过
NP3Util_IsAutoScrollMode()这类访问器函数供外部读取,保持封装性;生命周期由Init/Cleanup函数管理。 - 巨型 switch 转责任链:360+ case 的
MsgCommand()拆成 10 个按命令族划分的处理器,配合短路返回语义;重复的工具栏 case 用查找表 + 特例兜底替代,既消除重复又保留分支语义。 - 循环依赖靠头文件与链接器消解:跨模块回调(如
AtomicSaveTimerProc→InstallFileWatching)通过#include "Notepad3.h"声明 + 链接器解析,与项目现有模式(Edit.c调用FileLoad())保持一致。
截至当前仓库状态,Part 1(Phases 1-3)与 Part 2 已落地(Notepad3Util.c/.h已存在并被项目文件引用,10 个 handler 与s_ToolbarDispatch[]已实现在 src/Notepad3.c 中);Phases 4-6 的迁移对象仍位于Notepad3.c原位置,属于文档规划的后续工作。
- 桌面应用
- 开发工具
【免费下载链接】Notepad3
Notepad like text editor based on the Scintilla source code. Notepad3 based on code from Notepad2 and MiniPath on code from metapath. Download Notepad3:
相关推荐
PicoClaw Pipeline 重构实践:将 1400 行单体 runTurn 拆分为职责清晰的多文件管线
PicoClaw Pipeline 重构实践:将 1400 行单体 runTurn 拆分为职责清晰的多文件管线 导读 本文基于 PicoClaw 开源仓库中的《
人工智能AI 应用AI Agent交互助手工具调用MCP ClientsAgent 记忆omo/lazycodex 常量文件拆分实战:delegate-task constants.ts 重构执行计划深度解析
omo/lazycodex 常量文件拆分实战:delegate task constants.ts 重构执行计划深度解析 在 omo/lazycodex(oh
人工智能AI Agent代码智能体多智能体MCP ClientsAgent 编排opencodex 大型文件重构:零行为变更的模块拆分实战
opencodex 大型文件重构:零行为变更的模块拆分实战 本指南基于 opencodex 仓库 devlog/_fin/260723_large_file_r
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考