99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
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
|
||
|
|
}
|