Skip to content

Commit

Permalink
Merge pull request #2637 from continuedev/nate/autocomplete-tests
Browse files Browse the repository at this point in the history
Multiline autocomplete improvements & tests
  • Loading branch information
sestinj authored Oct 30, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents f64661e + 896138a commit ae91f84
Showing 30 changed files with 2,563 additions and 128 deletions.
178 changes: 106 additions & 72 deletions core/autocomplete/completionProvider.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,6 @@ import { RecentlyEditedRange } from "./recentlyEdited.js";
import { RootPathContextService } from "./services/RootPathContextService.js";
import {
avoidPathLineAndEmptyComments,
noTopLevelKeywordsMidline,
skipPrefixes,
stopAtLines,
stopAtRepeatingLines,
@@ -50,11 +49,7 @@ import Handlebars from "handlebars";
import { getConfigJsonPath } from "../util/paths.js";
import { BracketMatchingService } from "./services/BracketMatchingService.js";
import { ImportDefinitionsService } from "./services/ImportDefinitionsService.js";
import {
noFirstCharNewline,
onlyWhitespaceAfterEndOfLine,
stopAtStopTokens,
} from "./streamTransforms/charStream.js";
import { stopAtStopTokens } from "./streamTransforms/charStream.js";

export interface AutocompleteInput {
completionId: string;
@@ -95,6 +90,7 @@ const autocompleteCache = AutocompleteLruCache.get();

const DOUBLE_NEWLINE = "\n\n";
const WINDOWS_DOUBLE_NEWLINE = "\r\n\r\n";
// TODO: Do we want to stop completions when reaching a `/src/` string?
const SRC_DIRECTORY = "/src/";
// Starcoder2 tends to output artifacts starting with the letter "t"
const STARCODER2_T_ARTIFACTS = ["t.", "\nt", "<file_sep>"];
@@ -111,6 +107,14 @@ const ERRORS_TO_IGNORE = [
"unexpected server status",
];

const LOCAL_PROVIDERS: ModelProvider[] = [
"ollama",
"lmstudio",
"llama.cpp",
"llamafile",
"text-gen-webui",
];

function formatExternalSnippet(
filepath: string,
snippet: string,
@@ -329,14 +333,6 @@ export class CompletionProvider {
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)
@@ -355,7 +351,7 @@ export class CompletionProvider {
* elsewhere in the code. That said, I'm not yet confident enough to
* remove this.
*/
if (isOnlyWhitespace(outcome.completion)) {
if (options.transform && isOnlyWhitespace(outcome.completion)) {
return undefined;
}

@@ -437,6 +433,33 @@ export class CompletionProvider {
};
}

private isMultiline({
language,
prefix,
suffix,
selectedCompletionInfo,
multilineCompletions,
completeMultiline,
}: {
language: AutocompleteLanguageInfo;
prefix: string;
suffix: string;
selectedCompletionInfo: AutocompleteInput["selectedCompletionInfo"];
multilineCompletions: TabAutocompleteOptions["multilineCompletions"];
completeMultiline: boolean;
}) {
let langMultilineDecision = language.useMultiline?.({ prefix, suffix });
if (langMultilineDecision) {
return langMultilineDecision;
} else {
return (
!selectedCompletionInfo && // Only ever single-line if using intellisense selected value
multilineCompletions !== "never" &&
(multilineCompletions === "always" || completeMultiline)
);
}
}

async getTabCompletion(
token: AbortSignal,
options: TabAutocompleteOptions,
@@ -460,10 +483,13 @@ export class CompletionProvider {

// 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;

if (options.transform) {
const line = fileLines[pos.line] ?? "";
for (const endOfLine of lang.endOfLine) {
if (line.endsWith(endOfLine) && pos.character >= line.length) {
return undefined;
}
}
}

@@ -634,25 +660,25 @@ export class CompletionProvider {
} else {
const stop = [
...(completionOptions?.stop || []),
...multilineStops,
// ...multilineStops,
...commonStops,
...(llm.model.toLowerCase().includes("starcoder2")
? STARCODER2_T_ARTIFACTS
: []),
...(lang.stopWords ?? []),
...lang.topLevelKeywords.map((word) => `\n${word}`),
// ...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);
}
const multiline =
!options.transform ||
this.isMultiline({
multilineCompletions: options.multilineCompletions,
language: lang,
selectedCompletionInfo: input.selectedCompletionInfo,
prefix,
suffix,
completeMultiline,
});

// Try to reuse pending requests if what the user typed matches start of completion
const generator = this.generatorReuseManager.getGenerator(
@@ -687,46 +713,52 @@ export class CompletionProvider {
}
};
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,
);

if (options.transform) {
// 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 = avoidPathLineAndEmptyComments(
lineGenerator,
lang.singleLineComment,
);
lineGenerator = skipPrefixes(lineGenerator);
lineGenerator = noTopLevelKeywordsMidline(
lineGenerator,
lang.topLevelKeywords,
fullStop,
);
if (options.transform) {
lineGenerator = stopAtLines(lineGenerator, fullStop);
lineGenerator = stopAtRepeatingLines(lineGenerator, fullStop);
lineGenerator = avoidPathLineAndEmptyComments(
lineGenerator,
lang.singleLineComment,
);
lineGenerator = skipPrefixes(lineGenerator);

// lineGenerator = noTopLevelKeywordsMidline(
// lineGenerator,
// lang.topLevelKeywords,
// fullStop,
// );
for (const lineFilter of lang.lineFilters ?? []) {
lineGenerator = lineFilter({ lines: lineGenerator, fullStop });
}

for (const lineFilter of lang.lineFilters ?? []) {
lineGenerator = lineFilter({ lines: lineGenerator, fullStop });
lineGenerator = stopAtSimilarLine(
lineGenerator,
lineBelowCursor,
fullStop,
);
}

lineGenerator = streamWithNewLines(lineGenerator);

const finalGenerator = stopAtSimilarLine(
lineGenerator,
lineBelowCursor,
fullStop,
);
const finalGenerator = streamWithNewLines(lineGenerator);

try {
for await (const update of finalGenerator) {
@@ -743,12 +775,14 @@ export class CompletionProvider {
return undefined;
}

const processedCompletion = postprocessCompletion({
completion,
prefix,
suffix,
llm,
});
const processedCompletion = options.transform
? postprocessCompletion({
completion,
prefix,
suffix,
llm,
})
: completion;

if (!processedCompletion) {
return undefined;
8 changes: 6 additions & 2 deletions core/autocomplete/services/BracketMatchingService.ts
Original file line number Diff line number Diff line change
@@ -84,9 +84,13 @@ export class BracketMatchingService {
// Add corresponding open brackets from suffix to stack
// because we overwrite them and the diff is displayed, and this allows something to be edited after that
for (let i = 0; i < suffix.length; i++) {
if (suffix[i] === " ") {continue;}
if (suffix[i] === " ") {
continue;
}
const openBracket = BracketMatchingService.BRACKETS_REVERSE[suffix[i]];
if (!openBracket) {break;}
if (!openBracket) {
break;
}
stack.unshift(openBracket);
}

44 changes: 1 addition & 43 deletions core/autocomplete/shouldCompleteMultiline.ts
Original file line number Diff line number Diff line change
@@ -3,33 +3,6 @@ import { AutocompleteLanguageInfo } from "./languages";

const BLOCK_TYPES = ["body", "statement_block"];

function shouldCompleteMultilineAst(
treePath: AstPath,
cursorLine: number,
): boolean {
// If at the base of the file, do multiline
if (treePath.length === 1) {
return true;
}

// If at the first line of an otherwise empty funtion body, do multiline
for (let i = treePath.length - 1; i >= 0; i--) {
const node = treePath[i];
if (
BLOCK_TYPES.includes(node.type) &&
Math.abs(node.startPosition.row - cursorLine) <= 1
) {
let text = node.text;
text = text.slice(text.indexOf("{") + 1);
text = text.slice(0, text.lastIndexOf("}"));
text = text.trim();
return text.split("\n").length === 1;
}
}

return false;
}

function isMidlineCompletion(prefix: string, suffix: string): boolean {
return !suffix.startsWith("\n");
}
@@ -56,20 +29,5 @@ export async function shouldCompleteMultiline(
return false;
}

// First, if the line before ends with an opening bracket, then assume multi-line
if (
["{", "(", "["].includes(
fullPrefix.split("\n").slice(-2)[0]?.trim().slice(-1)[0],
)
) {
return true;
}

// Use AST to determine whether to complete multiline
let completeMultiline = false;
if (treePath) {
const cursorLine = fullPrefix.split("\n").length - 1;
completeMultiline = shouldCompleteMultilineAst(treePath, cursorLine);
}
return completeMultiline;
return true;
}
51 changes: 51 additions & 0 deletions core/autocomplete/test/NEGATIVE_CASES_STARCODER.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
##### Prompt #####
class Calculator {
constructor() {
this.result = 0;
}

add(number) {
this.result += number;
return this;
}

subtract(number) {
this.result -= number;
return this;
}

multiply(number) {
this.result *= number;
return this;
}

divide(number) {
if (number === 0) {
throw new Error("Cannot divide by zero");
}

<FIM>

this.result /= number;
return this;
}

getResult() {
return this.result;
}

reset() {
this.result = 0;
return this;
}
}
==========================================================================
==========================================================================
Completion:

this.result /= number;
return this;
}
}

module.exports = Calculator;
Loading

0 comments on commit ae91f84

Please sign in to comment.