Skip to content

feat(code-editor): improve definition navigation - #3468

Open
qingqing-ux wants to merge 1 commit into
goplus:uifrom
qingqing-ux:issue-3455
Open

qingqing-ux wants to merge 1 commit into
goplus:uifrom
qingqing-ux:issue-3455

Conversation

@qingqing-ux

@qingqing-ux qingqing-ux commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

改动说明

优化代码定义的查看与跳转体验,让用户能够明确区分:

  • 临时查看定义;
  • 跳转到定义所在文档;
  • 返回跳转前的代码位置。

本次改动包括:

  • 在悬浮卡片中分别提供「查看定义」和「跳转到定义」;
  • 新增只读的内嵌定义预览面板;
  • 跳转后显示明确的来源位置和当前位置;
  • 返回时恢复原文档、光标、选区和滚动位置;
  • 移除右侧临时文档列表及「全部关闭」;
  • 保留原有缩放控制;
  • 支持中英文界面文案。

Fixes #3455

设计考量

1. 区分「查看定义」与「跳转到定义」

悬浮卡片提供两个独立入口:

  • 查看定义:在当前代码下方打开只读 Peek,不离开来源文档;
  • 跳转到定义:在主编辑器中打开定义所在文档。

用户可以根据当前目的选择临时阅读,或进入目标文档继续查看和编辑。

Peek 保持只读,因为它是临时阅读界面。代码编辑仍在主编辑器中完成,避免在临时面板内引入保存和二次确认等额外状态。

2. 明确表达「从哪里来」和「现在在哪里」

执行「跳转到定义」后,编辑器顶部显示导航条:

  • 左侧 返回 Board · 第 4 行:说明跳转来源,同时提供返回入口;
  • 右侧 当前位置:Stage:说明主编辑器当前打开的文档。

返回时恢复来源文档以及跳转前的 Monaco 编辑器视图状态,包括光标、选区和滚动位置。

连续执行多次定义跳转时,来源位置按栈保存,用户可以依次返回上一处代码位置。

3. 移除临时文档管理心智

原来的临时文档列表同时承担代码导航和文档管理职责,存在以下问题:

  • 缩略图无法清楚表达具体代码位置;
  • 「全部关闭」表达的是文档管理,而不是返回;
  • 用户难以理解跳转前后的位置关系。

因此移除右侧临时文档列表及「全部关闭」,使用编辑器顶部的返回入口承接代码导航。右侧只保留原有缩放控制。

4. 让状态反馈贴近代码上下文

导航条属于整个编辑器的导航状态,因此横跨代码区和右侧工具栏上方,并保留对称的左右间距。

定义 Peek 显示在来源代码下方,并对 Monaco 编辑器边栏进行精简:

  • 关闭折叠栏和不需要的装饰栏;
  • 保留行号;
  • 保留行号与代码之间的必要间距;
  • 避免出现过大的左侧空白。

5. 中英文支持

所有新增文案均使用项目现有的国际化机制。

场景 English 中文
悬浮卡片预览入口 View definition 查看定义
悬浮卡片跳转入口 Go to definition 跳转到定义
Peek 标题 Definition in Stage · Line 39 定义位于 Stage · 第 39 行
返回入口 Back to Board · Line 4 返回 Board · 第 4 行
当前位置 Current: Stage 当前位置:Stage

文档名称优先使用项目已有的本地化显示名称;用户自定义名称保持不变。

验收路径

使用 PR 前端预览中的 Match3 项目,进入 Board 精灵的代码页面。

场景一:不离开来源文档查看定义

  1. 悬浮 Board 第 4 行的 getRowCol
  2. 确认悬浮卡片中同时显示「查看定义」和「跳转到定义」;
  3. 点击「查看定义」;
  4. 确认来源代码下方打开只读 Peek;
  5. 确认标题显示:
    • 英文:Definition in Stage · Line 39
    • 中文:定义位于 Stage · 第 39 行
  6. 确认主编辑器仍然停留在 Board
  7. 分别验证点击关闭按钮和按下 Escape;
  8. 确认关闭后仍停留在原来的代码位置。

场景二:跳转到定义并返回

  1. 再次悬浮 Board 第 4 行的 getRowCol
  2. 点击「跳转到定义」;
  3. 确认主编辑器打开 Stage 中的定义;
  4. 确认顶部导航条显示:
    • 左侧:返回 Board · 第 4 行
    • 右侧:当前位置:Stage
  5. 确认右侧不再显示临时文档列表或「全部关闭」;
  6. 确认缩放控制仍然可用;
  7. 点击 返回 Board · 第 4 行
  8. 确认编辑器返回 Board
  9. 确认跳转前的光标、选区和滚动位置得到恢复。

场景三:连续跳转和逐级返回

  1. 从来源代码跳转到第一个定义;
  2. 在目标文档中继续跳转到另一个定义;
  3. 连续点击返回入口;
  4. 确认每次都返回到上一个文档及对应的编辑器视图状态。

