Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 46 additions & 5 deletions src/features/editor/components/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,14 @@ export function Editor({
const vimModeEnabled = useSettingsStore((state) => state.settings.vimMode);
const setIsFindVisible = useUIState((state) => state.setIsFindVisible);
const aiCompletionEnabled = useSettingsStore((state) => state.settings.aiCompletion);
const aiAutocompleteProvider = useSettingsStore((state) => state.settings.aiAutocompleteProvider);
const aiAutocompleteModelId = useSettingsStore((state) => state.settings.aiAutocompleteModelId);
const aiAutocompleteCustomBaseUrl = useSettingsStore(
(state) => state.settings.aiAutocompleteCustomBaseUrl,
);
const aiAutocompleteCustomModelId = useSettingsStore(
(state) => state.settings.aiAutocompleteCustomModelId,
);
const inlineGitBlameEnabled = useSettingsStore((state) => state.settings.enableInlineGitBlame);
const gitGutterEnabled = useSettingsStore((state) => state.settings.enableGitGutter);
const vimMode = useVimStore.use.mode();
Expand Down Expand Up @@ -723,7 +730,10 @@ export function Editor({

useAutocomplete({
enabled: aiCompletionEnabled && !isPreviewMode && !readOnly,
model: aiAutocompleteModelId,
provider: aiAutocompleteProvider,
model:
aiAutocompleteProvider === "custom" ? aiAutocompleteCustomModelId : aiAutocompleteModelId,
customBaseUrl: aiAutocompleteCustomBaseUrl,
filePath: filePath || null,
languageId: filePath ? getLanguageId(filePath) : null,
content,
Expand Down Expand Up @@ -1083,12 +1093,26 @@ export function Editor({

const lineText = lines[visualCursorLine] || "";
const cursorColumn = Math.min(cursorPosition.column, lineText.length);
const textAfterCursorOnLine = lineText.slice(cursorColumn);
if (textAfterCursorOnLine.trim().length > 0) return null;

const cursorX = getAccurateCursorX(lineText, cursorColumn, fontSize, fontFamily, tabSize);
const previewLines: Array<{ text: string; index: number }> = [];

for (const [index, text] of normalized.split("\n").entries()) {
if (index > 0 && lines[visualCursorLine + index]?.trim()) {
break;
}
previewLines.push({ text, index });
}

if (previewLines.every((line) => line.text.length === 0)) return null;

return {
text: normalized,
lines: previewLines,
top: visualCursorLine * lineHeight + EDITOR_CONSTANTS.EDITOR_PADDING_TOP,
left: cursorX + EDITOR_CONSTANTS.EDITOR_PADDING_LEFT,
firstLineLeft: cursorX + EDITOR_CONSTANTS.EDITOR_PADDING_LEFT,
continuationLeft: EDITOR_CONSTANTS.EDITOR_PADDING_LEFT,
};
}, [
autocompleteCompletion,
Expand Down Expand Up @@ -1330,7 +1354,7 @@ export function Editor({
style={{
position: "absolute",
top: `${inlineAutocompletePreview.top}px`,
left: `${inlineAutocompletePreview.left}px`,
left: 0,
fontSize: `${fontSize}px`,
fontFamily,
lineHeight: `${lineHeight}px`,
Expand All @@ -1339,7 +1363,24 @@ export function Editor({
color: "var(--text-lighter, #94a3b8)",
}}
>
{inlineAutocompletePreview.text}
{inlineAutocompletePreview.lines.map((line) => {
if (line.text.length === 0) return null;
return (
<div
key={line.index}
style={{
position: "absolute",
top: `${line.index * lineHeight}px`,
left:
line.index === 0
? `${inlineAutocompletePreview.firstLineLeft}px`
: `${inlineAutocompletePreview.continuationLeft}px`,
}}
>
{line.text}
</div>
);
})}
</div>
</div>
)}
Expand Down
74 changes: 30 additions & 44 deletions src/features/editor/hooks/use-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {

interface UseAutocompleteOptions {
enabled: boolean;
provider: "openrouter" | "custom";
model: string;
customBaseUrl: string;
filePath: string | null;
languageId: string | null;
content: string;
Expand All @@ -24,16 +26,6 @@ const COMPLETION_OVERLAP_SCAN_LIMIT = 256;

const WORD_LIKE_TRIGGER_REGEX = /[\w\]})>"'`.]/;
const CONTEXT_FOLLOWUP_TRIGGER_REGEX = /[\w\]})>"'`.{;=:[\],(]/;
const DEBUG_AUTOCOMPLETE = false;

function debugLog(message: string, payload?: Record<string, unknown>) {
if (!DEBUG_AUTOCOMPLETE) return;
if (payload) {
console.log(`[Autocomplete] ${message}`, payload);
return;
}
console.log(`[Autocomplete] ${message}`);
}

function isWhitespace(char: string): boolean {
return char === " " || char === "\t" || char === "\n" || char === "\r";
Expand Down Expand Up @@ -107,7 +99,9 @@ function shouldTriggerForCharacter(content: string, cursorOffset: number): boole

export function useAutocomplete({
enabled,
provider,
model,
customBaseUrl,
filePath,
languageId,
content,
Expand All @@ -129,6 +123,7 @@ export function useAutocomplete({
const managedPolicy = enterprisePolicy?.managedMode ? enterprisePolicy : null;
const isPro = subscriptionStatus === "pro";
const useByok = managedPolicy ? managedPolicy.allowByok && !isPro : !isPro;
const needsAthasAuth = provider !== "custom";

useEffect(() => {
return () => {
Expand All @@ -152,24 +147,14 @@ export function useAutocomplete({

if (
!enabled ||
!isAuthenticated ||
(needsAthasAuth && !isAuthenticated) ||
(managedPolicy ? !managedPolicy.aiCompletionEnabled : false) ||
!model.trim() ||
(provider === "custom" && !customBaseUrl.trim()) ||
hasActiveFolds ||
lastInputTimestamp === 0 ||
cursorOffset <= 0
) {
if (didUserType) {
debugLog("skip-prereq", {
enabled,
isAuthenticated,
subscriptionStatus,
enterpriseManaged: Boolean(managedPolicy),
aiCompletionEnabled: managedPolicy ? managedPolicy.aiCompletionEnabled : true,
hasActiveFolds,
lastInputTimestamp,
cursorOffset,
});
}
setAutocompleteCompletion(null);
return;
}
Expand All @@ -181,12 +166,6 @@ export function useAutocomplete({
}

if (!shouldTriggerForCharacter(content, cursorOffset)) {
const previousSignificantChar = getPreviousNonWhitespaceChar(content, cursorOffset - 2);
debugLog("skip-trigger-char", {
charBeforeCursor: content[cursorOffset - 1] || "",
previousChar: content[cursorOffset - 2] || "",
previousSignificantChar,
});
setAutocompleteCompletion(null);
return;
}
Expand All @@ -209,13 +188,6 @@ export function useAutocomplete({
abortControllerRef.current = abortController;

try {
debugLog("request", {
model,
filePath: filePath || "untitled",
languageId: languageId || "unknown",
cursorOffset,
});

const result = await requestAutocomplete(
{
model,
Expand All @@ -224,7 +196,25 @@ export function useAutocomplete({
filePath: filePath || undefined,
languageId: languageId || undefined,
},
{ useByok },
{
useByok: provider === "custom" ? false : useByok,
provider,
customBaseUrl,
onChunk: (partialCompletion) => {
if (abortController.signal.aborted || requestIdRef.current !== requestId) {
return;
}

const normalizedText = normalizeCompletionText(
partialCompletion,
beforeCursor,
afterCursor,
);
if (!normalizedText) return;

setAutocompleteCompletion({ text: normalizedText, cursorOffset });
},
},
);

if (abortController.signal.aborted || requestIdRef.current !== requestId) {
Expand All @@ -233,22 +223,16 @@ export function useAutocomplete({

const text = result.completion;
if (!text) {
debugLog("empty-completion");
setAutocompleteCompletion(null);
return;
}

const normalizedText = normalizeCompletionText(text, beforeCursor, afterCursor);
if (!normalizedText) {
debugLog("empty-normalized-completion");
setAutocompleteCompletion(null);
return;
}

debugLog("suggestion-ready", {
rawLength: text.length,
normalizedLength: normalizedText.length,
});
setAutocompleteCompletion({ text: normalizedText, cursorOffset });
} catch (error) {
if (abortController.signal.aborted || requestIdRef.current !== requestId) {
Expand Down Expand Up @@ -276,16 +260,18 @@ export function useAutocomplete({
};
}, [
enabled,
provider,
needsAthasAuth,
isAuthenticated,
managedPolicy,
useByok,
subscriptionStatus,
filePath,
hasActiveFolds,
lastInputTimestamp,
cursorOffset,
content,
model,
customBaseUrl,
languageId,
setAutocompleteCompletion,
]);
Expand Down
Loading
Loading