feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
# Diff Integration Guide
|
||||
|
||||
This guide focuses on integrating the `stream-monaco` diff editor, which options to pass, and which TypeScript types are available out of the box.
|
||||
|
||||
## 1. Minimal setup
|
||||
|
||||
```ts
|
||||
import { useMonaco } from 'stream-monaco'
|
||||
|
||||
const container = document.getElementById('diff')!
|
||||
|
||||
const { createDiffEditor, updateDiff, cleanupEditor } = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript', 'javascript', 'json'],
|
||||
readOnly: true,
|
||||
MAX_HEIGHT: 560,
|
||||
})
|
||||
|
||||
await createDiffEditor(
|
||||
container,
|
||||
'export const a = 1\n',
|
||||
'export const a = 2\n',
|
||||
'typescript',
|
||||
)
|
||||
|
||||
updateDiff('export const a = 1\n', 'export const a = 3\n', 'typescript')
|
||||
|
||||
cleanupEditor()
|
||||
```
|
||||
|
||||
The mount/update/unmount pattern is the same in Vue, React, Svelte, Solid, and vanilla JS:
|
||||
|
||||
- call `createDiffEditor(...)` on mount
|
||||
- call `updateDiff(...)` / `updateOriginal(...)` / `updateModified(...)` when content changes
|
||||
- call `cleanupEditor()` on unmount
|
||||
|
||||
## 2. Which options to pass
|
||||
|
||||
All options are passed through `useMonaco(options)`.
|
||||
|
||||
### Core options
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript', 'javascript'],
|
||||
theme: 'github-light',
|
||||
readOnly: true,
|
||||
MAX_HEIGHT: 560,
|
||||
})
|
||||
```
|
||||
|
||||
Most common:
|
||||
|
||||
- `themes`: available themes, ideally one dark and one light
|
||||
- `languages`: languages to register
|
||||
- `theme`: initial theme
|
||||
- `readOnly`: usually `true` for review/preview diffs
|
||||
- `MAX_HEIGHT`: max editor height
|
||||
|
||||
### stream-monaco diff UX options
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript'],
|
||||
readOnly: true,
|
||||
|
||||
diffHideUnchangedRegions: {
|
||||
enabled: true,
|
||||
contextLineCount: 3,
|
||||
minimumLineCount: 3,
|
||||
revealLineCount: 5,
|
||||
},
|
||||
diffLineStyle: 'background',
|
||||
diffAppearance: 'auto',
|
||||
diffUnchangedRegionStyle: 'line-info',
|
||||
diffHunkActionsOnHover: true,
|
||||
diffHunkHoverHideDelayMs: 160,
|
||||
diffUpdateThrottleMs: 50,
|
||||
revealDebounceMs: 75,
|
||||
})
|
||||
```
|
||||
|
||||
What they do:
|
||||
|
||||
- `diffHideUnchangedRegions`
|
||||
Controls unchanged-region folding. Accepts `true`, `false`, or Monaco's native `hideUnchangedRegions` object.
|
||||
- `diffLineStyle`
|
||||
Visual emphasis for changed lines.
|
||||
- `background`: richer filled blocks
|
||||
- `bar`: subtler review-style leading bars
|
||||
- `diffAppearance`
|
||||
Controls diff chrome appearance.
|
||||
- `auto`: infer from active Monaco theme
|
||||
- `light`: force light shell
|
||||
- `dark`: force dark shell
|
||||
- `diffUnchangedRegionStyle`
|
||||
Controls collapsed unchanged-region rendering.
|
||||
- `line-info`: `71 unmodified lines` with line-number-width reveal buttons in a compact 32px collapsed row
|
||||
- `line-info-basic`: `71 unmodified lines` with the wider legacy reveal rail in a compact 32px row
|
||||
- `metadata`: `@@ -59,9 +59,11 @@` in a compact 32px row; lines below the collapsed region reflow to close the extra whitespace
|
||||
- `simple`: gray placeholder bar in a tighter 28px row
|
||||
- `diffHunkActionsOnHover`
|
||||
Enables `Revert / Stage` on hovered hunks
|
||||
- `diffHunkHoverHideDelayMs`
|
||||
Hide delay for hunk hover actions
|
||||
- `diffUpdateThrottleMs`
|
||||
Diff streaming throttle. Default is `50`.
|
||||
When `diffHideUnchangedRegions` is enabled, unchanged-region folding is deferred until streaming goes idle so the diff does not collapse while content is still streaming in.
|
||||
- `revealDebounceMs`
|
||||
Debounce for reveal/auto-scroll behavior
|
||||
|
||||
### Native Monaco diff options
|
||||
|
||||
`MonacoOptions` now includes both:
|
||||
|
||||
- `monaco.editor.IStandaloneEditorConstructionOptions`
|
||||
- `monaco.editor.IDiffEditorConstructionOptions`
|
||||
|
||||
So you can also pass native Monaco diff options directly:
|
||||
|
||||
```ts
|
||||
useMonaco({
|
||||
renderSideBySide: true,
|
||||
enableSplitViewResizing: true,
|
||||
ignoreTrimWhitespace: false,
|
||||
originalEditable: false,
|
||||
})
|
||||
```
|
||||
|
||||
To switch between side-by-side and inline at runtime:
|
||||
|
||||
```ts
|
||||
const diff = monaco.getDiffEditorView()
|
||||
diff?.updateOptions({ renderSideBySide: false })
|
||||
```
|
||||
|
||||
## 3. Runtime methods
|
||||
|
||||
```ts
|
||||
const {
|
||||
createDiffEditor,
|
||||
updateDiff,
|
||||
updateOriginal,
|
||||
updateModified,
|
||||
appendOriginal,
|
||||
appendModified,
|
||||
setDiffModels,
|
||||
setTheme,
|
||||
refreshDiffPresentation,
|
||||
getDiffEditorView,
|
||||
getDiffModels,
|
||||
getCode,
|
||||
} = useMonaco()
|
||||
```
|
||||
|
||||
Most useful diff methods:
|
||||
|
||||
- `createDiffEditor(container, original, modified, language)`
|
||||
- `updateDiff(original, modified, language?)`
|
||||
- `updateOriginal(code, language?)`
|
||||
- `updateModified(code, language?)`
|
||||
- `appendOriginal(text, language?)`
|
||||
- `appendModified(text, language?)`
|
||||
- `await setDiffModels({ original, modified }, options?)`
|
||||
- `setTheme(theme, force?)`
|
||||
- `refreshDiffPresentation()`
|
||||
- `getDiffEditorView()`
|
||||
- `getDiffModels()`
|
||||
- `getCode()`
|
||||
|
||||
In diff mode, `getCode()` returns:
|
||||
|
||||
```ts
|
||||
{
|
||||
original: string
|
||||
modified: string
|
||||
}
|
||||
```
|
||||
|
||||
### In-place theme / presentation switches
|
||||
|
||||
If you are only changing theme, appearance, line style, or unchanged-region style, prefer the in-place runtime methods instead of remounting the diff editor:
|
||||
|
||||
```ts
|
||||
await monaco.setTheme('github-dark')
|
||||
monaco.refreshDiffPresentation()
|
||||
```
|
||||
|
||||
This keeps the existing diff shell, scroll position, and unchanged-region overlay alive while recomputing the presentation layer.
|
||||
|
||||
Theme and appearance switches should stay visually stable. When you switch between unchanged-region styles with different collapsed heights, such as `metadata` to `simple`, the shell stays mounted but Monaco will reflow the lines below those collapsed regions to match the new height.
|
||||
|
||||
### Whole-model swaps without remounting
|
||||
|
||||
If your app needs to replace the entire `original/modified` model pair, prefer `await setDiffModels(...)` over calling `diffEditor.setModel(...)` yourself:
|
||||
|
||||
```ts
|
||||
const monacoApi = useMonaco()
|
||||
|
||||
const originalModel = monacoApi
|
||||
.getMonacoInstance()
|
||||
.editor.createModel(leftText, 'typescript')
|
||||
const modifiedModel = monacoApi
|
||||
.getMonacoInstance()
|
||||
.editor.createModel(rightText, 'typescript')
|
||||
|
||||
await monacoApi.setDiffModels(
|
||||
{
|
||||
original: originalModel,
|
||||
modified: modifiedModel,
|
||||
},
|
||||
{
|
||||
codeLanguage: 'typescript',
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Why this is better than raw `diffEditor.setModel(...)`:
|
||||
|
||||
- the library can precompute Monaco's diff view model before the visible swap, so same-content transitions avoid the first-frame scroll jump
|
||||
- if the new models contain the same text, `stream-monaco` automatically takes the low-jitter path and preserves view state by default
|
||||
- unchanged-region state and diff chrome are refreshed together
|
||||
- internally-owned models are disposed safely, while externally-supplied models are left under your control
|
||||
|
||||
Options:
|
||||
|
||||
- `codeLanguage`: normalize both models to the target language before swapping
|
||||
- `preserveViewState`: force preserving the diff view state even when content changed
|
||||
|
||||
## 4. Hunk action callback
|
||||
|
||||
If you want to intercept `Revert / Stage`, use `onDiffHunkAction`:
|
||||
|
||||
```ts
|
||||
useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
await saveHunk(ctx)
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Return semantics:
|
||||
|
||||
- `false`: skip built-in edit behavior, fully controlled by your app
|
||||
- `true` or `undefined`: continue built-in behavior
|
||||
|
||||
The callback context type is `DiffHunkActionContext`.
|
||||
|
||||
## 5. Binding to real Git (`revert`, `stage`, `stash`)
|
||||
|
||||
`stream-monaco` does not talk to Git by default. The built-in hover actions only edit the in-memory Monaco models.
|
||||
|
||||
Important distinctions:
|
||||
|
||||
- Hover `Revert` is hunk-level undo. It is not `git revert <commit>`, because `git revert` works at commit granularity.
|
||||
- Hover `Stage` is closest to `git add -p` / `git apply --cached`.
|
||||
- If you want a stash workflow, treat `onDiffHunkAction` as a patch-intent callback and hand the selected half-hunk to a backend stash API or patch queue.
|
||||
|
||||
The practical pattern is:
|
||||
|
||||
1. Intercept `onDiffHunkAction`.
|
||||
2. Send `action`, `side`, `lineChange`, `original`, and `modified` to your backend.
|
||||
3. On the backend, compute the target file contents for that half-hunk.
|
||||
4. Turn the current content -> target content delta into a unified patch for that file.
|
||||
5. Apply it to Git:
|
||||
- working tree revert: `git apply --recount -`
|
||||
- index stage: `git apply --cached --recount -`
|
||||
- custom stash: store the same patch in your own stash service, or wrap a server-side Git stash flow
|
||||
6. Return the refreshed `original` / `modified` pair to the browser and call `updateDiff(...)` or `setDiffModels(...)`.
|
||||
|
||||
Frontend example:
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
const response = await fetch('/api/git/hunks/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
repoPath,
|
||||
filePath,
|
||||
action: ctx.action,
|
||||
side: ctx.side,
|
||||
range: ctx.lineChange,
|
||||
original: ctx.originalModel.getValue(),
|
||||
modified: ctx.modifiedModel.getValue(),
|
||||
}),
|
||||
})
|
||||
|
||||
const next = await response.json()
|
||||
monaco.updateDiff(next.original, next.modified, language)
|
||||
|
||||
// Skip the built-in local-only model edit because Git already applied it.
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Common variant: backend returns the refreshed file pair, and the client updates the diff in-place with `updateDiff(...)`.
|
||||
|
||||
This is usually the simplest integration shape when your server already knows the post-apply file contents for both sides:
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
const response = await fetch('/api/git/hunks/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: ctx.action,
|
||||
side: ctx.side,
|
||||
range: ctx.lineChange,
|
||||
original: ctx.originalModel.getValue(),
|
||||
modified: ctx.modifiedModel.getValue(),
|
||||
}),
|
||||
})
|
||||
|
||||
const next = await response.json()
|
||||
|
||||
// The server returns the refreshed left/right file contents after Git apply.
|
||||
monaco.updateDiff(next.original, next.modified, language)
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
When to prefer this:
|
||||
|
||||
- your backend already computes the full refreshed `original` / `modified`
|
||||
- you want to keep the current diff editor instance and just refresh its content
|
||||
- you do not need to swap to externally-owned Monaco models
|
||||
|
||||
If the backend gives you brand-new Monaco models instead of plain strings, use `setDiffModels(...)` instead.
|
||||
|
||||
Backend mapping to keep in mind:
|
||||
|
||||
- `revert + lower`: remove the modified-side lines from the working tree
|
||||
- `revert + upper`: restore the original-side lines into the working tree
|
||||
- `stage + lower`: copy the modified-side lines into the index
|
||||
- `stage + upper`: remove the original-side lines from the index
|
||||
|
||||
For replace hunks that have both upper and lower halves, do not think in terms of a single Git command name. Instead, compute the next desired file text for the selected half-hunk, build a patch for that exact delta, then apply it to the worktree or index.
|
||||
|
||||
If your product wants a real "stash this hunk" action, the safest route is usually a backend "saved patch" queue. `git stash push --patch` exists, but it is interactive and repo-scoped, so browser-driven review tools typically wrap it server-side or implement their own stash layer around patch files.
|
||||
|
||||
Reference validations in this repo:
|
||||
|
||||
- `pnpm run validate:diff-hunk-actions`: verifies the built-in local Revert/Stage semantics
|
||||
- `pnpm run validate:diff-hunk-custom-flow`: verifies async custom intercept flow where user code mutates the models directly
|
||||
- `pnpm run validate:diff-hunk-update-diff-flow`: verifies async custom intercept flow where the server returns refreshed file contents and the UI calls `updateDiff(...)`
|
||||
|
||||
## 6. TypeScript completeness
|
||||
|
||||
The public TS surface for diff integration is now fairly complete. These types are exported:
|
||||
|
||||
- `MonacoOptions`
|
||||
- `UseMonacoReturn`
|
||||
- `MonacoTheme`
|
||||
- `MonacoLanguage`
|
||||
- `MonacoEditorInstance`
|
||||
- `MonacoDiffEditorInstance`
|
||||
- `DiffHideUnchangedRegions`
|
||||
- `DiffLineStyle`
|
||||
- `DiffAppearance`
|
||||
- `DiffUnchangedRegionStyle`
|
||||
- `DiffModels`
|
||||
- `DiffModelPair`
|
||||
- `DiffModelTransitionOptions`
|
||||
- `DiffCodeValue`
|
||||
- `MonacoCodeValue`
|
||||
- `DiffHunkActionContext`
|
||||
|
||||
### What is covered
|
||||
|
||||
- `useMonaco(options)` now has an explicit return type: `UseMonacoReturn`
|
||||
- custom diff UX options have dedicated union types
|
||||
- `MonacoOptions` covers both single-editor and native diff-editor construction options
|
||||
- return values such as `getCode()` and `getDiffModels()` have named exported types
|
||||
|
||||
### Practical answer
|
||||
|
||||
If your question is "do I still need to write custom declarations for diff integration?", the answer is now basically no.
|
||||
|
||||
More directly:
|
||||
|
||||
- custom diff options: typed
|
||||
- `useMonaco()` return value: typed
|
||||
- native Monaco diff option pass-through: typed
|
||||
|
||||
## 6. Recommended typed setup
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DiffAppearance,
|
||||
DiffUnchangedRegionStyle,
|
||||
MonacoOptions,
|
||||
UseMonacoReturn,
|
||||
} from 'stream-monaco'
|
||||
|
||||
const options: MonacoOptions = {
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript'],
|
||||
readOnly: true,
|
||||
diffAppearance: 'auto',
|
||||
diffLineStyle: 'background',
|
||||
diffUnchangedRegionStyle: 'line-info',
|
||||
diffHideUnchangedRegions: true,
|
||||
diffHunkActionsOnHover: true,
|
||||
}
|
||||
|
||||
const monacoApi: UseMonacoReturn = useMonaco(options)
|
||||
```
|
||||
|
||||
This gives the cleanest IDE autocomplete and keeps later UX upgrades easy to maintain.
|
||||
@@ -0,0 +1,422 @@
|
||||
# Diff 接入指南
|
||||
|
||||
本文档聚焦 `stream-monaco` 的 Diff Editor 接入方式、参数传法,以及当前可直接使用的 TypeScript 类型。
|
||||
|
||||
## 1. 最小接入
|
||||
|
||||
```ts
|
||||
import { useMonaco } from 'stream-monaco'
|
||||
|
||||
const container = document.getElementById('diff')!
|
||||
|
||||
const { createDiffEditor, updateDiff, cleanupEditor } = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript', 'javascript', 'json'],
|
||||
readOnly: true,
|
||||
MAX_HEIGHT: 560,
|
||||
})
|
||||
|
||||
await createDiffEditor(
|
||||
container,
|
||||
'export const a = 1\n',
|
||||
'export const a = 2\n',
|
||||
'typescript',
|
||||
)
|
||||
|
||||
updateDiff('export const a = 1\n', 'export const a = 3\n', 'typescript')
|
||||
|
||||
cleanupEditor()
|
||||
```
|
||||
|
||||
Vue/React/Svelte/Solid 的接入方式都一样:
|
||||
|
||||
- 挂载时调用 `createDiffEditor(...)`
|
||||
- 内容变化时调用 `updateDiff(...)` / `updateOriginal(...)` / `updateModified(...)`
|
||||
- 卸载时调用 `cleanupEditor()`
|
||||
|
||||
## 2. 常用参数怎么传
|
||||
|
||||
所有参数都通过 `useMonaco(options)` 传入。
|
||||
|
||||
### 基础参数
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript', 'javascript'],
|
||||
theme: 'github-light',
|
||||
readOnly: true,
|
||||
MAX_HEIGHT: 560,
|
||||
})
|
||||
```
|
||||
|
||||
最常用的是:
|
||||
|
||||
- `themes`: 主题列表,建议至少提供一套 dark/light
|
||||
- `languages`: 允许高亮的语言列表
|
||||
- `theme`: 初始主题
|
||||
- `readOnly`: Diff 预览通常建议 `true`
|
||||
- `MAX_HEIGHT`: 编辑器最大高度
|
||||
|
||||
### Diff 专属增强参数
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript'],
|
||||
readOnly: true,
|
||||
|
||||
diffHideUnchangedRegions: {
|
||||
enabled: true,
|
||||
contextLineCount: 3,
|
||||
minimumLineCount: 3,
|
||||
revealLineCount: 5,
|
||||
},
|
||||
diffLineStyle: 'background',
|
||||
diffAppearance: 'auto',
|
||||
diffUnchangedRegionStyle: 'line-info',
|
||||
diffHunkActionsOnHover: true,
|
||||
diffHunkHoverHideDelayMs: 160,
|
||||
diffUpdateThrottleMs: 50,
|
||||
revealDebounceMs: 75,
|
||||
})
|
||||
```
|
||||
|
||||
这几个参数的作用:
|
||||
|
||||
- `diffHideUnchangedRegions`
|
||||
控制未改动区折叠。支持 `true` / `false`,也支持直接传 Monaco 原生 `hideUnchangedRegions` 配置对象。
|
||||
- `diffLineStyle`
|
||||
变更行的强调方式。
|
||||
- `background`: 背景块更明显
|
||||
- `bar`: 更接近 review UI 的竖条风格
|
||||
- `diffAppearance`
|
||||
控制 diff chrome 的明暗。
|
||||
- `auto`: 跟随当前 Monaco 主题明暗
|
||||
- `light`: 强制浅色外框
|
||||
- `dark`: 强制深色外框
|
||||
- `diffUnchangedRegionStyle`
|
||||
折叠未改动区的展示方式。
|
||||
- `line-info`: `71 unmodified lines`,展开按钮背景宽度跟随行号区,折叠行高为 `32px`
|
||||
- `line-info-basic`: `71 unmodified lines`,保留当前更宽的 legacy rail 效果,折叠行高为 `32px`
|
||||
- `metadata`: `@@ -59,9 +59,11 @@`,更紧凑的 `32px` 折叠行;切换过去时,下方内容会一起回流,避免多余留白
|
||||
- `simple`: 灰色占位条,折叠行高为 `28px`
|
||||
- `diffHunkActionsOnHover`
|
||||
是否启用 hunk hover 的 `Revert / Stage`
|
||||
- `diffHunkHoverHideDelayMs`
|
||||
hover 浮层隐藏延迟
|
||||
- `diffUpdateThrottleMs`
|
||||
流式 diff 更新节流。默认是 `50`。
|
||||
如果开启未改动区折叠,库会等流式更新进入 idle 后再恢复折叠,避免内容还在持续流入时就开始收起未改动区。
|
||||
- `revealDebounceMs`
|
||||
自动滚动 reveal 的合并延迟
|
||||
|
||||
### Monaco 原生 Diff 参数
|
||||
|
||||
`MonacoOptions` 现在同时包含:
|
||||
|
||||
- `monaco.editor.IStandaloneEditorConstructionOptions`
|
||||
- `monaco.editor.IDiffEditorConstructionOptions`
|
||||
|
||||
所以除了上面这些 `stream-monaco` 自己的参数,你也可以直接传 Monaco 原生 diff 参数,例如:
|
||||
|
||||
```ts
|
||||
useMonaco({
|
||||
renderSideBySide: true,
|
||||
enableSplitViewResizing: true,
|
||||
ignoreTrimWhitespace: false,
|
||||
originalEditable: false,
|
||||
})
|
||||
```
|
||||
|
||||
如果你想在运行时切换单列/双列,也可以直接操作实例:
|
||||
|
||||
```ts
|
||||
const diff = monaco.getDiffEditorView()
|
||||
diff?.updateOptions({ renderSideBySide: false })
|
||||
```
|
||||
|
||||
## 3. 运行时常用方法
|
||||
|
||||
```ts
|
||||
const {
|
||||
createDiffEditor,
|
||||
updateDiff,
|
||||
updateOriginal,
|
||||
updateModified,
|
||||
appendOriginal,
|
||||
appendModified,
|
||||
setDiffModels,
|
||||
setTheme,
|
||||
refreshDiffPresentation,
|
||||
getDiffEditorView,
|
||||
getDiffModels,
|
||||
getCode,
|
||||
} = useMonaco()
|
||||
```
|
||||
|
||||
Diff 常用方法:
|
||||
|
||||
- `createDiffEditor(container, original, modified, language)`
|
||||
- `updateDiff(original, modified, language?)`
|
||||
- `updateOriginal(code, language?)`
|
||||
- `updateModified(code, language?)`
|
||||
- `appendOriginal(text, language?)`
|
||||
- `appendModified(text, language?)`
|
||||
- `await setDiffModels({ original, modified }, options?)`
|
||||
- `setTheme(theme, force?)`
|
||||
- `refreshDiffPresentation()`
|
||||
- `getDiffEditorView()`
|
||||
- `getDiffModels()`
|
||||
- `getCode()`
|
||||
|
||||
`getCode()` 在 diff 模式下会返回:
|
||||
|
||||
```ts
|
||||
{
|
||||
original: string
|
||||
modified: string
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时切换主题 / 展现层
|
||||
|
||||
如果你切换的只是 theme、appearance、line style、unchanged-region style 这类“换皮”能力,优先走运行时原地刷新,而不是 remount 整个 diff editor:
|
||||
|
||||
```ts
|
||||
await monaco.setTheme('github-dark')
|
||||
monaco.refreshDiffPresentation()
|
||||
```
|
||||
|
||||
这样会保留现有 diff 壳、滚动位置和 unchanged overlay,只重算表现层。
|
||||
|
||||
theme / appearance 切换本身应该保持稳定不抖动。对于 `metadata`、`simple` 这种折叠高度不同的模式切换,外层 editor shell 仍然原地复用,但 Monaco 会把折叠区下方的内容一起回流到新的高度。
|
||||
|
||||
### 整对 model 切换但不 remount
|
||||
|
||||
如果你的场景是一次性替换整对 `original / modified` model,优先使用 `await setDiffModels(...)`,不要自己直接调用 `diffEditor.setModel(...)`:
|
||||
|
||||
```ts
|
||||
const monacoApi = useMonaco()
|
||||
|
||||
const originalModel = monacoApi
|
||||
.getMonacoInstance()
|
||||
.editor.createModel(leftText, 'typescript')
|
||||
const modifiedModel = monacoApi
|
||||
.getMonacoInstance()
|
||||
.editor.createModel(rightText, 'typescript')
|
||||
|
||||
await monacoApi.setDiffModels(
|
||||
{
|
||||
original: originalModel,
|
||||
modified: modifiedModel,
|
||||
},
|
||||
{
|
||||
codeLanguage: 'typescript',
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
它比直接调原生 `setModel(...)` 更适合这套 diff UX,原因是:
|
||||
|
||||
- 库会在可见切换前预热 Monaco 的 diff view model,所以同内容切换可以避开首帧 scroll jump
|
||||
- 如果新旧 models 的文本内容相同,会自动走默认的低抖动路径并保留 view state
|
||||
- unchanged-region 状态和 diff chrome 会一起刷新,不会只换了 model、overlay 却不同步
|
||||
- 库自己创建的 models 会安全 dispose,外部传入的 models 仍由调用方自己管理
|
||||
|
||||
可选项:
|
||||
|
||||
- `codeLanguage`:切换前先把两侧 model 归一到目标语言
|
||||
- `preserveViewState`:即使内容变了,也强制保留当前 diff 视图状态
|
||||
|
||||
## 4. Hunk 操作回调
|
||||
|
||||
如果你想接管 `Revert / Stage`,可以用 `onDiffHunkAction`:
|
||||
|
||||
```ts
|
||||
useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
await saveHunk(ctx)
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
返回值语义:
|
||||
|
||||
- `false`: 阻止内置编辑逻辑,完全由你接管
|
||||
- `true` 或 `undefined`: 继续执行内置逻辑
|
||||
|
||||
回调上下文类型是 `DiffHunkActionContext`。
|
||||
|
||||
## 5. 如何绑定到真实 Git(`revert` / `stage` / `stash`)
|
||||
|
||||
`stream-monaco` 默认并不会直接操作 Git。内置 hover action 只会修改内存中的 Monaco model。
|
||||
|
||||
先区分三个概念:
|
||||
|
||||
- hover `Revert` 表示 hunk 级别的局部回退,不等于 `git revert <commit>`;后者是 commit 粒度。
|
||||
- hover `Stage` 更接近 `git add -p` / `git apply --cached`。
|
||||
- 如果你要做 stash 流程,建议把 `onDiffHunkAction` 当成“生成 patch 意图”的回调,把选中的 half-hunk 交给后端 stash API 或 patch 队列。
|
||||
|
||||
一条比较稳妥的真实 Git 绑定链路是:
|
||||
|
||||
1. 在前端拦截 `onDiffHunkAction`。
|
||||
2. 把 `action`、`side`、`lineChange`、`original`、`modified` 发给后端。
|
||||
3. 后端根据这次 half-hunk 操作,算出目标文件内容。
|
||||
4. 再把“当前内容 -> 目标内容”的差异生成为该文件的 unified patch。
|
||||
5. 应用到 Git:
|
||||
- 回退 working tree:`git apply --recount -`
|
||||
- 暂存到 index:`git apply --cached --recount -`
|
||||
- 自定义 stash:把同一份 patch 存进你自己的 stash 服务,或在服务端包装 Git stash 流程
|
||||
6. 把刷新后的 `original` / `modified` 返回前端,再调用 `updateDiff(...)` 或 `setDiffModels(...)`。
|
||||
|
||||
前端示例:
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
const response = await fetch('/api/git/hunks/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
repoPath,
|
||||
filePath,
|
||||
action: ctx.action,
|
||||
side: ctx.side,
|
||||
range: ctx.lineChange,
|
||||
original: ctx.originalModel.getValue(),
|
||||
modified: ctx.modifiedModel.getValue(),
|
||||
}),
|
||||
})
|
||||
|
||||
const next = await response.json()
|
||||
monaco.updateDiff(next.original, next.modified, language)
|
||||
|
||||
// Git 已经完成实际修改,这里跳过内置的本地 model 编辑。
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
另一种很常见的接法是:后端直接返回刷新后的左右文件内容,前端再用 `updateDiff(...)` 原地刷新当前 diff。
|
||||
|
||||
当服务端已经能算出 Git apply 之后两侧文件应该是什么内容时,这通常是最直接的一种集成方式:
|
||||
|
||||
```ts
|
||||
const monaco = useMonaco({
|
||||
diffHunkActionsOnHover: true,
|
||||
onDiffHunkAction: async (ctx) => {
|
||||
const response = await fetch('/api/git/hunks/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: ctx.action,
|
||||
side: ctx.side,
|
||||
range: ctx.lineChange,
|
||||
original: ctx.originalModel.getValue(),
|
||||
modified: ctx.modifiedModel.getValue(),
|
||||
}),
|
||||
})
|
||||
|
||||
const next = await response.json()
|
||||
|
||||
// 服务端返回 Git 应用后的左右文件内容,前端直接刷新当前 diff。
|
||||
monaco.updateDiff(next.original, next.modified, language)
|
||||
return false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
这种方式更适合:
|
||||
|
||||
- 后端已经能直接给出完整的 `original` / `modified`
|
||||
- 你希望保留当前 diff editor 实例,只刷新内容
|
||||
- 你不需要切换成外部持有的 Monaco models
|
||||
|
||||
如果后端返回的是全新的 Monaco models,而不是普通字符串内容,那么更适合用 `setDiffModels(...)`。
|
||||
|
||||
后端侧的语义映射可以按这张表理解:
|
||||
|
||||
- `revert + lower`:把 modified 侧这部分内容从 working tree 里去掉
|
||||
- `revert + upper`:把 original 侧这部分内容补回 working tree
|
||||
- `stage + lower`:把 modified 侧这部分内容写进 index
|
||||
- `stage + upper`:把 original 侧这部分内容从 index 移除
|
||||
|
||||
对于同时有 upper/lower 两半的 replace hunk,不建议硬套某一个 Git 子命令名。更稳妥的方式是:先算出“这次点击之后文件应该长什么样”,再针对这份精确 delta 生成 patch,然后把 patch 应用到 worktree 或 index。
|
||||
|
||||
如果你产品里真的想要一个“stash 这个 hunk”的动作,通常最稳的实现不是直接在浏览器里调 Git,而是后端维护一层“已保存 patch”队列。`git stash push --patch` 虽然存在,但它是交互式、并且是 repo 级行为,所以 review 工具更常见的做法是服务端包装,或者直接实现自己的 stash 层。
|
||||
|
||||
仓库里现在也提供了几条可直接参考的验证脚本:
|
||||
|
||||
- `pnpm run validate:diff-hunk-actions`:验证内置 local Revert / Stage 语义
|
||||
- `pnpm run validate:diff-hunk-custom-flow`:验证异步自定义拦截后,业务代码直接改 model 的流程
|
||||
- `pnpm run validate:diff-hunk-update-diff-flow`:验证异步自定义拦截后,由服务端返回刷新后的文件内容,再调用 `updateDiff(...)` 的流程
|
||||
|
||||
## 6. TS 类型是否完善
|
||||
|
||||
目前对外类型已经比较完整,Diff 接入常用到的都已经有正式导出:
|
||||
|
||||
- `MonacoOptions`
|
||||
- `UseMonacoReturn`
|
||||
- `MonacoTheme`
|
||||
- `MonacoLanguage`
|
||||
- `MonacoEditorInstance`
|
||||
- `MonacoDiffEditorInstance`
|
||||
- `DiffHideUnchangedRegions`
|
||||
- `DiffLineStyle`
|
||||
- `DiffAppearance`
|
||||
- `DiffUnchangedRegionStyle`
|
||||
- `DiffModels`
|
||||
- `DiffModelPair`
|
||||
- `DiffModelTransitionOptions`
|
||||
- `DiffCodeValue`
|
||||
- `MonacoCodeValue`
|
||||
- `DiffHunkActionContext`
|
||||
|
||||
### 当前类型覆盖到什么程度
|
||||
|
||||
- `useMonaco(options)` 的返回值现在有显式 `UseMonacoReturn`
|
||||
- Diff 自定义参数都有独立联合类型,不需要手写字符串
|
||||
- `MonacoOptions` 同时覆盖单编辑器和原生 Diff Editor 的 construction options
|
||||
- `getCode()`、`getDiffModels()` 这类返回值都有正式类型
|
||||
|
||||
### 结论
|
||||
|
||||
如果你的问题是“接入时会不会还要自己补一堆声明”,现在答案基本是不会。
|
||||
|
||||
更直接地说:
|
||||
|
||||
- 自定义 diff 参数:类型已完善
|
||||
- `useMonaco()` 返回值:类型已完善
|
||||
- Monaco 原生 diff 参数透传:类型已补齐
|
||||
|
||||
## 6. 建议的接入写法
|
||||
|
||||
如果你是在业务里做代码 review / patch 预览,我建议按这套接:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DiffAppearance,
|
||||
DiffUnchangedRegionStyle,
|
||||
MonacoOptions,
|
||||
UseMonacoReturn,
|
||||
} from 'stream-monaco'
|
||||
|
||||
const options: MonacoOptions = {
|
||||
themes: ['github-dark', 'github-light'],
|
||||
languages: ['typescript'],
|
||||
readOnly: true,
|
||||
diffAppearance: 'auto',
|
||||
diffLineStyle: 'background',
|
||||
diffUnchangedRegionStyle: 'line-info',
|
||||
diffHideUnchangedRegions: true,
|
||||
diffHunkActionsOnHover: true,
|
||||
}
|
||||
|
||||
const monacoApi: UseMonacoReturn = useMonaco(options)
|
||||
```
|
||||
|
||||
这样 IDE 补全会比较完整,后面接 `Reference / Streaming / Metadata / Simple` 这些模式也更稳。
|
||||
Reference in New Issue
Block a user