diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-10-09 18:37:27 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-09 18:37:27 -0700 |
commit | f09150617ed2454f3074bcf93f53aae5ae637d40 (patch) | |
tree | 5cfe614a64d921dfe58b049f426d67a8b832c71f /server/continuedev/plugins/context_providers/highlighted_code.py | |
parent | 985304a213f620cdff3f8f65f74ed7e3b79be29d (diff) | |
download | sncontinue-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.py | 293 |
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, + ) + ) |