summaryrefslogtreecommitdiff
path: root/continuedev/src
diff options
context:
space:
mode:
Diffstat (limited to 'continuedev/src')
-rw-r--r--continuedev/src/continuedev/server/ide.py6
-rw-r--r--continuedev/src/continuedev/server/ide_protocol.py2
-rw-r--r--continuedev/src/continuedev/steps/core/core.py448
3 files changed, 226 insertions, 230 deletions
diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py
index c83fbc8a..c2ebaccf 100644
--- a/continuedev/src/continuedev/server/ide.py
+++ b/continuedev/src/continuedev/server/ide.py
@@ -146,8 +146,10 @@ class IdeProtocolServer(AbstractIdeProtocolServer):
# ------------------------------- #
# Request actions in IDE, doesn't matter which Session
- def showSuggestion():
- pass
+ async def showSuggestion(self, file_edit: FileEdit):
+ await self._send_json("showSuggestion", {
+ "edit": file_edit.dict()
+ })
async def setFileOpen(self, filepath: str, open: bool = True):
# Autopilot needs access to this.
diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py
index 2dcedc30..79820c36 100644
--- a/continuedev/src/continuedev/server/ide_protocol.py
+++ b/continuedev/src/continuedev/server/ide_protocol.py
@@ -12,7 +12,7 @@ class AbstractIdeProtocolServer(ABC):
"""Handle a json message"""
@abstractmethod
- def showSuggestion():
+ def showSuggestion(self, file_edit: FileEdit):
"""Show a suggestion to the user"""
@abstractmethod
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index 71a5b5b2..eb6a00c6 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -159,253 +159,247 @@ class DefaultModelEditCodeStep(Step):
self.name = await models.gpt35.complete(f"Write a very short title to describe this requested change: '{self.user_input}'. This is the title:")
return f"`{self.user_input}`\n\n" + description
- async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:
- self.description = f"`{self.user_input}`"
- await sdk.update_ui()
-
- rif_with_contents = []
- for range_in_file in map(lambda x: RangeInFile(
- filepath=x.filepath,
- # Only consider the range line-by-line. Maybe later don't if it's only a single line.
- range=x.range.to_full_lines()
- ), self.range_in_files):
- file_contents = await sdk.ide.readRangeInFile(range_in_file)
- rif_with_contents.append(
- RangeInFileWithContents.from_range_in_file(range_in_file, file_contents))
-
- rif_dict = {}
- for rif in rif_with_contents:
- rif_dict[rif.filepath] = rif.contents
-
- for rif in rif_with_contents:
- await sdk.ide.setFileOpen(rif.filepath)
-
- model_to_use = sdk.models.default
-
- full_file_contents = await sdk.ide.readFile(rif.filepath)
-
- full_file_contents_lst = full_file_contents.split("\n")
-
- max_start_line = rif.range.start.line
- min_end_line = rif.range.end.line
- cur_start_line = 0
- cur_end_line = len(full_file_contents_lst) - 1
-
- def cut_context(model_to_use, total_tokens, cur_start_line, cur_end_line):
-
- if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]:
- while cur_end_line > min_end_line:
- total_tokens -= model_to_use.count_tokens(
- full_file_contents_lst[cur_end_line])
- cur_end_line -= 1
- if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]:
- return cur_start_line, cur_end_line
-
- if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]:
- while cur_start_line < max_start_line:
- cur_start_line += 1
- total_tokens -= model_to_use.count_tokens(
- full_file_contents_lst[cur_end_line])
- if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]:
- return cur_start_line, cur_end_line
-
- return cur_start_line, cur_end_line
-
- if model_to_use.name == "gpt-4":
-
- total_tokens = model_to_use.count_tokens(full_file_contents + self._prompt)
- cur_start_line, cur_end_line = cut_context(
- model_to_use, total_tokens, cur_start_line, cur_end_line)
-
- elif model_to_use.name == "gpt-3.5-turbo" or model_to_use.name == "gpt-3.5-turbo-16k":
-
- if sdk.models.gpt35.count_tokens(full_file_contents) > MAX_TOKENS_FOR_MODEL["gpt-3.5-turbo"]:
-
- model_to_use = sdk.models.gpt3516k
- total_tokens = model_to_use.count_tokens(
- full_file_contents + self._prompt)
- cur_start_line, cur_end_line = cut_context(
- model_to_use, total_tokens, cur_start_line, cur_end_line)
-
- else:
-
- raise Exception("Unknown default model")
+ async def get_prompt_parts(self, rif: RangeInFileWithContents, sdk: ContinueSDK, full_file_contents: str):
+ # If using 3.5 and overflows, upgrade to 3.5.16k
+ model_to_use = sdk.models.default
+ if model_to_use.name == "gpt-3.5-turbo":
+ if sdk.models.gpt35.count_tokens(full_file_contents) > MAX_TOKENS_FOR_MODEL["gpt-3.5-turbo"]:
+ model_to_use = sdk.models.gpt3516k
+
+ # Remove tokens from the end first, and then the start to clear space
+ # This part finds the start and end lines
+ full_file_contents_lst = full_file_contents.split("\n")
+ max_start_line = rif.range.start.line
+ min_end_line = rif.range.end.line
+ cur_start_line = 0
+ cur_end_line = len(full_file_contents_lst) - 1
+
+ total_tokens = model_to_use.count_tokens(
+ full_file_contents + self._prompt)
+
+ if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]:
+ while cur_end_line > min_end_line:
+ total_tokens -= model_to_use.count_tokens(
+ full_file_contents_lst[cur_end_line])
+ cur_end_line -= 1
+ if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]:
+ return cur_start_line, cur_end_line
+
+ if total_tokens > MAX_TOKENS_FOR_MODEL[model_to_use.name]:
+ while cur_start_line < max_start_line:
+ cur_start_line += 1
+ total_tokens -= model_to_use.count_tokens(
+ full_file_contents_lst[cur_end_line])
+ if total_tokens < MAX_TOKENS_FOR_MODEL[model_to_use.name]:
+ return cur_start_line, cur_end_line
+
+ # Now use the found start/end lines to get the prefix and suffix strings
+ file_prefix = "\n".join(
+ full_file_contents_lst[cur_start_line:max_start_line])
+ file_suffix = "\n".join(
+ full_file_contents_lst[min_end_line:cur_end_line - 1])
+
+ # Move any surrounding blank line in rif.contents to the prefix/suffix
+ # TODO: Keep track of start line of the range, because it's needed below for offset stuff
+ rif_start_line = rif.range.start.line
+ if len(rif.contents) > 0:
+ first_line = rif.contents.splitlines(keepends=True)[0]
+ while first_line.strip() == "":
+ file_prefix += first_line
+ rif.contents = rif.contents[len(first_line):]
+ first_line = rif.contents.splitlines(keepends=True)[0]
- code_before = "\n".join(
- full_file_contents_lst[cur_start_line:max_start_line])
- code_after = "\n".join(
- full_file_contents_lst[min_end_line:cur_end_line - 1])
+ last_line = rif.contents.splitlines(keepends=True)[-1]
+ while last_line.strip() == "":
+ file_suffix = last_line + file_suffix
+ rif.contents = rif.contents[:len(
+ rif.contents) - len(last_line)]
+ last_line = rif.contents.splitlines(keepends=True)[-1]
- segs = [code_before, code_after]
- if segs[0].strip() == "":
- segs[0] = segs[0].strip()
- if segs[1].strip() == "":
- segs[1] = segs[1].strip()
+ while rif.contents.startswith("\n"):
+ file_prefix += "\n"
+ rif.contents = rif.contents[1:]
+ while rif.contents.endswith("\n"):
+ file_suffix = "\n" + file_suffix
+ rif.contents = rif.contents[:-1]
- # Move any surrounding blank line in rif.contents to the prefix/suffix
- if len(rif.contents) > 0:
- first_line = rif.contents.splitlines(keepends=True)[0]
- while first_line.strip() == "":
- segs[0] += first_line
- rif.contents = rif.contents[len(first_line):]
- first_line = rif.contents.splitlines(keepends=True)[0]
+ return file_prefix, rif.contents, file_suffix, model_to_use
- last_line = rif.contents.splitlines(keepends=True)[-1]
- while last_line.strip() == "":
- segs[1] = last_line + segs[1]
- rif.contents = rif.contents[:len(
- rif.contents) - len(last_line)]
- last_line = rif.contents.splitlines(keepends=True)[-1]
-
- while rif.contents.startswith("\n"):
- segs[0] += "\n"
- rif.contents = rif.contents[1:]
- while rif.contents.endswith("\n"):
- segs[1] = "\n" + segs[1]
- rif.contents = rif.contents[:-1]
-
- # .format(code=rif.contents, user_request=self.user_input, file_prefix=segs[0], file_suffix=segs[1])
- prompt = self._prompt
- if segs[0].strip() != "":
- prompt += dedent(f"""
+ def compile_prompt(self, file_prefix: str, contents: str, file_suffix: str, sdk: ContinueSDK) -> str:
+ prompt = self._prompt
+ if file_prefix.strip() != "":
+ prompt += dedent(f"""
<file_prefix>
-{segs[0]}
+{file_prefix}
</file_prefix>""")
- prompt += dedent(f"""
+ prompt += dedent(f"""
<code_to_edit>
-{rif.contents}
+{contents}
</code_to_edit>""")
- if segs[1].strip() != "":
- prompt += dedent(f"""
+ if file_suffix.strip() != "":
+ prompt += dedent(f"""
<file_suffix>
-{segs[1]}
+{file_suffix}
</file_suffix>""")
- prompt += dedent(f"""
+ prompt += dedent(f"""
<user_request>
{self.user_input}
</user_request>
<modified_code_to_edit>
""")
- lines = []
- unfinished_line = ""
- i = 0
- original_lines = rif.contents.split("\n")
-
- async def add_line(i: int, line: str):
- if i == 0:
- # First line indentation, because the model will assume that it is replacing in this way
- line = original_lines[0].replace(
- original_lines[0].strip(), "") + line
-
- if i < len(original_lines):
- # Replace original line
- range = Range.from_shorthand(
- rif.range.start.line + i, rif.range.start.character if i == 0 else 0, rif.range.start.line + i + 1, 0)
- else:
- # Insert a line
- range = Range.from_shorthand(
- rif.range.start.line + i, 0, rif.range.start.line + i, 0)
-
- await sdk.ide.applyFileSystemEdit(FileEdit(
- filepath=rif.filepath,
- range=range,
- replacement=line + "\n"
- ))
-
- lines_of_prefix_copied = 0
- line_below_highlighted_range = segs[1].lstrip().split("\n")[0]
- should_stop = False
- async for chunk in model_to_use.stream_chat(prompt, with_history=await sdk.get_chat_context(), temperature=0):
- if should_stop:
+ return prompt
+
+ def is_end_line(self, line: str) -> bool:
+ return "</modified_code_to_edit>" in line
+
+ def line_to_be_ignored(self, line: str) -> bool:
+ return "```" in line or "<modified_code_to_edit>" in line or "<file_prefix>" in line or "</file_prefix>" in line or "<file_suffix>" in line or "</file_suffix>" in line or "<user_request>" in line or "</user_request>" in line or "<code_to_edit>" in line or "</code_to_edit>" in line
+
+ async def stream_rif(self, rif: RangeInFileWithContents, sdk: ContinueSDK):
+ full_file_contents = await sdk.ide.readFile(rif.filepath)
+
+ file_prefix, contents, file_suffix, model_to_use = await self.get_prompt_parts(
+ rif, sdk, full_file_contents)
+ prompt = self.compile_prompt(file_prefix, contents, file_suffix, sdk)
+
+ full_file_contents_lines = full_file_contents.split("\n")
+ original_lines = rif.contents.split("\n")
+ i = 0
+ lines = []
+ unfinished_line = ""
+
+ current_block = []
+ offset_from_blocks = 0
+
+ async def insert_line(line: str, line_no: int):
+ nonlocal current_block
+ # Insert line, highlight green, highlight corresponding line red
+ range = Range.from_shorthand(
+ line_no, 0, line_no, 0)
+ red_range = Range.from_shorthand(
+ line_no + len(current_block), 0, line_no + len(current_block), 0)
+
+ await sdk.ide.applyFileSystemEdit(FileEdit(
+ filepath=rif.filepath,
+ range=range,
+ replacement=line + "\n"
+ ))
+ await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=range), "#00FF0022")
+ await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=red_range), "#FF000022")
+
+ async def show_block_as_suggestion():
+ nonlocal i, offset_from_blocks, current_block
+ await sdk.ide.showSuggestion(FileEdit(
+ filepath=rif.filepath,
+ range=Range.from_shorthand(
+ i + offset_from_blocks - len(current_block) + rif.range.start.line, 0, i + offset_from_blocks + rif.range.start.line, 0),
+ replacement="\n".join(current_block) + "\n"
+ ))
+ offset_from_blocks += len(current_block)
+ current_block.clear()
+
+ async def add_to_block(line: str):
+ current_block.append(line)
+ # TODO: This start line might have changed above
+ # await insert_line(line, i + offset_from_blocks +
+ # rif.range.start.line)
+
+ async def handle_generated_line(line: str):
+ nonlocal i, lines, current_block, offset_from_blocks, original_lines
+ # diff = list(difflib.ndiff(rif.contents.splitlines(
+ # keepends=True), completion.splitlines(keepends=True)))
+ if i < len(original_lines) and line == original_lines[i]:
+ # Line is the same as the original. Start a new block
+ await show_block_as_suggestion()
+ else:
+ # Add to the current block
+ await add_to_block(line)
+
+ lines_of_prefix_copied = 0
+ repeating_file_suffix = False
+ line_below_highlighted_range = file_suffix.lstrip().split("\n")[0]
+ async for chunk in model_to_use.stream_chat(prompt, with_history=await sdk.get_chat_context(), temperature=0):
+ # Stop early if it is repeating the file_suffix
+ if repeating_file_suffix:
+ break
+
+ # Accumulate lines
+ chunk_lines = chunk.split("\n")
+ chunk_lines[0] = unfinished_line + chunk_lines[0]
+ if chunk.endswith("\n"):
+ unfinished_line = ""
+ chunk_lines.pop() # because this will be an empty string
+ else:
+ unfinished_line = chunk_lines.pop()
+ lines.extend(chunk_lines)
+
+ # Deal with newly accumulated lines
+ for line in chunk_lines:
+ # Lines that should signify the end of generation
+ if self.is_end_line(line):
+ break
+ # Lines that should be ignored, like the <> tags
+ elif self.line_to_be_ignored(line):
+ continue
+ # Check if we are currently just copying the prefix
+ elif (lines_of_prefix_copied > 0 or i == 0) and lines_of_prefix_copied < len(file_prefix.splitlines()) and line == full_file_contents_lines[lines_of_prefix_copied]:
+ # This is a sketchy way of stopping it from repeating the file_prefix. Is a bug if output happens to have a matching line
+ lines_of_prefix_copied += 1
+ continue
+ # Because really short lines might be expected to be repeated, this is only a !heuristic!
+ # Stop when it starts copying the file_suffix
+ elif line.strip() == line_below_highlighted_range.strip() and len(line.strip()) > 4:
+ repeating_file_suffix = True
break
- chunk_lines = chunk.split("\n")
- chunk_lines[0] = unfinished_line + chunk_lines[0]
- if chunk.endswith("\n"):
- unfinished_line = ""
- chunk_lines.pop() # because this will be an empty string
- else:
- unfinished_line = chunk_lines.pop()
- lines.extend(chunk_lines)
-
- for line in chunk_lines:
- if "</modified_code_to_edit>" in line:
- break
- elif "```" in line or "<modified_code_to_edit>" in line or "<file_prefix>" in line or "</file_prefix>" in line or "<file_suffix>" in line or "</file_suffix>" in line or "<user_request>" in line or "</user_request>" in line or "<code_to_edit>" in line or "</code_to_edit>" in line:
- continue
- elif (lines_of_prefix_copied > 0 or i == 0) and lines_of_prefix_copied < len(segs[0].splitlines()) and line == full_file_contents_lst[lines_of_prefix_copied]:
- # This is a sketchy way of stopping it from repeating the file_prefix. Is a bug if output happens to have a matching line
- lines_of_prefix_copied += 1
- continue
- elif i < len(original_lines) and line == original_lines[i]:
- i += 1
- continue
- # Because really short lines might be expected to be repeated !heuristic!
- elif line.strip() == line_below_highlighted_range.strip() and len(line.strip()) > 4:
- should_stop = True
- break
- await add_line(i, line)
- i += 1
-
- # Add the unfinished line
- if unfinished_line != "":
- unfinished_line = unfinished_line.replace(
- "</modified_code_to_edit>", "").replace("</code_to_edit>", "").replace("```", "").replace("</file_suffix>", "").replace("</file_prefix", "").replace(
- "<modified_code_to_edit>", "").replace("<code_to_edit>", "").replace("<file_suffix>", "").replace("<file_prefix", "")
- if not i < len(original_lines) or not unfinished_line == original_lines[i]:
- await add_line(i, unfinished_line)
- lines.append(unfinished_line)
- i += 1
- # Remove the leftover original lines
- while i < len(original_lines):
- range = Range.from_shorthand(
- rif.range.start.line + i, rif.range.start.character, rif.range.start.line + i, len(original_lines[i]) + 1)
- await sdk.ide.applyFileSystemEdit(FileEdit(
- filepath=rif.filepath,
- range=range,
- replacement=""
- ))
+ # If none of the above, insert the line!
+ await handle_generated_line(line)
i += 1
- completion = "\n".join(lines)
-
- self._prompt_and_completion += prompt + completion
-
- diff = list(difflib.ndiff(rif.contents.splitlines(
- keepends=True), completion.splitlines(keepends=True)))
-
- lines_to_highlight = set()
- index = 0
- for line in diff:
- if line.startswith("-"):
- pass
- elif line.startswith("+"):
- lines_to_highlight.add(index + rif.range.start.line)
- index += 1
- elif line.startswith(" "):
- index += 1
-
- current_hl_start = None
- last_hl = None
- rifs_to_highlight = []
- for line in lines_to_highlight:
- if current_hl_start is None:
- current_hl_start = line
- elif line != last_hl + 1:
- rifs_to_highlight.append(RangeInFile(
- filepath=rif.filepath, range=Range.from_shorthand(current_hl_start, 0, last_hl, 0)))
- current_hl_start = line
- last_hl = line
-
- if current_hl_start is not None:
- rifs_to_highlight.append(RangeInFile(
- filepath=rif.filepath, range=Range.from_shorthand(current_hl_start, 0, last_hl, 0)))
-
- for rif_to_hl in rifs_to_highlight:
- await sdk.ide.highlightCode(rif_to_hl)
-
- await sdk.ide.saveFile(rif.filepath)
+ # Add the unfinished line
+ if unfinished_line != "" and not self.line_to_be_ignored(unfinished_line) and not self.is_end_line(unfinished_line):
+ lines.append(unfinished_line)
+ await handle_generated_line(unfinished_line)
+ i += 1
+
+ # Highlight the remainder of the range red
+ if i < len(original_lines):
+ await handle_generated_line("")
+ # range = Range.from_shorthand(
+ # i + 1 + offset_from_blocks + rif.range.start.line, 0, len(original_lines) + offset_from_blocks + rif.range.start.line, 0)
+ # await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=range), "#FF000022")
+
+ # If the current block isn't empty, add that suggestion
+ if len(current_block) > 0:
+ await show_block_as_suggestion()
+
+ # Record the completion
+ completion = "\n".join(lines)
+ self._prompt_and_completion += prompt + completion
+
+ async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:
+ self.description = f"`{self.user_input}`"
+ await sdk.update_ui()
+
+ rif_with_contents = []
+ for range_in_file in map(lambda x: RangeInFile(
+ filepath=x.filepath,
+ # Only consider the range line-by-line. Maybe later don't if it's only a single line.
+ range=x.range.to_full_lines()
+ ), self.range_in_files):
+ file_contents = await sdk.ide.readRangeInFile(range_in_file)
+ rif_with_contents.append(
+ RangeInFileWithContents.from_range_in_file(range_in_file, file_contents))
+
+ rif_dict = {}
+ for rif in rif_with_contents:
+ rif_dict[rif.filepath] = rif.contents
+
+ for rif in rif_with_contents:
+ await sdk.ide.setFileOpen(rif.filepath)
+ await self.stream_rif(rif, sdk)
+ # await sdk.ide.saveFile(rif.filepath)
class EditFileStep(Step):