Uninote
Uninote
用户根目录
brdr
common
programming
docs
后端试题
问题讨论

可以。下面给你一版修正后的、可直接给产品/开发使用的 VS Code 风格 Preview Editor 产品文档。这版严格区分:

  • Preview Editor
  • Normal Editor
  • Pinned Editor
  • Dirty 状态

并且把 Keep OpenPin 分开处理。

Preview Editor 产品设计文档

1. 功能名称

Preview Editor

中文建议统一使用:

预览编辑器

相关概念:

Preview Editor
Normal Editor
Pinned Editor
Active Editor
Editor Group

需要特别说明:

Preview 和 Pinned 不是同一维度的状态。


2. 产品目标

用户在文件树中浏览文件时,如果每次单击文件都创建一个永久 Tab,会很快产生大量不需要长期保留的 Tab。

Preview Editor 的目标是:

区分“浏览文件”和“真正打开文件”。

用户只是浏览:

Single Click
↓
Preview

用户明确要继续使用:

Double Click
Edit
Keep Open
↓
Normal

用户明确希望锁定:

Pin
↓
Pinned

最终形成三类 Tab:

Preview
Normal
Pinned

3. 核心概念

3.1 Preview Editor

Preview 是临时打开状态。

特点:

  • 同一个 Editor Group 中最多一个 Preview
  • 可以被下一个 Preview 替换
  • 默认用于文件树单击、Quick Open 等弱打开行为
  • 不应该处于 Dirty 状态
  • 不应该处于 Pinned 状态

例如:

用户依次单击:

README.md
package.json
src/index.ts

最终 Tab 不是:

README.md | package.json | index.ts

而是:

index.ts

因为 Preview Tab 被复用。


3.2 Normal Editor

Normal 是普通已保留 Tab。

特点:

  • 不会被后续 Preview 替换
  • 会参与普通批量关闭操作
  • 可以 Dirty
  • 可以进一步 Pin

例如:

README.md | editor.ts | package.json

这些都是 Normal。


3.3 Pinned Editor

Pinned 是“锁定/固定”状态。

它的语义是:

在执行批量关闭类操作时受到保护。

所有 Pinned Editor 构成标签栏最左侧的连续区域。新固定的标签移动到该区域末尾;取消固定后移动到未固定区域开头。拖拽只调整标签在所属区域内的顺序,不能让固定与未固定标签交错。恢复旧会话时也按此规则稳定归组,同时保留两组各自的原始顺序。

例如:

A | B | C
    ↑ pinned

执行:

Close All

如果产品定义 Close All 忽略 pinned,则结果是:

B

Pinned 和 Preview 不能混为一谈。


4. 状态维度

建议不要把 Tab 设计成单一三态枚举,而是分成两个维度。

生命周期维度

Preview
Normal

锁定维度

Unpinned
Pinned

因此合法组合:

Preview + Unpinned
Normal + Unpinned
Normal + Pinned

非法组合:

Preview + Pinned

因为一个会被 Preview 替换的临时 Tab,不应该同时又是锁定 Tab。


5. 推荐数据模型

interface EditorTab {
  id: string

  resource: URI

  groupId: string

  isPreview: boolean
  isPinned: boolean

  dirty: boolean
  active: boolean
}

必须维护以下约束:

if (tab.isPreview) {
  tab.isPinned = false
}

以及:

if (tab.dirty) {
  tab.isPreview = false
}

6. 状态转换

完整状态关系:

              Single Click
                   │
                   ▼
               Preview
              /   |    \
             /    |     \
        Edit   Double   Keep Open
           \      |      /
            \     |     /
               Normal
                  │
                 Pin
                  │
                  ▼
                Pinned
                  │
                Unpin
                  │
                  ▼
                Normal

注意:

Preview → Normal

和:

Normal → Pinned

是两种完全不同的动作。


7. 用户操作映射

建议定义:

用户操作 结果
文件树单击 Preview
文件树双击 Normal
Quick Open 预览 Preview
Quick Open 明确打开 Normal
开始编辑 Preview Normal
双击 Preview Tab Normal
Keep Open Preview → Normal
Pin Normal → Pinned
Unpin Pinned → Normal
Drag Preview Tab Normal
Move Preview Tab Normal

8. Keep Open

Keep Open 的含义:

将当前 Preview Editor 转为普通持久 Tab。

状态:

Preview
↓
Keep Open
↓
Normal

它不会:

Normal
↓
Pinned

因此 Keep Open 和 Pin 必须是两个独立命令。

建议命令:

editor.keepOpen

9. Pin

Pin 的含义:

锁定当前普通 Tab,使其在批量关闭操作中受到保护。

状态:

