summaryrefslogtreecommitdiff
path: root/server/continuedev/plugins/context_providers/highlighted_code.py
diff options
context:
space:
mode:
authorNate Sesti <33237525+sestinj@users.noreply.github.com>2023-10-09 18:37:27 -0700
committerGitHub <noreply@github.com>2023-10-09 18:37:27 -0700
commitf09150617ed2454f3074bcf93f53aae5ae637d40 (patch)
tree5cfe614a64d921dfe58b049f426d67a8b832c71f /server/continuedev/plugins/context_providers/highlighted_code.py
parent985304a213f620cdff3f8f65f74ed7e3b79be29d (diff)
downloadsncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.gz
sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.tar.bz2
sncontinue-f09150617ed2454f3074bcf93f53aae5ae637d40.zip
Preview (#541)
* Strong typing (#533) * refactor: :recycle: get rid of continuedev.src.continuedev structure * refactor: :recycle: switching back to server folder * feat: :sparkles: make config.py imports shorter * feat: :bookmark: publish as pre-release vscode extension * refactor: :recycle: refactor and add more completion params to ui * build: :building_construction: download from preview S3 * fix: :bug: fix paths * fix: :green_heart: package:pre-release * ci: :green_heart: more time for tests * fix: :green_heart: fix build scripts * fix: :bug: fix import in run.py * fix: :bookmark: update version to try again * ci: 💚 Update package.json version [skip ci] * refactor: :fire: don't check for old extensions version * fix: :bug: small bug fixes * fix: :bug: fix config.py import paths * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: platform-specific builds test #1 * feat: :green_heart: ship with binary * fix: :green_heart: fix copy statement to include.exe for windows * fix: :green_heart: cd extension before packaging * chore: :loud_sound: count tokens generated * fix: :green_heart: remove npm_config_arch * fix: :green_heart: publish as pre-release! * chore: :bookmark: update version * perf: :green_heart: hardcode distro paths * fix: :bug: fix yaml syntax error * chore: :bookmark: update version * fix: :green_heart: update permissions and version * feat: :bug: kill old server if needed * feat: :lipstick: update marketplace icon for pre-release * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: auto-reload for config.py * feat: :wrench: update default config.py imports * feat: :sparkles: codelens in config.py * feat: :sparkles: select model param count from UI * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: more model options, ollama error handling * perf: :zap: don't show server loading immediately * fix: :bug: fixing small UI details * ci: 💚 Update package.json version [skip ci] * feat: :rocket: headers param on LLM class * fix: :bug: fix headers for openai.;y * feat: :sparkles: highlight code on cmd+shift+L * ci: 💚 Update package.json version [skip ci] * feat: :lipstick: sticky top bar in gui.tsx * fix: :loud_sound: websocket logging and horizontal scrollbar * ci: 💚 Update package.json version [skip ci] * feat: :sparkles: allow AzureOpenAI Service through GGML * ci: 💚 Update package.json version [skip ci] * fix: :bug: fix automigration * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: upload binaries in ci, download apple silicon * chore: :fire: remove notes * fix: :green_heart: use curl to download binary * fix: :green_heart: set permissions on apple silicon binary * fix: :green_heart: testing * fix: :green_heart: cleanup file * fix: :green_heart: fix preview.yaml * fix: :green_heart: only upload once per binary * fix: :green_heart: install rosetta * ci: :green_heart: download binary after tests * ci: 💚 Update package.json version [skip ci] * ci: :green_heart: prepare ci for merge to main --------- Co-authored-by: GitHub Action <action@github.com>
Diffstat (limited to 'server/continuedev/plugins/context_providers/highlighted_code.py')
-rw-r--r--server/continuedev/plugins/context_providers/highlighted_code.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/server/continuedev/plugins/context_providers/highlighted_code.py b/server/continuedev/plugins/context_providers/highlighted_code.py
new file mode 100644
index 00000000..3304a71d
--- /dev/null
+++ b/server/continuedev/plugins/context_providers/highlighted_code.py
@@ -0,0 +1,293 @@
+import os
+from typing import Any, Dict, List, Optional
+
+from pydantic import BaseModel
+
+from ...core.context import (
+ ContextItem,
+ ContextItemDescription,
+ ContextItemId,
+ ContextProvider,
+)
+from ...core.main import ChatMessage
+from ...models.filesystem import RangeInFileWithContents
+from ...models.main import Range
+
+
+class HighlightedRangeContextItem(BaseModel):
+ rif: RangeInFileWithContents
+ item: ContextItem
+
+
+class HighlightedCodeContextProvider(ContextProvider):
+ """
+ The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'.
+ When you type '@', the context provider will be asked to populate a list of options.
+ These options will be updated on each keystroke.
+ When you hit enter on an option, the context provider will add that item to the autopilot's list of context (which is all stored in the ContextManager object).
+ """
+
+ title = "code"
+ display_title = "Highlighted Code"
+ description = "Highlight code"
+ dynamic = True
+
+ ide: Any # IdeProtocolServer
+
+ highlighted_ranges: List[HighlightedRangeContextItem] = []
+ adding_highlighted_code: bool = True
+ # Controls whether you can have more than one highlighted range. Now always True.
+
+ should_get_fallback_context_item: bool = True
+ last_added_fallback: bool = False
+
+ async def _get_fallback_context_item(self) -> HighlightedRangeContextItem:
+ # Used to automatically include the currently open file. Disabled for now.
+ return None
+
+ if not self.should_get_fallback_context_item:
+ return None
+
+ visible_files = await self.ide.getVisibleFiles()
+ if len(visible_files) > 0:
+ content = await self.ide.readFile(visible_files[0])
+ rif = RangeInFileWithContents.from_entire_file(visible_files[0], content)
+
+ item = self._rif_to_context_item(rif, 0, True)
+ item.description.name = self._rif_to_name(rif, show_line_nums=False)
+
+ self.last_added_fallback = True
+ return HighlightedRangeContextItem(rif=rif, item=item)
+
+ return None
+
+ async def get_selected_items(self) -> List[ContextItem]:
+ items = [hr.item for hr in self.highlighted_ranges]
+
+ if len(items) == 0 and (
+ fallback_item := await self._get_fallback_context_item()
+ ):
+ items = [fallback_item.item]
+
+ return items
+
+ async def get_chat_messages(self) -> List[ContextItem]:
+ ranges = self.highlighted_ranges
+ if len(ranges) == 0 and (
+ fallback_item := await self._get_fallback_context_item()
+ ):
+ ranges = [fallback_item]
+
+ return [
+ ChatMessage(
+ role="user",
+ content=f"Code in this file is highlighted ({r.rif.filepath}):\n```\n{r.rif.contents}\n```",
+ summary=f"Code in this file is highlighted: {r.rif.filepath}",
+ )
+ for r in ranges
+ ]
+
+ def _make_sure_is_editing_range(self):
+ """If none of the highlighted ranges are currently being edited, the first should be selected"""
+ if len(self.highlighted_ranges) == 0:
+ return
+ if not any(map(lambda x: x.item.editing, self.highlighted_ranges)):
+ self.highlighted_ranges[0].item.editing = True
+
+ def _disambiguate_highlighted_ranges(self):
+ """If any files have the same name, also display their folder name"""
+ name_status: Dict[
+ str, set
+ ] = {} # basename -> set of full paths with that basename
+ for hr in self.highlighted_ranges:
+ basename = os.path.basename(hr.rif.filepath)
+ if basename in name_status:
+ name_status[basename].add(hr.rif.filepath)
+ else:
+ name_status[basename] = {hr.rif.filepath}
+
+ for hr in self.highlighted_ranges:
+ basename = os.path.basename(hr.rif.filepath)
+ if len(name_status[basename]) > 1:
+ hr.item.description.name = self._rif_to_name(
+ hr.rif,
+ display_filename=os.path.join(
+ os.path.basename(os.path.dirname(hr.rif.filepath)), basename
+ ),
+ )
+ else:
+ hr.item.description.name = self._rif_to_name(
+ hr.rif, display_filename=basename
+ )
+
+ async def provide_context_items(self, workspace_dir: str) -> List[ContextItem]:
+ return []
+
+ async def get_item(self, id: ContextItemId, query: str) -> ContextItem:
+ raise NotImplementedError()
+
+ async def clear_context(self):
+ self.highlighted_ranges = []
+ self.adding_highlighted_code = False
+ self.should_get_fallback_context_item = True
+ self.last_added_fallback = False
+
+ async def delete_context_with_ids(
+ self, ids: List[ContextItemId]
+ ) -> List[ContextItem]:
+ ids_to_delete = [id.item_id for id in ids]
+
+ kept_ranges = []
+ for hr in self.highlighted_ranges:
+ if hr.item.description.id.item_id not in ids_to_delete:
+ kept_ranges.append(hr)
+ self.highlighted_ranges = kept_ranges
+
+ self._make_sure_is_editing_range()
+
+ if len(self.highlighted_ranges) == 0 and self.last_added_fallback:
+ self.should_get_fallback_context_item = False
+
+ return [hr.item for hr in self.highlighted_ranges]
+
+ def _rif_to_name(
+ self,
+ rif: RangeInFileWithContents,
+ display_filename: str = None,
+ show_line_nums: bool = True,
+ ) -> str:
+ line_nums = (
+ f" ({rif.range.start.line + 1}-{rif.range.end.line + 1})"
+ if show_line_nums
+ else ""
+ )
+ return f"{display_filename or os.path.basename(rif.filepath)}{line_nums}"
+
+ def _rif_to_context_item(
+ self, rif: RangeInFileWithContents, idx: int, editing: bool
+ ) -> ContextItem:
+ return ContextItem(
+ description=ContextItemDescription(
+ name=self._rif_to_name(rif),
+ description=rif.filepath,
+ id=ContextItemId(provider_title=self.title, item_id=str(idx)),
+ ),
+ content=rif.contents,
+ editing=editing if editing is not None else False,
+ editable=True,
+ )
+
+ async def handle_highlighted_code(
+ self,
+ range_in_files: List[RangeInFileWithContents],
+ edit: Optional[bool] = False,
+ ):
+ self.should_get_fallback_context_item = True
+ self.last_added_fallback = False
+
+ # Filter out rifs from ~/.continue/diffs folder
+ range_in_files = [
+ rif
+ for rif in range_in_files
+ if not os.path.dirname(rif.filepath)
+ == os.path.expanduser("~/.continue/diffs")
+ ]
+
+ # If not adding highlighted code
+ if not self.adding_highlighted_code:
+ if (
+ len(self.highlighted_ranges) == 1
+ and len(range_in_files) <= 1
+ and (
+ len(range_in_files) == 0
+ or range_in_files[0].range.start == range_in_files[0].range.end
+ )
+ ):
+ # If un-highlighting the range to edit, then remove the range
+ self.highlighted_ranges = []
+ elif len(range_in_files) > 0:
+ # Otherwise, replace the current range with the new one
+ # This is the first range to be highlighted
+ self.highlighted_ranges = [
+ HighlightedRangeContextItem(
+ rif=range_in_files[0],
+ item=self._rif_to_context_item(range_in_files[0], 0, edit),
+ )
+ ]
+
+ return
+
+ # If editing, make sure none of the other ranges are editing
+ if edit:
+ for hr in self.highlighted_ranges:
+ hr.item.editing = False
+
+ # If new range overlaps with any existing, keep the existing but merged
+ new_ranges = []
+ for i, new_hr in enumerate(range_in_files):
+ found_overlap_with = None
+ for existing_rif in self.highlighted_ranges:
+ if (
+ new_hr.filepath == existing_rif.rif.filepath
+ and new_hr.range.overlaps_with(existing_rif.rif.range)
+ ):
+ existing_rif.rif.range = existing_rif.rif.range.merge_with(
+ new_hr.range
+ )
+ found_overlap_with = existing_rif
+ break
+
+ if found_overlap_with is None:
+ new_ranges.append(
+ HighlightedRangeContextItem(
+ rif=new_hr,
+ item=self._rif_to_context_item(
+ new_hr, len(self.highlighted_ranges) + i, edit
+ ),
+ )
+ )
+ elif edit:
+ # Want to update the range so it's only the newly selected portion
+ found_overlap_with.rif.range = new_hr.range
+ found_overlap_with.item.editing = True
+
+ self.highlighted_ranges = self.highlighted_ranges + new_ranges
+
+ self._make_sure_is_editing_range()
+ self._disambiguate_highlighted_ranges()
+
+ async def set_editing_at_ids(self, ids: List[str]):
+ # Don't do anything if there are no valid ids here
+ count = 0
+ for hr in self.highlighted_ranges:
+ if hr.item.description.id.item_id in ids:
+ count += 1
+
+ if count == 0:
+ return
+
+ for hr in self.highlighted_ranges:
+ hr.item.editing = hr.item.description.id.item_id in ids
+
+ async def add_context_item(
+ self, id: ContextItemId, query: str, prev: List[ContextItem] = None
+ ) -> List[ContextItem]:
+ raise NotImplementedError()
+
+ async def manually_add_context_item(self, context_item: ContextItem):
+ full_file_content = await self.ide.readFile(
+ context_item.description.description
+ )
+ self.highlighted_ranges.append(
+ HighlightedRangeContextItem(
+ rif=RangeInFileWithContents(
+ filepath=context_item.description.description,
+ range=Range.from_lines_snippet_in_file(
+ content=full_file_content,
+ snippet=context_item.content,
+ ),
+ contents=context_item.content,
+ ),
+ item=context_item,
+ )
+ )