745 lines
22 KiB
TypeScript
745 lines
22 KiB
TypeScript
import ignore from "ignore";
|
|
import OpenAI from "openai";
|
|
import path from "path";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { RangeInFileWithContents } from "../commands/util.js";
|
|
import { ConfigHandler } from "../config/ConfigHandler.js";
|
|
import { TRIAL_FIM_MODEL } from "../config/onboarding.js";
|
|
import { streamLines } from "../diff/util.js";
|
|
import {
|
|
IDE,
|
|
ILLM,
|
|
ModelProvider,
|
|
Position,
|
|
Range,
|
|
TabAutocompleteOptions,
|
|
} from "../index.js";
|
|
import { logDevData } from "../util/devdata.js";
|
|
import { getBasename, getLastNPathParts } from "../util/index.js";
|
|
import {
|
|
COUNT_COMPLETION_REJECTED_AFTER,
|
|
DEFAULT_AUTOCOMPLETE_OPTS,
|
|
} from "../util/parameters.js";
|
|
import { Telemetry } from "../util/posthog.js";
|
|
import { getRangeInString } from "../util/ranges.js";
|
|
import { ImportDefinitionsService } from "./ImportDefinitionsService.js";
|
|
import { BracketMatchingService } from "./brackets.js";
|
|
import AutocompleteLruCache from "./cache.js";
|
|
import {
|
|
noFirstCharNewline,
|
|
onlyWhitespaceAfterEndOfLine,
|
|
stopAtStopTokens,
|
|
} from "./charStream.js";
|
|
import {
|
|
constructAutocompletePrompt,
|
|
languageForFilepath,
|
|
} from "./constructPrompt.js";
|
|
import { isOnlyPunctuationAndWhitespace } from "./filter.js";
|
|
import { AutocompleteLanguageInfo } from "./languages.js";
|
|
import {
|
|
avoidPathLine,
|
|
noTopLevelKeywordsMidline,
|
|
skipPrefixes,
|
|
stopAtLines,
|
|
stopAtRepeatingLines,
|
|
stopAtSimilarLine,
|
|
streamWithNewLines,
|
|
} from "./lineStream.js";
|
|
import { postprocessCompletion } from "./postprocessing.js";
|
|
import { AutocompleteSnippet } from "./ranking.js";
|
|
import { RecentlyEditedRange } from "./recentlyEdited.js";
|
|
import { getTemplateForModel } from "./templates.js";
|
|
import { GeneratorReuseManager } from "./util.js";
|
|
// @prettier-ignore
|
|
import Handlebars from "handlebars";
|
|
|
|
export interface AutocompleteInput {
|
|
completionId: string;
|
|
filepath: string;
|
|
pos: Position;
|
|
recentlyEditedFiles: RangeInFileWithContents[];
|
|
recentlyEditedRanges: RecentlyEditedRange[];
|
|
clipboardText: string;
|
|
// Used for notebook files
|
|
manuallyPassFileContents?: string;
|
|
// Used for VS Code git commit input box
|
|
manuallyPassPrefix?: string;
|
|
selectedCompletionInfo?: {
|
|
text: string;
|
|
range: Range;
|
|
};
|
|
injectDetails?: string;
|
|
}
|
|
|
|
export interface AutocompleteOutcome extends TabAutocompleteOptions {
|
|
accepted?: boolean;
|
|
time: number;
|
|
prefix: string;
|
|
suffix: string;
|
|
prompt: string;
|
|
completion: string;
|
|
modelProvider: string;
|
|
modelName: string;
|
|
completionOptions: any;
|
|
cacheHit: boolean;
|
|
filepath: string;
|
|
gitRepo?: string;
|
|
completionId: string;
|
|
uniqueId: string;
|
|
}
|
|
|
|
const autocompleteCache = AutocompleteLruCache.get();
|
|
|
|
const DOUBLE_NEWLINE = "\n\n";
|
|
const WINDOWS_DOUBLE_NEWLINE = "\r\n\r\n";
|
|
const SRC_DIRECTORY = "/src/";
|
|
// Starcoder2 tends to output artifacts starting with the letter "t"
|
|
const STARCODER2_T_ARTIFACTS = ["t.", "\nt", "<file_sep>"];
|
|
const PYTHON_ENCODING = "#- coding: utf-8";
|
|
const CODE_BLOCK_END = "```";
|
|
|
|
const multilineStops: string[] = [DOUBLE_NEWLINE, WINDOWS_DOUBLE_NEWLINE];
|
|
const commonStops = [SRC_DIRECTORY, PYTHON_ENCODING, CODE_BLOCK_END];
|
|
|
|
// Errors that can be expected on occasion even during normal functioning should not be shown.
|
|
// Not worth disrupting the user to tell them that a single autocomplete request didn't go through
|
|
const ERRORS_TO_IGNORE = [
|
|
// From Ollama
|
|
"unexpected server status",
|
|
];
|
|
|
|
function formatExternalSnippet(
|
|
filepath: string,
|
|
snippet: string,
|
|
language: AutocompleteLanguageInfo,
|
|
) {
|
|
const comment = language.singleLineComment;
|
|
const lines = [
|
|
`${comment} Path: ${getBasename(filepath)}`,
|
|
...snippet
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => `${comment} ${line}`),
|
|
comment,
|
|
];
|
|
return lines.join("\n");
|
|
}
|
|
|
|
let shownGptClaudeWarning = false;
|
|
const nonAutocompleteModels = [
|
|
// "gpt",
|
|
// "claude",
|
|
"mistral",
|
|
"instruct",
|
|
];
|
|
|
|
export type GetLspDefinitionsFunction = (
|
|
filepath: string,
|
|
contents: string,
|
|
cursorIndex: number,
|
|
ide: IDE,
|
|
lang: AutocompleteLanguageInfo,
|
|
) => Promise<AutocompleteSnippet[]>;
|
|
|
|
export class CompletionProvider {
|
|
private static debounceTimeout: NodeJS.Timeout | undefined = undefined;
|
|
private static debouncing = false;
|
|
private static lastUUID: string | undefined = undefined;
|
|
|
|
constructor(
|
|
private readonly configHandler: ConfigHandler,
|
|
private readonly ide: IDE,
|
|
private readonly getLlm: () => Promise<ILLM | undefined>,
|
|
private readonly _onError: (e: any) => void,
|
|
private readonly getDefinitionsFromLsp: GetLspDefinitionsFunction,
|
|
) {
|
|
this.generatorReuseManager = new GeneratorReuseManager(
|
|
this.onError.bind(this),
|
|
);
|
|
this.importDefinitionsService = new ImportDefinitionsService(this.ide);
|
|
}
|
|
|
|
private importDefinitionsService: ImportDefinitionsService;
|
|
private generatorReuseManager: GeneratorReuseManager;
|
|
private autocompleteCache = AutocompleteLruCache.get();
|
|
public errorsShown: Set<string> = new Set();
|
|
private bracketMatchingService = new BracketMatchingService();
|
|
// private nearbyDefinitionsService = new NearbyDefinitionsService();
|
|
|
|
private onError(e: any) {
|
|
console.warn("Error generating autocompletion: ", e);
|
|
if (
|
|
ERRORS_TO_IGNORE.some((err) =>
|
|
typeof e === "string" ? e.includes(err) : e?.message?.includes(err),
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (!this.errorsShown.has(e.message)) {
|
|
this.errorsShown.add(e.message);
|
|
this._onError(e);
|
|
}
|
|
}
|
|
|
|
public cancel() {
|
|
this._abortControllers.forEach((abortController, id) => {
|
|
abortController.abort();
|
|
});
|
|
this._abortControllers.clear();
|
|
}
|
|
|
|
// Key is completionId
|
|
private _abortControllers = new Map<string, AbortController>();
|
|
private _logRejectionTimeouts = new Map<string, NodeJS.Timeout>();
|
|
private _outcomes = new Map<string, AutocompleteOutcome>();
|
|
|
|
public accept(completionId: string) {
|
|
if (this._logRejectionTimeouts.has(completionId)) {
|
|
clearTimeout(this._logRejectionTimeouts.get(completionId));
|
|
this._logRejectionTimeouts.delete(completionId);
|
|
}
|
|
|
|
if (this._outcomes.has(completionId)) {
|
|
const outcome = this._outcomes.get(completionId)!;
|
|
outcome.accepted = true;
|
|
logDevData("autocomplete", outcome);
|
|
Telemetry.capture(
|
|
"autocomplete",
|
|
{
|
|
accepted: outcome.accepted,
|
|
modelName: outcome.modelName,
|
|
modelProvider: outcome.modelProvider,
|
|
time: outcome.time,
|
|
cacheHit: outcome.cacheHit,
|
|
},
|
|
true,
|
|
);
|
|
this._outcomes.delete(completionId);
|
|
|
|
this.bracketMatchingService.handleAcceptedCompletion(
|
|
outcome.completion,
|
|
outcome.filepath,
|
|
);
|
|
}
|
|
}
|
|
|
|
public cancelRejectionTimeout(completionId: string) {
|
|
if (this._logRejectionTimeouts.has(completionId)) {
|
|
clearTimeout(this._logRejectionTimeouts.get(completionId)!);
|
|
this._logRejectionTimeouts.delete(completionId);
|
|
}
|
|
|
|
if (this._outcomes.has(completionId)) {
|
|
this._outcomes.delete(completionId);
|
|
}
|
|
}
|
|
|
|
public async provideInlineCompletionItems(
|
|
input: AutocompleteInput,
|
|
token: AbortSignal | undefined,
|
|
): Promise<AutocompleteOutcome | undefined> {
|
|
try {
|
|
// Debounce
|
|
const uuid = uuidv4();
|
|
CompletionProvider.lastUUID = uuid;
|
|
|
|
const config = await this.configHandler.loadConfig();
|
|
const options = {
|
|
...DEFAULT_AUTOCOMPLETE_OPTS,
|
|
...config.tabAutocompleteOptions,
|
|
};
|
|
|
|
// Check whether autocomplete is disabled for this file
|
|
if (options.disableInFiles) {
|
|
// Relative path needed for `ignore`
|
|
const workspaceDirs = await this.ide.getWorkspaceDirs();
|
|
let filepath = input.filepath;
|
|
for (const workspaceDir of workspaceDirs) {
|
|
if (filepath.startsWith(workspaceDir)) {
|
|
filepath = path.relative(workspaceDir, filepath);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Worst case we can check filetype glob patterns
|
|
if (filepath === input.filepath) {
|
|
filepath = getBasename(filepath);
|
|
}
|
|
|
|
const pattern = ignore().add(options.disableInFiles);
|
|
if (pattern.ignores(filepath)) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// Create abort signal if not given
|
|
if (!token) {
|
|
const controller = new AbortController();
|
|
token = controller.signal;
|
|
this._abortControllers.set(input.completionId, controller);
|
|
}
|
|
|
|
// Allow disabling autocomplete from config.json
|
|
if (options.disable) {
|
|
return undefined;
|
|
}
|
|
|
|
// Debounce
|
|
if (CompletionProvider.debouncing) {
|
|
CompletionProvider.debounceTimeout?.refresh();
|
|
const lastUUID = await new Promise((resolve) =>
|
|
setTimeout(() => {
|
|
resolve(CompletionProvider.lastUUID);
|
|
}, options.debounceDelay),
|
|
);
|
|
if (uuid !== lastUUID) {
|
|
return undefined;
|
|
}
|
|
} else {
|
|
CompletionProvider.debouncing = true;
|
|
CompletionProvider.debounceTimeout = setTimeout(async () => {
|
|
CompletionProvider.debouncing = false;
|
|
}, options.debounceDelay);
|
|
}
|
|
|
|
// Get completion
|
|
const llm = await this.getLlm();
|
|
if (!llm) {
|
|
return undefined;
|
|
}
|
|
|
|
// Set temperature (but don't overrride)
|
|
if (llm.completionOptions.temperature === undefined) {
|
|
llm.completionOptions.temperature = 0.01;
|
|
}
|
|
|
|
// Set model-specific options
|
|
const LOCAL_PROVIDERS: ModelProvider[] = [
|
|
"ollama",
|
|
"lmstudio",
|
|
"llama.cpp",
|
|
"llamafile",
|
|
"text-gen-webui",
|
|
];
|
|
if (
|
|
!config.tabAutocompleteOptions?.maxPromptTokens &&
|
|
LOCAL_PROVIDERS.includes(llm.providerName)
|
|
) {
|
|
options.maxPromptTokens = 500;
|
|
}
|
|
|
|
const outcome = await this.getTabCompletion(token, options, llm, input);
|
|
|
|
if (!outcome?.completion) {
|
|
return undefined;
|
|
}
|
|
|
|
// Filter out unwanted results
|
|
if (isOnlyPunctuationAndWhitespace(outcome.completion)) {
|
|
return undefined;
|
|
}
|
|
|
|
// Do some stuff later so as not to block return. Latency matters
|
|
const completionToCache = outcome.completion;
|
|
setTimeout(async () => {
|
|
if (!outcome.cacheHit) {
|
|
(await this.autocompleteCache).put(outcome.prefix, completionToCache);
|
|
}
|
|
}, 100);
|
|
|
|
return outcome;
|
|
} catch (e: any) {
|
|
this.onError(e);
|
|
} finally {
|
|
this._abortControllers.delete(input.completionId);
|
|
}
|
|
}
|
|
|
|
_lastDisplayedCompletion: { id: string; displayedAt: number } | undefined =
|
|
undefined;
|
|
|
|
markDisplayed(completionId: string, outcome: AutocompleteOutcome) {
|
|
const logRejectionTimeout = setTimeout(() => {
|
|
// Wait 10 seconds, then assume it wasn't accepted
|
|
outcome.accepted = false;
|
|
logDevData("autocomplete", outcome);
|
|
const { prompt, completion, ...restOfOutcome } = outcome;
|
|
Telemetry.capture(
|
|
"autocomplete",
|
|
{
|
|
...restOfOutcome,
|
|
},
|
|
true,
|
|
);
|
|
this._logRejectionTimeouts.delete(completionId);
|
|
}, COUNT_COMPLETION_REJECTED_AFTER);
|
|
this._outcomes.set(completionId, outcome);
|
|
this._logRejectionTimeouts.set(completionId, logRejectionTimeout);
|
|
|
|
// If the previously displayed completion is still waiting for rejection,
|
|
// and this one is a continuation of that (the outcome.completion is the same modulo prefix)
|
|
// then we should cancel the rejection timeout
|
|
const previous = this._lastDisplayedCompletion;
|
|
const now = Date.now();
|
|
if (previous && this._logRejectionTimeouts.has(previous.id)) {
|
|
const previousOutcome = this._outcomes.get(previous.id);
|
|
const c1 = previousOutcome?.completion.split("\n")[0] ?? "";
|
|
const c2 = outcome.completion.split("\n")[0];
|
|
if (
|
|
previousOutcome &&
|
|
(c1.endsWith(c2) ||
|
|
c2.endsWith(c1) ||
|
|
c1.startsWith(c2) ||
|
|
c2.startsWith(c1))
|
|
) {
|
|
this.cancelRejectionTimeout(previous.id);
|
|
} else if (now - previous.displayedAt < 500) {
|
|
// If a completion isn't shown for more than
|
|
this.cancelRejectionTimeout(previous.id);
|
|
}
|
|
}
|
|
|
|
this._lastDisplayedCompletion = {
|
|
id: completionId,
|
|
displayedAt: now,
|
|
};
|
|
}
|
|
|
|
async getTabCompletion(
|
|
token: AbortSignal,
|
|
options: TabAutocompleteOptions,
|
|
llm: ILLM,
|
|
input: AutocompleteInput,
|
|
): Promise<AutocompleteOutcome | undefined> {
|
|
const startTime = Date.now();
|
|
|
|
const {
|
|
filepath,
|
|
pos,
|
|
recentlyEditedFiles,
|
|
recentlyEditedRanges,
|
|
clipboardText,
|
|
manuallyPassFileContents,
|
|
manuallyPassPrefix,
|
|
} = input;
|
|
const fileContents =
|
|
manuallyPassFileContents ?? (await this.ide.readFile(filepath));
|
|
const fileLines = fileContents.split("\n");
|
|
|
|
// Filter
|
|
const lang = languageForFilepath(filepath);
|
|
const line = fileLines[pos.line] ?? "";
|
|
for (const endOfLine of lang.endOfLine) {
|
|
if (line.endsWith(endOfLine) && pos.character >= line.length) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// Model
|
|
if (!llm) {
|
|
return;
|
|
}
|
|
if (llm instanceof OpenAI) {
|
|
llm.useLegacyCompletionsEndpoint = true;
|
|
} else if (
|
|
llm.providerName === "free-trial" &&
|
|
llm.model !== TRIAL_FIM_MODEL
|
|
) {
|
|
llm.model = TRIAL_FIM_MODEL;
|
|
}
|
|
|
|
if (
|
|
!shownGptClaudeWarning &&
|
|
nonAutocompleteModels.some((model) => llm.model.includes(model)) &&
|
|
!llm.model.includes("deepseek") &&
|
|
!llm.model.includes("codestral")
|
|
) {
|
|
shownGptClaudeWarning = true;
|
|
throw new Error(
|
|
`Warning: ${llm.model} is not trained for tab-autocomplete, and will result in low-quality suggestions. See the docs to learn more about why: https://docs.continue.dev/features/tab-autocomplete#i-want-better-completions-should-i-use-gpt-4`,
|
|
);
|
|
}
|
|
|
|
// Prompt
|
|
let fullPrefix =
|
|
getRangeInString(fileContents, {
|
|
start: { line: 0, character: 0 },
|
|
end: input.selectedCompletionInfo?.range.start ?? pos,
|
|
}) + (input.selectedCompletionInfo?.text ?? "");
|
|
|
|
if (input.injectDetails) {
|
|
const lines = fullPrefix.split("\n");
|
|
fullPrefix = `${lines.slice(0, -1).join("\n")}\n${
|
|
lang.singleLineComment
|
|
} ${input.injectDetails.split("\n").join(`\n${lang.singleLineComment} `)}\n${
|
|
lines[lines.length - 1]
|
|
}`;
|
|
}
|
|
|
|
const fullSuffix = getRangeInString(fileContents, {
|
|
start: pos,
|
|
end: { line: fileLines.length - 1, character: Number.MAX_SAFE_INTEGER },
|
|
});
|
|
|
|
// First non-whitespace line below the cursor
|
|
let lineBelowCursor = "";
|
|
let i = 1;
|
|
while (
|
|
lineBelowCursor.trim() === "" &&
|
|
pos.line + i <= fileLines.length - 1
|
|
) {
|
|
lineBelowCursor = fileLines[Math.min(pos.line + i, fileLines.length - 1)];
|
|
i++;
|
|
}
|
|
|
|
let extrasSnippets = options.useOtherFiles
|
|
? ((await Promise.race([
|
|
this.getDefinitionsFromLsp(
|
|
filepath,
|
|
fullPrefix + fullSuffix,
|
|
fullPrefix.length,
|
|
this.ide,
|
|
lang,
|
|
),
|
|
new Promise((resolve) => {
|
|
setTimeout(() => resolve([]), 100);
|
|
}),
|
|
])) as AutocompleteSnippet[])
|
|
: [];
|
|
|
|
const workspaceDirs = await this.ide.getWorkspaceDirs();
|
|
if (options.onlyMyCode) {
|
|
extrasSnippets = extrasSnippets.filter((snippet) => {
|
|
return workspaceDirs.some((dir) => snippet.filepath.startsWith(dir));
|
|
});
|
|
}
|
|
|
|
let { prefix, suffix, completeMultiline, snippets } =
|
|
await constructAutocompletePrompt(
|
|
filepath,
|
|
pos.line,
|
|
fullPrefix,
|
|
fullSuffix,
|
|
clipboardText,
|
|
lang,
|
|
options,
|
|
recentlyEditedRanges,
|
|
recentlyEditedFiles,
|
|
llm.model,
|
|
extrasSnippets,
|
|
this.importDefinitionsService,
|
|
);
|
|
|
|
// If prefix is manually passed
|
|
if (manuallyPassPrefix) {
|
|
prefix = manuallyPassPrefix;
|
|
suffix = "";
|
|
}
|
|
|
|
// Template prompt
|
|
const {
|
|
template,
|
|
completionOptions,
|
|
compilePrefixSuffix = undefined,
|
|
} = options.template
|
|
? { template: options.template, completionOptions: {} }
|
|
: getTemplateForModel(llm.model);
|
|
|
|
let prompt: string;
|
|
const filename = getBasename(filepath);
|
|
const reponame = getBasename(workspaceDirs[0] ?? "myproject");
|
|
|
|
// Some models have prompts that need two passes. This lets us pass the compiled prefix/suffix
|
|
// into either the 2nd template to generate a raw string, or to pass prefix, suffix to a FIM endpoint
|
|
if (compilePrefixSuffix) {
|
|
[prefix, suffix] = compilePrefixSuffix(
|
|
prefix,
|
|
suffix,
|
|
filepath,
|
|
reponame,
|
|
snippets,
|
|
);
|
|
}
|
|
|
|
if (typeof template === "string") {
|
|
const compiledTemplate = Handlebars.compile(template);
|
|
|
|
// Format snippets as comments and prepend to prefix
|
|
const formattedSnippets = snippets
|
|
.map((snippet) =>
|
|
formatExternalSnippet(snippet.filepath, snippet.contents, lang),
|
|
)
|
|
.join("\n");
|
|
if (formattedSnippets.length > 0) {
|
|
prefix = `${formattedSnippets}\n\n${prefix}`;
|
|
} else if (prefix.trim().length === 0 && suffix.trim().length === 0) {
|
|
// If it's an empty file, include the file name as a comment
|
|
prefix = `${lang.singleLineComment} ${getLastNPathParts(filepath, 2)}\n${prefix}`;
|
|
}
|
|
|
|
prompt = compiledTemplate({
|
|
prefix,
|
|
suffix,
|
|
filename,
|
|
reponame,
|
|
language: lang.name,
|
|
});
|
|
} else {
|
|
// Let the template function format snippets
|
|
prompt = template(prefix, suffix, filepath, reponame, lang.name, snippets);
|
|
}
|
|
|
|
// Completion
|
|
let completion = "";
|
|
|
|
const cache = await autocompleteCache;
|
|
const cachedCompletion = options.useCache
|
|
? await cache.get(prefix)
|
|
: undefined;
|
|
let cacheHit = false;
|
|
if (cachedCompletion) {
|
|
// Cache
|
|
cacheHit = true;
|
|
completion = cachedCompletion;
|
|
} else {
|
|
const stop = [
|
|
...(completionOptions?.stop || []),
|
|
...multilineStops,
|
|
...commonStops,
|
|
...(llm.model.toLowerCase().includes("starcoder2")
|
|
? STARCODER2_T_ARTIFACTS
|
|
: []),
|
|
...(lang.stopWords ?? []),
|
|
...lang.topLevelKeywords.map((word) => `\n${word}`),
|
|
];
|
|
|
|
let langMultilineDecision = lang.useMultiline?.({ prefix, suffix });
|
|
let multiline: boolean = false;
|
|
if (langMultilineDecision) {
|
|
multiline = langMultilineDecision;
|
|
} else {
|
|
multiline =
|
|
!input.selectedCompletionInfo && // Only ever single-line if using intellisense selected value
|
|
options.multilineCompletions !== "never" &&
|
|
(options.multilineCompletions === "always" || completeMultiline);
|
|
}
|
|
|
|
// Try to reuse pending requests if what the user typed matches start of completion
|
|
const generator = this.generatorReuseManager.getGenerator(
|
|
prefix,
|
|
() =>
|
|
llm.supportsFim()
|
|
? llm.streamFim(prefix, suffix, {
|
|
...completionOptions,
|
|
stop,
|
|
})
|
|
: llm.streamComplete(prompt, {
|
|
...completionOptions,
|
|
raw: true,
|
|
stop,
|
|
}),
|
|
multiline,
|
|
);
|
|
|
|
// Full stop means to stop the LLM's generation, instead of just truncating the displayed completion
|
|
const fullStop = () =>
|
|
this.generatorReuseManager.currentGenerator?.cancel();
|
|
|
|
// LLM
|
|
let cancelled = false;
|
|
const generatorWithCancellation = async function* () {
|
|
for await (const update of generator) {
|
|
if (token.aborted) {
|
|
cancelled = true;
|
|
return;
|
|
}
|
|
yield update;
|
|
}
|
|
};
|
|
let charGenerator = generatorWithCancellation();
|
|
charGenerator = noFirstCharNewline(charGenerator);
|
|
charGenerator = onlyWhitespaceAfterEndOfLine(
|
|
charGenerator,
|
|
lang.endOfLine,
|
|
fullStop,
|
|
);
|
|
charGenerator = stopAtStopTokens(charGenerator, stop);
|
|
charGenerator = this.bracketMatchingService.stopOnUnmatchedClosingBracket(
|
|
charGenerator,
|
|
prefix,
|
|
suffix,
|
|
filepath,
|
|
multiline,
|
|
);
|
|
|
|
let lineGenerator = streamLines(charGenerator);
|
|
lineGenerator = stopAtLines(lineGenerator, fullStop);
|
|
lineGenerator = stopAtRepeatingLines(lineGenerator, fullStop);
|
|
lineGenerator = avoidPathLine(lineGenerator, lang.singleLineComment);
|
|
lineGenerator = skipPrefixes(lineGenerator);
|
|
lineGenerator = noTopLevelKeywordsMidline(
|
|
lineGenerator,
|
|
lang.topLevelKeywords,
|
|
fullStop,
|
|
);
|
|
|
|
for (const lineFilter of lang.lineFilters ?? []) {
|
|
lineGenerator = lineFilter({ lines: lineGenerator, fullStop });
|
|
}
|
|
|
|
lineGenerator = streamWithNewLines(lineGenerator);
|
|
|
|
const finalGenerator = stopAtSimilarLine(
|
|
lineGenerator,
|
|
lineBelowCursor,
|
|
fullStop,
|
|
);
|
|
|
|
try {
|
|
for await (const update of finalGenerator) {
|
|
completion += update;
|
|
}
|
|
} catch (e: any) {
|
|
if (ERRORS_TO_IGNORE.some((err) => e.includes(err))) {
|
|
return undefined;
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
if (cancelled) {
|
|
return undefined;
|
|
}
|
|
|
|
const processedCompletion = postprocessCompletion({
|
|
completion,
|
|
prefix,
|
|
suffix,
|
|
llm,
|
|
});
|
|
|
|
if (!processedCompletion) {
|
|
return undefined;
|
|
}
|
|
completion = processedCompletion;
|
|
}
|
|
|
|
const time = Date.now() - startTime;
|
|
return {
|
|
time,
|
|
completion,
|
|
prefix,
|
|
suffix,
|
|
prompt,
|
|
modelProvider: llm.providerName,
|
|
modelName: llm.model,
|
|
completionOptions,
|
|
cacheHit,
|
|
filepath: input.filepath,
|
|
completionId: input.completionId,
|
|
gitRepo: await this.ide.getRepoName(input.filepath),
|
|
uniqueId: await this.ide.getUniqueId(),
|
|
...options,
|
|
};
|
|
}
|
|
}
|