Normal
↓
Pin
↓
Pinned

建议命令:

editor.pin

解除:

editor.unpin

10. 文件树交互

单击

Explorer Single Click
↓
Preview

例如:

A | B | C
        ↑ preview

再单击 D:

A | B | D
        ↑ preview

C 被替换。


双击

Explorer Double Click
↓
Normal

例如:

第一次单击:

A | B
    ↑ Preview

双击后:

A | B
    ↑ Normal

之后再单击 C:

A | B | C
            ↑ Preview

B 不会被替换。


11. 双击 Tab

Preview Tab 双击:

Preview
↓
Normal

本质上就是:

Keep Open

不应该自动变为 Pinned。


12. 编辑 Preview

如果用户在 Preview 中开始输入:

Preview
↓
dirty

应立即执行:

Preview → Normal

然后:

Normal + Dirty

因此系统应维护:

dirty => !isPreview

禁止出现:

Preview + Dirty

13. Preview 替换规则

当前:

A | B | C
        ↑ Preview

用户 Preview D:

A | B | D
        ↑ Preview

要求:

  • C 被替换
  • D 使用 C 原来的 Tab index
  • 不新增 Tab
  • 不影响 A、B

14. Preview Slot

建议 Editor Group 明确维护:

interface EditorGroup {
  editors: EditorTab[]

  activeEditorId?: string
  previewEditorId?: string
}

一个 Editor Group 最多:

1 个 Preview

15. 为什么 Preview 属于 Editor Group

假设有分屏:

Group A              Group B

A | B                C | D
    Preview              Preview

两个 Group 可以各有自己的 Preview。

因此应该是:

group.previewEditorId

而不是:

workspace.previewEditorId

16. 已存在文件再次 Preview

例如:

A | B | C
    ↑ Normal

用户单击 B。

此时:

activate B

不能重新创建一个 Preview B。

也不能:

Normal → Preview

规则:

已经是 Normal / Pinned 的 Editor,弱打开操作不能让它降级。


17. Pinned 文件再次打开

例如:

A | B | C
    ↑ Pinned

用户单击 B:

activate B

B 保持:

Pinned

18. Preview 文件再次单击

当前:

A | B
    ↑ Preview

再次点击 B:

仅 activate

不创建新实例。


19. Close 行为

这里必须严格区分。

Close 当前 Tab

用户明确关闭当前 Tab:

Close

建议:

  • Preview:直接关闭
  • Normal:直接关闭
  • Pinned:第一次关闭仅提示保护状态;在 5 秒提示期内再次按关闭快捷键或点击关闭按钮后关闭

Pin 仍主要保护批量操作。显式关闭增加一次轻量确认,避免误关重要标签,同时不弹出阻塞式对话框;关闭快捷键和标签关闭按钮共享同一次确认状态。Force Close All 保持一次生效。


20. Close All

建议产品定义:

Close All

只关闭:

Preview
Normal

保留:

Pinned

例如:

A | B | C | D
    ↑ pinned
            ↑ pinned

执行 Close All:

B | D

21. Close Others

当前:

A | B | C | D
    ↑ pinned
        ↑ current
            ↑ pinned

执行:

Close Others

结果:

B | C | D

因为:

  • 当前 C 保留
  • Pinned B、D 保留
  • 其他关闭

22. Close to the Right

建议默认:

关闭右侧所有未 pinned Tab。

例如:

A | B | C | D | E
        ↑ current
            ↑ pinned

执行:

Close to the Right

结果:

A | B | C | D

E 被关闭,D 保留。


23. Force Close All

如果确实需要彻底关闭所有 Tab,可以提供单独命令:

Close All Editors

或:

Force Close All

这个命令可以包含 Pinned。

但建议它不要和普通:

Close All

混在一起。


24. Pinned 的 UI 表现

Preview:

  • 文件名斜体

Normal:

  • 普通文件名

Pinned:

  • 普通文件名
  • 可增加 Pin 图标

例如:

README.md | editor.ts 📌 | package.json

是否显示 Pin 图标可根据 UI 密度决定。


25. Tab 状态矩阵

状态 标题 Dirty 可被 Preview 替换 批量关闭
Preview Italic
Normal Normal
Pinned Normal + Pin

26. Dirty 与 Pin 是独立状态

例如:

Pinned + Dirty

是合法的。

Normal + Dirty

也是合法的。

只有:

Preview + Dirty

非法。


27. 推荐数据模型升级

如果未来状态更多,可以明确分为:

interface EditorTab {
  id: string
  resource: URI

  openMode:
    | 'preview'
    | 'normal'

  pinned: boolean

  dirty: boolean
  active: boolean
}

这样比:

