Skip to content

Commit

Permalink
Merge pull request #53 from meysamhadeli/fix/fix-bug-extract-code-cha…
Browse files Browse the repository at this point in the history
…nges

Fix/fix bug extract code changes
  • Loading branch information
meysamhadeli authored Nov 11, 2024
2 parents 6a5d9e5 + 5991a66 commit 0ea002d
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 32 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
and performing detailed code reviews. What makes codai stand out is its deep understanding of the entire context of your project, enabling it to analyze your code base
and suggest improvements or new code based on your context. This AI-powered tool supports multiple LLM models, including GPT-4o, GPT-4, Ollama, and more.**

![](./assets/codai-demo.gif)


We use **two** main methods to manage context: **RAG** (Retrieval-Augmented Generation) and **Summarize Full Context of Code**.
Each method has its own benefits and is chosen depending on the specific needs of the request. Below is a description of each method.

Expand Down
Binary file added assets/codai-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions cmd/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ startLoop: // Label for the start loop
finalPrompt, userInputPrompt := rootDependencies.Analyzer.GeneratePrompt(fullContextCodes, rootDependencies.ChatHistory.GetHistory(), userInput, requestedContext)

// Step 7: Send the relevant code and user input to the AI API
var b = finalPrompt + userInputPrompt
fmt.Println(b)
responseChan := rootDependencies.CurrentProvider.ChatCompletionRequest(ctx, userInputPrompt, finalPrompt)

// Iterate over response channel to handle streamed data or errors.
Expand Down
88 changes: 60 additions & 28 deletions code_analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,42 +244,74 @@ func (analyzer *CodeAnalyzer) TryGetInCompletedCodeBlocK(relativePaths string) (
return requestedContext, nil
}

