Skip to content

Commit

Permalink
Merge pull request #196 from rjmacarthy/feature/commit-message
Browse files Browse the repository at this point in the history
(feature) generate commit messages from staged changes
  • Loading branch information
rjmacarthy authored Apr 1, 2024
2 parents 26b122f + 16ed581 commit ff38a07
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 178 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Through the side bar, have a conversation with your model and get explanations a
- Conforms to the OpenAI API standard
- Single or multiline fill-in-middle completions
- Customisable prompt templates to add context to completions
- Generate git commit messages from staged changes (CTRL+SHIFT+T CTRL+SHIFT+G)
- Easy installation via vscode extensions marketplace or by downloading and running a binary directly
- Customisable settings to change API provider, model name, port number and path
- Ollama, llamacpp, oobabooga and LM Studio API compatible
Expand Down Expand Up @@ -123,13 +124,13 @@ If using Llama the model must support the Llama special tokens.

## Keyboard shortcuts

| Shortcut | Description |
| ---------------------------- | ---------------------------------------- |
| `ALT+\` | Trigger inline code completion |
| `CTRL+SHIFT+/` | Stop the inline code generation |
| `Tab` | Accept the inline code generated |
| `CTRL+SHIFT+t` | Open twinny sidebar |

| Shortcut | Description |
| ---------------------------- | ---------------------------------------------|
| `ALT+\` | Trigger inline code completion |
| `CTRL+SHIFT+/` | Stop the inline code generation |
| `Tab` | Accept the inline code generated |
| `CTRL+SHIFT+t` | Open twinny sidebar |
| `CTRL+SHIFT+t CTRL+SHIT+g` | Generate commit messages from staged changes |

## Workspace context

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "twinny",
"displayName": "twinny - AI Code Completion and Chat",
"description": "Locally hosted AI code completion plugin for vscode",
"version": "3.10.7",
"version": "3.10.8",
"icon": "assets/icon.png",
"keywords": [
"code-inference",
Expand Down Expand Up @@ -138,6 +138,10 @@
"command": "twinny.stopGeneration",
"title": "Stop generation"
},
{
"command": "twinny.getGitCommitMessage",
"title": "Generate git commit message"
},
{
"command": "twinny.disable",
"title": "Disable twinny",
Expand Down Expand Up @@ -194,6 +198,10 @@
"key": "CTRL+SHIFT+/",
"command": "twinny.stopGeneration",
"when": "twinnyGeneratingText"
},
{
"key": "ctrl+shift+t ctrl+shift+g",
"command": "twinny.getGitCommitMessage"
}
],
"viewsContainers": {
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const ALL_BRACKETS = [...OPENING_BRACKETS, ...CLOSING_BRACKETS] as const
export const BRACKET_REGEX = /^[()[\]{}]+$/
export const NORMALIZE_REGEX = /\s*\r?\n|\r/g
export const LINE_BREAK_REGEX = /\r?\n|\r|\n/g
export const QUOTES_REGEX = /["'`]/g
export const MAX_CONTEXT_LINE_COUNT = 200
export const SKIP_DECLARATION_SYMBOLS = ['=']
export const IMPORT_SEPARATOR = [',', '{']
Expand Down Expand Up @@ -51,6 +52,7 @@ export const MESSAGE_NAME = {
twinnySetOllamaModel: 'twinny-set-ollama-model',
twinnySetConfigValue: 'twinny-set-config-value',
twinnyGetConfigValue: 'twinny-get-config-value',
twinnyGetGitChanges: 'twinny-get-git-changes',
}

