-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathwatcher.js
More file actions
165 lines (147 loc) · 5.71 KB
/
Copy pathwatcher.js
File metadata and controls
165 lines (147 loc) · 5.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import fs from 'node:fs';
import path from 'node:path';
import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
import { info } from '../../infrastructure/logger.js';
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../shared/constants.js';
import { DbError } from '../../shared/errors.js';
import { createParseTreeCache, getActiveEngine } from '../parser.js';
import { rebuildFile } from './builder/incremental.js';
import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
import { appendJournalEntries } from './journal.js';
function shouldIgnore(filePath) {
const parts = filePath.split(path.sep);
return parts.some((p) => IGNORE_DIRS.has(p));
}
function isTrackedExt(filePath) {
return EXTENSIONS.has(path.extname(filePath));
}
export async function watchProject(rootDir, opts = {}) {
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
if (!fs.existsSync(dbPath)) {
throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
}
const db = openDb(dbPath);
initSchema(db);
const engineOpts = { engine: opts.engine || 'auto' };
const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
console.log(
`Watch mode using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`,
);
const cache = createParseTreeCache();
console.log(
cache
? 'Incremental parsing enabled (native tree cache)'
: 'Incremental parsing unavailable (full re-parse)',
);
const stmts = {
insertNode: db.prepare(
'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
),
getNodeId: {
get: (name, kind, file, line) => {
const id = getNodeIdQuery(db, name, kind, file, line);
return id != null ? { id } : undefined;
},
},
insertEdge: db.prepare(
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
),
deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
deleteEdgesForFile: null,
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
countEdgesForFile: null,
findNodeInFile: db.prepare(
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
),
findNodeByName: db.prepare(
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')",
),
listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
};
// Use named params for statements needing the same value twice
const origDeleteEdges = db.prepare(
`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
);
const origCountEdges = db.prepare(
`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
);
stmts.deleteEdgesForFile = { run: (f) => origDeleteEdges.run({ f }) };
stmts.countEdgesForFile = { get: (f) => origCountEdges.get({ f }) };
const pending = new Set();
let timer = null;
const DEBOUNCE_MS = 300;
async function processPending() {
const files = [...pending];
pending.clear();
const results = [];
for (const filePath of files) {
const result = await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
diffSymbols,
});
if (result) results.push(result);
}
const updates = results;
// Append processed files to journal for Tier 0 detection on next build
if (updates.length > 0) {
const entries = updates.map((r) => ({
file: r.file,
deleted: r.deleted || false,
}));
try {
appendJournalEntries(rootDir, entries);
} catch {
/* journal write failure is non-fatal */
}
const changeEvents = updates.map((r) =>
buildChangeEvent(r.file, r.event, r.symbolDiff, {
nodesBefore: r.nodesBefore,
nodesAfter: r.nodesAfter,
edgesAdded: r.edgesAdded,
}),
);
try {
appendChangeEvents(rootDir, changeEvents);
} catch {
/* change event write failure is non-fatal */
}
}
for (const r of updates) {
const nodeDelta = r.nodesAdded - r.nodesRemoved;
const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
if (r.deleted) {
info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
} else {
info(`Updated: ${r.file} (${nodeStr} nodes, +${r.edgesAdded} edges)`);
}
}
}
console.log(`Watching ${rootDir} for changes...`);
console.log('Press Ctrl+C to stop.\n');
const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
if (!filename) return;
if (shouldIgnore(filename)) return;
if (!isTrackedExt(filename)) return;
const fullPath = path.join(rootDir, filename);
pending.add(fullPath);
if (timer) clearTimeout(timer);
timer = setTimeout(processPending, DEBOUNCE_MS);
});
process.on('SIGINT', () => {
console.log('\nStopping watcher...');
watcher.close();
// Flush any pending file paths to journal before exit
if (pending.size > 0) {
const entries = [...pending].map((filePath) => ({
file: normalizePath(path.relative(rootDir, filePath)),
}));
try {
appendJournalEntries(rootDir, entries);
} catch {
/* best-effort */
}
}
if (cache) cache.clear();
closeDb(db);
process.exit(0);
});
}