场景四:中文界面

  1. 登录后将产品语言切换为中文;
  2. 重新执行查看定义和跳转定义流程;
  3. 确认悬浮卡片操作、Peek 标题、返回入口和当前位置均显示中文;
  4. 确认项目文档名称仍使用其配置的显示名称。

场景五:布局检查

  1. 确认顶部导航条左右边距一致;
  2. 确认导航条横跨代码区与右侧工具栏上方;
  3. 确认 Peek 行号与代码之间保留合理间距;
  4. 确认 Peek 左侧没有过大的空白区域;
  5. 确认不同代码缩放比例下布局仍然正常。

验证结果

  • pnpm exec vitest --run src/components/xgo-code-editor
    • 3 个测试文件通过
    • 7 项测试通过
  • pnpm run lint
  • pnpm run type-check
  • pnpm run build

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 003a7c199f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

closeTempTextDocuments() {
this.tempTextDocumentIds.splice(0)
if (!this.isDisposed) this.setActiveTextDocument(this.mainTextDocumentId)
private goToDefinition(target: TextDocumentPosition | TextDocumentRange, source?: TextDocumentPosition) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve return navigation for direct document opens

The navigation stack is populated only by this private definition-specific path, but cross-document links in components/editor/spx-code-editor/CodeLink.vue:52-66 still call the public ui.open(...) directly. Since this commit also removes DocumentTabs, following a Console or Copilot code link to another document leaves no tab or back action for returning to the original document and view state; direct cross-document opens should participate in the new navigation mechanism as well.

Useful? React with 👍 / 👎.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

This PR replaces the multi-tab "temporary document" navigation with an inline definition peek preview plus a back-navigation stack. The design is clean: removed members (tempTextDocuments, mainTextDocument, closeTempTextDocuments, the DocumentTab/DocumentTabs components, and the now-unused textDocumentIdEq import) are fully cleaned up, hover.ts's getGoToDefinitionActiongetDefinitionActions rename is consistent, and the new behavior is well covered by hover.test.ts and code-editor-ui.test.ts.

Findings below are minor/maintainability. No security issues were found (interpolated URIs go through setAttribute, and the shared Monaco model is correctly view-only via readOnly).

A couple of low-confidence notes not attached inline:

  • navigationStack is unbounded — every goToDefinition pushes an entry (with a full Monaco view state) and only goBack pops. In a long session with many jumps and little back-navigation this grows without bound. A max-depth cap (drop oldest) would bound it.
  • docs/develop/code-editor/base.ts still documents builtInCommandGoToDefinition with a single [TextDocumentPosition] arg (and spx.* prefix), and does not mention the new xgo.viewDefinition. The doc already predated the xgo.* implementation, so this is likely out of scope — flagging only if that doc is meant to track this contract.

const previous = this.navigationStack.pop()
if (previous == null) return
this.setActiveTextDocument(previous.textDocument.id)
if (previous.viewState != null) this.editor.restoreViewState(previous.viewState)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] goBack restores view state twice (redundant)

goBack calls setActiveTextDocument(previous.textDocument.id), which already restores that document's saved view state from viewStateMap. It then restores again from previous.viewState. Both values are the same source-document view state, so the second restoreViewState is redundant — and the same view state is persisted twice on every goToDefinition (once in the navigation stack, once in the WeakMap). Consider a single source of truth: either let setActiveTextDocument own view-state restoration (and drop viewState from CodeNavigationEntry), or bypass the map here. Also note: if saveViewState() ever returns null, goBack restores nothing, so the cursor won't return to the captured position (which is otherwise only used for the back-bar label). Falling back to open(previous.textDocument.id, previous.position) when viewState == null would keep label and restored location consistent.

/>
<DefinitionPeek
v-if="uiRef.definitionPeek != null"
:key="`${uiRef.definitionPeek.textDocument.id.uri}:${uiRef.definitionPeek.range.start.line}:${uiRef.definitionPeek.range.start.column}`"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] DefinitionPeek :key omits range.end

DefinitionPeek applies model/selection/reveal only once in handleEditorInit; it does not watch textDocument/range, so it only updates when the :key forces a full remount. The key is uri:range.start.line:range.start.column and omits range.end. Two "view definition" actions with the same start but a different end range would reuse the same editor instance and show a stale selection. Low likelihood, but either include the full range in the key or add a watch on the props so the component is self-consistent regardless of keying. (Note: the full remount also tears down and recreates the Monaco editor instance on every peek-target change, which is one of the heavier editor operations — a stable instance updated via setModel/setSelection/revealRange would avoid that cost.)

This branch was successfully deployed

1 active deployment
Preview – builder 003a7c19 Deployed Aug 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

设计清晰的「查看定义」返回体验

1 participant