Skip to content

Commit

Permalink
initial commit file mentions
Browse files Browse the repository at this point in the history
  • Loading branch information
rjmacarthy committed Sep 6, 2024
1 parent 7e0b3dd commit e28fc1d
Show file tree
Hide file tree
Showing 10 changed files with 477 additions and 60 deletions.
4 changes: 3 additions & 1 deletion src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ export const EVENT_NAME = {
twinnyChatMessage: 'twinny-chat-message',
twinnyClickSuggestion: 'twinny-click-suggestion',
twinnyConnectedToSymmetry: 'twinny-connected-to-symmetry',
twinnySymmetryModeles: 'twinny-symmetry-models',
twinnyConnectSymmetry: 'twinny-connect-symmetry',
twinnyDisconnectedFromSymmetry: 'twinny-disconnected-from-symmetry',
twinnyDisconnectSymmetry: 'twinny-disconnect-symmetry',
twinnyEmbedDocuments: 'twinny-embed-documents',
twinnyEnableModelDownload: 'twinny-enable-model-download',
twinnyFetchOllamaModels: 'twinny-fetch-ollama-models',
twinnyFileListRequest: 'twinny-file-list-request',
twinnyFileListResponse: 'twinny-file-list-response',
twinnyGetConfigValue: 'twinny-get-config-value',
twinnyGetGitChanges: 'twinny-get-git-changes',
twinnyGlobalContext: 'twinny-global-context',
Expand All @@ -61,6 +62,7 @@ export const EVENT_NAME = {
twinnyOpenDiff: 'twinny-open-diff',
twinnyRerankThresholdChanged: 'twinny-rerank-threshold-changed',
twinnySendLanguage: 'twinny-send-language',
twinnySymmetryModeles: 'twinny-symmetry-models',
twinnySendLoader: 'twinny-send-loader',
twinnySendSymmetryMessage: 'twinny-send-symmetry-message',
twinnySendSystemMessage: 'twinny-send-system-message',
Expand Down
18 changes: 13 additions & 5 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ export interface LanguageType {
languageId: string | undefined
}

export interface ClientMessage<T = string | boolean | Message[]> {
data?: T
export interface ClientMessage<T = string | boolean | Message[], Y = unknown> {
data?: T,
meta?: Y,
type?: string
key?: string
}
Expand All @@ -78,9 +79,6 @@ export interface ServerMessage<T = LanguageType> {
export interface Message {
role: string
content: string | undefined
type?: string
language?: LanguageType
error?: boolean
}

export interface Conversation {
Expand Down Expand Up @@ -276,3 +274,13 @@ export type EmbeddedDocument = {
vector: number[] | undefined
file: string
}

export interface FileItem {
name: string;
path: string;
}

export interface MentionType {
name: string;
path: string;
}
25 changes: 23 additions & 2 deletions src/extension/chat-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
ServerMessage,
TemplateData,
Message,
StreamRequestOptions
StreamRequestOptions,
FileItem
} from '../common/types'
import {
getChatDataFromProvider,
Expand Down Expand Up @@ -557,7 +558,22 @@ export class ChatService {
return combinedContext.trim() || null
}

public async streamChatCompletion(messages: Message[]) {
private async loadFileContents(files: FileItem[]): Promise<string> {
let fileContents = '';

for (const file of files) {
try {
const content = await fs.readFile(file.path, 'utf-8');
fileContents += `File: ${file.name}\n\n${content}\n\n`;
} catch (error) {
console.error(`Error reading file ${file.path}:`, error);
}
}
return fileContents.trim();
}


public async streamChatCompletion(messages: Message[], filePaths: FileItem[]) {
this._completion = ''
this.sendEditorLanguage()
const editor = window.activeTextEditor
Expand Down Expand Up @@ -587,6 +603,11 @@ export class ChatService {
additionalContext += `Additional Context:\n${ragContext}\n\n`
}

const fileContents = await this.loadFileContents(filePaths);
if (fileContents) {
additionalContext += `File Contents:\n${fileContents}\n\n`;
}

const updatedMessages = [systemMessage, ...messages.slice(0, -1)]

if (additionalContext) {
Expand Down
26 changes: 23 additions & 3 deletions src/extension/providers/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
ApiModel,
ServerMessage,
InferenceRequest,
SymmetryModelProvider
SymmetryModelProvider,
FileItem
} from '../../common/types'
import { TemplateProvider } from '../template-provider'
import { OllamaService } from '../ollama-service'
Expand All @@ -35,6 +36,7 @@ import { SymmetryService } from '../symmetry-service'
import { SessionManager } from '../session-manager'
import { Logger } from '../../common/logger'
import { DiffManager } from '../diff'
import { FileTreeProvider } from '../tree'

const logger = new Logger()

Expand All @@ -52,6 +54,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
public conversationHistory: ConversationHistory | undefined = undefined
public symmetryService?: SymmetryService | undefined
public view?: vscode.WebviewView
private _fileTreeProvider: FileTreeProvider

constructor(
statusBar: vscode.StatusBarItem,
Expand All @@ -66,6 +69,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
this._sessionManager = sessionManager
this._templateProvider = new TemplateProvider(templateDir)
this._ollamaService = new OllamaService()
this._fileTreeProvider = new FileTreeProvider()
if (db) {
this._db = db
}
Expand Down Expand Up @@ -157,7 +161,8 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
[EVENT_NAME.twinnyDisconnectSymmetry]: this.disconnectSymmetry,
[EVENT_NAME.twinnySessionContext]: this.getSessionContext,
[EVENT_NAME.twinnyStartSymmetryProvider]: this.createSymmetryProvider,
[EVENT_NAME.twinnyStopSymmetryProvider]: this.stopSymmetryProvider
[EVENT_NAME.twinnyStopSymmetryProvider]: this.stopSymmetryProvider,
[EVENT_NAME.twinnyFileListRequest]: this.requestFileList
}
eventHandlers[message.type as string]?.(message)
})
Expand All @@ -176,6 +181,18 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
} as ServerMessage<string>)
}

public requestFileList = async (message: ClientMessage) => {
if (message.type === EVENT_NAME.twinnyFileListRequest) {
const files = await this._fileTreeProvider?.getAllFiles()
this.view?.webview.postMessage({
type: EVENT_NAME.twinnyFileListResponse,
value: {
data: files
}
})
}
}

public embedDocuments = async () => {
const dirs = vscode.workspace.workspaceFolders
if (!dirs?.length) {
Expand Down Expand Up @@ -274,7 +291,10 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
)
}

this.chatService?.streamChatCompletion(data.data || [])
this.chatService?.streamChatCompletion(
data.data || [],
data.meta as FileItem[]
)
}

public async streamTemplateCompletion(template: string) {
Expand Down
183 changes: 183 additions & 0 deletions src/extension/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import * as vscode from 'vscode'
import * as fs from 'fs'
import * as path from 'path'
import { minimatch } from 'minimatch'
import ignore, { Ignore } from 'ignore'
import { EMBEDDING_IGNORE_LIST } from '../common/constants'
import { Logger } from '../common/logger'

const logger = new Logger()

export class FileTreeProvider {
private ignoreRules: Ignore
private workspaceRoot = ''

constructor() {
this.ignoreRules = this.setupIgnoreRules()

const workspaceFolders = vscode.workspace.workspaceFolders

if (!workspaceFolders) return

this.workspaceRoot = workspaceFolders[0].uri.fsPath
}

provideTextDocumentContent(): string {
return this.generateFileTree(this.workspaceRoot)
}

getAllFiles = async (): Promise<string[]> => {
return this.getAllFilePaths(this.workspaceRoot)
}

private setupIgnoreRules(): Ignore {
const ig = ignore()
ig.add(['.git', '.git/**'])

const gitIgnorePath = path.join(this.workspaceRoot, '.gitignore')
if (fs.existsSync(gitIgnorePath)) {
const ignoreContent = fs.readFileSync(gitIgnorePath, 'utf8')
const rules = ignoreContent
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'))
ig.add(rules)
}

return ig
}

private generateFileTree(dir: string, prefix = ''): string {
let output = ''
const entries = fs.readdirSync(dir, { withFileTypes: true })

const filteredEntries = entries.filter((entry) => {
const relativePath = path.relative(
this.workspaceRoot,
path.join(dir, entry.name)
)
return !this.ignoreRules.ignores(relativePath)
})

filteredEntries.forEach((entry, index) => {
const isLast = index === filteredEntries.length - 1
const marker = isLast ? '└── ' : '├── '
output += `${prefix}${marker}${entry.name}\n`

if (entry.isDirectory()) {
const newPrefix = prefix + (isLast ? ' ' : '│ ')
output += this.generateFileTree(path.join(dir, entry.name), newPrefix)
}
})

return output
}

private readGitIgnoreFile(): string[] | undefined {
try {
const folders = vscode.workspace.workspaceFolders
if (!folders || folders.length === 0) {
console.log('No workspace folders found')
return undefined
}

const rootPath = folders[0].uri.fsPath
if (!rootPath) {
console.log('Root path is undefined')
return undefined
}

const gitIgnoreFilePath = path.join(rootPath, '.gitignore')
if (!fs.existsSync(gitIgnoreFilePath)) {
console.log('.gitignore file not found at', gitIgnoreFilePath)
return undefined
}

const ignoreFileContent = fs.readFileSync(gitIgnoreFilePath, 'utf8')
return ignoreFileContent
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'))
.map((pattern) => {
if (pattern.endsWith('/')) {
return pattern + '**'
}
return pattern
})
} catch (e) {
console.error('Error reading .gitignore file:', e)
return undefined
}
}

private readGitSubmodulesFile(): string[] | undefined {
try {
const folders = vscode.workspace.workspaceFolders
if (!folders || folders.length === 0) return undefined
const rootPath = folders[0].uri.fsPath
if (!rootPath) return undefined
const gitSubmodulesFilePath = path.join(rootPath, '.gitmodules')
if (!fs.existsSync(gitSubmodulesFilePath)) return undefined
const submodulesFileContent = fs
.readFileSync(gitSubmodulesFilePath)
.toString()
const submodulePaths: string[] = []
submodulesFileContent.split('\n').forEach((line: string) => {
if (line.startsWith('\tpath = ')) {
submodulePaths.push(line.slice(8))
}
})
return submodulePaths
} catch (e) {
return undefined
}
}

private getAllFilePaths = async (dirPath: string): Promise<string[]> => {
if (!dirPath) return []
let filePaths: string[] = []
const dirents = await fs.promises.readdir(dirPath, { withFileTypes: true })
const gitIgnoredFiles = this.readGitIgnoreFile() || []
const submodules = this.readGitSubmodulesFile()

const rootPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''

for (const dirent of dirents) {
const fullPath = path.join(dirPath, dirent.name)
const relativePath = path.relative(rootPath, fullPath)

if (this.getIgnoreDirectory(dirent.name)) continue

if (submodules?.some((submodule) => fullPath.includes(submodule))) {
continue
}

if (
gitIgnoredFiles.some((pattern) => {
const isIgnored =
minimatch(relativePath, pattern, { dot: true, matchBase: true }) &&
!pattern.startsWith('!')
if (isIgnored) {
logger.log(`Ignoring ${relativePath} due to pattern: ${pattern}`)
}
return isIgnored
})
) {
continue
}

if (dirent.isDirectory()) {
filePaths = filePaths.concat(await this.getAllFilePaths(fullPath))
} else if (dirent.isFile()) {
filePaths.push(fullPath)
}
}
return filePaths
}

private getIgnoreDirectory(fileName: string): boolean {
return EMBEDDING_IGNORE_LIST.some((ignoreItem: string) =>
fileName.includes(ignoreItem)
)
}
}
Loading

0 comments on commit e28fc1d

Please sign in to comment.