// ExtractCodeChanges extracts code changes from the given text.
func (analyzer *CodeAnalyzer) ExtractCodeChanges(text string) []models.CodeChange {
if text == "" {
return nil
}
func (analyzer *CodeAnalyzer) ExtractCodeChanges(diff string) []models.CodeChange {
filePathPattern := regexp.MustCompile(`(?i)(?:\d+\.\s*|File:\s*)(\S+\.[a-zA-Z0-9]+)`)

// Regex patterns for file paths and code blocks
filePathPattern := regexp.MustCompile("(?i)(?:\\d+\\.\\s*|File:\\s*)[`']?([^\\s*`']+?\\.[a-zA-Z0-9]+)[`']?\\b")
// Capture entire diff blocks, assuming they are enclosed in ```diff ... ```
codeBlockPattern := regexp.MustCompile("(?s)```[a-zA-Z0-9]*\\s*(.*?)\\s*```")
lines := strings.Split(diff, "\n")
var fileChanges []models.CodeChange

// Find all file path matches and code block matches
filePathMatches := filePathPattern.FindAllStringSubmatch(text, -1)
codeMatches := codeBlockPattern.FindAllStringSubmatch(text, -1)
var currentFilePath string
var currentCodeBlock []string
insideCodeBlock := false

// Ensure pairs are processed up to the minimum count of matches
minLength := len(filePathMatches)
if len(codeMatches) < minLength {
minLength = len(codeMatches)
}
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)

// Detect a new file path
if !insideCodeBlock && filePathPattern.MatchString(trimmedLine) {
// Add the previous file's change if there was one
if currentFilePath != "" && len(currentCodeBlock) > 0 {
fileChanges = append(fileChanges, models.CodeChange{
RelativePath: currentFilePath,
Code: strings.Join(currentCodeBlock, "\n"),
})
currentCodeBlock = nil
}

// Capture the new file path
matches := filePathPattern.FindStringSubmatch(trimmedLine)
currentFilePath = matches[1]
continue
}

// Initialize code changes
var codeChanges []models.CodeChange
for i := 0; i < minLength; i++ {
relativePath := strings.TrimSpace(filePathMatches[i][1])
code := strings.TrimSpace(codeMatches[i][1])
// Start of a code block
if strings.HasPrefix(trimmedLine, "```") {
if !insideCodeBlock {
// Start a code block only if a file path is defined
if currentFilePath != "" {
insideCodeBlock = true
}
continue
} else {
// End the code block
insideCodeBlock = false
if currentFilePath != "" && len(currentCodeBlock) > 0 {
fileChanges = append(fileChanges, models.CodeChange{
RelativePath: currentFilePath,
Code: strings.Join(currentCodeBlock, "\n"),
})
currentCodeBlock = nil
currentFilePath = ""
}
continue
}
}

// Capture the relative path and associated diff content
codeChange := models.CodeChange{
RelativePath: relativePath,
Code: code,
// Collect lines inside a code block
if insideCodeBlock {
currentCodeBlock = append(currentCodeBlock, line)
}
codeChanges = append(codeChanges, codeChange)
}

return codeChanges
// Add the last collected code block if any
if currentFilePath != "" && len(currentCodeBlock) > 0 {
fileChanges = append(fileChanges, models.CodeChange{
RelativePath: currentFilePath,
Code: strings.Join(currentCodeBlock, "\n"),
})
}

return fileChanges
}

func (analyzer *CodeAnalyzer) ApplyChanges(relativePath, diff string) error {
Expand Down
22 changes: 22 additions & 0 deletions code_analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,28 @@ func TestApplyChanges_DeletedFile(t *testing.T) {
assert.Equal(t, content, string(savedContent))
}

// / Test for ExtractCodeChanges with standard input
func TestExtractCodeChangesComplexText(t *testing.T) {
setup(t)
text := "Sure, I can help you create a simple Pacman game using Python. We'll use the `pygame` library for this purpose. If you don't have `pygame` installed, you can install it using `pip install pygame`.\n\nHere is a basic implementation of a Pacman game:\n\nFile: pacman_game.py\n```python\nimport pygame\nimport random\n\n# Initialize pygame\npygame.init()\n\n# Screen dimensions\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 600\n\n# Colors\nBLACK = (0, 0, 0)\nWHITE = (255, 255, 255)\nYELLOW = (255, 255, 0)\nRED = (255, 0, 0)\n\n# Pacman settings\nPACMAN_SIZE = 50\nPACMAN_SPEED = 5\n\n# Ghost settings\nGHOST_SIZE = 50\nGHOST_SPEED = 3\n\n# Create the screen\nscreen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\npygame.display.set_caption(\"Pacman Game\")\n\n# Load images\npacman_image = pygame.image.load(\"pacman.png\")\npacman_image = pygame.transform.scale(pacman_image, (PACMAN_SIZE, PACMAN_SIZE))\n\nghost_image = pygame.image.load(\"ghost.png\")\nghost_image = pygame.transform.scale(ghost_image, (GHOST_SIZE, GHOST_SIZE))\n\n# Pacman class\nclass Pacman:\n def __init__(self):\n self.x = SCREEN_WIDTH // 2\n self.y = SCREEN_HEIGHT // 2\n self.speed = PACMAN_SPEED\n self.image = pacman_image\n\n def move(self, dx, dy):\n self.x += dx * self.speed\n self.y += dy * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - PACMAN_SIZE:\n self.x = SCREEN_WIDTH - PACMAN_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - PACMAN_SIZE:\n self.y = SCREEN_HEIGHT - PACMAN_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Ghost class\nclass Ghost:\n def __init__(self):\n self.x = random.randint(0, SCREEN_WIDTH - GHOST_SIZE)\n self.y = random.randint(0, SCREEN_HEIGHT - GHOST_SIZE)\n self.speed = GHOST_SPEED\n self.image = ghost_image\n\n def move(self):\n self.x += random.choice([-1, 1]) * self.speed\n self.y += random.choice([-1, 1]) * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - GHOST_SIZE:\n self.x = SCREEN_WIDTH - GHOST_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - GHOST_SIZE:\n self.y = SCREEN_HEIGHT - GHOST_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Main game loop\ndef main():\n clock = pygame.time.Clock()\n pacman = Pacman()\n ghosts = [Ghost() for _ in range(4)]\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n\n keys = pygame.key.get_pressed()\n dx = dy = 0\n if keys[pygame.K_LEFT]:\n dx = -1\n if keys[pygame.K_RIGHT]:\n dx = 1\n if keys[pygame.K_UP]:\n dy = -1\n if keys[pygame.K_DOWN]:\n dy = 1\n\n pacman.move(dx, dy)\n\n for ghost in ghosts:\n ghost.move()\n\n screen.fill(BLACK)\n pacman.draw()\n for ghost in ghosts:\n ghost.draw()\n\n pygame.display.flip()\n clock.tick(30)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()\n```\n\nThis code creates a simple Pacman game where Pacman can move around the screen using the arrow keys, and there are ghosts that move randomly. You need to have `pacman.png` and `ghost.png` images in the same directory as the script for it to work.\n\nTo run the game, save the code in a file named `pacman_game.py` and execute it with Python:\n\n```sh\npython pacman_game.py\n```\n\nEnjoy your game!"

codeChanges := analyzer.ExtractCodeChanges(text)

assert.Len(t, codeChanges, 1)
assert.Equal(t, "pacman_game.py", codeChanges[0].RelativePath)
assert.Equal(t, "import pygame\nimport random\n\n# Initialize pygame\npygame.init()\n\n# Screen dimensions\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 600\n\n# Colors\nBLACK = (0, 0, 0)\nWHITE = (255, 255, 255)\nYELLOW = (255, 255, 0)\nRED = (255, 0, 0)\n\n# Pacman settings\nPACMAN_SIZE = 50\nPACMAN_SPEED = 5\n\n# Ghost settings\nGHOST_SIZE = 50\nGHOST_SPEED = 3\n\n# Create the screen\nscreen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\npygame.display.set_caption(\"Pacman Game\")\n\n# Load images\npacman_image = pygame.image.load(\"pacman.png\")\npacman_image = pygame.transform.scale(pacman_image, (PACMAN_SIZE, PACMAN_SIZE))\n\nghost_image = pygame.image.load(\"ghost.png\")\nghost_image = pygame.transform.scale(ghost_image, (GHOST_SIZE, GHOST_SIZE))\n\n# Pacman class\nclass Pacman:\n def __init__(self):\n self.x = SCREEN_WIDTH // 2\n self.y = SCREEN_HEIGHT // 2\n self.speed = PACMAN_SPEED\n self.image = pacman_image\n\n def move(self, dx, dy):\n self.x += dx * self.speed\n self.y += dy * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - PACMAN_SIZE:\n self.x = SCREEN_WIDTH - PACMAN_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - PACMAN_SIZE:\n self.y = SCREEN_HEIGHT - PACMAN_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Ghost class\nclass Ghost:\n def __init__(self):\n self.x = random.randint(0, SCREEN_WIDTH - GHOST_SIZE)\n self.y = random.randint(0, SCREEN_HEIGHT - GHOST_SIZE)\n self.speed = GHOST_SPEED\n self.image = ghost_image\n\n def move(self):\n self.x += random.choice([-1, 1]) * self.speed\n self.y += random.choice([-1, 1]) * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - GHOST_SIZE:\n self.x = SCREEN_WIDTH - GHOST_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - GHOST_SIZE:\n self.y = SCREEN_HEIGHT - GHOST_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Main game loop\ndef main():\n clock = pygame.time.Clock()\n pacman = Pacman()\n ghosts = [Ghost() for _ in range(4)]\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n\n keys = pygame.key.get_pressed()\n dx = dy = 0\n if keys[pygame.K_LEFT]:\n dx = -1\n if keys[pygame.K_RIGHT]:\n dx = 1\n if keys[pygame.K_UP]:\n dy = -1\n if keys[pygame.K_DOWN]:\n dy = 1\n\n pacman.move(dx, dy)\n\n for ghost in ghosts:\n ghost.move()\n\n screen.fill(BLACK)\n pacman.draw()\n for ghost in ghosts:\n ghost.draw()\n\n pygame.display.flip()\n clock.tick(30)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()", codeChanges[0].Code)
}

// / Test for ExtractCodeChanges with standard input
func TestExtractCodeChangesComplexText2(t *testing.T) {
setup(t)
text := "Sure, I can help you create a simple \"Dino Game\" using Python and the Pygame library. This game will be similar to the Chrome Dino game where a dinosaur jumps over obstacles.\n\nFirst, ensure you have Pygame installed. You can install it using pip:\n```sh\npip install pygame\n```\n\nNow, let's create the game. We will need a few files:\n1. `main.py` - The main game logic.\n2. `dino.py` - The Dino character logic.\n3. `obstacle.py` - The obstacle logic.\n4. `settings.py` - Game settings and configurations.\n\n### File: `settings.py`\n```python\n# Game settings and configurations\n\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 400\nFPS = 30\n\n# Colors\nWHITE = (255, 255, 255)\nBLACK = (0, 0, 0)\n\n# Dino settings\nDINO_WIDTH = 40\nDINO_HEIGHT = 60\nDINO_COLOR = (0, 255, 0)\nDINO_JUMP_HEIGHT = 10\n\n# Obstacle settings\nOBSTACLE_WIDTH = 20\nOBSTACLE_HEIGHT = 40\nOBSTACLE_COLOR = (255, 0, 0)\nOBSTACLE_SPEED = 5\n```\n\n### File: `dino.py`\n```python\nimport pygame\nfrom settings import DINO_WIDTH, DINO_HEIGHT, DINO_COLOR, DINO_JUMP_HEIGHT\n\nclass Dino:\n def __init__(self, x, y):\n self.rect = pygame.Rect(x, y, DINO_WIDTH, DINO_HEIGHT)\n self.color = DINO_COLOR\n self.is_jumping = False\n self.jump_speed = DINO_JUMP_HEIGHT\n self.gravity = 1\n\n def update(self):\n if self.is_jumping:\n self.rect.y -= self.jump_speed\n self.jump_speed -= self.gravity\n if self.jump_speed < -DINO_JUMP_HEIGHT:\n self.is_jumping = False\n self.jump_speed = DINO_JUMP_HEIGHT\n\n def draw(self, screen):\n pygame.draw.rect(screen, self.color, self.rect)\n\n def jump(self):\n if not self.is_jumping:\n self.is_jumping = True\n```\n\n### File: `obstacle.py`\n```python\nimport pygame\nfrom settings import OBSTACLE_WIDTH, OBSTACLE_HEIGHT, OBSTACLE_COLOR, OBSTACLE_SPEED\n\nclass Obstacle:\n def __init__(self, x, y):\n self.rect = pygame.Rect(x, y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT)\n self.color = OBSTACLE_COLOR\n self.speed = OBSTACLE_SPEED\n\n def update(self):\n self.rect.x -= self.speed\n\n def draw(self, screen):\n pygame.draw.rect(screen, self.color, self.rect)\n```\n\n### File: `main.py`\n```python\nimport pygame\nimport random\nfrom settings import SCREEN_WIDTH, SCREEN_HEIGHT, FPS, WHITE\nfrom dino.py import Dino\nfrom obstacle.py import Obstacle\n\ndef main():\n pygame.init()\n screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\n pygame.display.set_caption(\"Dino Game\")\n clock = pygame.time.Clock()\n\n dino = Dino(50, SCREEN_HEIGHT - 60)\n obstacles = []\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n if event.type == pygame.KEYDOWN:\n if event.key == pygame.K_SPACE:\n dino.jump()\n\n screen.fill(WHITE)\n\n dino.update()\n dino.draw(screen)\n\n if random.randint(1, 100) < 2:\n obstacles.append(Obstacle(SCREEN_WIDTH, SCREEN_HEIGHT - 40))\n\n for obstacle in obstacles[:]:\n obstacle.update()\n obstacle.draw(screen)\n if obstacle.rect.x < 0:\n obstacles.remove(obstacle)\n if dino.rect.colliderect(obstacle.rect):\n running = False\n\n pygame.display.flip()\n clock.tick(FPS)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()\n```\n\nThis code sets up a basic Dino game where the dinosaur can jump over obstacles. The game will end if the dinosaur collides with an obstacle. You can expand and improve this game by adding more features, such as scoring, different types of obstacles, and animations."

codeChanges := analyzer.ExtractCodeChanges(text)

assert.Len(t, codeChanges, 4)
}

// / Test for ExtractCodeChanges with standard input
func TestExtractCodeChanges(t *testing.T) {
setup(t)
Expand Down
4 changes: 3 additions & 1 deletion embed_data/prompts/rag_context_prompt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
- Prefix **"-"** for removed lines.
- **Unchanged lines** should **not** be prefixed; they should remain as they are.
- **Last line**: End of the markdown highlighted **Code BLOCK**.
- If you provide a **diff** always give me full code and **do not summarize that** only for **modify part** and I want also **all of unchanged line**.
- If you provide a **diff** always give me **full code** and **do not summarize that** only for **modify part**, always I want **full code**.
- Always add **relative path** and **file name** **top** of each **Code BLOCK**.


## Code BLOCK Format:
- Every code modification **must follow** this pattern strictly, ensuring that added, removed, and unchanged lines are marked correctly with **"+"**, **"-"**, or skipped for unchanged lines and keep in mind **give me all of unchanged line and dont summarize them.**
Expand Down
3 changes: 2 additions & 1 deletion embed_data/prompts/summarize_full_context_prompt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
- Prefix **"-"** for removed lines.
- **Unchanged lines** should **not** be prefixed; they should remain as they are.
- **Last line**: End of the markdown highlighted **Code BLOCK**.
- If you provide a **diff** always give me full code and **do not summarize that** only for **modify part** and I want also **all of unchanged line**.
- If you provide a **diff** always give me **full code** and **do not summarize that** only for **modify part**, always I want **full code**.
- Always add **relative path** and **file name** **top** of each **Code BLOCK**.

## Code BLOCK Format:
- Every code modification **must follow** this pattern strictly, ensuring that added, removed, and unchanged lines are marked correctly with **"+"**, **"-"**, or skipped for unchanged lines and keep in mind **give me all of unchanged line and dont summarize them.**
Expand Down

0 comments on commit 0ea002d

Please sign in to comment.