@jetcrab/pi-desk 1.1.18-dev → 1.1.20-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/docs/plugins.md +1 -1
- package/next.config.ts +1 -0
- package/package.json +3 -2
- package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +1 -1
- package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-reload.ts +42 -0
- package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +19 -1
- package/src/client/l2_biz/workbench/l2-workbench-chat.ts +45 -0
- package/src/client/l2_biz/workbench/l2-workbench.tsx +4 -0
- package/src/client/l3_modules/conversation/l3-conversation-markdown.tsx +10 -1
- package/src/client/l3_modules/project-files/l3-markdown-preview.tsx +10 -1
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +2 -0
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +171 -229
- package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +133 -70
- package/src/client/l3_modules/project-files/l3-project-reader.ts +4 -3
- package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +201 -0
- package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +333 -0
- package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +47 -3
- package/src/client/l4_foundation/ui/markdown/l4-markdown-remark.ts +89 -0
- package/src/client/l4_foundation/ui/markdown/l4-markdown.module.css +58 -0
- package/src/client/l4_foundation/ui/markdown/l4-markdown.tsx +108 -0
- package/src/common/l2_biz/work-session/l2-work-session-file-contract.ts +4 -3
- package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +6 -0
- package/src/common/l3_modules/project-files/l3-project-files-contract.ts +2 -3
- package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +56 -10
- package/src/common/l4_foundation/file/l4-project-file-contract.ts +51 -1
- package/src/common/l4_foundation/locale/l4-host-error-messages.ts +11 -0
- package/src/common/l4_foundation/locale/l4-locale-messages.ts +16 -0
- package/src/instrumentation.ts +0 -2
- package/src/server/l1_entry/api/l1-project-read-routes.ts +6 -4
- package/src/server/l1_entry/api/l1-project-route.ts +9 -8
- package/src/server/l1_entry/api/l1-work-session-file-error.ts +23 -0
- package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +0 -3
- package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +30 -58
- package/src/server/l2_biz/work-session/l2-work-session-manage.ts +0 -4
- package/src/server/l4_foundation/file/l4-project-file-index.ts +276 -0
- package/src/server/l4_foundation/file/l4-project-search.ts +399 -0
- package/src/server/l4_foundation/file/l4-ripgrep-search.ts +192 -0
- package/src/server/l4_foundation/file/l4-work-session-project-file.ts +8 -332
- package/src/server/l4_foundation/pi/l4-pi-package-maintenance.ts +0 -6
- package/src/server/l4_foundation/pi/l4-pi-plugin-capability-probe.ts +1 -3
- package/src/server/l4_foundation/pi/l4-pi-plugin-probe.ts +1 -3
- package/src/server/l4_foundation/pi/l4-pi-runner-process.ts +2 -2
- package/src/server/l4_foundation/process/l4-process-tree.js +23 -4
- 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/prerender-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/required-server-files.js +3 -2
- package/temp/build/pi-desk/release/.next/required-server-files.json +3 -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/api/project-files/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +2 -1
- package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -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 +77 -77
- 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/{33658.js → 10881.js} +8 -8
- package/temp/build/pi-desk/release/.next/server/chunks/23054.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/25406.js +3 -3
- package/temp/build/pi-desk/release/.next/server/chunks/35985.js +5 -5
- package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -4
- package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/70780.js +1 -0
- package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/92386.js +1 -1
- package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
- 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/server-reference-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/27611-0890482fe76bd9d6.js +238 -0
- package/temp/build/pi-desk/release/.next/static/chunks/{31087-ba426d09d6f68bfd.js → 31087-d8e5765d541d5c26.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{49516-cc4dcd84d64ab4fe.js → 51474-8dc714cb76b2e60a.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/77592-5bef6acb8e7c4b96.js +13 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-a50c456648537afb.js +3 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-a984139e99586041.js → page-0789c43551b29dd2.js} +4 -4
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-40a782fc8a3535b9.js → webpack-d7248206b7fc7ba8.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/250a6b93cc2222d1.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/a4cf28fd76c70165.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/8584.js +0 -2
- package/temp/build/pi-desk/release/.next/static/chunks/39654-b5fedd762ea0312e.js +0 -238
- package/temp/build/pi-desk/release/.next/static/chunks/75130-d2ca74894aff8085.js +0 -13
- package/temp/build/pi-desk/release/.next/static/chunks/app/layout-c0e1c5cef8620cf8.js +0 -3
- package/temp/build/pi-desk/release/.next/static/css/6427d02b6edf406f.css +0 -1
- package/temp/build/pi-desk/release/.next/static/css/79e06f05df9edff0.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{n2LrXQgq8UkbH_AzhR3fw → RLKcK-RQF5FvEeDmS9uih}/_ssgManifest.js +0 -0
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
L4ProjectFileEntry,
|
|
3
3
|
L4ProjectFileImageMimeType,
|
|
4
|
-
L4ProjectFileImagePreviewMode
|
|
4
|
+
L4ProjectFileImagePreviewMode,
|
|
5
|
+
L4ProjectFileSearch,
|
|
6
|
+
L4ProjectSearchInput
|
|
5
7
|
} from '@common/l4_foundation/file/l4-project-file-contract'
|
|
6
8
|
import type { L4GitHistoricalDiff } from '@common/l4_foundation/git/l4-git-history-contract'
|
|
7
9
|
import { L3_PROJECT_FILES_LOCALE_MESSAGES } from '@common/l3_modules/project-files/l3-project-files-locale-messages'
|
|
8
10
|
import { l4LocalizedErrorMessage } from '@client/l4_foundation/locale/l4-localized-error'
|
|
9
11
|
import { createL4Base64ImageBlob } from '@client/l4_foundation/media/l4-base64-image'
|
|
10
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
L4CodePreviewState,
|
|
14
|
+
L4CodeLineNavigation
|
|
15
|
+
} from '@client/l4_foundation/ui/code/l4-code-preview'
|
|
11
16
|
import type { L4CodeDiffState } from '@client/l4_foundation/ui/code/l4-code-diff'
|
|
12
17
|
import { saveL4CodePreferences } from '@client/l4_foundation/ui/code/l4-code-preferences'
|
|
13
18
|
import { L3ProjectRequestError, type L3ProjectFileReader } from './l3-project-reader'
|
|
@@ -47,13 +52,26 @@ export type L3FileDocument =
|
|
|
47
52
|
}
|
|
48
53
|
| { status: 'image'; mimeType: L4ProjectFileImageMimeType; size: number; objectUrl: string }
|
|
49
54
|
| { status: 'error'; error: string }
|
|
50
|
-
export interface L3FileSearch {
|
|
51
|
-
query: string
|
|
55
|
+
export interface L3FileSearch extends L4ProjectSearchInput, L4ProjectFileSearch {
|
|
52
56
|
status: 'idle' | 'loading' | 'ready' | 'error'
|
|
53
|
-
matches: string[]
|
|
54
|
-
truncated: boolean
|
|
55
57
|
error: string | null
|
|
56
58
|
}
|
|
59
|
+
const DEFAULT_SEARCH_OPTIONS = {
|
|
60
|
+
scope: 'all',
|
|
61
|
+
caseSensitive: false,
|
|
62
|
+
wholeWord: false,
|
|
63
|
+
regex: false
|
|
64
|
+
} as const
|
|
65
|
+
function emptySearch(input: L4ProjectSearchInput): L3FileSearch {
|
|
66
|
+
return {
|
|
67
|
+
...input,
|
|
68
|
+
status: input.query.trim() ? 'loading' : 'idle',
|
|
69
|
+
fileMatches: [],
|
|
70
|
+
contentMatches: [],
|
|
71
|
+
truncated: false,
|
|
72
|
+
error: null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
57
75
|
interface DiffOptions {
|
|
58
76
|
repositoryRoot: string
|
|
59
77
|
path: string
|
|
@@ -132,7 +150,7 @@ export class L3ProjectFileRuntime {
|
|
|
132
150
|
directories: new Map(),
|
|
133
151
|
expandedPaths: new Set(),
|
|
134
152
|
treeScrollTop: 0,
|
|
135
|
-
search: { query: '',
|
|
153
|
+
search: emptySearch({ query: '', ...DEFAULT_SEARCH_OPTIONS }),
|
|
136
154
|
searchOpen: false,
|
|
137
155
|
documents: new Map(),
|
|
138
156
|
activeDiff: null
|
|
@@ -150,6 +168,7 @@ export class L3ProjectFileRuntime {
|
|
|
150
168
|
private readonly restoredFileViewModes = new Map<string, 'source' | 'preview'>()
|
|
151
169
|
private readonly restoredPreviewScrollTops = new Map<string, number>()
|
|
152
170
|
private searchRequest: AbortController | null = null
|
|
171
|
+
private navigation: L4CodeLineNavigation | null = null
|
|
153
172
|
private diffRequest: AbortController | null = null
|
|
154
173
|
private revision = 0
|
|
155
174
|
private disposed = false
|
|
@@ -165,8 +184,23 @@ export class L3ProjectFileRuntime {
|
|
|
165
184
|
}
|
|
166
185
|
getRevision = (): number => this.revision
|
|
167
186
|
setSearchOpen(open: boolean): void {
|
|
168
|
-
if (this.state.searchOpen === open) return
|
|
187
|
+
if (this.disposed || this.state.searchOpen === open) return
|
|
188
|
+
this.cancelSearch()
|
|
169
189
|
this.state.searchOpen = open
|
|
190
|
+
this.state.search = emptySearch({
|
|
191
|
+
...this.searchInput(),
|
|
192
|
+
scope: open ? 'all' : this.state.search.scope
|
|
193
|
+
})
|
|
194
|
+
this.publish()
|
|
195
|
+
}
|
|
196
|
+
get pendingNavigation(): L4CodeLineNavigation | null {
|
|
197
|
+
const path = this.state.activePath
|
|
198
|
+
return path && !this.documentRequests.has(path) ? this.navigation : null
|
|
199
|
+
}
|
|
200
|
+
consumeNavigation(target: L4CodeLineNavigation, exists: boolean): void {
|
|
201
|
+
if (this.navigation !== target) return
|
|
202
|
+
this.navigation = null
|
|
203
|
+
if (!exists) this.options.onNotice?.(projectText('searchLineUnavailable'))
|
|
170
204
|
this.publish()
|
|
171
205
|
}
|
|
172
206
|
get canReveal(): boolean {
|
|
@@ -203,13 +237,7 @@ export class L3ProjectFileRuntime {
|
|
|
203
237
|
input.activePath && input.tabs.includes(input.activePath) ? input.activePath : null
|
|
204
238
|
this.state.expandedPaths = new Set(input.expandedPaths)
|
|
205
239
|
this.state.treeScrollTop = input.treeScrollTop
|
|
206
|
-
this.state.search = {
|
|
207
|
-
query: input.searchQuery,
|
|
208
|
-
status: input.searchQuery ? 'loading' : 'idle',
|
|
209
|
-
matches: [],
|
|
210
|
-
truncated: false,
|
|
211
|
-
error: null
|
|
212
|
-
}
|
|
240
|
+
this.state.search = emptySearch({ query: input.searchQuery, ...DEFAULT_SEARCH_OPTIONS })
|
|
213
241
|
for (const path of input.tabs) this.state.documents.set(path, { status: 'unloaded' })
|
|
214
242
|
for (const path of input.expandedPaths) void this.ensureDirectory(path)
|
|
215
243
|
if (input.activeDiff) {
|
|
@@ -320,18 +348,12 @@ export class L3ProjectFileRuntime {
|
|
|
320
348
|
this.directoryRequests.clear()
|
|
321
349
|
this.state.directories.clear()
|
|
322
350
|
this.cancelSearch()
|
|
323
|
-
this.state.search =
|
|
324
|
-
query: this.state.search.query,
|
|
325
|
-
status: this.state.search.query ? 'loading' : 'idle',
|
|
326
|
-
matches: [],
|
|
327
|
-
truncated: false,
|
|
328
|
-
error: null
|
|
329
|
-
}
|
|
351
|
+
this.state.search = emptySearch(this.searchInput())
|
|
330
352
|
this.publish()
|
|
331
353
|
void this.ensureRoot().then(() => {
|
|
332
354
|
if (this.disposed) return
|
|
333
355
|
for (const path of this.state.expandedPaths) void this.ensureDirectory(path)
|
|
334
|
-
if (this.state.search.query) {
|
|
356
|
+
if (this.state.searchOpen && this.state.search.query.trim()) {
|
|
335
357
|
this.cancelSearch()
|
|
336
358
|
this.state.search = { ...this.state.search, status: 'loading' }
|
|
337
359
|
void this.search(this.state.search.query)
|
|
@@ -339,17 +361,31 @@ export class L3ProjectFileRuntime {
|
|
|
339
361
|
})
|
|
340
362
|
}
|
|
341
363
|
setSearchQuery(query: string): void {
|
|
342
|
-
if (this.state.search.query === query) return
|
|
364
|
+
if (this.disposed || this.state.search.query === query) return
|
|
343
365
|
this.cancelSearch()
|
|
344
|
-
this.state.search = {
|
|
345
|
-
query,
|
|
346
|
-
status: query.trim() ? 'loading' : 'idle',
|
|
347
|
-
matches: [],
|
|
348
|
-
truncated: false,
|
|
349
|
-
error: null
|
|
350
|
-
}
|
|
366
|
+
this.state.search = emptySearch({ ...this.searchInput(), query })
|
|
351
367
|
this.publish()
|
|
352
368
|
}
|
|
369
|
+
setSearchOptions(options: Partial<Omit<L4ProjectSearchInput, 'query'>>): void {
|
|
370
|
+
if (this.disposed) return
|
|
371
|
+
const previous = this.searchInput()
|
|
372
|
+
const next = { ...previous, ...options }
|
|
373
|
+
if (
|
|
374
|
+
next.scope === previous.scope &&
|
|
375
|
+
next.caseSensitive === previous.caseSensitive &&
|
|
376
|
+
next.wholeWord === previous.wholeWord &&
|
|
377
|
+
next.regex === previous.regex
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
this.cancelSearch()
|
|
381
|
+
this.state.search = emptySearch(next)
|
|
382
|
+
this.publish()
|
|
383
|
+
if (this.state.searchOpen) void this.search(next.query)
|
|
384
|
+
}
|
|
385
|
+
private searchInput(): L4ProjectSearchInput {
|
|
386
|
+
const { query, scope, caseSensitive, wholeWord, regex } = this.state.search
|
|
387
|
+
return { query, scope, caseSensitive, wholeWord, regex }
|
|
388
|
+
}
|
|
353
389
|
async search(query: string): Promise<void> {
|
|
354
390
|
if (
|
|
355
391
|
this.disposed ||
|
|
@@ -361,27 +397,22 @@ export class L3ProjectFileRuntime {
|
|
|
361
397
|
return
|
|
362
398
|
const request = new AbortController()
|
|
363
399
|
this.searchRequest = request
|
|
364
|
-
|
|
400
|
+
const input = this.searchInput()
|
|
401
|
+
this.state.search = emptySearch(input)
|
|
365
402
|
this.publish()
|
|
366
403
|
try {
|
|
367
|
-
const result = await this.reader.search(
|
|
404
|
+
const result = await this.reader.search(input, request.signal)
|
|
368
405
|
if (this.searchRequest !== request) return
|
|
369
|
-
this.state.search = {
|
|
406
|
+
this.state.search = { ...input, status: 'ready', ...result, error: null }
|
|
370
407
|
} catch (cause) {
|
|
371
408
|
if (this.searchRequest !== request) return
|
|
372
|
-
this.state.search = {
|
|
373
|
-
query,
|
|
374
|
-
status: 'error',
|
|
375
|
-
matches: [],
|
|
376
|
-
truncated: false,
|
|
377
|
-
error: message(cause)
|
|
378
|
-
}
|
|
409
|
+
this.state.search = { ...emptySearch(input), status: 'error', error: message(cause) }
|
|
379
410
|
}
|
|
380
411
|
if (this.searchRequest !== request) return
|
|
381
412
|
this.searchRequest = null
|
|
382
413
|
this.publish()
|
|
383
414
|
}
|
|
384
|
-
openFile(input: string): void {
|
|
415
|
+
openFile(input: string, line?: number): void {
|
|
385
416
|
if (this.disposed) return
|
|
386
417
|
const normalized = normalizePath(this.cwd, input)
|
|
387
418
|
const windows = /^[a-z]:[\\/]/i.test(this.cwd) || this.cwd.startsWith('\\\\')
|
|
@@ -399,8 +430,11 @@ export class L3ProjectFileRuntime {
|
|
|
399
430
|
this.clearDiff()
|
|
400
431
|
if (!this.state.documents.has(path)) this.state.documents.set(path, { status: 'unloaded' })
|
|
401
432
|
this.state.activePath = path
|
|
433
|
+
this.navigation = line === undefined ? null : { line }
|
|
434
|
+
const document = this.state.documents.get(path)
|
|
435
|
+
if (line !== undefined && document?.status === 'text') document.viewMode = 'source'
|
|
436
|
+
void this.ensureDocument(path, line !== undefined)
|
|
402
437
|
this.publish()
|
|
403
|
-
void this.ensureDocument(path)
|
|
404
438
|
}
|
|
405
439
|
closeTabs(target: string | null, scope: L3FileTabCloseScope): void {
|
|
406
440
|
const state = this.state
|
|
@@ -439,6 +473,7 @@ export class L3ProjectFileRuntime {
|
|
|
439
473
|
}
|
|
440
474
|
state.tabs = state.tabs.filter((path) => !removed.has(path))
|
|
441
475
|
if (state.activePath && removed.has(state.activePath)) {
|
|
476
|
+
this.navigation = null
|
|
442
477
|
const activeIndex = tabs.indexOf(state.activePath)
|
|
443
478
|
state.activePath =
|
|
444
479
|
tabs.slice(activeIndex + 1).find((path) => path !== null && !removed.has(path)) ??
|
|
@@ -454,17 +489,23 @@ export class L3ProjectFileRuntime {
|
|
|
454
489
|
}
|
|
455
490
|
reloadFile(path: string): void {
|
|
456
491
|
if (!this.state.tabs.includes(path)) return
|
|
492
|
+
if (this.state.activePath === path && this.navigation) {
|
|
493
|
+
// 面板显示时的自动重读与搜索定位共用刷新,不能清除待消费的目标行。
|
|
494
|
+
void this.ensureDocument(path, true)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
457
497
|
this.invalidateDocument(path)
|
|
458
498
|
this.publish()
|
|
459
499
|
void this.ensureDocument(path)
|
|
460
500
|
}
|
|
461
|
-
async ensureDocument(path: string): Promise<void> {
|
|
462
|
-
if (this.disposed || !this.state.tabs.includes(path)) return
|
|
501
|
+
async ensureDocument(path: string, refresh = false): Promise<void> {
|
|
502
|
+
if (this.disposed || !this.state.tabs.includes(path) || this.documentRequests.has(path)) return
|
|
463
503
|
const current = this.state.documents.get(path)
|
|
464
|
-
if (current && ['loading', 'text', 'image'].includes(current.status)) return
|
|
504
|
+
if (!refresh && current && ['loading', 'text', 'image'].includes(current.status)) return
|
|
465
505
|
const request = new AbortController()
|
|
466
506
|
this.documentRequests.set(path, request)
|
|
467
|
-
|
|
507
|
+
// 搜索定位刷新正文时保留已挂载的代码视图,内容未变时不重建编辑器。
|
|
508
|
+
if (current?.status !== 'text') this.state.documents.set(path, { status: 'loading' })
|
|
468
509
|
this.publish()
|
|
469
510
|
try {
|
|
470
511
|
const result = await this.reader.get(path, this.options.imagePreviewMode(), request.signal)
|
|
@@ -472,29 +513,47 @@ export class L3ProjectFileRuntime {
|
|
|
472
513
|
this.documentRequests.delete(path)
|
|
473
514
|
this.closeMissing.delete(path)
|
|
474
515
|
this.restoredTabs.delete(path)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
result.kind === 'text'
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
516
|
+
const unchanged =
|
|
517
|
+
current?.status === 'text' &&
|
|
518
|
+
result.kind === 'text' &&
|
|
519
|
+
current.content === result.content &&
|
|
520
|
+
current.size === result.size
|
|
521
|
+
if (!unchanged)
|
|
522
|
+
this.state.documents.set(
|
|
523
|
+
path,
|
|
524
|
+
result.kind === 'text'
|
|
525
|
+
? {
|
|
526
|
+
status: 'text',
|
|
527
|
+
content: result.content,
|
|
528
|
+
size: result.size,
|
|
529
|
+
editorState:
|
|
530
|
+
current?.status === 'text'
|
|
531
|
+
? current.editorState
|
|
532
|
+
: (this.restoredEditorStates.get(path) ?? null),
|
|
533
|
+
viewMode:
|
|
534
|
+
this.state.activePath === path && this.navigation
|
|
535
|
+
? 'source'
|
|
536
|
+
: current?.status === 'text'
|
|
537
|
+
? current.viewMode
|
|
538
|
+
: (this.restoredFileViewModes.get(path) ??
|
|
539
|
+
(getL3ProjectTextPreviewKind(path) === 'markdown' ||
|
|
540
|
+
(this.options.htmlPreview && getL3ProjectTextPreviewKind(path) === 'html')
|
|
541
|
+
? 'preview'
|
|
542
|
+
: 'source')),
|
|
543
|
+
previewScrollTop:
|
|
544
|
+
current?.status === 'text'
|
|
545
|
+
? current.previewScrollTop
|
|
546
|
+
: (this.restoredPreviewScrollTops.get(path) ?? 0)
|
|
547
|
+
}
|
|
548
|
+
: {
|
|
549
|
+
status: 'image',
|
|
550
|
+
mimeType: result.mimeType,
|
|
551
|
+
size: result.size,
|
|
552
|
+
objectUrl: URL.createObjectURL(createL4Base64ImageBlob(result))
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
if (this.state.activePath === path && result.kind !== 'text') this.navigation = null
|
|
556
|
+
this.releaseDocument(current)
|
|
498
557
|
} catch (cause) {
|
|
499
558
|
if (this.documentRequests.get(path) !== request) return
|
|
500
559
|
this.documentRequests.delete(path)
|
|
@@ -516,6 +575,8 @@ export class L3ProjectFileRuntime {
|
|
|
516
575
|
error: message(cause)
|
|
517
576
|
})
|
|
518
577
|
this.state.documents.set(path, { status: 'error', error: message(cause) })
|
|
578
|
+
this.releaseDocument(current)
|
|
579
|
+
if (this.state.activePath === path) this.navigation = null
|
|
519
580
|
}
|
|
520
581
|
this.publish()
|
|
521
582
|
}
|
|
@@ -703,6 +764,7 @@ export class L3ProjectFileRuntime {
|
|
|
703
764
|
this.publish()
|
|
704
765
|
}
|
|
705
766
|
private clearDiff(): void {
|
|
767
|
+
this.navigation = null
|
|
706
768
|
this.diffRequest?.abort()
|
|
707
769
|
this.diffRequest = null
|
|
708
770
|
this.state.activeDiff = null
|
|
@@ -712,6 +774,7 @@ export class L3ProjectFileRuntime {
|
|
|
712
774
|
this.searchRequest = null
|
|
713
775
|
}
|
|
714
776
|
private invalidateDocument(path: string): void {
|
|
777
|
+
if (this.state.activePath === path) this.navigation = null
|
|
715
778
|
this.documentRequests.get(path)?.abort()
|
|
716
779
|
this.documentRequests.delete(path)
|
|
717
780
|
const document = this.state.documents.get(path)
|
|
@@ -5,6 +5,7 @@ import * as contract from '@common/l3_modules/project-files/l3-project-files-con
|
|
|
5
5
|
import type {
|
|
6
6
|
L4ProjectFileList,
|
|
7
7
|
L4ProjectFileSearch,
|
|
8
|
+
L4ProjectSearchInput,
|
|
8
9
|
L4ProjectFileContent,
|
|
9
10
|
L4ProjectFileImagePreviewMode
|
|
10
11
|
} from '@common/l4_foundation/file/l4-project-file-contract'
|
|
@@ -62,7 +63,7 @@ export async function requestL3Project<I, O>(
|
|
|
62
63
|
|
|
63
64
|
export interface L3ProjectFileReader {
|
|
64
65
|
list(path: string, signal?: AbortSignal): Promise<L4ProjectFileList>
|
|
65
|
-
search(
|
|
66
|
+
search(input: L4ProjectSearchInput, signal?: AbortSignal): Promise<L4ProjectFileSearch>
|
|
66
67
|
get(
|
|
67
68
|
path: string,
|
|
68
69
|
imagePreviewMode: L4ProjectFileImagePreviewMode,
|
|
@@ -89,8 +90,8 @@ export function createL3ProjectReader(
|
|
|
89
90
|
return {
|
|
90
91
|
list: (path, signal) =>
|
|
91
92
|
requestL3Project(clientId, contract.L3ProjectFileList, { ...context, path }, signal),
|
|
92
|
-
search: (
|
|
93
|
-
requestL3Project(clientId, contract.L3ProjectFileSearch, { ...context,
|
|
93
|
+
search: (input, signal) =>
|
|
94
|
+
requestL3Project(clientId, contract.L3ProjectFileSearch, { ...context, ...input }, signal),
|
|
94
95
|
get: (path, imagePreviewMode, signal) =>
|
|
95
96
|
requestL3Project(
|
|
96
97
|
clientId,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { SearchIcon, XIcon } from 'lucide-react'
|
|
4
|
+
import { useEffect, useRef, useState, type Ref } from 'react'
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
import type { L4ProjectSearchInput } from '@common/l4_foundation/file/l4-project-file-contract'
|
|
7
|
+
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
8
|
+
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
9
|
+
import {
|
|
10
|
+
InputGroup,
|
|
11
|
+
InputGroupAddon,
|
|
12
|
+
InputGroupButton,
|
|
13
|
+
InputGroupInput
|
|
14
|
+
} from '@client/l4_foundation/ui/shadcn/input-group'
|
|
15
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@client/l4_foundation/ui/shadcn/tooltip'
|
|
16
|
+
|
|
17
|
+
type SearchOptions = Partial<Omit<L4ProjectSearchInput, 'query'>>
|
|
18
|
+
|
|
19
|
+
interface L3ProjectSearchControlsProps {
|
|
20
|
+
input: L4ProjectSearchInput
|
|
21
|
+
inputRef: Ref<HTMLInputElement>
|
|
22
|
+
invalid: boolean
|
|
23
|
+
onQueryChange: (query: string) => void
|
|
24
|
+
onOptionsChange: (options: SearchOptions) => void
|
|
25
|
+
onComposingChange: (composing: boolean) => void
|
|
26
|
+
onClose: () => void
|
|
27
|
+
onOpenFirst: () => void
|
|
28
|
+
onFocusResults: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SCOPES = ['all', 'files', 'content'] as const
|
|
32
|
+
const OPTIONS = [
|
|
33
|
+
{ key: 'caseSensitive', label: 'searchCaseSensitive', text: 'Aa' },
|
|
34
|
+
{ key: 'wholeWord', label: 'searchWholeWord', text: 'Ab' },
|
|
35
|
+
{ key: 'regex', label: 'searchRegex', text: '.*' }
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
export function L3ProjectSearchControls({
|
|
39
|
+
input,
|
|
40
|
+
inputRef,
|
|
41
|
+
invalid,
|
|
42
|
+
onQueryChange,
|
|
43
|
+
onOptionsChange,
|
|
44
|
+
onComposingChange,
|
|
45
|
+
onClose,
|
|
46
|
+
onOpenFirst,
|
|
47
|
+
onFocusResults
|
|
48
|
+
}: L3ProjectSearchControlsProps): React.JSX.Element {
|
|
49
|
+
const { t } = useTranslation('projectFiles')
|
|
50
|
+
const [composition, setComposition] = useState<string | null>(null)
|
|
51
|
+
const composing = useRef(false)
|
|
52
|
+
useEffect(() => () => onComposingChange(false), [onComposingChange])
|
|
53
|
+
const placeholder = t(
|
|
54
|
+
input.scope === 'files'
|
|
55
|
+
? 'searchFiles'
|
|
56
|
+
: input.scope === 'content'
|
|
57
|
+
? 'searchContent'
|
|
58
|
+
: 'searchAll'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="shrink-0 border-b px-2.5 py-2">
|
|
63
|
+
<div className="rounded-lg border border-input bg-card p-1 focus-within:border-ring focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-ring">
|
|
64
|
+
<div className="flex min-h-7 items-center justify-between gap-1">
|
|
65
|
+
<div
|
|
66
|
+
className="flex min-w-0 items-center gap-1"
|
|
67
|
+
role="radiogroup"
|
|
68
|
+
aria-label={t('searchScope')}
|
|
69
|
+
>
|
|
70
|
+
{SCOPES.map((scope, index) => (
|
|
71
|
+
<Button
|
|
72
|
+
key={scope}
|
|
73
|
+
type="button"
|
|
74
|
+
role="radio"
|
|
75
|
+
aria-checked={input.scope === scope}
|
|
76
|
+
tabIndex={input.scope === scope ? 0 : -1}
|
|
77
|
+
disabled={composition !== null}
|
|
78
|
+
variant="ghost"
|
|
79
|
+
size="sm"
|
|
80
|
+
data-testid={`project-search-scope-${scope}`}
|
|
81
|
+
className={cn(
|
|
82
|
+
'min-h-7 px-2 text-xs',
|
|
83
|
+
input.scope === scope && 'bg-accent text-accent-foreground'
|
|
84
|
+
)}
|
|
85
|
+
onClick={() => onOptionsChange({ scope })}
|
|
86
|
+
onKeyDown={(event) => {
|
|
87
|
+
const forward = event.key === 'ArrowRight' || event.key === 'ArrowDown'
|
|
88
|
+
const backward = event.key === 'ArrowLeft' || event.key === 'ArrowUp'
|
|
89
|
+
if (!forward && !backward && event.key !== 'Home' && event.key !== 'End') return
|
|
90
|
+
event.preventDefault()
|
|
91
|
+
const next =
|
|
92
|
+
event.key === 'Home'
|
|
93
|
+
? 0
|
|
94
|
+
: event.key === 'End'
|
|
95
|
+
? 2
|
|
96
|
+
: (index + (forward ? 1 : 2)) % 3
|
|
97
|
+
onOptionsChange({ scope: SCOPES[next] })
|
|
98
|
+
event.currentTarget.parentElement
|
|
99
|
+
?.querySelectorAll<HTMLButtonElement>('[role="radio"]')
|
|
100
|
+
[next]?.focus()
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{t(
|
|
104
|
+
scope === 'all'
|
|
105
|
+
? 'searchScopeAll'
|
|
106
|
+
: scope === 'files'
|
|
107
|
+
? 'searchScopeFiles'
|
|
108
|
+
: 'searchScopeContent'
|
|
109
|
+
)}
|
|
110
|
+
</Button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
<Tooltip>
|
|
114
|
+
<TooltipTrigger
|
|
115
|
+
render={
|
|
116
|
+
<Button
|
|
117
|
+
type="button"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
size="icon-sm"
|
|
120
|
+
className="size-7 shrink-0"
|
|
121
|
+
aria-label={t('closeSearch')}
|
|
122
|
+
onClick={onClose}
|
|
123
|
+
/>
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
<XIcon className="size-4" />
|
|
127
|
+
</TooltipTrigger>
|
|
128
|
+
<TooltipContent>{t('closeSearch')}</TooltipContent>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
</div>
|
|
131
|
+
<InputGroup className="min-h-8 rounded-none border-0 bg-transparent has-[[data-slot=input-group-control]:focus-visible]:outline-none">
|
|
132
|
+
<InputGroupAddon className="pl-1.5 py-0">
|
|
133
|
+
<SearchIcon className="size-3.5" />
|
|
134
|
+
</InputGroupAddon>
|
|
135
|
+
<InputGroupInput
|
|
136
|
+
ref={inputRef}
|
|
137
|
+
data-testid="project-file-search"
|
|
138
|
+
className="min-w-0 px-1.5 text-sm placeholder:text-xs"
|
|
139
|
+
value={composition ?? input.query}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
aria-label={placeholder}
|
|
142
|
+
aria-invalid={invalid || undefined}
|
|
143
|
+
autoComplete="off"
|
|
144
|
+
spellCheck={false}
|
|
145
|
+
onChange={(event) => {
|
|
146
|
+
if (composing.current) setComposition(event.target.value)
|
|
147
|
+
else onQueryChange(event.target.value)
|
|
148
|
+
}}
|
|
149
|
+
onCompositionStart={(event) => {
|
|
150
|
+
composing.current = true
|
|
151
|
+
setComposition(event.currentTarget.value)
|
|
152
|
+
onComposingChange(true)
|
|
153
|
+
}}
|
|
154
|
+
onCompositionEnd={(event) => {
|
|
155
|
+
composing.current = false
|
|
156
|
+
setComposition(null)
|
|
157
|
+
onQueryChange(event.currentTarget.value)
|
|
158
|
+
onComposingChange(false)
|
|
159
|
+
}}
|
|
160
|
+
onKeyDown={(event) => {
|
|
161
|
+
if (event.nativeEvent.isComposing || composing.current) return
|
|
162
|
+
if (event.key === 'Enter') {
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
onOpenFirst()
|
|
165
|
+
} else if (event.key === 'ArrowDown') {
|
|
166
|
+
event.preventDefault()
|
|
167
|
+
onFocusResults()
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
<InputGroupAddon align="inline-end" className="gap-0 pr-1 py-0">
|
|
172
|
+
{OPTIONS.map(({ key, label, text }) => (
|
|
173
|
+
<Tooltip key={key}>
|
|
174
|
+
<TooltipTrigger
|
|
175
|
+
render={
|
|
176
|
+
<InputGroupButton
|
|
177
|
+
size="icon-xs"
|
|
178
|
+
disabled={composition !== null}
|
|
179
|
+
aria-label={t(label)}
|
|
180
|
+
aria-pressed={input[key]}
|
|
181
|
+
data-testid={`project-search-option-${key}`}
|
|
182
|
+
className={cn(
|
|
183
|
+
'border border-transparent font-mono text-xs text-muted-foreground',
|
|
184
|
+
key === 'wholeWord' && 'underline underline-offset-4',
|
|
185
|
+
input[key] && 'border-input bg-accent text-accent-foreground'
|
|
186
|
+
)}
|
|
187
|
+
onClick={() => onOptionsChange({ [key]: !input[key] })}
|
|
188
|
+
/>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{text}
|
|
192
|
+
</TooltipTrigger>
|
|
193
|
+
<TooltipContent>{t(label)}</TooltipContent>
|
|
194
|
+
</Tooltip>
|
|
195
|
+
))}
|
|
196
|
+
</InputGroupAddon>
|
|
197
|
+
</InputGroup>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|