state: 'preview' | 'normal' | 'pinned'

更准确。

因为 Pinned 并不是与 Preview/Normal 完全同级的一维状态。


28. Editor Open Intent

建议所有模块只表达打开意图。

type EditorOpenIntent =
  | 'preview'
  | 'normal'
  | 'preserve'

例如:

editorService.open(uri, {
  intent: 'preview'
})

或:

editorService.open(uri, {
  intent: 'normal'
})

不要让 Explorer 直接操作:

isPreview

29. 各入口默认 Open Intent

建议:

来源 Intent
Explorer single click preview
Explorer double click normal
Quick Open browse preview
Quick Open confirm normal / configurable
Open File normal
Open Recent normal
Restore Workspace preserve
Go to Definition preview / configurable
Search Result single click preview
Search Result double click normal

30. EditorService

建议统一入口:

editorService.open(resource, {
  intent: 'preview'
})

Preview 转 Normal:

editorService.keepOpen(editor)

Pin:

editorService.pin(editor)

Unpin:

editorService.unpin(editor)

31. Keep Open 实现

function keepOpen(editor: EditorTab) {
  if (!editor.isPreview) return

  editor.isPreview = false

  const group = getGroup(editor.groupId)

  if (group.previewEditorId === editor.id) {
    group.previewEditorId = undefined
  }
}

32. Pin 实现

function pin(editor: EditorTab) {
  if (editor.isPreview) {
    keepOpen(editor)
  }

  editor.isPinned = true
  editor.group.moveToEndOfPinnedRegion(editor)
}

这里建议:

如果用户直接对 Preview 执行 Pin:

Preview
↓
Normal
↓
Pinned

也就是 Pin 可以隐式先 Keep Open。


33. Unpin

function unpin(editor: EditorTab) {
  editor.isPinned = false
  editor.group.moveToStartOfUnpinnedRegion(editor)
}

结果:

Pinned → Normal

不会:

Pinned → Preview

34. Preview 打开算法

function openPreview(resource: URI) {
  const group = getActiveGroup()

  const existing = group.findByResource(resource)

  if (existing) {
    group.activate(existing)
    return existing
  }

  const preview = group.previewEditor

  if (preview) {
    return group.replaceEditor(preview, resource, {
      isPreview: true
    })
  }

  return group.openEditor(resource, {
    isPreview: true
  })
}

35. Normal 打开算法

function openNormal(resource: URI) {
  const group = getActiveGroup()

  const existing = group.findByResource(resource)

  if (existing) {
    if (existing.isPreview) {
      keepOpen(existing)
    }

    group.activate(existing)

    return existing
  }

  return group.openEditor(resource, {
    isPreview: false
  })
}

36. Preview Slot 保持位置

例如:

A | Preview C | B

Preview D:

A | Preview D | B

不要变成:

A | B | Preview D

否则 Tab 会一直跳动。


37. Drag Preview Tab

推荐:

Drag Preview
↓
Keep Open
↓
Normal

原因:

用户主动拖动 Tab,已经表达了明确保留意图。

不自动 Pin。


38. Move Editor

执行:

Move Left
Move Right
Move to Group

如果当前是 Preview:

Preview → Normal

不应该自动:

Normal → Pinned

39. Quick Open

Quick Open 可以分两种动作。

浏览候选:

ArrowDown
↓
Preview

明确选择:

Enter

这里产品可以有两种策略。

VS Code 风格偏浏览

Enter → Preview

更明确的产品语义

Enter → Normal

建议你根据产品定位决定。

如果是 Markdown / 文件浏览型产品,我更推荐:

Arrow navigation → Preview
Enter → Normal

用户认知更明确。


40. Quick Open 取消

用户:

Ctrl+P
↓
浏览多个 Preview
↓
Esc

推荐:

恢复打开 Quick Open 之前的:

active editor

以及必要的:

preview state

避免最后一个浏览结果被意外留下。


41. Navigation History

Preview 被替换,不应该意味着历史被删除。

例如:

A
↓
Preview B
↓
Preview C
↓
Preview D

Alt+Left:

D → C → B → A

因此:

Editor Tabs

和:

Navigation History

应该是两套独立模型。


42. View State

即使 Preview 已经被替换,也建议保存:

cursor
selection
scrollTop
folding

例如:

interface EditorViewState {
  resource: URI

  cursor?: Position
  selection?: Selection

  scrollTop?: number
}

历史返回时可以恢复。


43. Split Editor

每个 Group 独立维护:

previewEditorId

例如:

Group A                  Group B

A | Preview B            C | Preview D

A 中打开 Preview E:

A | Preview E            C | Preview D

Group B 不受影响。


44. Context Keys