export const MESSAGE_KEY = {
Expand Down
52 changes: 36 additions & 16 deletions src/extension/chat-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,14 @@ export class ChatService {
return { requestOptions, requestBody }
}

private onStreamData = (streamResponse: StreamResponse | undefined) => {
private onStreamData = (
streamResponse: StreamResponse | undefined,
onEnd?: (completion: string) => void
) => {
try {
const data = getChatDataFromProvider(this._apiProvider, streamResponse)
this._completion = this._completion + data
if (onEnd) return
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinnyOnCompletion,
value: {
Expand All @@ -98,13 +102,20 @@ export class ChatService {
}
}

private onStreamEnd = () => {
private onStreamEnd = (onEnd?: (completion: string) => void) => {
this._statusBar.text = '🤖'
commands.executeCommand(
'setContext',
CONTEXT_NAME.twinnyGeneratingText,
false
)
if (onEnd) {
onEnd(this._completion)
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinnyOnEnd,
} as ServerMessage)
return
}
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinnyOnEnd,
value: {
Expand All @@ -126,7 +137,6 @@ export class ChatService {
}

private onStreamStart = (controller: AbortController) => {
this._statusBar.text = '$(loading~spin)'
this._controller = controller
commands.executeCommand(
'setContext',
Expand Down Expand Up @@ -215,11 +225,13 @@ export class ChatService {

private buildTemplatePrompt = async (
template: string,
language: CodeLanguageDetails
language: CodeLanguageDetails,
context?: string
) => {
const editor = window.activeTextEditor
const selection = editor?.selection
const selectionContext = editor?.document.getText(selection) || ''
const selectionContext =
editor?.document.getText(selection) || context || ''
const prompt = await this._templateProvider?.renderTemplate<TemplateData>(
template,
{
Expand All @@ -232,16 +244,19 @@ export class ChatService {

private streamResponse({
requestBody,
requestOptions
requestOptions,
onEnd
}: {
requestBody: StreamBodyBase
requestOptions: StreamRequestOptions
onEnd?: (completion: string) => void
}) {
return streamResponse({
body: requestBody,
options: requestOptions,
onData: this.onStreamData,
onEnd: this.onStreamEnd,
onData: (streamResponse: StreamResponse | undefined) =>
this.onStreamData(streamResponse, onEnd),
onEnd: () => this.onStreamEnd(onEnd),
onStart: this.onStreamStart,
onError: this.onStreamError
})
Expand Down Expand Up @@ -277,26 +292,31 @@ export class ChatService {
return this.streamResponse({ requestBody, requestOptions })
}

public async streamTemplateCompletion(promptTemplate: string) {
public async streamTemplateCompletion(
promptTemplate: string,
context?: string,
onEnd?: (completion: string) => void
) {
const { language } = getLanguage()
this._completion = ''
this._promptTemplate = promptTemplate
this.sendEditorLanguage()
this.focusChatTab()
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinnyOnLoading
})
const { prompt, selection } = await this.buildTemplatePrompt(
promptTemplate,
language
language,
context
)
this._statusBar.text = '$(loading~spin)'
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinnyOnLoading
})
this._view?.webview.postMessage({
type: MESSAGE_NAME.twinngAddMessage,
value: {
completion:
kebabToSentence(promptTemplate) + '\n\n' + '```\n' + selection,
data: getLanguage(),
type: this._promptTemplate
data: getLanguage()
}
} as ServerMessage)
const messageRoleContent = await this.buildMesageRoleContent(
Expand All @@ -312,7 +332,7 @@ export class ChatService {
prompt,
messageRoleContent
)
return this.streamResponse({ requestBody, requestOptions })
return this.streamResponse({ requestBody, requestOptions, onEnd })
}

private updateConfig() {
Expand Down
31 changes: 29 additions & 2 deletions src/extension/providers/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as vscode from 'vscode'

import { getLanguage, getTextSelection, getTheme } from '../utils'
import {
getGitChanges,
getLanguage,
getTextSelection,
getTheme
} from '../utils'
import { MESSAGE_KEY, MESSAGE_NAME } from '../../common/constants'
import { ChatService } from '../chat-service'
import {
Expand Down Expand Up @@ -91,13 +96,35 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
[MESSAGE_NAME.twinnyTextSelection]: this.getSelectedText,
[MESSAGE_NAME.twinnyWorkspaceContext]: this.getTwinnyWorkspaceContext,
[MESSAGE_NAME.twinnySetConfigValue]: this.setConfigurationValue,
[MESSAGE_NAME.twinnyGetConfigValue]: this.getConfigurationValue
[MESSAGE_NAME.twinnyGetConfigValue]: this.getConfigurationValue,
[MESSAGE_NAME.twinnyGetGitChanges]: this.getGitCommitMessage
}
eventHandlers[message.type as string]?.(message)
}
)
}

public getGitCommitMessage = async () => {
const diff = await getGitChanges()
if (!diff.length) {
vscode.window.showInformationMessage(
'No changes found in the current workspace.'
)
return
}
this.setTwinnyWorkspaceContext({
key: MESSAGE_KEY.lastConversation,
data: []
})
this.chatService?.streamTemplateCompletion(
'commit-message',
diff,
(completion: string) => {
vscode.commands.executeCommand('twinny.sendTerminalText', completion)
}
)
}

public getConfigurationValue = (data: ClientMessage) => {
if (!data.key) return
const config = vscode.workspace.getConfiguration('twinny')
Expand Down
14 changes: 14 additions & 0 deletions src/extension/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ Always reply with using markdown.
For code refactoring, use markdown with code formatting.
`
},
{
name: 'commit-message',
template: `You are an agent who generates concise git commit messages.
Only reply with one line of text.
- Answer under 100 characters.
E.g "Added a new feature"
Here is the unidiff: \`\`\`{{code}}\`\`\`
<commit message goes here>
`
},
{
name: 'chat',
template: `{{#if (eq messages.length 1)}}
Expand Down
61 changes: 59 additions & 2 deletions src/extension/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import {
InlineCompletionTriggerKind,
Position,
Range,
Terminal,
TextDocument,
window,
workspace
} from 'vscode'
import * as util from 'util'
import { exec } from 'child_process'

const execAsync = util.promisify(exec)

import {
Theme,
Expand All @@ -21,9 +27,12 @@ import { supportedLanguages } from '../common/languages'
import {
ALL_BRACKETS,
CLOSING_BRACKETS,
LINE_BREAK_REGEX,
OPENING_BRACKETS,
QUOTES,
SKIP_DECLARATION_SYMBOLS
QUOTES_REGEX,
SKIP_DECLARATION_SYMBOLS,
TWINNY
} from '../common/constants'
import { Logger } from '../common/logger'

Expand Down Expand Up @@ -242,7 +251,7 @@ export const getNextLineIsClosingBracket = () => {
if (!editor) return false
const position = editor.selection.active
const nextLineText = editor.document
.lineAt(Math.min(position.line + 1, editor.document.lineCount -1))
.lineAt(Math.min(position.line + 1, editor.document.lineCount - 1))
.text.trim()
return getIsOnlyClosingBrackets(nextLineText)
}
Expand Down Expand Up @@ -360,6 +369,54 @@ export function safeParseJsonResponse(
}
}

export const getCurrentWorkspacePath = (): string | undefined => {
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
const workspaceFolder = workspace.workspaceFolders[0]
return workspaceFolder.uri.fsPath
} else {
window.showInformationMessage('No workspace is open.')
return undefined
}
}

export const getGitChanges = async (): Promise<string> => {
try {
const path = getCurrentWorkspacePath()
const { stdout } = await execAsync('git diff --cached', {
cwd: path
})
return stdout
} catch (error) {
console.error('Error executing git command:', error)
return ''
}
}

export const getTerminal = async (): Promise<Terminal | undefined> => {
const twinnyTerminal = window.terminals.find((t) => t.name === TWINNY)
if (twinnyTerminal) return twinnyTerminal
const terminal = window.createTerminal({ name: TWINNY })
terminal.show()
return terminal
}

export const getTerminalExists = (): boolean => {
if (window.terminals.length === 0) {
window.showErrorMessage('No active terminals')
return false
}
return true
}

export const getSanitizedCommitMessage = (commitMessage: string) => {
const sanitizedMessage = commitMessage
.replace(QUOTES_REGEX, '')
.replace(LINE_BREAK_REGEX, '')
.trim()

return `git commit -m "${sanitizedMessage}"`
}

export const logStreamOptions = (opts: StreamRequest) => {
logger.log(
`
Expand Down
Loading

0 comments on commit ff38a07

Please sign in to comment.