@jetcrab/pi-desk 1.1.6-dev → 1.1.8-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/docs/README.md +8 -0
- package/docs/doc_asset/client-overview.webp +0 -0
- package/docs/doc_asset/multi-window.webp +0 -0
- package/docs/{pi-desk/plugins.md → plugins.md} +108 -1
- package/package.json +2 -2
- package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +3 -0
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-view.tsx +8 -0
- package/src/client/l3_modules/project-files/l3-project-document-git.ts +82 -0
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +102 -5
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +2 -6
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +83 -1
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +24 -2
- package/src/client/l3_modules/project-files/l3-project-git-model.ts +32 -0
- package/src/client/l4_foundation/ui/code/l4-code-diff.tsx +25 -5
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.ts +49 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-changes.worker.ts +10 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.module.css +60 -0
- package/src/client/l4_foundation/ui/code/l4-code-line-markers.ts +156 -0
- package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +61 -5
- package/src/client/l4_foundation/ui/code/l4-monaco-lines-diff.d.ts +21 -0
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +16 -0
- package/src/server/l4_foundation/pidesk/l4-pidesk-runtime.ts +1 -1
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +5 -4
- package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
- package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +3 -3
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/20842.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/{9248.js → 354.js} +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +6 -6
- package/temp/build/pi-desk/release/.next/server/chunks/45671.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/47757.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/56284.js +62 -0
- package/temp/build/pi-desk/release/.next/server/chunks/73691.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/82499.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/94296.js +2 -2
- package/temp/build/pi-desk/release/.next/server/chunks/pi-desk-line-changes.js +1 -0
- package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
- package/temp/build/pi-desk/release/.next/server/webpack-runtime.js +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/28739.540a4f24a8eaa3fd.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/31087-333cae396a52f232.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/3814-35cfab0ce3aa7c41.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/38459.9a9b6f4f3ec3ff50.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/45992-a39a014497ec4dd1.js +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/53291.ab6ef49016107124.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{54206.82a63ab971911eda.js → 54206.ffb8c581c6661ae2.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/5598.f34dce2a9de0d0e3.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/70669.8b4231a8675499e0.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/95287.34b860abf26f64a5.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-586f6f88483051a7.js +3 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-83ed165c9a3835c1.js → page-0f2cf68a4666420b.js} +3 -3
- package/temp/build/pi-desk/release/.next/static/chunks/pi-desk-line-changes.49f8d59c256621d6.js +1 -0
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-26261285e181235a.js +1 -0
- package/temp/build/pi-desk/release/.next/static/css/{f6a76a493aae9ae3.css → adb1d4e48dd43c0a.css} +1 -1
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/docs/pi-desk/README.md +0 -47
- package/temp/build/pi-desk/release/.next/server/chunks/6656.js +0 -1
- package/temp/build/pi-desk/release/.next/server/chunks/89236.js +0 -62
- package/temp/build/pi-desk/release/.next/static/chunks/19991.02fcb7c8ca3b1fe4.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/26285.54b8bcbf7a198c84.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/52354-6461358c522130be.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/79615.7d68b269d93fcb93.js +0 -1
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-4faf3a52d219296d.js +0 -3
- package/temp/build/pi-desk/release/.next/static/chunks/webpack-7bfbf627379fb4c1.js +0 -1
- /package/docs/{pi-desk/commands.md → commands.md} +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/application.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/browser/entry.js +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/package.json +0 -0
- /package/docs/{pi-desk/examples → examples}/local-application/pi-desk.ts +0 -0
- /package/temp/build/pi-desk/release/.next/static/{NwX_o5zDfv-NABRLmLIjY → -pemk30NXpiExEU5tjb1t}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{NwX_o5zDfv-NABRLmLIjY → -pemk30NXpiExEU5tjb1t}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { ChevronDownIcon, ChevronUpIcon, FileCode2Icon, XIcon } from 'lucide-react'
|
|
4
|
-
import { useLayoutEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
7
7
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
@@ -13,8 +13,12 @@ import {
|
|
|
13
13
|
} from '@client/l4_foundation/ui/shadcn/context-menu'
|
|
14
14
|
import type { L3FileTabCloseScope } from './l3-project-file-runtime'
|
|
15
15
|
import { L3FileContextMenuContent } from './l3-file-context-menu-content'
|
|
16
|
+
import type { L3ProjectGitBrowser } from './l3-project-git-runtime'
|
|
17
|
+
import { findL3GitFile, getL3GitChangeBadge, getL3GitChangeClass } from './l3-project-git-model'
|
|
16
18
|
|
|
17
19
|
interface L3ProjectFileTabsProps {
|
|
20
|
+
cwd: string
|
|
21
|
+
gitRuntime: L3ProjectGitBrowser
|
|
18
22
|
paths: readonly string[]
|
|
19
23
|
activePath: string | null
|
|
20
24
|
diffPath: string | null
|
|
@@ -39,6 +43,8 @@ function fileLabel(path: string, paths: readonly string[]): string {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function L3ProjectFileTabs({
|
|
46
|
+
cwd,
|
|
47
|
+
gitRuntime,
|
|
42
48
|
paths,
|
|
43
49
|
activePath,
|
|
44
50
|
diffPath,
|
|
@@ -47,6 +53,8 @@ export function L3ProjectFileTabs({
|
|
|
47
53
|
onClose
|
|
48
54
|
}: L3ProjectFileTabsProps): React.JSX.Element | null {
|
|
49
55
|
const { t } = useTranslation('projectFiles')
|
|
56
|
+
useSyncExternalStore(gitRuntime.subscribe, gitRuntime.getRevision, gitRuntime.getRevision)
|
|
57
|
+
const snapshot = gitRuntime.getWorkspace(cwd)?.snapshot
|
|
50
58
|
const rowsRef = useRef<HTMLDivElement>(null)
|
|
51
59
|
const suppressTouchClickRef = useRef(false)
|
|
52
60
|
const [expanded, setExpanded] = useState(false)
|
|
@@ -98,6 +106,10 @@ export function L3ProjectFileTabs({
|
|
|
98
106
|
{tabs.map((tab, index) => {
|
|
99
107
|
const clipped = !expanded && clippedIndexes.includes(index)
|
|
100
108
|
const selected = tab.path === active
|
|
109
|
+
const change =
|
|
110
|
+
tab.path && snapshot?.status === 'ready'
|
|
111
|
+
? findL3GitFile(cwd, tab.path, snapshot.repositories)?.change
|
|
112
|
+
: undefined
|
|
101
113
|
const closeLabel =
|
|
102
114
|
tab.path === null
|
|
103
115
|
? t('closeDiff', { name: tab.title.split('/').at(-1) })
|
|
@@ -186,7 +198,17 @@ export function L3ProjectFileTabs({
|
|
|
186
198
|
}}
|
|
187
199
|
>
|
|
188
200
|
<FileCode2Icon className="size-3.5 shrink-0" />
|
|
189
|
-
<span className=
|
|
201
|
+
<span className={cn('truncate', change && getL3GitChangeClass(change))}>
|
|
202
|
+
{tab.label}
|
|
203
|
+
</span>
|
|
204
|
+
{change ? (
|
|
205
|
+
<span
|
|
206
|
+
className={cn('shrink-0 text-xs', getL3GitChangeClass(change))}
|
|
207
|
+
aria-label={t('gitStatus', { status: getL3GitChangeBadge(change) })}
|
|
208
|
+
>
|
|
209
|
+
{getL3GitChangeBadge(change)}
|
|
210
|
+
</span>
|
|
211
|
+
) : null}
|
|
190
212
|
</button>
|
|
191
213
|
<Button
|
|
192
214
|
variant="ghost"
|
|
@@ -131,6 +131,38 @@ export function getL3GitChangeBadge(change: L4GitFileChange): string {
|
|
|
131
131
|
return '?'
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export function getL3GitChangeClass(change: L4GitFileChange): string {
|
|
135
|
+
const badge = getL3GitChangeBadge(change)
|
|
136
|
+
if (badge === 'D') return 'text-muted-foreground'
|
|
137
|
+
if (badge === '?') return 'text-red-600 dark:text-red-400'
|
|
138
|
+
if (badge === 'A') return 'text-emerald-600 dark:text-emerald-400'
|
|
139
|
+
if (badge === '!') return 'text-violet-600 dark:text-violet-400'
|
|
140
|
+
return 'text-blue-600 dark:text-blue-400'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function findL3GitFile(
|
|
144
|
+
cwd: string,
|
|
145
|
+
path: string,
|
|
146
|
+
repositories: readonly L4GitRepository[]
|
|
147
|
+
): { repository: L4GitRepository; path: string; change: L4GitFileChange | undefined } | null {
|
|
148
|
+
if (/^(?:[a-z]:|\/|\\\\)/i.test(path)) return null
|
|
149
|
+
const windows = /^[a-z]:[\\/]/i.test(cwd) || cwd.startsWith('\\\\')
|
|
150
|
+
const key = (value: string): string => (windows ? value.toLowerCase() : value)
|
|
151
|
+
const repository = [...repositories]
|
|
152
|
+
.sort((left, right) => right.root.length - left.root.length)
|
|
153
|
+
.find((item) => !item.root || key(path).startsWith(`${key(item.root)}/`))
|
|
154
|
+
if (!repository) return null
|
|
155
|
+
const relative = repository.root ? path.slice(repository.root.length + 1) : path
|
|
156
|
+
return {
|
|
157
|
+
repository,
|
|
158
|
+
path: relative,
|
|
159
|
+
change:
|
|
160
|
+
repository.state === 'ready'
|
|
161
|
+
? repository.changes.find((item) => key(item.path) === key(relative))
|
|
162
|
+
: undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
export function listL3GitProjectChanges(
|
|
135
167
|
repositories: readonly L4GitRepository[]
|
|
136
168
|
): L3GitProjectChange[] {
|
|
@@ -44,10 +44,11 @@ interface L4CodeDiffProps {
|
|
|
44
44
|
onEscape?: () => void
|
|
45
45
|
savedState?: L4CodeDiffState | null
|
|
46
46
|
onSaveState?: (state: L4CodeDiffState | null) => void
|
|
47
|
+
initialLine?: number
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function L4CodeDiffComponent(
|
|
50
|
-
{ original, modified, viewMode, onEscape, savedState, onSaveState }: L4CodeDiffProps,
|
|
51
|
+
{ original, modified, viewMode, onEscape, savedState, onSaveState, initialLine }: L4CodeDiffProps,
|
|
51
52
|
ref: ForwardedRef<L4CodeDiffHandle>
|
|
52
53
|
): React.JSX.Element {
|
|
53
54
|
const { t } = useTranslation('common')
|
|
@@ -58,12 +59,14 @@ function L4CodeDiffComponent(
|
|
|
58
59
|
const escapeRef = useRef(onEscape)
|
|
59
60
|
const savedStateRef = useRef(savedState)
|
|
60
61
|
const saveStateRef = useRef(onSaveState)
|
|
62
|
+
const initialLineRef = useRef(initialLine)
|
|
61
63
|
|
|
62
64
|
useEffect(() => {
|
|
63
65
|
escapeRef.current = onEscape
|
|
64
66
|
savedStateRef.current = savedState
|
|
65
67
|
saveStateRef.current = onSaveState
|
|
66
|
-
|
|
68
|
+
initialLineRef.current = initialLine
|
|
69
|
+
}, [initialLine, onEscape, onSaveState, savedState])
|
|
67
70
|
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
68
71
|
const [changeCount, setChangeCount] = useState<number | null>(null)
|
|
69
72
|
|
|
@@ -130,6 +133,7 @@ function L4CodeDiffComponent(
|
|
|
130
133
|
useInlineViewWhenSpaceIsLimited: false,
|
|
131
134
|
enableSplitViewResizing: true,
|
|
132
135
|
diffAlgorithm: 'advanced',
|
|
136
|
+
ignoreTrimWhitespace: false,
|
|
133
137
|
maxComputationTime: 5_000,
|
|
134
138
|
hideUnchangedRegions: initialSettings.collapseUnchanged
|
|
135
139
|
? {
|
|
@@ -156,13 +160,29 @@ function L4CodeDiffComponent(
|
|
|
156
160
|
contextmenu: true,
|
|
157
161
|
ariaLabel: t('gitDiffReadOnly')
|
|
158
162
|
})
|
|
159
|
-
|
|
163
|
+
const requestedLine = initialLineRef.current
|
|
164
|
+
let initialPositionApplied = false
|
|
160
165
|
scrollListeners.push(
|
|
161
166
|
diffEditor.onDidUpdateDiff(() => {
|
|
162
|
-
if (active
|
|
167
|
+
if (!active || !diffEditor) return
|
|
168
|
+
const changes = diffEditor.getLineChanges()
|
|
169
|
+
setChangeCount(changes?.length ?? null)
|
|
170
|
+
if (!initialPositionApplied && changes !== null && requestedLine !== undefined) {
|
|
171
|
+
initialPositionApplied = true
|
|
172
|
+
const lineNumber = Math.max(
|
|
173
|
+
1,
|
|
174
|
+
Math.min(requestedLine, modifiedModel?.getLineCount() ?? 1)
|
|
175
|
+
)
|
|
176
|
+
const modifiedEditor = diffEditor.getModifiedEditor()
|
|
177
|
+
modifiedEditor.setPosition({ lineNumber, column: 1 })
|
|
178
|
+
modifiedEditor.revealLineInCenter(lineNumber)
|
|
179
|
+
}
|
|
163
180
|
})
|
|
164
181
|
)
|
|
165
|
-
|
|
182
|
+
diffEditor.setModel({ original: originalModel, modified: modifiedModel })
|
|
183
|
+
if (requestedLine === undefined && savedStateRef.current) {
|
|
184
|
+
diffEditor.restoreViewState(savedStateRef.current)
|
|
185
|
+
}
|
|
166
186
|
const save = (): void => saveStateRef.current?.(diffEditor?.saveViewState() ?? null)
|
|
167
187
|
scrollListeners.push(
|
|
168
188
|
diffEditor.getOriginalEditor().onDidScrollChange(save),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DefaultLinesDiffComputer } from 'monaco-editor/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer'
|
|
2
|
+
|
|
3
|
+
export interface L4LineChange {
|
|
4
|
+
kind: 'added' | 'modified' | 'deleted'
|
|
5
|
+
originalStartLineNumber: number
|
|
6
|
+
originalEndLineNumberExclusive: number
|
|
7
|
+
modifiedStartLineNumber: number
|
|
8
|
+
modifiedEndLineNumberExclusive: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 行号从 1 开始,结束行不包含在范围内;纯删除的 modified 范围为空。 */
|
|
12
|
+
export function computeL4LineChanges(original: string, modified: string): L4LineChange[] | null {
|
|
13
|
+
if (original.length + modified.length > 2 * 1024 * 1024) return null
|
|
14
|
+
const originalLines = original.split(/\r\n|\r|\n/)
|
|
15
|
+
const modifiedLines = modified.split(/\r\n|\r|\n/)
|
|
16
|
+
if (originalLines.length + modifiedLines.length > 50_000) return null
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const startedAt = Date.now()
|
|
20
|
+
const result = new DefaultLinesDiffComputer().computeDiff(originalLines, modifiedLines, {
|
|
21
|
+
ignoreTrimWhitespace: false,
|
|
22
|
+
maxComputationTimeMs: 500,
|
|
23
|
+
computeMoves: false
|
|
24
|
+
})
|
|
25
|
+
if (result.hitTimeout || Date.now() - startedAt > 500) return null
|
|
26
|
+
return result.changes.map(({ original: before, modified: after }) => {
|
|
27
|
+
// Monaco 的空模型占一行,但空文件不应被归类为替换了这一行。
|
|
28
|
+
const originalEnd = original === '' ? 1 : before.endLineNumberExclusive
|
|
29
|
+
const modifiedEnd = modified === '' ? 1 : after.endLineNumberExclusive
|
|
30
|
+
return {
|
|
31
|
+
kind:
|
|
32
|
+
before.startLineNumber === originalEnd
|
|
33
|
+
? 'added'
|
|
34
|
+
: after.startLineNumber === modifiedEnd
|
|
35
|
+
? 'deleted'
|
|
36
|
+
: 'modified',
|
|
37
|
+
originalStartLineNumber: before.startLineNumber,
|
|
38
|
+
originalEndLineNumberExclusive: originalEnd,
|
|
39
|
+
modifiedStartLineNumber: after.startLineNumber,
|
|
40
|
+
modifiedEndLineNumberExclusive: modifiedEnd
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
} catch (error: unknown) {
|
|
44
|
+
console.warn('[Pi Desk][CodePreview] 行差异计算失败', {
|
|
45
|
+
errorName: error instanceof Error ? error.name : 'UnknownError'
|
|
46
|
+
})
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { computeL4LineChanges, type L4LineChange } from './l4-code-line-changes'
|
|
2
|
+
|
|
3
|
+
const scope = self as unknown as {
|
|
4
|
+
onmessage: ((event: MessageEvent<{ original: string; modified: string }>) => void) | null
|
|
5
|
+
postMessage: (changes: L4LineChange[] | null) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
scope.onmessage = (event): void => {
|
|
9
|
+
scope.postMessage(computeL4LineChanges(event.data.original, event.data.modified))
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.host {
|
|
2
|
+
--git-added: #16803c;
|
|
3
|
+
--git-modified: #216bcc;
|
|
4
|
+
--git-deleted: #d12c38;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:global(.dark) .host {
|
|
8
|
+
--git-added: #59c77b;
|
|
9
|
+
--git-modified: #6aabff;
|
|
10
|
+
--git-deleted: #ff757f;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.added,
|
|
14
|
+
.modified,
|
|
15
|
+
.deletedBefore,
|
|
16
|
+
.deletedAfter {
|
|
17
|
+
/* 实际移动命中区,保持点击相对元素左缘小于 4px,避免触发 Monaco 折叠。 */
|
|
18
|
+
margin-left: 8px;
|
|
19
|
+
width: 4px !important;
|
|
20
|
+
z-index: 1;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.added::before,
|
|
25
|
+
.modified::before {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 0;
|
|
28
|
+
bottom: 0;
|
|
29
|
+
left: 0;
|
|
30
|
+
width: 3px;
|
|
31
|
+
content: '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.added::before {
|
|
35
|
+
background: var(--git-added);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.modified::before {
|
|
39
|
+
background: var(--git-modified);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.deletedBefore::before,
|
|
43
|
+
.deletedAfter::before {
|
|
44
|
+
position: absolute;
|
|
45
|
+
left: 0;
|
|
46
|
+
width: 0;
|
|
47
|
+
height: 0;
|
|
48
|
+
border-top: 4px solid transparent;
|
|
49
|
+
border-bottom: 4px solid transparent;
|
|
50
|
+
border-left: 4px solid var(--git-deleted);
|
|
51
|
+
content: '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.deletedBefore::before {
|
|
55
|
+
top: -3px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.deletedAfter::before {
|
|
59
|
+
bottom: -3px;
|
|
60
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { editor } from 'monaco-editor'
|
|
2
|
+
import type { L4Monaco } from './l4-monaco-runtime'
|
|
3
|
+
import type { L4LineChange } from './l4-code-line-changes'
|
|
4
|
+
import styles from './l4-code-line-markers.module.css'
|
|
5
|
+
|
|
6
|
+
export interface L4CodeLineMarkers {
|
|
7
|
+
previousChange: () => void
|
|
8
|
+
nextChange: () => void
|
|
9
|
+
dispose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function bindL4CodeLineMarkers(
|
|
13
|
+
codeEditor: editor.IStandaloneCodeEditor,
|
|
14
|
+
monaco: L4Monaco,
|
|
15
|
+
input: {
|
|
16
|
+
path: string
|
|
17
|
+
original: string
|
|
18
|
+
modified: string
|
|
19
|
+
tooltip: (kind: L4LineChange['kind'], count: number) => string
|
|
20
|
+
onChanges: (count: number | null) => void
|
|
21
|
+
onOpenDiff: (lineNumber: number) => void
|
|
22
|
+
}
|
|
23
|
+
): L4CodeLineMarkers {
|
|
24
|
+
const decorations = codeEditor.createDecorationsCollection()
|
|
25
|
+
const lineCount = codeEditor.getModel()?.getLineCount() ?? 1
|
|
26
|
+
let changes: L4LineChange[] = []
|
|
27
|
+
let active = true
|
|
28
|
+
let worker: Worker | null = null
|
|
29
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
30
|
+
const markerSelector = [styles.added, styles.modified, styles.deletedBefore, styles.deletedAfter]
|
|
31
|
+
.map((className) => `.${className}`)
|
|
32
|
+
.join(',')
|
|
33
|
+
|
|
34
|
+
const markerLine = (change: L4LineChange): number =>
|
|
35
|
+
Math.min(
|
|
36
|
+
lineCount,
|
|
37
|
+
change.kind === 'deleted'
|
|
38
|
+
? Math.max(1, change.modifiedStartLineNumber - 1)
|
|
39
|
+
: change.modifiedStartLineNumber
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const stopWorker = (): void => {
|
|
43
|
+
if (timeout !== null) clearTimeout(timeout)
|
|
44
|
+
timeout = null
|
|
45
|
+
worker?.terminate()
|
|
46
|
+
worker = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const finish = (result: L4LineChange[] | null): void => {
|
|
50
|
+
if (!active) return
|
|
51
|
+
stopWorker()
|
|
52
|
+
changes = result ?? []
|
|
53
|
+
decorations.set(
|
|
54
|
+
changes.map((change): editor.IModelDeltaDecoration => {
|
|
55
|
+
const deleted = change.kind === 'deleted'
|
|
56
|
+
const startLine = markerLine(change)
|
|
57
|
+
const endLine = deleted ? startLine : change.modifiedEndLineNumberExclusive - 1
|
|
58
|
+
const count = deleted
|
|
59
|
+
? change.originalEndLineNumberExclusive - change.originalStartLineNumber
|
|
60
|
+
: change.modifiedEndLineNumberExclusive - change.modifiedStartLineNumber
|
|
61
|
+
const colors = {
|
|
62
|
+
added: ['#16803c', '#59c77b'],
|
|
63
|
+
modified: ['#216bcc', '#6aabff'],
|
|
64
|
+
deleted: ['#d12c38', '#ff757f']
|
|
65
|
+
}[change.kind]
|
|
66
|
+
const markerClass = deleted
|
|
67
|
+
? change.modifiedStartLineNumber === 1
|
|
68
|
+
? styles.deletedBefore
|
|
69
|
+
: styles.deletedAfter
|
|
70
|
+
: styles[change.kind]
|
|
71
|
+
return {
|
|
72
|
+
range: new monaco.Range(startLine, 1, endLine, 1),
|
|
73
|
+
options: {
|
|
74
|
+
isWholeLine: !deleted,
|
|
75
|
+
showIfCollapsed: true,
|
|
76
|
+
linesDecorationsClassName: deleted ? undefined : markerClass,
|
|
77
|
+
firstLineDecorationClassName: deleted ? markerClass : undefined,
|
|
78
|
+
linesDecorationsTooltip: input.tooltip(change.kind, count),
|
|
79
|
+
overviewRuler: {
|
|
80
|
+
color: colors[0],
|
|
81
|
+
darkColor: colors[1],
|
|
82
|
+
position: monaco.editor.OverviewRulerLane.Left
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
if (result === null) {
|
|
89
|
+
console.warn('[Pi Desk][CodePreview] 行标记不可用,计算失败或超过限制', {
|
|
90
|
+
path: input.path
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
input.onChanges(result?.length ?? null)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const mouseListener = codeEditor.onMouseDown((event) => {
|
|
97
|
+
if (
|
|
98
|
+
event.target.type !== monaco.editor.MouseTargetType.GUTTER_LINE_DECORATIONS ||
|
|
99
|
+
!event.target.element?.closest(markerSelector)
|
|
100
|
+
) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const line = event.target.position?.lineNumber
|
|
104
|
+
if (line === undefined) return
|
|
105
|
+
const change = changes.find((item) =>
|
|
106
|
+
item.kind === 'deleted'
|
|
107
|
+
? markerLine(item) === line
|
|
108
|
+
: item.modifiedStartLineNumber <= line && line < item.modifiedEndLineNumberExclusive
|
|
109
|
+
)
|
|
110
|
+
if (change) input.onOpenDiff(line)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
worker = new Worker(new URL('./l4-code-line-changes.worker.ts', import.meta.url), {
|
|
115
|
+
type: 'module',
|
|
116
|
+
name: 'pi-desk-line-changes'
|
|
117
|
+
})
|
|
118
|
+
worker.onmessage = (event: MessageEvent<L4LineChange[] | null>): void => finish(event.data)
|
|
119
|
+
worker.onerror = (): void => finish(null)
|
|
120
|
+
worker.onmessageerror = (): void => finish(null)
|
|
121
|
+
// 含 Worker 启动预算;取消时终止 Worker,旧文件结果没有回流通道。
|
|
122
|
+
timeout = setTimeout(() => finish(null), 3_000)
|
|
123
|
+
worker.postMessage({ original: input.original, modified: input.modified })
|
|
124
|
+
} catch {
|
|
125
|
+
finish(null)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const navigate = (direction: 'previous' | 'next'): void => {
|
|
129
|
+
if (changes.length === 0) return
|
|
130
|
+
const currentLine = codeEditor.getPosition()?.lineNumber ?? 1
|
|
131
|
+
const lines = changes.map(markerLine)
|
|
132
|
+
const line =
|
|
133
|
+
direction === 'next'
|
|
134
|
+
? (lines.find((item) => item > currentLine) ?? lines[0])
|
|
135
|
+
: (lines.findLast((item) => item < currentLine) ?? lines[lines.length - 1])
|
|
136
|
+
codeEditor.setPosition({ lineNumber: line, column: 1 })
|
|
137
|
+
codeEditor.revealLineInCenter(line)
|
|
138
|
+
codeEditor.focus()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
previousChange(): void {
|
|
143
|
+
navigate('previous')
|
|
144
|
+
},
|
|
145
|
+
nextChange(): void {
|
|
146
|
+
navigate('next')
|
|
147
|
+
},
|
|
148
|
+
dispose(): void {
|
|
149
|
+
active = false
|
|
150
|
+
stopWorker()
|
|
151
|
+
mouseListener.dispose()
|
|
152
|
+
decorations.clear()
|
|
153
|
+
changes = []
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import { bindL4MonacoPreviewEscape } from './l4-monaco-preview-escape'
|
|
25
25
|
import { useL4CodePreferences } from './l4-code-preferences'
|
|
26
|
+
import { bindL4CodeLineMarkers, type L4CodeLineMarkers } from './l4-code-line-markers'
|
|
27
|
+
import markerStyles from './l4-code-line-markers.module.css'
|
|
26
28
|
|
|
27
29
|
export type L4CodePreviewState = editor.ICodeEditorViewState
|
|
28
30
|
|
|
@@ -32,6 +34,8 @@ export interface L4CodePreviewHandle {
|
|
|
32
34
|
getSelectionText: () => string
|
|
33
35
|
getViewState: () => L4CodePreviewState | null
|
|
34
36
|
focus: () => void
|
|
37
|
+
previousChange: () => void
|
|
38
|
+
nextChange: () => void
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
interface L4CodePreviewProps {
|
|
@@ -41,13 +45,27 @@ interface L4CodePreviewProps {
|
|
|
41
45
|
savedState: L4CodePreviewState | null
|
|
42
46
|
onSaveState: (path: string, state: L4CodePreviewState | null) => void
|
|
43
47
|
onEscape?: () => void
|
|
48
|
+
originalContent?: string
|
|
49
|
+
onChanges?: (count: number | null) => void
|
|
50
|
+
onOpenDiff?: (lineNumber: number) => void
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function L4CodePreviewComponent(
|
|
47
|
-
{
|
|
54
|
+
{
|
|
55
|
+
path,
|
|
56
|
+
content,
|
|
57
|
+
size,
|
|
58
|
+
savedState,
|
|
59
|
+
onSaveState,
|
|
60
|
+
onEscape,
|
|
61
|
+
originalContent,
|
|
62
|
+
onChanges,
|
|
63
|
+
onOpenDiff
|
|
64
|
+
}: L4CodePreviewProps,
|
|
48
65
|
ref: ForwardedRef<L4CodePreviewHandle>
|
|
49
66
|
): React.JSX.Element {
|
|
50
67
|
const { t } = useTranslation('common')
|
|
68
|
+
const { t: projectFilesT } = useTranslation('projectFiles')
|
|
51
69
|
const { wrapLines } = useL4CodePreferences()
|
|
52
70
|
const hostRef = useRef<HTMLDivElement>(null)
|
|
53
71
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
|
@@ -55,11 +73,16 @@ function L4CodePreviewComponent(
|
|
|
55
73
|
const wrapLinesRef = useRef(wrapLines)
|
|
56
74
|
const savedStateRef = useRef(savedState)
|
|
57
75
|
const escapeRef = useRef(onEscape)
|
|
76
|
+
const markersRef = useRef<L4CodeLineMarkers | null>(null)
|
|
77
|
+
const changesRef = useRef(onChanges)
|
|
78
|
+
const openDiffRef = useRef(onOpenDiff)
|
|
58
79
|
|
|
59
80
|
useEffect(() => {
|
|
60
81
|
savedStateRef.current = savedState
|
|
61
82
|
escapeRef.current = onEscape
|
|
62
|
-
|
|
83
|
+
changesRef.current = onChanges
|
|
84
|
+
openDiffRef.current = onOpenDiff
|
|
85
|
+
}, [onChanges, onEscape, onOpenDiff, savedState])
|
|
63
86
|
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
64
87
|
|
|
65
88
|
useEffect(() => {
|
|
@@ -90,6 +113,12 @@ function L4CodePreviewComponent(
|
|
|
90
113
|
},
|
|
91
114
|
focus(): void {
|
|
92
115
|
editorRef.current?.focus()
|
|
116
|
+
},
|
|
117
|
+
previousChange(): void {
|
|
118
|
+
markersRef.current?.previousChange()
|
|
119
|
+
},
|
|
120
|
+
nextChange(): void {
|
|
121
|
+
markersRef.current?.nextChange()
|
|
93
122
|
}
|
|
94
123
|
}),
|
|
95
124
|
[]
|
|
@@ -101,6 +130,7 @@ function L4CodePreviewComponent(
|
|
|
101
130
|
let active = true
|
|
102
131
|
let codeEditor: editor.IStandaloneCodeEditor | null = null
|
|
103
132
|
let model: editor.ITextModel | null = null
|
|
133
|
+
let markers: L4CodeLineMarkers | null = null
|
|
104
134
|
let stopThemeObserver = (): void => undefined
|
|
105
135
|
let stopTypographyObserver = (): void => undefined
|
|
106
136
|
|
|
@@ -131,6 +161,7 @@ function L4CodePreviewComponent(
|
|
|
131
161
|
wrappingIndent: 'same',
|
|
132
162
|
...getL4MonacoTypography(),
|
|
133
163
|
lineNumbersMinChars: 3,
|
|
164
|
+
lineDecorationsWidth: 26,
|
|
134
165
|
folding: size <= 1024 * 1024,
|
|
135
166
|
renderValidationDecorations: 'off',
|
|
136
167
|
renderWhitespace: 'selection',
|
|
@@ -145,6 +176,25 @@ function L4CodePreviewComponent(
|
|
|
145
176
|
if (escapeRef.current)
|
|
146
177
|
bindL4MonacoPreviewEscape(codeEditor, monaco.KeyCode.Escape, () => escapeRef.current?.())
|
|
147
178
|
editorRef.current = codeEditor
|
|
179
|
+
if (originalContent !== undefined) {
|
|
180
|
+
markers = bindL4CodeLineMarkers(codeEditor, monaco, {
|
|
181
|
+
path,
|
|
182
|
+
original: originalContent,
|
|
183
|
+
modified: content,
|
|
184
|
+
tooltip: (kind, count) =>
|
|
185
|
+
projectFilesT(
|
|
186
|
+
{
|
|
187
|
+
added: 'gitAddedLines',
|
|
188
|
+
modified: 'gitModifiedLines',
|
|
189
|
+
deleted: 'gitDeletedLines'
|
|
190
|
+
}[kind],
|
|
191
|
+
{ count }
|
|
192
|
+
),
|
|
193
|
+
onChanges: (count) => changesRef.current?.(count),
|
|
194
|
+
onOpenDiff: (lineNumber) => openDiffRef.current?.(lineNumber)
|
|
195
|
+
})
|
|
196
|
+
markersRef.current = markers
|
|
197
|
+
}
|
|
148
198
|
stopThemeObserver = observeL4MonacoTheme((theme) => monaco.editor.setTheme(theme))
|
|
149
199
|
stopTypographyObserver = observeL4MonacoTypography((typography) =>
|
|
150
200
|
codeEditor?.updateOptions(typography)
|
|
@@ -158,18 +208,24 @@ function L4CodePreviewComponent(
|
|
|
158
208
|
errorName: error instanceof Error ? error.name : 'UnknownError'
|
|
159
209
|
})
|
|
160
210
|
setLoadState('error')
|
|
211
|
+
if (originalContent !== undefined) changesRef.current?.(null)
|
|
161
212
|
})
|
|
162
213
|
|
|
163
214
|
return () => {
|
|
164
215
|
active = false
|
|
165
216
|
stopThemeObserver()
|
|
166
217
|
stopTypographyObserver()
|
|
167
|
-
|
|
218
|
+
markers?.dispose()
|
|
219
|
+
markersRef.current = null
|
|
220
|
+
if (codeEditor) {
|
|
221
|
+
savedStateRef.current = codeEditor.saveViewState()
|
|
222
|
+
onSaveStateRef.current(path, savedStateRef.current)
|
|
223
|
+
}
|
|
168
224
|
editorRef.current = null
|
|
169
225
|
codeEditor?.dispose()
|
|
170
226
|
model?.dispose()
|
|
171
227
|
}
|
|
172
|
-
}, [content, path, size, t])
|
|
228
|
+
}, [content, originalContent, path, projectFilesT, size, t])
|
|
173
229
|
|
|
174
230
|
useEffect(() => {
|
|
175
231
|
editorRef.current?.updateOptions({ wordWrap: wrapLines ? 'on' : 'off' })
|
|
@@ -180,7 +236,7 @@ function L4CodePreviewComponent(
|
|
|
180
236
|
data-testid="code-preview"
|
|
181
237
|
className="relative h-full min-h-0 w-full overflow-hidden bg-background"
|
|
182
238
|
>
|
|
183
|
-
<div ref={hostRef} className=
|
|
239
|
+
<div ref={hostRef} className={`size-full ${markerStyles.host}`} />
|
|
184
240
|
{loadState === 'loading' ? (
|
|
185
241
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center gap-2 bg-background text-sm text-muted-foreground">
|
|
186
242
|
<LoaderCircleIcon className="size-4 animate-spin" /> {t('codeLoading')}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'monaco-editor/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer' {
|
|
2
|
+
interface LineRange {
|
|
3
|
+
startLineNumber: number
|
|
4
|
+
endLineNumberExclusive: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class DefaultLinesDiffComputer {
|
|
8
|
+
computeDiff(
|
|
9
|
+
originalLines: string[],
|
|
10
|
+
modifiedLines: string[],
|
|
11
|
+
options: {
|
|
12
|
+
ignoreTrimWhitespace: boolean
|
|
13
|
+
maxComputationTimeMs: number
|
|
14
|
+
computeMoves: boolean
|
|
15
|
+
}
|
|
16
|
+
): {
|
|
17
|
+
changes: { original: LineRange; modified: LineRange }[]
|
|
18
|
+
hitTimeout: boolean
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -45,6 +45,14 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
|
|
|
45
45
|
copyContentFailed: 'Could not copy the file. Please copy it manually.',
|
|
46
46
|
fileActions: 'File actions',
|
|
47
47
|
headToWorking: 'HEAD ↔ working tree',
|
|
48
|
+
gitChangesCount: 'Against HEAD · {{count}} changes',
|
|
49
|
+
gitChangesCompact: '{{count}} changes',
|
|
50
|
+
gitMarkersLoading: 'Comparing with HEAD…',
|
|
51
|
+
gitMarkersFailed: 'Could not read changes · Reload to retry',
|
|
52
|
+
gitMarkersUnavailable: 'Line change markers unavailable',
|
|
53
|
+
gitAddedLines: '{{count}} lines added',
|
|
54
|
+
gitModifiedLines: '{{count}} lines modified',
|
|
55
|
+
gitDeletedLines: '{{count}} lines deleted',
|
|
48
56
|
noDiffChanges: ' · No content differences',
|
|
49
57
|
previousChange: 'Previous change',
|
|
50
58
|
nextChange: 'Next change',
|
|
@@ -131,6 +139,14 @@ export const L3_PROJECT_FILES_LOCALE_MESSAGES = {
|
|
|
131
139
|
copyContentFailed: '复制文件内容失败,请手动复制。',
|
|
132
140
|
fileActions: '文件操作',
|
|
133
141
|
headToWorking: 'HEAD ↔ 工作区',
|
|
142
|
+
gitChangesCount: '相对 HEAD · {{count}} 处变更',
|
|
143
|
+
gitChangesCompact: '{{count}} 处变更',
|
|
144
|
+
gitMarkersLoading: '正在与 HEAD 比较…',
|
|
145
|
+
gitMarkersFailed: '变更读取失败 · 刷新重试',
|
|
146
|
+
gitMarkersUnavailable: '此文件无法显示行变更',
|
|
147
|
+
gitAddedLines: '新增 {{count}} 行',
|
|
148
|
+
gitModifiedLines: '修改 {{count}} 行',
|
|
149
|
+
gitDeletedLines: '删除 {{count}} 行',
|
|
134
150
|
noDiffChanges: ' · 当前比较无内容差异',
|
|
135
151
|
previousChange: '上一个变更',
|
|
136
152
|
nextChange: '下一个变更',
|
|
@@ -60,7 +60,7 @@ export async function initializeL4PiDeskCommands(
|
|
|
60
60
|
const sdkRoot = dirname(
|
|
61
61
|
dirname(createRequire(join(hostRoot, 'package.json')).resolve('@jetcrab/pi-desk-sdk'))
|
|
62
62
|
)
|
|
63
|
-
const docsDirectory = join(hostRoot, 'docs'
|
|
63
|
+
const docsDirectory = join(hostRoot, 'docs')
|
|
64
64
|
await Promise.all(
|
|
65
65
|
['README.md', 'commands.md', 'plugins.md'].map((file) => access(join(docsDirectory, file)))
|
|
66
66
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
-pemk30NXpiExEU5tjb1t
|