Solomd抖动问题扩展方式解决:多LLM合作,DSV4.1flash收尾

先说结论:你说得对,这不是扩展的问题,是 CodeMirror 6 在拖选时的一个正反馈循环,而且可以用一段很轻的"护栏"代码消除。我在同一手势下做了开启/关闭的实测对比。

一、现象(和你的描述一致)

在编辑器里按住鼠标向下拖选,当指针停在可视区底部附近时:

  • 文字会从指针下面"滑走",选区感觉很不稳(抖动);
  • 有时向下滚一点,却直接选中到页面最下方;
  • 有时向下选择却向上跳选并高亮;
  • 一路跑到文档末尾后,会"啪"地一下被拽回顶部锚点,然后循环重来。

用 44 步标准手势复现,纯应用(不依赖任何扩展)下:

  • scrollTop 被写入 316 次,其中 >120px 的跳变 10 次,最大一次 −17287px(直接回弹到顶);
  • 选区头与指针偏差最坏 8 行。

二、原因

关键事实:每一次 scrollTop 写入都来自 CodeMirror 自己的 measure → scrollIntoView(scrollRectIntoView),而且写入那一刻,选区头其实已经在视口内了------不是因为没滚到位才补滚。

正反馈循环是这样的:

  1. 指针停在底部边带,CM6 在 measure 阶段调用 scrollRectIntoView,按"底距留白"把视图往下滚一屏(约 +3000px/次);
  2. 视图一滚,指针正下方变成更靠后的某一行;
  3. 选区头(head)被指针位置带着前进约 8 行;
  4. 下一帧 measure 又发现 head 在底带 → 再次 scrollRectIntoView。

增益≈1,永不衰减,所以会以帧率一路跑飞,直到文档末尾;末尾那次 measure 把视图拽回锚点(−17287px),再重来。

旁证:整个过程 contentHeight 恒定(spread 0),offsetHeight/clientHeight = 1,可以排除"高度表失效"或"缩放补偿系数错"这类猜想。

三、修复用的关键护栏代码

思路:不去跟 CM6 的 measure 较劲,只在拖选进行中、只对编辑器滚动容器 .cm-scroller 的 scrollTop 写入做约束。正常点击、正常滚动、拖选中滚轮都不受影响。

js 复制代码
// 在编辑器页面里,拖选进行中给 scrollTop 写入加一道护栏
(function () {
  'use strict';
  if (window.__zcDragGuardInstalled) return;
  window.__zcDragGuardInstalled = 1;

  var S = window.__zcDragGuard = {
    on: true, budget: 24, freezeInside: true,
    active: false, dir: 0, lastY: 0, ptrY: 0,
    rejected: 0, clamped: 0, frozen: 0, passed: 0, wheelUntil: 0
  };
  var bucket = 0, used = 0;

  var d = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop');
  if (!d || !d.set || !d.get) return;
  function now() {
    return (window.performance && performance.now) ? performance.now() : Date.now();
  }

  Object.defineProperty(Element.prototype, 'scrollTop', {
    configurable: true, enumerable: d.enumerable, get: d.get,
    set: function (x) {
      // 非拖选 / 非编辑器容器 / 滚轮旁路:原样放行
      if (!S.on || !S.active || !this.classList || !this.classList.contains('cm-scroller'))
        return d.set.call(this, x);
      if (now() < S.wheelUntil) { S.passed++; return d.set.call(this, x); }

      var before = d.get.call(this);
      var delta = x - before;
      if (!delta) return;

      // 1) 反向滚动直接丢弃(消灭跑到末尾后的 -17287 回弹)
      if (S.dir > 0 && delta < -4) { S.rejected++; return; }
      if (S.dir < 0 && delta > 4) { S.rejected++; return; }

      // 2) 指针已在视口内(离上下边 >24px)时完全冻结:
      //    这时 head 就在指针下,没有要揭示的内容,这里滚动只会让文字从指针下滑走
      if (S.freezeInside) {
        var r = this.getBoundingClientRect();
        var band = 24;
        if (S.ptrY >= r.top + band && S.ptrY <= r.bottom - band) { S.frozen++; return; }
      }

      // 3) 每帧预算:其余滚动按 24px/帧(≈1400px/s,正常自动滚速度)截断
      var t = now();
      if (t - bucket > 16) { bucket = t; used = 0; }
      var room = S.budget - used;
      if (room < 0) room = 0;
      var mag = delta < 0 ? -delta : delta;
      if (mag > room) { mag = room; S.clamped++; }
      if (!mag) return;
      used += mag;
      d.set.call(this, delta < 0 ? before - mag : before + mag);
    }
  });

  // 拖选开始:记录方向、指针位置;松手/失焦:解除
  document.addEventListener('pointerdown', function (e) {
    var t = e.target;
    if (!t || !t.closest || !t.closest('.cm-content')) return;
    S.active = true; S.dir = 0; S.lastY = S.ptrY = e.clientY;
    bucket = 0; used = 0;
  }, true);

  document.addEventListener('pointermove', function (e) {
    if (!S.active) return;
    if (e.buttons === 0) { S.active = false; return; }
    S.ptrY = e.clientY;
    if (e.clientY > S.lastY + 1) S.dir = 1;
    else if (e.clientY < S.lastY - 1) S.dir = -1;
    S.lastY = e.clientY;
  }, true);

  function release() { S.active = false; }
  document.addEventListener('pointerup', release, true);
  document.addEventListener('pointercancel', release, true);
  window.addEventListener('blur', release, true);

  // 拖选中滚轮永远可用(这才是延长选区的正道),180ms 内不受护栏约束
  document.addEventListener('wheel', function () { S.wheelUntil = now() + 180; },
    { capture: true, passive: true });
})();

