feat: add DSH IDE open-file references
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { basename, isAbsolute } from 'node:path'
|
||||
import type { IdeSnapshot, OpenFileItem, OpenFileResponse, SnapshotDocument } from './protocol.js'
|
||||
|
||||
const MAX_TEXT = 32_768
|
||||
const MAX_DOCUMENTS = 10_000
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
||||
const keys = new Set(allowed)
|
||||
return Object.keys(value).every((key) => keys.has(key))
|
||||
}
|
||||
|
||||
function boundedString(value: unknown, max = MAX_TEXT): value is string {
|
||||
return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\0')
|
||||
}
|
||||
|
||||
function parseDocument(value: unknown): SnapshotDocument | undefined {
|
||||
if (!isRecord(value) || !hasOnlyKeys(value, ['path', 'label', 'dirty']) || !boundedString(value.path) || !isAbsolute(value.path)) return undefined
|
||||
if (!boundedString(value.label, 1024) || typeof value.dirty !== 'boolean') return undefined
|
||||
return { path: value.path, label: value.label, dirty: value.dirty }
|
||||
}
|
||||
|
||||
export function parseSnapshot(value: unknown, now = Date.now(), maxAgeMs = 60_000): IdeSnapshot | undefined {
|
||||
if (!isRecord(value) || !hasOnlyKeys(value, ['version', 'instanceId', 'ide', 'processId', 'workspace', 'updatedAt', 'documents']) || value.version !== 1) return undefined
|
||||
if (!boundedString(value.instanceId, 128)) return undefined
|
||||
if (value.ide !== 'vscode' && value.ide !== 'visualstudio') return undefined
|
||||
if (!Number.isSafeInteger(value.processId) || (value.processId as number) <= 0) return undefined
|
||||
if (value.workspace !== undefined && !boundedString(value.workspace)) return undefined
|
||||
if (!boundedString(value.updatedAt, 128) || !Array.isArray(value.documents) || value.documents.length > MAX_DOCUMENTS) return undefined
|
||||
const updated = Date.parse(value.updatedAt)
|
||||
if (!Number.isFinite(updated) || now - updated > maxAgeMs || updated - now > 300_000) return undefined
|
||||
|
||||
const documents: SnapshotDocument[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const raw of value.documents) {
|
||||
const document = parseDocument(raw)
|
||||
if (!document) continue
|
||||
const key = document.path.toLocaleLowerCase('en-US')
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
documents.push(document)
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
instanceId: value.instanceId,
|
||||
ide: value.ide,
|
||||
processId: value.processId as number,
|
||||
...(value.workspace === undefined ? {} : { workspace: value.workspace }),
|
||||
updatedAt: value.updatedAt,
|
||||
documents,
|
||||
}
|
||||
}
|
||||
|
||||
function score(item: OpenFileItem, query: string): number {
|
||||
if (!query) return 0
|
||||
const q = query.toLocaleLowerCase('en-US')
|
||||
const label = item.label.toLocaleLowerCase('en-US')
|
||||
const path = item.path.toLocaleLowerCase('en-US')
|
||||
if (label === q) return 0
|
||||
if (label.startsWith(q)) return 1
|
||||
if (label.includes(q)) return 2
|
||||
if (path.includes(q)) return 3
|
||||
return 10
|
||||
}
|
||||
|
||||
export function combineSnapshots(snapshots: readonly IdeSnapshot[], query = '', limit = 100): OpenFileResponse {
|
||||
const normalizedQuery = query.trim().slice(0, 256)
|
||||
const byPath = new Map<string, OpenFileItem>()
|
||||
for (const snapshot of snapshots) {
|
||||
for (const document of snapshot.documents) {
|
||||
const key = document.path.toLocaleLowerCase('en-US')
|
||||
const current = byPath.get(key)
|
||||
const next: OpenFileItem = {
|
||||
...document,
|
||||
ide: snapshot.ide,
|
||||
instanceId: snapshot.instanceId,
|
||||
...(snapshot.workspace === undefined ? {} : { workspace: snapshot.workspace }),
|
||||
instanceOpenCount: snapshot.documents.length,
|
||||
}
|
||||
if (!current || (!current.dirty && next.dirty)) byPath.set(key, next)
|
||||
}
|
||||
}
|
||||
|
||||
const matching = [...byPath.values()].filter((item) => score(item, normalizedQuery) < 10)
|
||||
matching.sort((a, b) => score(a, normalizedQuery) - score(b, normalizedQuery)
|
||||
|| a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })
|
||||
|| a.path.localeCompare(b.path, undefined, { sensitivity: 'base' }))
|
||||
const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? Math.floor(limit) : 100, 200))
|
||||
return { total: matching.length, instances: snapshots.length, items: matching.slice(0, safeLimit) }
|
||||
}
|
||||
|
||||
export function displayLabel(path: string): string {
|
||||
return basename(path) || path
|
||||
}
|
||||
Reference in New Issue
Block a user