-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfimCompletionService.js
More file actions
111 lines (99 loc) · 3.64 KB
/
Copy pathfimCompletionService.js
File metadata and controls
111 lines (99 loc) · 3.64 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
'use strict';
/**
* FIM (Fill-In-the-Middle) Tab completion — prefix/suffix aware, fast path.
* Uses dedicated FIM prompt template; avoids full chat history.
*/
class FimCompletionService {
constructor(chatEngine) {
this._chatEngine = chatEngine;
this._cache = new Map();
this._cacheMax = 200;
}
_cacheKey(ctx) {
const { prefix = '', suffix = '', language = '' } = ctx || {};
return `${language}:${prefix.slice(-120)}|${suffix.slice(0, 80)}`;
}
/**
* FIM-style completion — insert text between prefix and suffix.
*/
async complete(ctx) {
const { prefix = '', suffix = '', language = 'text', filePath } = ctx || {};
const key = this._cacheKey(ctx);
if (this._cache.has(key)) return this._cache.get(key);
if (!this._chatEngine?.isReady) {
const h = this._heuristicFim(prefix, suffix, language);
return h;
}
try {
const fimPrompt = [
'You are a code completion engine. Fill in the middle between PREFIX and SUFFIX.',
'Output ONLY the text to insert at the cursor. No quotes, markdown, or explanation.',
'Stop at a natural boundary; do not repeat PREFIX or SUFFIX.',
filePath ? `File: ${filePath}` : '',
`Language: ${language}`,
'',
'<|fim_prefix|>',
prefix.slice(-1200),
'<|fim_suffix|>',
suffix.slice(0, 400),
'<|fim_middle|>',
].filter(Boolean).join('\n');
const result = await this._chatEngine.completeOnce(fimPrompt, {
maxTokens: 96,
temperature: 0.1,
});
let text = (result?.text || '').trim();
text = this._sanitizeFimOutput(text, prefix, suffix);
if (text && text.length <= 800) {
const out = { text };
this._setCache(key, out);
return out;
}
} catch (_) {}
return this._heuristicFim(prefix, suffix, language);
}
_sanitizeFimOutput(text, prefix, suffix) {
if (!text) return '';
if (text.startsWith('```')) {
text = text.replace(/^```[\w]*\n?/, '').replace(/\n?```$/, '').trim();
}
if (suffix && text.endsWith(suffix.slice(0, Math.min(20, suffix.length)))) {
text = text.slice(0, -Math.min(20, suffix.length));
}
const plines = prefix.split('\n');
const lastPrefixLine = plines[plines.length - 1] || '';
if (text.startsWith(lastPrefixLine) && lastPrefixLine.length > 3) {
text = text.slice(lastPrefixLine.length);
}
return text.trimEnd();
}
_heuristicFim(prefix, suffix, language) {
const lines = prefix.split('\n');
const current = lines[lines.length - 1] || '';
const trimmed = current.trimStart();
const indent = current.slice(0, current.length - trimmed.length);
if (language === 'javascript' || language === 'typescript') {
if (trimmed.endsWith('console.')) return { text: 'log()' };
if (trimmed.endsWith('import ')) return { text: " { } from '';" };
if (trimmed.endsWith('const ')) return { text: 'name = ' };
if (trimmed.endsWith('function ')) return { text: 'name() {\n \n}' };
}
if (language === 'python') {
if (trimmed.endsWith('def ')) return { text: 'name():\n pass' };
if (trimmed === 'import ') return { text: 'module' };
}
if (trimmed.endsWith('{') && suffix.trimStart().startsWith('}')) {
return { text: `\n${indent} \n${indent}` };
}
if (suffix.startsWith(')') && trimmed.endsWith('(')) return { text: '' };
return null;
}
_setCache(key, val) {
if (this._cache.size >= this._cacheMax) {
const first = this._cache.keys().next().value;
this._cache.delete(first);
}
this._cache.set(key, val);
}
}
module.exports = { FimCompletionService };