From 4ec170713bfcf3442886a3da02b3c3c3d8a4770e Mon Sep 17 00:00:00 2001 From: rjmacarthy Date: Wed, 24 Apr 2024 11:58:00 +0100 Subject: [PATCH 1/2] multiline completion parsing 3.11.21 lint more refactoring fix fix spelling 3.11.22 update description --- package-lock.json | 4 +- package.json | 22 +- src/common/constants.ts | 44 +++- src/extension/completion-formatter.ts | 16 +- src/extension/parser-utils.ts | 35 ++- src/extension/providers/completion.ts | 334 +++++++++++++++----------- src/extension/utils.ts | 35 +-- src/index.ts | 2 +- 8 files changed, 314 insertions(+), 178 deletions(-) diff --git a/package-lock.json b/package-lock.json index 278688ed..be166fd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twinny", - "version": "3.11.20", + "version": "3.11.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twinny", - "version": "3.11.20", + "version": "3.11.22", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 57faa7b3..fc474259 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "twinny", "displayName": "twinny - AI Code Completion and Chat", "description": "Locally hosted AI code completion plugin for vscode", - "version": "3.11.20", + "version": "3.11.22", "icon": "assets/icon.png", "keywords": [ "code-inference", @@ -255,11 +255,11 @@ "default": true, "description": "Activates or deactivates the Twinny extension." }, - "twinny.disableAutoSuggest": { + "twinny.autoSuggestEnabled": { "order": 1, "type": "boolean", - "default": false, - "description": "Disables automatic suggestions, manual trigger (default shortcut Alt+\\)." + "default": true, + "description": "Enable automatic completion suggestions, manual trigger (default shortcut Alt+\\)." }, "twinny.contextLength": { "order": 2, @@ -282,20 +282,20 @@ "description": "Sets the model's creativity level (temperature) for generating completions.", "required": true }, - "twinny.useMultiLineCompletions": { + "twinny.multilineCompletionsEnabled": { "order": 5, "type": "boolean", - "default": false, - "description": "Use multiline completions (Can be inaccurate)." + "default": true, + "description": "Experimental feature: enables the generation of multi-line completions." }, "twinny.maxLines": { "dependencies": { - "twinny.useMultiLineCompletions": true + "twinny.multilineCompletionsEnabled": true }, "order": 6, "type": "number", - "default": 7, - "description": "Maximum number of lines to use for multi line completions. Applicable only when useMultiLineCompletions is enabled." + "default": 30, + "description": "Maximum number of lines to use for multi line completions. Applicable only when multilineCompletionsEnabled is enabled." }, "twinny.useFileContext": { "order": 8, @@ -303,7 +303,7 @@ "default": false, "description": "Enables scanning of neighbouring documents to enhance completion prompts. (Experimental)" }, - "twinny.enableCompletionCache": { + "twinny.completionCacheEnabled": { "order": 9, "type": "boolean", "default": false, diff --git a/src/common/constants.ts b/src/common/constants.ts index 125c2291..ce8c17a2 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -21,6 +21,7 @@ export const MAX_CONTEXT_LINE_COUNT = 200 export const SKIP_DECLARATION_SYMBOLS = ['='] export const IMPORT_SEPARATOR = [',', '{'] export const SKIP_IMPORT_KEYWORDS_AFTER = ['from', 'as', 'import'] +export const MIN_COMPLETION_CHUNKS = 2 export const EVENT_NAME = { twinngAddMessage: 'twinny-add-message', @@ -53,7 +54,7 @@ export const EVENT_NAME = { twinnySetWorkspaceContext: 'twinny-set-workspace-context', twinnyStopGeneration: 'twinny-stop-generation', twinnyTextSelection: 'twinny-text-selection', - twinnyWorkspaceContext: 'twinny-workspace-context', + twinnyWorkspaceContext: 'twinny-workspace-context' } export const TWINNY_COMMAND_NAME = { @@ -76,8 +77,8 @@ export const TWINNY_COMMAND_NAME = { sendTerminalText: 'twinny.sendTerminalText', getGitCommitMessage: 'twinny.getGitCommitMessage', newChat: 'twinny.newChat', - focusSidebar: 'twinny.sidebar.focus', -}; + focusSidebar: 'twinny.sidebar.focus' +} export const CONVERSATION_EVENT_NAME = { saveConversation: 'twinny.save-conversation', @@ -85,7 +86,7 @@ export const CONVERSATION_EVENT_NAME = { setActiveConversation: 'twinny.set-active-conversation', getActiveConversation: 'twinny.get-active-conversation', saveLastConversation: 'twinny.save-last-conversation', - removeConversation: 'twinny.remove-conversation', + removeConversation: 'twinny.remove-conversation' } export const PROVIDER_EVENT_NAME = { @@ -251,3 +252,38 @@ export const WASM_LANGAUAGES: { [key: string]: string } = { rdl: 'systemrdl', toml: 'toml' } + +export const MULTILINE_OUTSIDE = [ + 'class_body', + 'interface_body', + 'interface', + 'class', + 'program', + 'identifier', + 'export' +] + +export const MULTILINE_INSIDE = [ + 'body', + 'export_statement', + 'formal_parameters', + 'function_definition', + 'named_imports', + 'object_pattern', + 'object_type', + 'object', + 'parenthesized_expression', + 'statement_block' +] + +export const MULTILINE_TYPES = [...MULTILINE_OUTSIDE, ...MULTILINE_INSIDE] + +export const MULTI_LINE_DELIMITERS = ['\n\n', '\r\n\r\n'] + +export const MULTI_LINE_REACT = [ + 'jsx_closing_element', + 'jsx_element', + 'jsx_element', + 'jsx_opening_element', + 'jsx_self_closing_element', +] diff --git a/src/extension/completion-formatter.ts b/src/extension/completion-formatter.ts index e1bd0b36..523336f3 100644 --- a/src/extension/completion-formatter.ts +++ b/src/extension/completion-formatter.ts @@ -4,6 +4,7 @@ import { CLOSING_BRACKETS, OPENING_BRACKETS, QUOTES } from '../common/constants' import { Bracket } from '../common/types' import { getLanguage } from './utils' import { supportedLanguages } from '../common/languages' +import { getLineBreakCount } from '../webview/utils' export class CompletionFormatter { private _characterAfterCursor: string @@ -235,7 +236,7 @@ export class CompletionFormatter { const languageId = supportedLanguages[language.languageId as keyof typeof supportedLanguages] - if (this._normalisedCompletion.startsWith('// File:')) { + if (this._normalisedCompletion.startsWith('// File:') || this._normalisedCompletion === '//') { this._completion = '' return this } @@ -248,17 +249,12 @@ export class CompletionFormatter { return this } - const comments = `${languageId.syntaxComments.start}${ - languageId.syntaxComments.end ?? '' - }` - const score = this._normalisedCompletion.score(comments, 0.1) - if (this._normalisedCompletion.startsWith(comments) || score > 0.3) { - this._completion = '' - return this - } - if (!languageId || !languageId.syntaxComments) return this + const lineCount = getLineBreakCount(this._completion) + + if (lineCount > 1) return this + const completionLines = this._completion.split('\n').filter((line) => { const startsWithComment = line.startsWith(languageId.syntaxComments.start) const includesCommentReference = /\b(Language|File|End):\s*(.*)\b/.test( diff --git a/src/extension/parser-utils.ts b/src/extension/parser-utils.ts index 1bd314de..b0e7b750 100644 --- a/src/extension/parser-utils.ts +++ b/src/extension/parser-utils.ts @@ -1,12 +1,13 @@ -import Parser from 'web-tree-sitter' +import Parser, { SyntaxNode } from 'web-tree-sitter' import { WASM_LANGAUAGES } from '../common/constants' import path from 'path' import { Logger } from '../common/logger' +import { Position } from 'vscode' const logger = new Logger() -export const getParserForFile = async ( +export const getParser = async ( filePath: string ): Promise => { await Parser.init() @@ -28,3 +29,33 @@ export const getParserForFile = async ( return parser } + +export function getNodeAtPosition( + tree: Parser.Tree | undefined, + position: Position +): SyntaxNode | null { + let foundNode: SyntaxNode | null = null + const visitedNodes: SyntaxNode[] = [] + if (!tree || !position) { + return null + } + + function searchNode(node: SyntaxNode): boolean { + if ( + position.line >= node.startPosition.row && + position.line <= node.endPosition.row + ) { + foundNode = node + for (const child of node.children) { + visitedNodes.push(child) + if (searchNode(child)) break + } + return true + } + return false + } + + searchNode(tree.rootNode) + + return foundNode +} diff --git a/src/extension/providers/completion.ts b/src/extension/providers/completion.ts index 923838ed..7a25442e 100644 --- a/src/extension/providers/completion.ts +++ b/src/extension/providers/completion.ts @@ -13,14 +13,16 @@ import { InlineCompletionTriggerKind, ExtensionContext } from 'vscode' +import Parser, { SyntaxNode } from 'web-tree-sitter' import AsyncLock from 'async-lock' import 'string_score' import { - getFimDataFromProvider, + getFimDataFromProvider as getProviderFimData, getPrefixSuffix, getShouldSkipCompletion, - getIsMiddleWord, - getIsMultiLineCompletion + getIsMiddleOfString, + getIsMultilineCompletion, + getCurrentLineText } from '../utils' import { cache } from '../cache' import { supportedLanguages } from '../../common/languages' @@ -36,7 +38,11 @@ import { ACTIVE_FIM_PROVIDER_STORAGE_KEY, FIM_TEMPLATE_FORMAT, LINE_BREAK_REGEX, - MAX_CONTEXT_LINE_COUNT + MAX_CONTEXT_LINE_COUNT, + MIN_COMPLETION_CHUNKS, + MULTI_LINE_DELIMITERS, + MULTILINE_INSIDE, + MULTILINE_OUTSIDE } from '../../common/constants' import { streamResponse } from '../stream' import { createStreamRequestBodyFim } from '../provider-options' @@ -46,37 +52,50 @@ import { FileInteractionCache } from '../file-interaction' import { getLineBreakCount } from '../../webview/utils' import { TemplateProvider } from '../template-provider' import { TwinnyProvider } from '../provider-manager' +import { getNodeAtPosition, getParser } from '../parser-utils' export class CompletionProvider implements InlineCompletionItemProvider { private _config = workspace.getConfiguration('twinny') private _abortController: AbortController | null private _acceptedLastCompletion = false - private _cacheEnabled = this._config.get('enableCompletionCache') as boolean + private _completionCacheEnabled = this._config.get( + 'completionCacheEnabled' + ) as boolean private _chunkCount = 0 private _completion = '' + private _nodeAtPosition: SyntaxNode | null = null private _debouncer: NodeJS.Timeout | undefined private _debounceWait = this._config.get('debounceWait') as number - private _disableAuto = this._config.get('disableAutoSuggest') as boolean + private _autoSuggestEnabled = this._config.get( + 'autoSuggestEnabled' + ) as boolean private _document: TextDocument | null private _enabled = this._config.get('enabled') - private _enableSubsequent = this._config.get('enableSubsequent') as boolean + private enableSubsequentCompletions = this._config.get( + 'enableSubsequent' + ) as boolean private _extensionContext: ExtensionContext private _fileInteractionCache: FileInteractionCache + private _isMultilineCompletion = false private _keepAlive = this._config.get('keepAlive') as string | number private _lastCompletionMultiline = false - private _lastCompletionText = '' + public lastCompletionText = '' private _lock: AsyncLock private _logger: Logger private _maxLines = this._config.get('maxLines') as number + private _multilineCompletionsEnabled = this._config.get( + 'multilineCompletionsEnabled' + ) as boolean private _nonce = 0 private _numLineContext = this._config.get('contextLength') as number private _numPredictFim = this._config.get('numPredictFim') as number + private _parser: Parser | undefined private _position: Position | null + private _prefixSuffix: PrefixSuffix = { prefix: '', suffix: '' } private _statusBar: StatusBarItem private _temperature = this._config.get('temperature') as number private _templateProvider: TemplateProvider private _useFileContext = this._config.get('useFileContext') as boolean - private _useMultiLine = this._config.get('useMultiLineCompletions') as boolean private _usingFimTemplate = false constructor( @@ -104,20 +123,26 @@ export class CompletionProvider implements InlineCompletionItemProvider { const editor = window.activeTextEditor const isLastCompletionAccepted = - this._acceptedLastCompletion && !this._enableSubsequent + this._acceptedLastCompletion && !this.enableSubsequentCompletions - const prefixSuffix = getPrefixSuffix( + this._prefixSuffix = getPrefixSuffix( this._numLineContext, document, position ) + const cachedCompletion = cache.getCache(this._prefixSuffix) + if (cachedCompletion && this._completionCacheEnabled) { + this._completion = cachedCompletion + return this.provideInlineCompletion() + } + if ( context.triggerKind === InlineCompletionTriggerKind.Invoke && - !this._disableAuto + this._autoSuggestEnabled ) { - this._completion = this._lastCompletionText - return this.triggerInlineCompletion(prefixSuffix) + this._completion = this.lastCompletionText + return this.provideInlineCompletion() } if ( @@ -125,48 +150,56 @@ export class CompletionProvider implements InlineCompletionItemProvider { !editor || isLastCompletionAccepted || this._lastCompletionMultiline || - getShouldSkipCompletion(context, this._disableAuto) || - getIsMiddleWord() + getShouldSkipCompletion(context, this._autoSuggestEnabled) || + getIsMiddleOfString() ) { this._statusBar.text = '🤖' return } + this._chunkCount = 0 this._document = document this._position = position - this._chunkCount = 0 this._nonce = this._nonce + 1 this._statusBar.text = '$(loading~spin)' this._statusBar.command = 'twinny.stopGeneration' - const prompt = await this.getPrompt(prefixSuffix) - const cachedCompletion = cache.getCache(prefixSuffix) - if (cachedCompletion && this._cacheEnabled) { - this._completion = cachedCompletion - return this.triggerInlineCompletion(prefixSuffix) - } + this._parser = await getParser(document.uri.fsPath) + this._nodeAtPosition = getNodeAtPosition( + this._parser?.parse(this._document?.getText()), + this._position + ) + + this._isMultilineCompletion = getIsMultilineCompletion({ + node: this._nodeAtPosition, + prefixSuffix: this._prefixSuffix + }) if (this._debouncer) clearTimeout(this._debouncer) + const prompt = await this.getPrompt(this._prefixSuffix) + + if (!prompt) return + return new Promise((resolve, reject) => { this._debouncer = setTimeout(() => { - this._lock.acquire('twinny.completion', () => { - const request = this.buildStreamRequest(prompt) - if (!request || !prompt) return - const { requestBody, requestOptions } = request - + this._lock.acquire('twinny.completion', async () => { + const provider = this.getProvider() + if (!provider) return + const request = this.buildStreamRequest(prompt, provider) try { - streamResponse({ - body: requestBody, - options: requestOptions, - onStart: (controller) => this.onStart(controller), - onEnd: () => this.onEnd(prefixSuffix, resolve), + await streamResponse({ + body: request.body, + options: request.options, + onStart: (controller) => (this._abortController = controller), + onEnd: () => this.onEnd(resolve), + onError: this.onError, onData: (data) => { - if (this.onData(data)) { + const completion = this.onData(data) + if (completion) { this._abortController?.abort() } - }, - onError: this.onError + } }) } catch (error) { this.onError() @@ -177,30 +210,15 @@ export class CompletionProvider implements InlineCompletionItemProvider { }) } - public getLastCompletion = () => this._lastCompletionText - - public setAcceptedLastCompletion(value: boolean) { - this._acceptedLastCompletion = value - this._lastCompletionMultiline = value - } - - public abortCompletion() { - this._abortController?.abort() - this._statusBar.text = '🤖' - } - - private buildStreamRequest(prompt: string) { - const provider = this.getFimProvider() - if (!provider) return - - const requestBody = createStreamRequestBodyFim(provider.provider, prompt, { + private buildStreamRequest(prompt: string, provider: TwinnyProvider) { + const body = createStreamRequestBodyFim(provider.provider, prompt, { model: provider.modelName, numPredictFim: this._numPredictFim, temperature: this._temperature, keepAlive: this._keepAlive }) - const requestOptions: StreamRequestOptions = { + const options: StreamRequestOptions = { hostname: provider.apiHostname, port: Number(provider.apiPort), path: provider.apiPath, @@ -208,51 +226,87 @@ export class CompletionProvider implements InlineCompletionItemProvider { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${provider.apiKey}` + Authorization: provider.apiKey ? `Bearer ${provider.apiKey}` : '' } } - return { requestOptions, requestBody } + return { options, body } } private onData(data: StreamResponse | undefined): string { - const provider = this.getFimProvider() + const provider = this.getProvider() if (!provider) return '' + try { - const completionData = getFimDataFromProvider(provider.provider, data) - if (completionData === undefined) return '' + const providerFimData = getProviderFimData(provider.provider, data) + if (providerFimData === undefined) return '' - this._completion = this._completion + completionData + this._completion = this._completion + providerFimData this._chunkCount = this._chunkCount + 1 if ( - !this._useMultiLine && - this._chunkCount >= 2 && + !this._multilineCompletionsEnabled && + this._chunkCount >= MIN_COMPLETION_CHUNKS && LINE_BREAK_REGEX.test(this._completion.trimStart()) ) { this._logger.log( - `Streaming response end due to line break ${this._nonce} \nCompletion: ${this._completion}` + `Streaming response end due to single line completion: ${this._nonce} \nCompletion: ${this._completion}` ) return this._completion } - if ( - !getIsMultiLineCompletion() && - this._useMultiLine && - this._chunkCount >= 2 && + const isMultilineCompletionRequired = + !this._isMultilineCompletion && + this._multilineCompletionsEnabled && + this._chunkCount >= MIN_COMPLETION_CHUNKS && LINE_BREAK_REGEX.test(this._completion.trimStart()) - ) { + if (isMultilineCompletionRequired) { this._logger.log( - `Streaming response end due to line break ${this._nonce} \nCompletion: ${this._completion}` + `Streaming response end due to multiline not required ${this._nonce} \nCompletion: ${this._completion}` ) return this._completion } - const lineBreakCount = getLineBreakCount(this._completion) + if (this._nodeAtPosition) { + const takeFirst = + MULTILINE_OUTSIDE.includes(this._nodeAtPosition?.type) || + (MULTILINE_INSIDE.includes(this._nodeAtPosition?.type) && + this._nodeAtPosition?.childCount > 2) + + const lineText = getCurrentLineText(this._position) || '' + if (!this._parser) return '' + + const { rootNode } = this._parser.parse( + `${lineText}${this._completion}` + ) + const { hasError } = rootNode + + if ( + this._parser && + this._nodeAtPosition && + this._isMultilineCompletion && + this._chunkCount >= 2 && + takeFirst && + !hasError + ) { + if ( + MULTI_LINE_DELIMITERS.some((delimiter) => + this._completion.endsWith(delimiter) + ) + ) { + this._logger.log( + `Streaming response end due to delimiter ${this._nonce} \nCompletion: ${this._completion}` + ) + return this._completion + } + } + } - if (lineBreakCount >= this._maxLines) { + if (getLineBreakCount(this._completion) >= this._maxLines) { this._logger.log( - `Streaming response end due to max lines ${this._nonce} \nCompletion: ${this._completion}` + ` + Streaming response end due to max line count ${this._nonce} \nCompletion: ${this._completion} + ` ) return this._completion } @@ -264,20 +318,12 @@ export class CompletionProvider implements InlineCompletionItemProvider { } } - private onStart(controller: AbortController) { - this._abortController = controller - } - - private onEnd( - prefixSuffix: PrefixSuffix, - done: (completion: ResolvedInlineCompletion) => void - ) { - return done(this.triggerInlineCompletion(prefixSuffix)) + private onEnd(resolve: (completion: ResolvedInlineCompletion) => void) { + return resolve(this.provideInlineCompletion()) } public onError = () => { this._abortController?.abort() - this._statusBar.text = '🤖' } private getPromptHeader(languageId: string | undefined, uri: Uri) { @@ -309,9 +355,7 @@ export class CompletionProvider implements InlineCompletionItemProvider { for (const interaction of interactions) { const filePath = interaction.name - if (filePath.toString().match('.git')) { - continue - } + if (filePath.toString().match('.git')) continue const uri = Uri.file(filePath) @@ -334,34 +378,33 @@ export class CompletionProvider implements InlineCompletionItemProvider { Math.min(lineCount, Math.ceil(averageLine || 0) + 100), 0 ) - fileChunks.push(` -// File: ${filePath} -// Content: \n ${document.getText(new Range(start, end))} - `) + fileChunks.push( + ` + // File: ${filePath} + // Content: \n ${document.getText(new Range(start, end))} + `.trim() + ) } else { - fileChunks.push(` -// File: ${filePath} -// Content: \n ${document.getText()} - `) + fileChunks.push( + ` + // File: ${filePath} + // Content: \n ${document.getText()} + `.trim() + ) } } return fileChunks.join('\n') } - private getFimProvider = () => { - const provider = this._extensionContext.globalState.get( - ACTIVE_FIM_PROVIDER_STORAGE_KEY - ) - return provider - } - private removeStopWords(completion: string) { - const provider = this.getFimProvider() + const provider = this.getProvider() if (!provider) return completion - const template = provider.fimTemplate || FIM_TEMPLATE_FORMAT.automatic let filteredCompletion = completion - const stopWords = getStopWords(provider.modelName, template) + const stopWords = getStopWords( + provider.modelName, + provider.fimTemplate || FIM_TEMPLATE_FORMAT.automatic + ) stopWords.forEach((stopWord) => { filteredCompletion = filteredCompletion.split(stopWord).join('') }) @@ -369,12 +412,12 @@ export class CompletionProvider implements InlineCompletionItemProvider { } private async getPrompt(prefixSuffix: PrefixSuffix) { - const provider = this.getFimProvider() + const provider = this.getProvider() if (!provider) return '' if (!this._document || !this._position || !provider) return '' - const language = this._document.languageId - const interactionContext = await this.getFileInteractionContext() + const documentLanguage = this._document.languageId + const fileInteractionContext = await this.getFileInteractionContext() if (provider.fimTemplate === FIM_TEMPLATE_FORMAT.custom) { const systemMessage = @@ -385,7 +428,7 @@ export class CompletionProvider implements InlineCompletionItemProvider { prefix: prefixSuffix.prefix, suffix: prefixSuffix.suffix, systemMessage, - context: interactionContext, + context: fileInteractionContext, fileName: this._document.uri.fsPath }) @@ -395,34 +438,50 @@ export class CompletionProvider implements InlineCompletionItemProvider { } } - const template = provider.fimTemplate || FIM_TEMPLATE_FORMAT.automatic + return getFimPrompt( + provider.modelName, + provider.fimTemplate || FIM_TEMPLATE_FORMAT.automatic, + { + context: fileInteractionContext || '', + prefixSuffix, + header: this.getPromptHeader(documentLanguage, this._document.uri), + useFileContext: this._useFileContext, + language: documentLanguage + } + ) + } + + private getProvider = () => { + return this._extensionContext.globalState.get( + ACTIVE_FIM_PROVIDER_STORAGE_KEY + ) + } - const prompt = getFimPrompt(provider.modelName, template, { - context: interactionContext || '', - prefixSuffix, - header: this.getPromptHeader(language, this._document.uri), - useFileContext: this._useFileContext, - language: language - }) + public setAcceptedLastCompletion(value: boolean) { + this._acceptedLastCompletion = value + this._lastCompletionMultiline = getLineBreakCount(this._completion) > 1 + } - return prompt + public abortCompletion() { + this._abortController?.abort() + this._statusBar.text = '🤖' } private logCompletion(formattedCompletion: string) { - this._logger.log(` -*** Twinny completion triggered for file: ${this._document?.uri} *** -Original completion: ${this._completion} -Formatted completion: ${formattedCompletion} -Max Lines: ${this._maxLines} -Use file context: ${this._useFileContext} -Completed lines count ${getLineBreakCount(formattedCompletion)} -Using custom FIM template fim.bhs?: ${this._usingFimTemplate} - `) + this._logger.log( + ` + *** Twinny completion triggered for file: ${this._document?.uri} *** + Original completion: ${this._completion} + Formatted completion: ${formattedCompletion} + Max Lines: ${this._maxLines} + Use file context: ${this._useFileContext} + Completed lines count ${getLineBreakCount(formattedCompletion)} + Using custom FIM template fim.bhs?: ${this._usingFimTemplate} + `.trim() + ) } - private triggerInlineCompletion( - prefixSuffix: PrefixSuffix - ): InlineCompletionItem[] { + private provideInlineCompletion(): InlineCompletionItem[] { const editor = window.activeTextEditor if (!editor || !this._position) return [] @@ -433,11 +492,12 @@ Using custom FIM template fim.bhs?: ${this._usingFimTemplate} this.logCompletion(formattedCompletion) - if (this._cacheEnabled) cache.setCache(prefixSuffix, formattedCompletion) + if (this._completionCacheEnabled) + cache.setCache(this._prefixSuffix, formattedCompletion) this._completion = '' this._statusBar.text = '🤖' - this._lastCompletionText = formattedCompletion + this.lastCompletionText = formattedCompletion this._lastCompletionMultiline = getLineBreakCount(this._completion) > 1 return [ @@ -449,18 +509,24 @@ Using custom FIM template fim.bhs?: ${this._usingFimTemplate} } public updateConfig() { - this._cacheEnabled = this._config.get('enableCompletionCache') as boolean this._config = workspace.getConfiguration('twinny') + this._completionCacheEnabled = this._config.get( + 'completionCacheEnabled' + ) as boolean this._debounceWait = this._config.get('debounceWait') as number - this._disableAuto = this._config.get('disableAutoSuggest') as boolean - this._enableSubsequent = this._config.get('enableSubsequent') as boolean + this._autoSuggestEnabled = this._config.get('autoSuggestEnabled') as boolean + this.enableSubsequentCompletions = this._config.get( + 'enableSubsequentCompletions' + ) as boolean this._keepAlive = this._config.get('keepAlive') as string | number this._maxLines = this._config.get('maxLines') as number this._numLineContext = this._config.get('contextLength') as number this._numPredictFim = this._config.get('numPredictFim') as number this._temperature = this._config.get('temperature') as number this._useFileContext = this._config.get('useFileContext') as boolean - this._useMultiLine = this._config.get('useMultiLineCompletions') as boolean + this._multilineCompletionsEnabled = this._config.get( + 'multilineCompletionsEnabled' + ) as boolean this._logger.updateConfig() } } diff --git a/src/extension/utils.ts b/src/extension/utils.ts index cc481327..2741bba4 100644 --- a/src/extension/utils.ts +++ b/src/extension/utils.ts @@ -28,6 +28,7 @@ import { ALL_BRACKETS, CLOSING_BRACKETS, LINE_BREAK_REGEX, + MULTILINE_TYPES, OPENING_BRACKETS, QUOTES, QUOTES_REGEX, @@ -35,6 +36,7 @@ import { TWINNY } from '../common/constants' import { Logger } from '../common/logger' +import { SyntaxNode } from 'web-tree-sitter' const logger = new Logger() @@ -132,7 +134,7 @@ export const getSkipVariableDeclataion = ( export const getShouldSkipCompletion = ( context: InlineCompletionContext, - disableAuto: boolean + autoSuggestEnabled: boolean ) => { const editor = window.activeTextEditor if (!editor) return true @@ -148,7 +150,8 @@ export const getShouldSkipCompletion = ( } return ( - context.triggerKind === InlineCompletionTriggerKind.Automatic && disableAuto + context.triggerKind === InlineCompletionTriggerKind.Automatic && + !autoSuggestEnabled ) } @@ -217,7 +220,7 @@ export const getBeforeAndAfter = () => { } } -export const getIsMiddleWord = () => { +export const getIsMiddleOfString = () => { const { charBefore, charAfter } = getBeforeAndAfter() return ( @@ -268,17 +271,21 @@ export const getPreviousLineIsOpeningBracket = () => { return getIsOnlyOpeningBrackets(previousLineCharacter) } -export const getIsMultiLineCompletion = () => { - const nextLineIsClosingBracket = getNextLineIsClosingBracket() - const previousLineIsOpeningBracket = getPreviousLineIsOpeningBracket() - if ( - previousLineIsOpeningBracket && - nextLineIsClosingBracket && - !getHasLineTextBeforeAndAfter() - ) { - return true - } - return !getHasLineTextBeforeAndAfter() && !isCursorInEmptyString() +export const getIsMultilineCompletion = ({ + node, + prefixSuffix +}: { + node: SyntaxNode | null + prefixSuffix: PrefixSuffix | null +}) => { + if (!node) return false + + const isMultilineCompletion = + !getHasLineTextBeforeAndAfter() && + !isCursorInEmptyString() && + MULTILINE_TYPES.includes(node.type) + + return !!(isMultilineCompletion || !prefixSuffix?.suffix.trim()) } export const getTheme = () => { diff --git a/src/index.ts b/src/index.ts index 19d29094..c0247ed8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -218,7 +218,7 @@ export async function activate(context: ExtensionContext) { workspace.onDidChangeTextDocument((e) => { const changes = e.contentChanges[0] if (!changes) return - const lastCompletion = completionProvider.getLastCompletion() + const lastCompletion = completionProvider.lastCompletionText const isLastCompltionMultiline = getLineBreakCount(lastCompletion) > 1 completionProvider.setAcceptedLastCompletion( !!( From e2db1e8862fed21b766ce57baf8402f33d5ca047 Mon Sep 17 00:00:00 2001 From: rjmacarthy Date: Wed, 24 Apr 2024 14:52:01 +0100 Subject: [PATCH 2/2] 3.11.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index be166fd2..5ee1b037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twinny", - "version": "3.11.22", + "version": "3.11.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twinny", - "version": "3.11.22", + "version": "3.11.23", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fc474259..6121a717 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "twinny", "displayName": "twinny - AI Code Completion and Chat", "description": "Locally hosted AI code completion plugin for vscode", - "version": "3.11.22", + "version": "3.11.23", "icon": "assets/icon.png", "keywords": [ "code-inference",