(上面省略了可配置的 localStorage 读取部分,需要的话可以加一个开关项。)

实测同一 44 步手势,开启护栏后:

  • scrollTop 写入 316 → 38 次,跳变 >120px 10 → 0 ,最大 −17287 → 0;
  • 选区头与指针偏差 8 行 → 0 行;
  • 编辑器 DOM 变更 6158 → 147 次(顺带大幅减少重排)。
  • 点选落点、拖选锚点、拖选中滚轮、松手后正常滚动等日常交互回归全部正常。

四、可行性与取舍(供评估)

  • 是否可行:可行。这段是纯浏览器侧的"结果约束",不侵入 CM6 内部状态,不改动选区逻辑,风险面很小,适合作为临时修复或灰度开关。
  • 一个有意取舍 :护栏不再提供"指针停在底边持续自动滚"------那个循环正是跑飞本体。要选到视口外,保留两种手段:①把指针拖到编辑器边带的"自动滚热区"继续移动;②拖选中用滚轮延长选区(已被旁路放行)。我选了"可预测 > 快"。
  • 更干净的原生方案(建议长期做) :CM6 本身提供了 EditorView.scrollMargins / 自定义 scrollIntoView 行为,可在拖选时把触底自动滚的留白调小、或拖选期间临时降低 scrollMargin,从根源上让 scrollRectIntoView 不每帧触发。护栏代码只是先把现象压住并定位到 measure 阶段,便于在 CM6 配置层做根治。
  • 开关建议 :挂一个偏好项(如 dragGuard,默认开),值为 off 即恢复原生行为,便于 A/B 对比。

如果方便,我可以把护栏封装成一个可开关的偏好,或者直接给一版基于 EditorView.scrollMargins 的 CM6 原生修复。

相关推荐
stereohomology8 天前
外挂可能是用AI工具自定义solomd插件和主题适合的方式
人工智能·markdown·薅羊毛·solomd·外挂新特性
唐诺4 个月前
OC中协议、分类、扩展:核心对比与总结
extension·protocol·category
Sylvan Ding1 年前
VSCode插件推荐 2025 - 拥抱 Agentic Coding 时代:是时候从 PyCharm 切换到 VSCode 生态了!
ide·vscode·pycharm·extension·插件·agentic·氛围编程
九河_1 年前
服务器在线安装插件失败/离线安装VSIX
vscode·extension·pylance
螺旋小蜗2 年前
Maven工具学习使用(十二)——extension和depency的区别
学习·extension·depency
00圈圈2 年前
Swift 创建扩展(Extension)
ios·swift·extension
命运之手2 年前
【Android】根据URI获取文件扩展名或MimeType
android·extension·uri·mimetype
oscar9992 年前
如何获取VS Code扩展的版本更新信息
version·extension·vs code
oscar9992 年前
VS Code扩展开发介绍和快速示例
开发·extension·vs code·扩展