diff --git a/README.md b/README.md index 84129ea5..5c63e99d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Copilot Chat for Neovim +<<<<<<< HEAD [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) @@ -8,6 +9,10 @@ > [!NOTE] > There is a new command: `CopilotChatInPlace`, which functions similarly to ChatGPT plugin. You can find it in the [canary](https://github.com/jellydn/CopilotChat.nvim/tree/canary?tab=readme-ov-file#lazynvim) branch. +======= +> [!NOTE] +> You might want to take a look at [this fork](https://github.com/jellydn/CopilotChat.nvim) which is more well maintained & is more configurable. I personally use it now as well. +>>>>>>> main ## Authentication @@ -18,9 +23,13 @@ It will prompt you with instructions on your first start. If you already have `C ### Lazy.nvim 1. `pip install python-dotenv requests pynvim==0.5.0 prompt-toolkit` +<<<<<<< HEAD 2. `pip install tiktoken` (optional for displaying prompt token counts) 3. Put it in your lazy setup +======= +2. Put it in your lazy setup +>>>>>>> main ```lua return { { diff --git a/cspell.json b/cspell.json deleted file mode 100644 index d7404183..00000000 --- a/cspell.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "version": "0.2", - "language": "en", - "globRoot": ".", - "dictionaryDefinitions": [ - { - "name": "cspell-tool", - "path": "./cspell-tool.txt", - "addWords": true - } - ], - "dictionaries": [ - "cspell-tool" - ], - "ignorePaths": [ - "node_modules", - "dist", - "build", - "/cspell-tool.txt" - ] -} \ No newline at end of file diff --git a/rplugin/python3/copilot.py b/rplugin/python3/copilot.py index 41a43c03..c41a9dce 100644 --- a/rplugin/python3/copilot.py +++ b/rplugin/python3/copilot.py @@ -11,6 +11,13 @@ import utilities from prompt_toolkit import PromptSession from prompt_toolkit.history import InMemoryHistory +<<<<<<< HEAD +======= +import utilities +import typings +import prompts +from typing import List, Dict +>>>>>>> main LOGIN_HEADERS = { "accept": "application/json", @@ -91,7 +98,10 @@ def ask(self, prompt: str, code: str, language: str = ""): # If expired, reauthenticate if self.token.get("expires_at") <= round(time.time()): self.authenticate() +<<<<<<< HEAD +======= +>>>>>>> main url = "https://api.githubcopilot.com/chat/completions" self.chat_history.append(typings.Message(prompt, "user")) system_prompt = prompts.COPILOT_INSTRUCTIONS @@ -134,7 +144,11 @@ def ask(self, prompt: str, code: str, language: str = ""): self.chat_history.append(typings.Message(full_response, "system")) +<<<<<<< HEAD def _get_embeddings(self, inputs: List[typings.FileExtract]): +======= + def _get_embeddings(self, inputs: list[typings.FileExtract]): +>>>>>>> main embeddings = [] url = "https://api.githubcopilot.com/embeddings" # If we have more than 18 files, we need to split them into multiple requests @@ -142,7 +156,11 @@ def _get_embeddings(self, inputs: List[typings.FileExtract]): if i + 18 > len(inputs): data = utilities.generate_embedding_request(inputs[i:]) else: +<<<<<<< HEAD data = utilities.generate_embedding_request(inputs[i : i + 18]) +======= + data = utilities.generate_embedding_request(inputs[i: i + 18]) +>>>>>>> main response = self.session.post(url, headers=self._headers(), json=data).json() if "data" not in response: raise Exception(f"Error fetching embeddings: {response}") diff --git a/rplugin/python3/plugin.py b/rplugin/python3/plugin.py new file mode 100644 index 00000000..dd2d4243 --- /dev/null +++ b/rplugin/python3/plugin.py @@ -0,0 +1,89 @@ +import os +import time + +import copilot +import prompts +import dotenv +import pynvim + +dotenv.load_dotenv() + + +@pynvim.plugin +class CopilotChatPlugin(object): + def __init__(self, nvim: pynvim.Nvim): + self.nvim = nvim + self.copilot = copilot.Copilot(os.getenv("COPILOT_TOKEN")) + if self.copilot.github_token is None: + req = self.copilot.request_auth() + self.nvim.out_write( + f"Please visit {req['verification_uri']} and enter the code {req['user_code']}\n" + ) + current_time = time.time() + wait_until = current_time + req["expires_in"] + while self.copilot.github_token is None: + self.copilot.poll_auth(req["device_code"]) + time.sleep(req["interval"]) + if time.time() > wait_until: + self.nvim.out_write("Timed out waiting for authentication\n") + return + self.nvim.out_write("Successfully authenticated with Copilot\n") + self.copilot.authenticate() + + @pynvim.command("CopilotChat", nargs="1") + def copilotChat(self, args: list[str]): + if self.copilot.github_token is None: + self.nvim.out_write("Please authenticate with Copilot first\n") + return + prompt = " ".join(args) + + if prompt == "/fix": + prompt = prompts.FIX_SHORTCUT + elif prompt == "/test": + prompt = prompts.TEST_SHORTCUT + elif prompt == "/explain": + prompt = prompts.EXPLAIN_SHORTCUT + + # Get code from the unnamed register + code = self.nvim.eval("getreg('\"')") + file_type = self.nvim.eval("expand('%')").split(".")[-1] + # Check if we're already in a chat buffer + if self.nvim.eval("getbufvar(bufnr(), '&buftype')") != "nofile": + # Create a new scratch buffer to hold the chat + self.nvim.command("enew") + self.nvim.command("setlocal buftype=nofile bufhidden=hide noswapfile") + # Set filetype as markdown and wrap with linebreaks + self.nvim.command("setlocal filetype=markdown wrap linebreak") + + # Get the current buffer + buf = self.nvim.current.buffer + self.nvim.api.buf_set_option(buf, "fileencoding", "utf-8") + + # Add start separator + start_separator = f"""### User +{prompt} + +### Copilot + +""" + buf.append(start_separator.split("\n"), -1) + + # Add chat messages + for token in self.copilot.ask(prompt, code, language=file_type): + buffer_lines = self.nvim.api.buf_get_lines(buf, 0, -1, 0) + last_line_row = len(buffer_lines) - 1 + last_line = buffer_lines[-1] + last_line_col = len(last_line.encode('utf-8')) + + self.nvim.api.buf_set_text( + buf, + last_line_row, + last_line_col, + last_line_row, + last_line_col, + token.split("\n"), + ) + + # Add end separator + end_separator = "\n---\n" + buf.append(end_separator.split("\n"), -1) diff --git a/rplugin/python3/prompts.py b/rplugin/python3/prompts.py index 8cbb25e8..2ea11f23 100644 --- a/rplugin/python3/prompts.py +++ b/rplugin/python3/prompts.py @@ -142,25 +142,34 @@ EXPLAIN_SHORTCUT = "Write a explanation for the code above as paragraphs of text." FIX_SHORTCUT = ( "There is a problem in this code. Rewrite the code to show it with the bug fixed." - "" ) - EMBEDDING_KEYWORDS = """You are a coding assistant who help the user answer questions about code in their workspace by providing a list of relevant keywords they can search for to answer the question. The user will provide you with potentially relevant information from the workspace. This information may be incomplete. DO NOT ask the user for additional information or clarification. DO NOT try to answer the user's question directly. + # Additional Rules + Think step by step: 1. Read the user's question to understand what they are asking about their workspace. + 2. If there are pronouns in the question, such as 'it', 'that', 'this', try to understand what they refer to by looking at the rest of the question and the conversation history. + 3. Output a precise version of question that resolves all pronouns to the nouns they stand for. Be sure to preserve the exact meaning of the question by only changing ambiguous pronouns. + 4. Then output a short markdown list of up to 8 relevant keywords that user could try searching for to answer their question. These keywords could used as file name, symbol names, abbreviations, or comments in the relevant code. Put the keywords most relevant to the question first. Do not include overly generic keywords. Do not repeat keywords. + 5. For each keyword in the markdown list of related keywords, if applicable add a comma separated list of variations after it. For example: for 'encode' possible variations include 'encoding', 'encoded', 'encoder', 'encoders'. Consider synonyms and plural forms. Do not repeat variations. + # Examples + User: Where's the code for base64 encoding? + Response: + Where's the code for base64 encoding? + - base64 encoding, base64 encoder, base64 encode - base64, base 64 - encode, encoded, encoder, encoders @@ -180,31 +189,48 @@ The user works in an IDE called Neovim which has a concept for editors with open files, integrated unit test support, an output pane that shows the output of running the code as well as an integrated terminal. The active document is the source code the user is looking at right now. You can only give one reply for each conversation turn. + Additional Rules Think step by step: + 1. Read the provided relevant workspace information (code excerpts, file names, and symbols) to understand the user's workspace. + 2. Consider how to answer the user's prompt based on the provided information and your specialized coding knowledge. Always assume that the user is asking about the code in their workspace instead of asking a general programming question. Prefer using variables, functions, types, and classes from the workspace over those from the standard library. + 3. Generate a response that clearly and accurately answers the user's question. In your response, add fully qualified links for referenced symbols (example: [`namespace.VariableName`](path/to/file.ts)) and links for files (example: [path/to/file](path/to/file.ts)) so that the user can open them. If you do not have enough information to answer the question, respond with "I'm sorry, I can't answer that question with what I currently know about your workspace". + Remember that you MUST add links for all referenced symbols from the workspace and fully qualify the symbol name in the link, for example: [`namespace.functionName`](path/to/util.ts). Remember that you MUST add links for all workspace files, for example: [path/to/file.js](path/to/file.js) + Examples: Question: What file implements base64 encoding? + Response: Base64 encoding is implemented in [src/base64.ts](src/base64.ts) as [`encode`](src/base64.ts) function. + + Question: How can I join strings with newlines? + Response: You can use the [`joinLines`](src/utils/string.ts) function from [src/utils/string.ts](src/utils/string.ts) to join multiple strings with newlines. + + Question: How do I build this project? + Response: To build this TypeScript project, run the `build` script in the [package.json](package.json) file: + ```sh npm run build ``` + + Question: How do I read a file? + Response: To read a file, you can use a [`FileReader`](src/fs/fileReader.ts) class from [src/fs/fileReader.ts](src/fs/fileReader.ts). """ diff --git a/rplugin/python3/utilities.py b/rplugin/python3/utilities.py index 1f6359a4..08fde688 100644 --- a/rplugin/python3/utilities.py +++ b/rplugin/python3/utilities.py @@ -1,10 +1,8 @@ -import json -import os -import random -from typing import List - import prompts import typings +import random +import os +import json def random_hex(length: int = 65): @@ -12,7 +10,7 @@ def random_hex(length: int = 65): def generate_request( - chat_history: List[typings.Message], + chat_history: list[typings.Message], code_excerpt: str, language: str = "", system_prompt=prompts.COPILOT_INSTRUCTIONS, @@ -49,7 +47,7 @@ def generate_request( } -def generate_embedding_request(inputs: List[typings.FileExtract]): +def generate_embedding_request(inputs: list[typings.FileExtract]): return { "input": [ f"File: `{i.filepath}`\n```{i.filepath.split('.')[-1]}\n{i.code}```"