建议和快捷键系统打通:

activeEditorIsPreview
activeEditorIsPinned
activeEditorIsDirty

editorGroupHasPreview

例如:

{
  "command": "editor.keepOpen",
  "when": "activeEditorIsPreview"
}
{
  "command": "editor.unpin",
  "when": "activeEditorIsPinned"
}

45. Command 设计

建议至少:

editor.keepOpen

editor.pin
editor.unpin

editor.close
editor.closeOthers
editor.closeToRight
editor.closeAll
editor.forceCloseAll

46. Tab 右键菜单

Preview:

Keep Open
Pin
Close
Close Others
Close to the Right
Close All

如果用户直接 Pin:

Preview → Normal → Pinned

Normal:

Pin
Close
Close Others
Close to the Right
Close All

Pinned:

Unpin
Close
Close Others
Close to the Right
Close All

47. 设置项

建议:

workbench.editor.enablePreview

默认:

true

再细分:

workbench.editor.enablePreviewFromExplorer
workbench.editor.enablePreviewFromQuickOpen
workbench.editor.enablePreviewFromSearch

默认都可以是:

true

48. Preview Disabled

如果:

enablePreview = false

则:

Explorer Single Click

直接:

Normal

不会创建 Preview。

Pinned 机制不受影响。


49. 推荐架构

Explorer
Quick Open
Search
Outline
References
    │
    │ open intent
    ▼
EditorService
    │
    ├── openPreview()
    ├── openNormal()
    ├── keepOpen()
    ├── pin()
    └── unpin()
    │
    ▼
EditorGroupService
    │
    ├── editors[]
    ├── activeEditorId
    └── previewEditorId

50. 模块拆分

建议:

editor/
├── editor-service.ts
├── editor-group-service.ts
├── editor-tab-model.ts
├── editor-input.ts
│
├── preview-controller.ts
├── pin-controller.ts
│
├── editor-history-service.ts
├── editor-view-state-service.ts
│
└── editor-settings.ts

Preview 和 Pin 建议逻辑上分开:

preview-controller
pin-controller

避免以后重新混在一起。


51. 关键 Invariant

建议直接写进开发规范。

Invariant 1

一个 Editor Group 最多:

1 Preview Editor

Invariant 2

Preview => !Pinned

Invariant 3

Dirty => !Preview

Invariant 4

Normal / Pinned Editor 不会因为弱打开降级成 Preview。

Invariant 5

Preview replacement 保持原 Tab index。

Invariant 6

Pin 不意味着不能手动 Close。

Pin 主要保护:

批量关闭

Invariant 7

Pinned Editor 必须连续排列在 Editor Group 最左侧;任何固定、取消固定、拖拽或会话恢复操作都不能让 Pinned 与 Unpinned 交错。


52. 状态真值表

Preview Pinned Dirty 合法
1 0 0
1 0 1
1 1 0
1 1 1
0 0 0
0 0 1
0 1 0
0 1 1

开发时非常适合直接写成 assertion。


53. MVP

P0

建议先做:

  • Explorer 单击 → Preview
  • Double Click → Normal
  • 一个 Group 一个 Preview
  • Preview replacement
  • Preview Tab 斜体
  • Edit → Preview 自动转 Normal
  • Double-click Tab → Normal
  • Keep Open
  • Preview slot 保持位置
  • Preview setting

P1

增加:

  • Pin / Unpin
  • Close All 跳过 Pinned
  • Close Others 跳过 Pinned
  • Close to Right 跳过 Pinned
  • Pinned UI
  • Context Key
  • Shortcut
  • Split Group Preview

P2

再增加:

  • Quick Open Preview
  • Search Preview
  • Go to Definition Preview
  • Navigation History
  • View State restore
  • Workspace restore
  • Extension API

54. 最终推荐模型

开发层最终只需要牢记:

Preview
=
临时打开生命周期

Normal
=
普通持久打开生命周期

Pinned
=
批量关闭保护状态

不要设计成:

Preview → Pinned

正确模型是:

Preview
   ↓ Keep Open / Edit / Double Click
Normal
   ↓ Pin
Pinned

以及:

Pinned
   ↓ Unpin
Normal

如果从数据建模角度表达,则最准确的是:

type OpenMode =
  | 'preview'
  | 'normal'

interface EditorTab {
  openMode: OpenMode
  pinned: boolean
  dirty: boolean
}

这比把 preview / normal / pinned 做成一个单独 enum 更符合 VS Code 这类编辑器的真实产品语义,也更适合后续继续扩展 Editor Group、Close 行为、Workspace Restore 和快捷键系统。

tech-arch

测试

点赞(0) 阅读(4) 举报
目录
标题