-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinlineComplete.js
More file actions
155 lines (136 loc) · 6.44 KB
/
Copy pathinlineComplete.js
File metadata and controls
155 lines (136 loc) · 6.44 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
/*---------------------------------------------------------------------------------------------
* LevelCode — AI (M2, feature 2: inline tab-completion)
*
* Ghost-text completions as you type, accepted with Tab. Uses VS Code's stable
* InlineCompletionItemProvider API (no core patches). Requests go directly to the
* provider (Claude / Ollama) — debounced, and cancelled the moment you keep typing.
*--------------------------------------------------------------------------------------------*/
// @ts-check
'use strict';
const vscode = require('vscode');
const PREFIX_MAX = 2500; // chars of context before the cursor (smaller = faster first token)
const SUFFIX_MAX = 1000; // chars of context after the cursor
const MAX_TOKENS = 160; // keep completions short & snappy
const SYSTEM_PROMPT =
'You are a code completion engine inside the LevelCode editor. You are given the code ' +
'before the cursor and the code after it, with the insertion point marked <CURSOR>. ' +
'Reply with ONLY the raw code that should be inserted at <CURSOR> to continue the code ' +
'naturally. Do NOT repeat code that already exists before or after the cursor. Do NOT ' +
'wrap your answer in markdown fences. Do NOT add explanations. If no completion is ' +
'appropriate, reply with an empty string.';
/** @param {vscode.CancellationToken} token */
function sleep(ms, token) {
return new Promise((resolve) => {
const t = setTimeout(resolve, ms);
token.onCancellationRequested(() => { clearTimeout(t); resolve(undefined); });
});
}
/** Build prefix/suffix context windows around the cursor. */
function buildContext(document, position) {
const start = new vscode.Position(0, 0);
const lastLine = document.lineAt(document.lineCount - 1);
const end = lastLine.range.end;
const pre = document.getText(new vscode.Range(start, position));
const suf = document.getText(new vscode.Range(position, end));
return { prefix: pre.slice(-PREFIX_MAX), suffix: suf.slice(0, SUFFIX_MAX) };
}
/** Strip accidental markdown fences / leading language tags the model may add. */
function clean(text) {
if (!text) { return ''; }
let t = text;
const fence = String.fromCharCode(96, 96, 96);
if (t.indexOf(fence) >= 0) {
// Take the content of the first fenced block if the whole thing got wrapped.
const m = t.match(new RegExp(fence + '[a-zA-Z0-9_+-]*\\n?([\\s\\S]*?)' + fence));
if (m) { t = m[1]; }
else { t = t.split(fence).join(''); }
}
return t;
}
/**
* @param {vscode.ExtensionContext} context
* @param {{ aiConfig: () => vscode.WorkspaceConfiguration,
* prepProviderRequest: (o?:any) => Promise<any>,
* complete: Function, fastCompletionModel: (providerId:string)=>(string|null) }} deps
*/
function registerInlineComplete(context, deps) {
const { aiConfig, prepProviderRequest, complete, fastCompletionModel } = deps;
// --- status bar toggle ---------------------------------------------------
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 50);
status.command = 'levelcode.ai.toggleCompletions';
function refreshStatus() {
const on = aiConfig().get('completions.enabled', true);
status.text = on ? '$(sparkle) AI' : '$(circle-slash) AI';
status.tooltip = on
? 'LevelCode AI inline completions: ON (click to disable)'
: 'LevelCode AI inline completions: OFF (click to enable)';
status.show();
}
refreshStatus();
context.subscriptions.push(
status,
vscode.commands.registerCommand('levelcode.ai.toggleCompletions', async () => {
const cfg = aiConfig();
const next = !cfg.get('completions.enabled', true);
await cfg.update('completions.enabled', next, vscode.ConfigurationTarget.Global);
refreshStatus();
vscode.window.setStatusBarMessage('LevelCode AI completions ' + (next ? 'enabled' : 'disabled'), 2000);
}),
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('levelcode.ai.completions')) { refreshStatus(); }
})
);
// --- the provider --------------------------------------------------------
/** @type {vscode.InlineCompletionItemProvider} */
const provider = {
async provideInlineCompletionItems(document, position, ctx, token) {
const cfg = aiConfig();
if (!cfg.get('completions.enabled', true)) { return null; }
if (document.uri.scheme !== 'file' && document.uri.scheme !== 'untitled') { return null; }
// Debounce: if the user keeps typing, this token is cancelled and we bail.
const debounce = Math.max(0, cfg.get('completions.debounce', 150));
await sleep(debounce, token);
if (token.isCancellationRequested) { return null; }
const { prefix, suffix } = buildContext(document, position);
if (!prefix.trim() && !suffix.trim()) { return null; }
const userContent =
'Language: ' + (document.languageId || 'plaintext') + '\n' +
'Insert code at <CURSOR>. Reply with ONLY the text to insert.\n\n' +
prefix + '<CURSOR>' + suffix;
const ac = new AbortController();
const sub = token.onCancellationRequested(() => ac.abort());
let text = '';
try {
// Silent (prompt:false): never pop a key dialog while the user is typing.
const req = await prepProviderRequest({ prompt: false });
if (!req.ok || token.isCancellationRequested) { return null; }
// Claude uses the configurable completions model; other providers use their curated fast
// model (gpt-4o-mini, llama-3.1-8b-instant, …) when one exists, else the active chat model.
const model = req.providerId === 'claude'
? cfg.get('completions.model', 'claude-haiku-4-5-20251001')
: (fastCompletionModel(req.providerId) || req.model);
text = await complete({
providerId: req.providerId, apiKey: req.apiKey, baseURL: req.baseURL,
model, maxTokens: MAX_TOKENS, system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: userContent }], signal: ac.signal
});
} catch (e) {
// Network/abort/key errors are silent — inline completion must never nag.
return null;
} finally {
sub.dispose();
}
if (token.isCancellationRequested) { return null; }
const insert = clean(text);
if (!insert.trim()) { return null; }
// Avoid suggesting text that just duplicates what's already after the cursor.
if (suffix.startsWith(insert)) { return null; }
const item = new vscode.InlineCompletionItem(insert, new vscode.Range(position, position));
return [item];
}
};
context.subscriptions.push(
vscode.languages.registerInlineCompletionItemProvider({ pattern: '**' }, provider)
);
}
module.exports = { registerInlineComplete };